diff --git a/doc/accessories-calculator.png b/doc/accessories-calculator.png new file mode 100644 index 000000000..1b1dd1e44 Binary files /dev/null and b/doc/accessories-calculator.png differ diff --git a/doc/details-categories.docbook b/doc/details-categories.docbook index e083ce111..8e38e0193 100644 --- a/doc/details-categories.docbook +++ b/doc/details-categories.docbook @@ -1,151 +1,151 @@ &Roger.Lum; &Roger.Lum.mail; 2014-08-30 4.7.01 -Categories +Categories The Categories screen provides a summary of all the existing categories under which transactions are filed. The categories are split into income and expense, and a balance is displayed for each category. Creating a category To create a new category, either select Category New category in the menu, or in the Categories view, select a parent in the tree, right click and select the New Category option. Either way will open the Create new categories wizard. Enter the new category name, and select the currency if the default shown is not correct. In the Hierarchy tab, ensure the required parent account is selected. Finally, in the Tax tab, there is a check box to enable VAT support, and a check box to include this category in certain tax related reports. Enter the category name any notes and click on OK to save the new category. To create a complete hierarchy of accounts, separate the names by colons (:) as in Bills:Car:Gasoline. Editing a Category To edit a category, right-click on a category name, and choose Edit category. A window comes up with three tabs: General, Hierarchy and Tax. The General tab allows you to edit the name of the category and any notes you entered when you created the category. The Hierarchy tab allows you to change the parent category. You can change the parent category by clicking on another category in this view of the category hierarchy. In an earlier version of &kmymoney; it was possible to change the parent of a category by dragging and dropping. This functionality will return in a future version, but possibly not until the conversion of &kmymoney; to &kde; Frameworks. The Tax tab allows you to assign or change VAT category, or alter the automatic VAT assignment. It also allows you to toggle whether or not the category is included in certain tax related reports. &Thomas.Baumgart; &Thomas.Baumgart.mail; VAT Support You can turn an expense/income category into a VAT category, which means that it receives all the splits that actually make up the VAT payment towards the government. You can also enter a specific percentage rate. Categories can be assigned a VAT category, which allows &kmymoney; to split a transaction for a category into two parts, one for the category and one for the VAT. Depending on the setting of the gross/net amount switch, the amount you enter for the transaction is the gross or net amount. Example: In Germany, there are three VAT percentages (0%, 7%, and 19%). So I have a main category "VAT paid" and three subcategories, one for each percentage rate. I create these on both the income and the expense sides so that I have eight categories in total. For the goods I buy, I select one of the above mentioned categories as the "VAT category assignment". When I buy goods, I enter them into a transaction. Let's assume I have selected the gross amount entry method, once I enter the category and amount, &kmymoney; will recognize that there's a VAT assignment and calculate the VAT part, create a second split with the VAT account and VAT amount and reduce the category amount. The same applies to income categories but the other way around. Hope that makes sense. Deleting a category To delete a category, select it in the tree, right click to bring up the popup menu, and select Delete. If there are any transactions assigned to this category, a popup will allow you to move these to another parent category. If you try to delete a parent category, a dialog allows the choice of either having the sub-categories moved up one level or alternatively having them all deleted at the same time. diff --git a/doc/details-forecast.docbook b/doc/details-forecast.docbook index a5ba2f234..eb9c12b87 100644 --- a/doc/details-forecast.docbook +++ b/doc/details-forecast.docbook @@ -1,308 +1,308 @@ &Colin.Wright; &Colin.Wright.mail; 2010-07-25 4.5 -Forecast +Forecast What is a Forecast? In &kmymoney;, a Forecast is a prediction of the balances of Accounts during a specified future period of time. Forecasts can be predicted using one of two methods. Scheduled and Future Transactions &kmymoney; uses a combination of future transactions already entered into the ledgers and the entries in the Schedule. History-based &kmymoney; uses values from past transactions in the ledgers to predict future transactions. The forecast method and forecast period can be set in the &kmymoney; Settings, in the Forecast pane. &kmymoney; Forecast provides a summary, showing the predicted balances at account cycle dates and the expected difference between the starting and ending balances. The length of the account cycle can be set by the user. The summary also displays messages about significant predicted changes to the accounts during the forecast period. As well as the summary, &kmymoney; Forecast also allows you to view day-by-day balances and view the minimum and maximum balances for each forecast account cycle. By default the Forecast is created for 90-days, with 30-day account cycles using Scheduled and Future Transactions. Viewing Forecasts You can view a Forecast by clicking the appropriate icon in the navigation pane at the left hand side of your &kmymoney; window. The Forecast window is split into five tabs Summary Tab The Summary tab is split vertically into two halves. Account Summary The Account Summary contains a grid displaying a row of information for each Asset and Liability. The following columns are displayed: Account The name of the Asset or Liability account Current Balance (Current) The current balance of the account is displayed. Account Cycle Balance The Forecast period is split into account cycles. The default account cycle length is 30 days, but can be set by the user. The first account cycle date is the first Forecast date. By default this is the current date plus one account cycle, but can change depending on the "Day of Month to start Forecast" setting. Remaining account cycle dates are determined by adding the account cycle length to the previous account cycle date. This is continued until the calculated account cycle date would be beyond the Forecast period. For each account cycle date the predicted balance is displayed. Total Variation The rightmost column displays the predicted difference in value between the balances at the start and end of the forecast. If the predicted end value of the account (in terms of net worth) is less than the start value the whole row is highlighted in red. Key Summary Information The bottom half of the Summary tab displays noteworthy information about accounts. This includes: Assets below zero A message is displayed for any asset for which the value starts or will drop below zero during the forecast period. Liabilities above zero A message is displayed for any liability for which the value starts or will rise above zero during the forecast period. Note that the value of a liability is the negative of its ledger balance, since a positive balance indicates money owed. Details Tab The Details tab contains a grid displaying a row of information for each Asset and Liability. Account The name of the Asset or Liability account Dates For each date in the Forecast period a column displays the predicted balance of the account on that date. The date columns are in ascending order from left to right. Total Variation The rightmost column displays the predicted difference in value between the balances at the start and end of the forecast. If the predicted end value of the account (in terms of net worth) is less than the start value the whole row is highlighted in red. Advanced Tab The Advanced tab contains a grid displaying a row of information for each Asset and Liability. The information is split into columns as follows: Account The name of the Asset or Liability account Minimum Account Cycle Balance For each account cycle number <n> in the Forecast period the following columns are displayed: Minimum Balance (Min Bal <n>) The minimum predicted balance during the account cycle Minimum Balance Date (Min Date <n>) The date on which the balance reaches its predicted minimum. Maximum Account Cycle Balance For each account cycle number <n> in the Forecast period the following columns are displayed: Maximum Balance (Max Bal <n>) The maximum predicted balance during the account cycle Maximum Balance Date (Max Date <n>) The date on which the balance reaches its predicted maximum. Average The average balance of the account during the forecast period Budget Forecast Tab The Budget tab displays a forecast calculation for the Income and Expense categories. The columns are similar to the Summary and Detailed tabs. Chart Tab It displays a chart showing the forecast. The level of detail varies depending on the detail level selected on the option above. diff --git a/doc/details-ledgers.docbook b/doc/details-ledgers.docbook index 13472c910..bdde5ec84 100644 --- a/doc/details-ledgers.docbook +++ b/doc/details-ledgers.docbook @@ -1,711 +1,733 @@ &Michael.T.Edwardes; &Michael.T.Edwardes.mail; &Roger.Lum; &Roger.Lum.mail; + + MichaelCarpino + mfcarpino@gmail.com + - 2010-07-23 - 4.5 + 2019-04-02 + 5.0.3 Ledgers The Ledger View - The Ledger view is where most of the functionality of &kmymoney; lies. This - view is for examining and entering transaction data in the various - accounts. The view is split into three main areas: the filter area, the + The Ledger view is functionally the data integration center of &kmymoney;. This + view is for entering, examining, deleting, and editing transactions in your defined + accounts. The view is split into three main areas: the filter area, the transaction list, and the input area. - This view allows for several key actions: searching and viewing transactions, - entering new transactions, and editing or deleting existing - transactions. Other functionality includes modifying account details and - reconciling the account. + This view allows for several key actions: searching and viewing transactions; + entering new transaction; and editing, duplicating, deleting, and scheduling + existing transactions. Along with this it also provides the ability to move, + mark, and go to additional account transaction details. Other functionality + within the Ledgers View includes modifying and reconciling the account. Ledger View The view has three elements: The filter area The transaction list - The transaction input form, which may or may not appear, depending upon - your configuration. + The transaction form, which may or may not appear, depending upon + your configuration as selected in the Configure &kmymoney; Ledger section. The filter area Filter area - You can select an account via the account dropdown list at the top of the view - area. Note that depending upon the type of the account the transaction input form at the - bottom of the view changes. + You can easily select any account via the account dropdown list at the top of + the view area. Note that depending upon the type of the account the transaction form at the + bottom of the view may change. To the right of the account dropdown list, are two additional fields. The - search box acts as a filter on the transactions that are shown in the list - view. Only transactions that include the text typed in the search box are - displayed. The text specified can be in any of the fields of the transaction. - The status field is a dropdown list. Only transactions of the type selected in - that dropdown are displayed. + filter box provides the ability to search for matching transactions + that will be displayed in the list view. The text specified can be in any + of the fields of the transaction for the specific account. The status field + is a dropdown list. The status field list allows for the selection of: + Any status, Imported, Matched, Erroneous, Not marked, Not reconciled, + Cleared, and scheduled transactions. The transaction list Transaction list - After you have entered a transaction, it is displayed in the transaction list. + After a transaction has been entered, it is displayed in the transaction list. You can also change transaction properties or even create new transactions - directly in the list. In the transactions list, the default arrangement of + directly in the list. In the transactions list, the default order of transactions is sorted by date with the most recent transaction on the bottom. Clicking the Right Mouse Button on the header of the transaction list brings - up a dialog that allows you to change the sort order of the transactions. For - instructions on how to change the default sort order, see the Sorting tab section of the &kmymoney; Settings Chapter. Note that the balance column is based on the currently displayed sort order, - and will not be calculated if the display is filtered by the search box or - transaction type dropdown, as described above. + and will not be calculated if the display is filtered by the searching within + the filter box or transaction type dropdown, as described above. At the bottom of the transaction list, &kmymoney; displays three values: Last reconciled - This is the most recent date on which you reconciled this account. + This is the most recent date when you reconciled this account. Reconciliation is an important process for many accounts, and is described here. Cleared This is the total of all cleared and reconciled transactions in this account. See this section for more information about the cleared and reconciled states of transactions. Balance This is where &kmymoney; displays the total balance of the account, which is the sum of all transactions in the account. However, if you select more than one transaction in the transaction list, this changes to display the sum of the selected transactions. This returns to the balance when only one transaction is selected. -The transaction input form +The transaction form The exact layout of the bottom area of the ledger view depends on your configuration and the type of account being displayed. However, it generally includes fields for all the details of a single transaction, as well as buttons for various actions that can apply to a transaction. It is described in more detail in the following sections. Entering Transactions There are two methods of entering transactions into the ledger: using the - transaction input form and entering the data directly into the transaction - list. The transaction input form is displayed by default and this is the - method we will discuss first. + transaction form or entering the data directly into the transaction + list. The transaction form is displayed by default and this is the + method we will discuss first. Turning off the transaction form can be + accomplished by going to the Ledger section in the Configure &kmymoney; that's + within the Settings. - The fields in the input area match the information fields in the transactions + The fields in the input area match the information fields in the transaction list. Additional fields include the Memo field, for a more detailed description of the transaction, and a Category selection. The Split button allows you to split the transaction into multiple categories. -Using the transaction input form +Using the transaction form Transaction Form - The transaction input form at the bottom of the ledger view is the interface - for creating transactions. + The transaction form at the bottom of the ledger view is the interface + for manually creating transactions. - Depending upon the type or method of transaction you wish to enter there are + Depending upon the type of transaction you wish to enter there are several tabs available on the transaction form. Click on the tab that best - describes your transaction (deposit, transfer, or withdrawal) and the form - will load several fields ready for your input. + defines your transaction (deposit, transfer, or withdrawal) and the form + will load several fields available for your input. Please note that the actual transaction method is not used directly by &kmymoney; but is purely for grouping/reporting purposes. If you are unsure which method to choose simply use Deposit for any money - going into the account, Withdrawal for money coming out of the account, and + coming into the account, Withdrawal for money going out of the account, and Transfer for money moving from one account to another. The transaction methods and the differences between them are discussed in more detail elsewhere. - Select the transaction method by clicking on one of the tabs. + Select the transaction method by clicking on the appropriate tab. The fields of a transaction Enter the information using the following notes on the available fields. Note that many fields have 'Auto Completion' turned on. That is, if you start - typing, &kmymoney; will offer alternatives matching the characters you have - entered so far. To select an entry click on it using the mouse or keyboard, - or if your entry is not listed keep typing to add the new value. The next - time you type the value in, &kmymoney; will find it for you after the first - few characters have been input. + typing, &kmymoney; will offer alternatives matching the characters you begin + to enter. To select the matching content simply click on the entry by using + your mouse or selecting the appropriate keyboard keys. If the entry is not + listed finish typing the content to add the new value. The next time you type + the content, &kmymoney; will find it for you as you begin to enter the initial + characters. The Payee The Payee is who the money came from/to. If the payee is a new entry &kmymoney; will ask if you wish to add this to the list of Payees. Any other - information related to a payee or payer, such as address details can be - updated in the Payees view later. + information related to a payee or payer, such as address, phone number and account + number as well as notes can be updated in the Payees view. The Category The Category associates a transaction with an income or expense category for accounting and reporting purposes, and enables you to group certain - transactions. Type the name of the category into the required field. If you + transactions. Type the name of the category into the defined field. If you have entered the category and it does not exist then &kmymoney; will ask if - you want to create a new one. + you want to create a new one. If the selection is Yes &kmymoney; will then + open a dialog box that allows for the further definition of the category based + on General, Hierarchy and Tax characteristics. If you wish to associate parts of the transaction with different categories, - &kmymoney; can let you do that. An example transaction might be a cash - machine withdrawal of 50 of which you use 10 on food, 20 on beer and 20 as - spare cash. The transaction will therefore be assigned three categories: + &kmymoney; can handle this need. An example transaction might be a cash + withdrawal of 50 of which you use 10 on food, 20 on beer and 20 as + cash. The transaction will therefore be assigned three categories: Food, Beer, and Cash. To do this, you need to use Split Transactions, which is described in more detail below. See the special &kmymoney; fields section for more information on how to use this field. The Tag - Tags were introduced to &kmymoney; in version 4.7, and they are not yet fully - documented in this handbook. Tags are similar to Categories, and can be used - to maintain an orthogonal view to Categories. For example, you might have - a Category for each different type of automotice expenses, and then have a Tag - for each vehicle. As with Categories, you can select from the dropdown list, or type - a new Tag name, and be prompted if you want to create a new Tag with that name. + Tags are similar to Categories, and can be used to maintain an orthogonal view + to Categories. They provide the ability to group transactions within a Category based + on your defined needs. This need could be defined by a specifics to Person, Place, or + Thing. For example, you might have a Category for each different type of automotive + expenses, and then have a Tag for each vehicle. As with Categories, you can select + from the dropdown list, or type a new Tag name, and be prompted if you want to create + a new Tag with that name. Within the Tag field &kmymoney; will allow for multiple tags + entered within a single transaction. Also, you can enter a tag on an individual split. The Memo - A multi-line memo can be entered if you wish to help you remember what the - transaction was for. + A multi-line memo can be entered if you wish to help you remember further + details of a specific transaction. The Check Number - The check number can be entered if needed. Note that the check number can - always be visible if desired. This is configured in the Settings dialog. + The check number can be entered if needed. Note that the check number field can + be made visible or invisible in the ledger if desired. This is configured in + the Ledger Settings dialog. The Date - The transaction's posting date must be entered to specify when the transaction - took place. See the special &kmymoney; + The transaction's date must be entered to specify when the transaction took + place. See the special &kmymoney; fields section for more information on how the date input field can be - used to make entering dates quicker and easier. + used to make entering dates quicker and easier. For transactions in checking + and credit card accounts, it is your choice whether to use the actual date + (when you wrote the check or made the purchase) or the posting date as + reported by the bank or credit card company. Using the actual date can help + you track when you made the purchase, for example, but the statement or + downloaded data from the bank is more likely to use the posting date. The Amount Finally, enter the transaction amount into the required field. Note that a simple calculator can be displayed, either by clicking the icon to the right of the amount field, or by entering the % character into the field, or by - entering a formula, as in 12 + 3. When entering the plus - sign, the calculator will be opened. + entering any of these mathematical symbols: + - * / in a formula, as in + 12 + 3. When entering the plus sign, the calculator will + be opened. Note that only the final result of any calculation will be saved + as the amount of the transaction. - When you are satisfied that all the fields have been filled in correctly, + When you are satisfied that all the fields have been adequately filled in, click on Enter. If you accidentally press or click on Enter before you have finished entering all the data, click on Edit to resume entering the data. Directly inputting transactions into the list Direct Transaction Entry The second method of entering transactions into the ledger involves editing - the transaction list itself directly. + the transaction list directly. To do this you must first let &kmymoney; know that you don't want to use the transaction form by opening the settings dialog and unchecking the Show transaction form option. This is performed by selecting Settings - Configure &kmymoney; from the menu bar and - selecting the Configure &kmymoney; from the menu bar + and selecting the Register icon from the list on the left. The option to uncheck is labeled Show transaction form. When finished click on - OK to be ready to directly enter transactions. + OK to be ready to directly enter or edit transactions. Starting the edit To enter a new transaction into the register you can now either click on an empty entry, press &Ctrl;Ins, or click New at the foot of the window. The Up and Down arrow keys let you navigate through the list. After pressing &Enter; or double clicking on an entry, the transaction list displays the fields required to enter the transaction and waits for input. To move through the fields press the key and when done press &Enter; to save the changes or &Esc; to cancel. In case the option Use Enter to move between fields is - selected, the &Enter; moves to the next field just as the - TAB key except for the last entry field where it stores the + selected, the &Enter; key moves to the next field just as the + TAB key except for the last entry field where it saves the data. Which method you use to enter transactions is up to you and is a matter of personal preference. Split Transactions The Split transaction feature allows you to divide up a transaction into multiple categories, representing, for example, the different items bought with a single purchase at a store. - To enter a split transaction, using either the transaction input form or the - transaction list, start a new transaction as normal, including entering the - total amount. Then, instead of selecting a category, click the - Split button. If you have already selected a category, + To enter a split transaction, using either the transaction form or the + transaction list, start a new transaction, including entering the total + amount. Then, instead of selecting a category, click the + Split button to the right of the + Category field. If you have already selected a category, that becomes the first entry in the split editor screen. In the split editor screen, double-click an empty line to enter a new sub-transaction or press &Ctrl;Ins. Specify the category, add an (optional) memo, and enter the amount. To save this part of the split, press the green check mark under the category. To cancel, press the red cross. After entering a split, the bottom of the split editor shows how much of the total transaction is still unassigned. After entering all the splits, press the OK button to save the entire transaction. If there is still an unassigned amount, you will be prompted to either return to editing the splits, change the total transaction amount, or leave part of the transaction unassigned. - Note that the category field in the transaction input form or the transaction + Note that the category field in the transaction form or the transaction list now displays Split transaction. Split transactions Split transaction Editing transactions + To edit a transaction, select it in the list view and either click on - Edit in the transaction input form or right click on - the entry and select Edit from the popup menu. If - you are editing transactions directly in the list you can edit the transaction - simply by double clicking on an entry or by pressing &Enter; - when a transaction is highlighted. + Edit in the transaction form or right click on the + entry and select Edit from the popup menu. If you + are editing transactions directly in the list you can edit the transaction + simply by double clicking on an entry or by pressing &Enter; when a + transaction is highlighted. Deleting transactions To delete a transaction, select it in the list view, right click on the entry, and select Delete from the popup menu when it appears, or click Delete on the transaction form. Matching Transactions - Generally, when importing transactions, either via QIF, OFX, or HBCI, &kmymoney; + Generally, when you import transactions, either via QIF, OFX, or HBCI, &kmymoney; will automatically attempt to match them against existing transactions. To allow for differences in the dates, there is a default setting of 4 days, which may be changed in the settings - Register/Import. Any transactions so matched will be highlighted in green. On completion of the import, you should review these and either accept or unmatch them. If you should find that an imported transaction was not automatically matched with an existing transaction when it should have matched, then it is possible - to match them manually. Note that there is a difference between manually + to match them manually. Note there is a difference between manually matching two transactions and simply deleting one of them, even though they may appear to have the same effect. Specifically with OFX or HBCI, it is important not to delete the imported transaction, because you will find that - the next time you import your transactions, the deleted transaction shows up + the next time you import your transactions, the deleted transaction may show up again. This is because modern import formats like OFX and HBCI use a Transaction ID to identify transactions. When you delete the imported transaction, the transaction ID goes with it, so the importer has no way to know this transaction was already imported. The solution is to tell &kmymoney; that the transactions are the same, using the manual matching interface. This allows you to match an imported transaction with a hand-entered (non-imported) transaction. To do so, select one of the transactions to be matched by clicking on it, then select the other by left clicking on it while pressing the &Ctrl; key, and then select - Match from the context menu. This changes the - background color to a pale green. This will match and combine the two - transactions together. The values of both transactions must be the same for - the match to work, except that the dates may differ by the window specified in - the settings, as described above. If you are happy with the result, right - click the matched transaction, then select Accept. + Match from the context menu. This will match and + combine the two transactions together. The values of both transactions must be + the same for the match to work, except that the dates may differ by the window + specified in the settings, as described above. If you are happy with the + result, right click the matched transaction, then select + Accept. - During import of online statements - either directly or by importing a - downloaded file - &kmymoney; performs matching as best as it can based on the + During import of online statements, either directly or by importing a + downloaded file, &kmymoney; performs matching as best as it can based on the name of the payee and the amount of the transaction. In case of an invalid match, a matched transaction can be unmatched. The matching interface will not allow you to match two transactions which have both been imported. Likewise, it won't allow matching between two transactions which have both been entered by hand. -Understanding the Cleared State +Understanding the State of a transaction - A transaction can have one of three states: non-reconciled, cleared (C), and - reconciled (R). When you enter a transaction, it has state - non-reconciled. Once the bank posts the transaction, the user can clear it + A transaction can have one of three states: non-reconciled (blank), cleared + (C), or reconciled (R). When a transaction is entered, it has state of + non-reconciled. Once the bank posts the transaction, the user can clear it and thus transform it to state (C). When you receive a statement from the bank, all cleared transactions should be on the statement. Understanding the cleared state cleared state - When you reconcile your account, you + When you reconcile your account, you actually mark the statements as cleared and check that the difference between the beginning balance and the cleared transactions equals the ending balance of the statement. When this is the case, you can 'finish reconciling' which actually changes the state of all cleared transactions (C) to reconciled (R). If you try to edit a transaction with at least one split marked as reconciled (R), you will be warned. - - - A fourth state is defined (frozen (F)) but there's currently no way to set - it. It is reserved for future use. Nevertheless, if a transaction has at least - one split in state frozen it cannot be edited anymore. - - -Changing Transaction Settings + +Changing Transaction Settings - There are several settings options that change the appearance and behavior of - the ledger view in terms of transactions. These settings are found by - selecting Settings Configure - &kmymoney; from the menu bar, and selecting the - - Register icon from the list on the - left. + There are several options that change the appearance and behavior of the + Ledger view in terms of transactions. These settings are found by selecting + SettingsConfigure &kmymoney; + from the menu bar, and selecting the + Ledger icon from the list on + the left. Most of the settings are self explanatory. For clarity, several of the settings are explained below. + Show transaction form (under the Display tab) - toggle to hide the - transaction input area at the bottom of this screen. Transactions can - still be entered directly into an empty line at the end of the transaction - list, through an automatic compact entry area. + transaction form at the bottom of this screen. Transactions can still be + entered directly into an empty line at the end of the transaction list, + through an automatic compact entry area. These images show what direct transaction entry looks like compared to the transaction form. -The transaction input form +The transaction form The transaction form Transaction form Transactions entered directly Transactions entered directly Direct input Keep changes when selecting a different transaction/split - by selecting the next line in the transaction list or split editor, the changes are kept, instead of the default behavior where you have to push the green check mark to save changes. Other Functionality Additional options are available from the Transaction Options menu, accessed by right-clicking any transaction in the list. The transaction options sub-menu Transaction options - Options include jumping to the Payee's page, creating a schedule, and changing - the reconciled or cleared indication. + Options include jumping to the Payees view for the Payee in the transaction, + creating a schedule, and changing the reconciled or cleared status. To edit the account information from the ledger view, select - Account from the menu bar. From this menu, you can change - the account details, or bring up the Reconcile menu, which allows you to match + Account from the menu bar. From the Edit + Account... menu item, you can change the account details. There + is also a menu item to bring up the Reconcile menu, which allows you to match transactions against an official bank statement or credit card notice. + diff --git a/doc/details-payees.docbook b/doc/details-payees.docbook index 77c85251f..b563e74cf 100644 --- a/doc/details-payees.docbook +++ b/doc/details-payees.docbook @@ -1,228 +1,254 @@ &Roger.Lum; &Roger.Lum.mail; &Ace.Jones; &Ace.Jones.mail; + + MichaelCarpino + mfcarpino@gmail.com + - 2010-07-25 - 4.5 + 2019-04-01 + 5.0.3 -Payees +Payees - The Payees screen provides detailed information about all the payees and - payers involved in transactions. The Payees screen is split into two main - areas: a payees and payers list and a detail area. + The Payees View provides detailed information about all the payees and payers + involved in transactions. These are the entities to and from which you + transfer money. &kmymoney; stores information about all the payees and payers + from all existing transactions. This makes it possible to pick a payee from a + list instead of having to type the full name, which also decreases the chance + of misspelling or typing the wrong name. - The Payees screen allows for viewing the transactions for the selected payee, - updating personal information associated with a particular payee or payer, and - specifying transaction matching criteria. + The Payees View is split into two main areas: a payees and payers list on the + left, and a detail area on the right. The Your payees + section allows for adding, deleting, renaming, and merging payees. The detail + area has several tabs, showing the transactions, address, matching + information, default account, and account numbers for the payee selected in + the list. Payees List - A list of payees and payers is on the left side of this screen. The payee list - is sorted alphabetically. + The list of all payees and payers in your data is shown on the left side of + this view. The payee list is sorted alphabetically. Adding a Payee or Payer - To add a payee or payer, right-click on any payee name and choose - New payee. This creates a new payee called - New Payee. Right-click on this payee and select - Rename payee to enter the name of the new payee or - payer. + To add a payee or payer, choose +New. This creates a + new payee called New Payee. You can also right-click on any + existing payee and select Add payee. Renaming a Payee or Payer - To rename a payee or payer, simply right-click on the name, select - Rename payee and the name becomes - editable. Changing the name will affect the name in all the transactions in - which it appears. + To rename a payee or payer, either double-click on the name or right-click on + the name and select Rename payee. The name then + becomes editable. Changing the name will affect the name in all the + transactions in which it appears. Deleting a Payee or Payer To remove a payee or payer, right-click the name and select - Delete payee. If the payee has any transactions, + Delete payee. If the payee has any transactions, you will be presented with a dialog that allows you to reassign them to a - different payee, and also a check box to enable a deleted name to be added to - the new payee's matching list. + different payee, and also a check box to enable the deleted name to be added + to the new payee's matching list. Another option for renaming an existing + payee is to select the payee or payer by left clicking it and then left + clicking -Del. - Additional Payee Details Personal Information To view and edit the personal information associated with a particular payee, - select the payee from the list and select the Address tab. To modify this - information, edit the detail area directly and press the Update button when - finished. + select the payee from the list and select the Address tab. Along with address + information you can also store a telephone number, email address, and notes + pertaining to the selected payee. To modify this information, edit the detail + area directly and press the Update button when finished. The personal information tab Personal information - + Transaction Matching Settings Overview &kmymoney; has the ability to automatically assign the proper payee to imported transactions. This can be handy when importing transactions from your bank, when the payee name has extra unnecessary characters, or worse, if the payee - names change. + names change. The ability to match transactions to a particualar payee assists + in providing a consist naming of payees within the ledger and reports. For example, let's say your monthly mortgage payment comes in from your bank like this SUNTRUST BANK MAPLE VALLEY, GA 111506 one month and then SUNTRUST BANK MAPLE VALLEY, GA 121606 the next month. You - would really like both of those transactions to be assigned simply to your - payee SunTrust. The transaction matching feature is for you! + would really like both of those transactions to be assigned to your payee + SunTrust. The transaction matching feature is for you! The payee matching tab Payee matching details Setting up - From the Matching tab, you can set the Transaction Matching Settings. You have - 3 major options: + From the Matching tab, you can set the transaction matching settings. You can + select one of four options with the Match method + dropdown. - No Matching. Disables the feature for this payee. This is the default - setting for all payees. + No Matching. This disables the feature for this payee. This is the + default setting for all payees. + + + + + + Match Payees name (partial). This enables the feature based on a partial + name matching of the Payee and and uses the payee name itself as the Match + Key. - Match on Payee Name. Enables the feature, and uses the payee name itself - as the Match Key + Match Payees name (exact). This enables the feature based on an exact + match of the Payee. You would use this matching ability when you want to + keep track of different Payees that have the same business name but have + different locations. - Match on Key. Enables the feature, and allows you to enter one or more - Match Keys of your choosing. In general, entering a plain string will - work perfectly. However, the match keys are actually regular expressions, - so it is possible to match on more complicated patterns. + Match on a name listed below. This enables matching based on multiple + different or optional names for a Payee. You would use this matching + ability when you have a particular Payee that has different business + names but you want all imported transactions identified by any of those + names to be associated with a single Payee in &kmymoney;. - You can also choose whether you want to ignore the case of the Match Key. If - you choose this option, the Match Key SunTrust would match - SUNTRUST BANK or SunTrust Bank. + With any of these options you can choose whether or not to ignore the case + sensitivity of the spelling of the payee based on whether you check + Ignore Case. Importing Transactions - When you import transactions using the QIF import, or using a plugin such as - OFX or AqBanking, the Match Keys you have set will be considered. If the - Match Key for one of your payees is found anywhere in the payee of the + The Match Keys you have set will be considered whenever you import + transactions using the QIF import, or using a plugin such as OFX or AqBanking, + If the Match Key for one of your payees is found anywhere in the payee of the imported transaction, the corresponding payee from your list will be assigned to that transaction. Thus, a Match Key of SunTrust will match SUNTRUST BANK MAPLE VALLEY,GA or even MORGENSUNTRUST&LOAN. So choose your Match Keys carefully. - If a transaction payee matches more than one Match Key, the - behavior is undefined. &kmymoney; will arbitrarily pick one of the matching - payees. + + If a transaction payee matches more than one Match Key, the behavior + is undefined. &kmymoney; will arbitrarily pick one of the matching + payees. + Viewing Transaction History To view all transactions associated with a particular payee, select the payee - from the list and the Transaction tab in the detail area. Double-clicking a - particular transaction will bring you directly to that transaction in the - Ledgers screen. + from the list and select the Transaction tab in the detail area. + Double-clicking a particular transaction will bring you directly to that + transaction in the Ledgers View for the appropriate account. The transaction tab Transaction history + diff --git a/doc/details-schedules.docbook b/doc/details-schedules.docbook index 2dfa4c2e7..901fd3d19 100644 --- a/doc/details-schedules.docbook +++ b/doc/details-schedules.docbook @@ -1,447 +1,375 @@ &Roger.Lum; &Roger.Lum.mail; &Michael.T.Edwardes; &Michael.T.Edwardes.mail; + + MichaelCarpino + mfcarpino@gmail.com + - 2010-07-25 - 4.5 + 2019-04-01 + 5.0.3 Schedules Introduction Schedules maintain information about transactions that occur one or more times over a specified period of time. Sometimes called a recurring transaction, a schedule provides a means to - record information about a transaction that happen on a regular basis. A - common schedule is your salary. Once a month, or maybe weekly, the company - you work for pays you for services rendered. This payment can happen in many - different ways, but each month or week you will receive a payment that needs - to be recorded. + record information about a transaction that happen on a regular basis. You + can schedule deposit, transfer, withdrawal, and loan transactions. There is + a lot of flexibility built in with scheduling transactions that include doing + it Once, or on a basis of number of Day(s), Week(s), Half-month(s), Month(s), + or Year(s). - Because you know these payments are regularly made to you, you can create a - Schedule to record information about the payment and even create the - transaction for you when pay day arrives. + Because you know these transactions are regularly made, whether they are + payments made to you or payments you make to someone else, you can create a + Schedule to record information about the recurring details to simplify and + easily remember when the event will occur. - Other types of schedules can also be recorded to reflect money coming in and - out of your accounts. Common expenses, such as utility bills or money - transfers, can be recorded with schedules, along with loan - repayments. Scheduling a payment can also provide a useful reminder that you - need to make a payment. + Schedules can be considered to reflect money coming in or out of your accounts + on a consistent basis. Common uses include paychecks, taxes, insurance, + credit cards, dues, interest, loans, mortgage, and rents. Scheduling a + payment like these provides for a useful reminder so you can manage expected + future financial matters effectively. - A schedule consists of two main parts: the scheduling data and the transaction - data. The scheduling data records the occurrence of the schedule, i.e., when + A schedule consists of two main parts: the transaction data and the scheduling + data. The scheduling data records the occurrence of the schedule, &ie;, when the transaction is to be entered into the ledger and how. The transaction - data records the normal details about the transaction, and will be entered in - to the ledger as-is. + data records the normal details about the transaction. These include options + for Payment method, Account, Pay to/from, Category, Tags, Memo, Due date, + Amount and status. Along with this, you can also select additional options + based on when to process the transaction. Types of schedules There are four different types of schedules in the current version of &kmymoney;: - Bills Deposits - Loans + Bills Transfers + Loans -Bills +Deposit - A Bill Schedule is used for money going out of your account, such as a Gym - membership or a utility bill. + A Deposit is for money coming into your account, such as paychecks, + bonuses, or interest income. -Deposits +Bills/Withdrawals - - A Deposit Schedule is for money coming into your account, such as a pay check. + + A Bill or Withdrawal is for money going out of your account, such as a + membership, utility bill, taxes, or car registration. -Loans +Transfer - - A Loan Schedule is a special type of schedule and is created when dealing with - Loan accounts. + + A Transfer is for money going out of one account and into another, + such as making a credit card payment from your checking account. -Transfers +Loans - A Transfer Schedule is used for money coming out of one account and going into - another, such as making a credit card payment from your checking account. + A Loan Schedule is a special type of schedule and is created when dealing with a + Loan account. - -The schedule view +The Schedules View - Open the schedule view by clicking on the Schedule + Open the Schedules View by clicking on the Schedule icon in the view selector. Schedule View - The schedule view consists of a view area with two tabs along with a - New Schedule button above it. The first tab - List View shows all schedules, grouped by type. The - second tab Calendar View is described in its own - section below. + The Schedules View consists of a view area with New + Schedule button and Filter and + Filter: text box above it. - To create a new schedule click on the button. For Bill, Deposit, and Transfer - schedules, &kmymoney; will display the new schedule dialog. Loan schedules are - handled differently, and are described in the section on Loans. + To create a new schedule click on the button. For Deposit, Transfer, and Withdrawal/Bills + schedules, &kmymoney; will display the new schedule dialog. Loan schedules are + handled differently, as described in the Loans section of this manual. -The new schedule dialog +The New Schedule dialog New Schedule - A new dialog window is now shown, prompting for the schedule and transaction - data. The requested data includes the following: + A New Schedule dialog is now shown, prompting for the schedule and transaction + details. The requested data include the following: Schedule Name Next due date Frequency Payment method Amount Other transaction details Schedule options -Filling in the fields - - - Enter the name and the date of the next occurrence of this schedule into their - respective fields. The name is how the schedule will be shown in the list - view. Specify how often the schedule should occur, using the Frequency field - and drop down list. For example "every 6 weeks" or "every 3 months". If the - amount varies transaction to transaction make sure to check the - Estimate check box and you will be prompted for the - correct amount when it is entered into the ledger. Fill in all the - transaction fields as you would normally with the ledger view. - +Filling in the Schedule name, Frequency, and Payment information. + + + Enter the name of the schedule and the date of the next occurrence into their + respective fields. The name is how the schedule will be labelled in the + Schedules View, and other displays. Specify how often the schedule should + occur, using the Frequency field and drop down list. For example "every 6 + weeks" or "every 3 months." If the amount varies transaction to transaction + make sure to check the Estimate check box and you will be + prompted for the correct amount when it is entered into the ledger. The other + required fields in the Payment information section include the Account and + Category. The remaining fields in this section are optional. + -Modifying the schedule behavior +Modifying the Schedule Options section - At the bottom of the dialog are some optional fields that can modify how the - schedule behaves. If the schedule occurs on a weekend you can choose whether - to enter the transaction on the weekend day, the previous Friday, or the next - Monday. Typically this option is used to replicate some real world operation - such as a bank transaction. + You can choose how to handle non-processing days. If the schedule occurs on a + non-processing day you can choose whether to Do not change the + date, Change the date to the previous processing + day, or Change the date to the next processing + day. This option is available to replicate some of the real world + banking operational processing. Non-processing days include weekends and + holidays known to &kmymoney; for the region specified in the appropriate configuration + option. - - - -Letting &kmymoney; enter the transaction into the register - To let &kmymoney; automatically enter the transaction for you when needed, - make sure that the check box labeled Enter this schedule into the - ledger automatically when it is due is checked. Otherwise, when - the transaction is due to be entered, &kmymoney; will open a dialog box asking - you to confirm that you want it to be entered. This behavior is configurable, - and more information can be found in the Settings section. + You can also select whether or not the amount is an estimate so you'll be + prompted to update the amount when the schedule is due. Selecting the process + at the last day of the month allows for scheduling for irregular month-end + date processing. By checking the box for automatically enter when the it's due + provides for &kmymoney; to enter the transaction into the ledger without human + intervention. Otherwise, when the transaction is due to be entered, + &kmymoney; will open a dialog box asking you to confirm that you want it to be + entered. This behavior is configurable, and more information can be found in + the Settings section. - - - -Letting &kmymoney; know when the schedule will finish If you know how many transactions are left or when they are due to finish, this information can be entered so the transactions are not entered - indefinitely. Check the appropriate check box and enter the appropriate - information. Only the date or the number of transactions remaining is needed - because the other can be calculated. + indefinitely. Checking the schedule will end box and enter either the number + of transactions remaining or date of final transaction will end the scheduled + transaction based on this criteria. When you have entered all the necessary information click on OK to continue. Editing schedules To edit a schedule simply select the entry in the list and right click to - bring up the context menu and select Edit. This - will bring up a dialog similar to the New Schedule dialog, where you can alter + bring up the context menu and select Edit scheduled transaction. This + will bring up a dialog similar to the Edit Schedule transaction dialog, where you can update the necessary data. Deleting schedules To delete a schedule simply select the entry in the list and right click to - bring up the context menu and select Delete. + bring up the context menu and select Delete scheduled transaction. - -The calendar view - - - The calendar tab presents the schedules in a calendar format so it is easier - to see when they occur. To switch to the calendar view click on the - Calendar View tab. - - - - - - - - - - Calendar View - - - - - - - The calendar type can be changed using the Select Style - button. Currently only two types exist: Week and Month. The monthly view is - the default, and is usually the most useful. Navigate through the dates using - the controls as you would with the date input control. - - - - On any day with scheduled transactions, the number of transactions scheduled - for that day is displayed on the calendar. - - - - - - - - - - - + +Duplicating schedules - To view the schedule(s) that fall on that day, move the mouse cursor over the - highlight and a popup window will be displayed allowing you to view the - schedule summary. + To duplicate a schedule simply select the entry in the list and right click to + bring up the context menu and select Duplicate scheduled transaction. - - - - - - - - - - - - - If more than one schedule falls on that day you can cycle through them using - the arrow buttons in the upper corners of the popup window. - - - - If you find that the display is cluttered with schedules and you only want to - view one type of schedule, then this is possible from the Select - Schedules dropdown. After clicking on this button the different - schedule types are listed and are checked by default - - - - - - - - - - - - - - To remove a type from the calendar view simply uncheck it in the list. - - Entering scheduled transactions Entering a scheduled transaction means actually entering a transaction into the appropriate ledger, using the details specified in the schedule. - Scheduled transactions can be entered in one of four different ways: + Scheduled transactions can be entered in one of 3 different ways: - Manually by right clicking on the list entry - Manually via the popup window in the calendar view + + + Manually by right clicking on the entry in the Schedules View + + &kmymoney; can enter them with or without your interaction upon startup &kmymoney; can enter them with your interaction during online banking or import of an electronic bank statement Manually entering scheduled transactions You can enter a scheduled transaction manually by right clicking on the list - entry and selecting Enter from the popup menu or by - clicking on the Enter button on the popup window in the - calendar view. + entry and selecting Enter from the popup menu. Confirming the transaction to enter After selecting either method the Enter Scheduled Transaction dialog window is shown prompting you with the required information that was entered when you created the schedule. This information can be changed, and the changes can be applied to just this one instance of the schedule or to all subsequent transactions. After - checking that all the data is correct clicking on OK + checking that all the data is correct clicking OK will add the transaction to the ledger and update the next due date of the schedule to the next occurrence. If you have changed any of the transaction data a further confirmation of what you changed will be displayed, giving you a final chance to accept or reject the changes. Select what you want to do with the information presented and click OK or Cancel. Letting &kmymoney; enter the scheduled transaction(s) for you Alternatively, &kmymoney; can check which schedules are due upon startup and enter them for you if the appropriate option was checked when setting up the schedule. If the option was not checked then &kmymoney; will open the previously mentioned dialog and you can follow the steps mentioned there. &kmymoney; will also open the dialog if the amount is an estimate so you can enter the real amount. You can also opt to have &kmymoney; not check which transactions are scheduled on startup by deselecting the option in the Settings dialog and any overdue transaction or transactions due 'today' will be shown in the Home view for you to enter manually. + diff --git a/doc/details-settings.docbook b/doc/details-settings.docbook index 353bab23d..887257abb 100644 --- a/doc/details-settings.docbook +++ b/doc/details-settings.docbook @@ -1,840 +1,978 @@ &Roger.Lum; &Roger.Lum.mail; + + MichaelCarpino + mfcarpino@gmail.com + + &Jack.H.Ostroff; &Jack.H.Ostroff.mail; - 2014-08-30 - 4.7.01 + 2019-03-31 + 5.0.3 -&kmymoney; Settings +&kmymoney; Settings - The settings described below modify the behavior of &kmymoney;. These - settings can be changed - through SettingsConfigure - &kmymoney;.... + The settings described below modify the behavior of &kmymoney;, allowing you + to customize it to meet the needs of your unique financial affairs, and to + modify its look and feel to match your personality. These settings can be + changed through SettingsConfigure + &kmymoney;... or by using the keyboard shortcut + &Ctrl; &Shift; + ,. In order for any changes to take effect the + Apply button must be clicked. + + + + On the left of the settings dialog is a list of icons and labels, each + representing a particular area of the program. When you click on one of + those icons, the right part of the dialog will display controls for + configuring that area of the program, often divided into several tabs. -General +General -Global options +Global tab - Price Precision + Startup options + + If you check the Show splash screen box, + &kmymoney; will display the splash screen when &kmymoney; starts. The splash + screen shows which version of the program you are running in the lower + left hand corner. Unchecking the box prevents the splash screen from + appearing at startup. + + + + + Autosave options + - Enter how many digits of precision your prices will have after the - decimal point. For example, setting this to 2 will result in prices - like 35.18, where setting it to 4 might cause the same price to show - 35.1791 + Checking the Autosave periodically box instructs + &kmymoney; to autosave your files at a defined interval. The default value is + 10 minutes. You can change this setting from 0 (backup immediately after + entering a transaction) up to 60 minutes. + + Checking the Autosave when file is modified upon close box + instructs &kmymoney; to autosave your file when the program is closed. You also have the + option upon closing the program to backup your &kmymoney; file. The Number of backup to + keep can be set at 0 (Off) upto to 20 backups. Any backup files will be stored in the same + folder as the main file. They will be labeled sequentially with 1 being the most current. - Autosave periodically + Fiscal Year - Check this box if you wish &kmymoney; to autosave your files - periodically. The default is 10 minutes. + Enter the day and month of your fiscal year. This value is used for + budgets and some reports. - Your fiscal year starts on + External programs - Enter the starting day and month of your fiscal year + This setting does not directly affect the operation of &kmymoney;, + but provides a convenient way to launch any external utility, such as an + advanced calculator. The first step is to enter the full path to your + chosen program in the Calculator text box. There are + then two ways to launch the program. + First, you can use + SettingsConfigure + Toolbars... to place the + Calculator icon on the Toolbar. Then, when you click + that icon on the Toolbar, &kmymoney; will launch the selected + application. + + Second, you can use + SettingsConfigure + Shortcuts... to set a custom keyboard shortcut. + (The entry is under KMyMoney/Calculator in that list.) You can then use + that shortcut to launch the application. + + + - -Views options +Views tab - Startup + Startup page options - Choose the initial view upon startup. - - Start with home - page - Start with last selected page - + When &kmymoney; starts up, this option indicates whether to start + with the Home View or with the view displayed when the program was last + closed. - + - Show titlebar on each page + Type of the KMyMoney view - Whether the title of the current view should be displayed on each - page. + You can select one of three general layouts for &kmymoney; to use. If + you select the List or Tree view, the icons and their labels will be located at + the left side of the window. The List view displays larger icons while the Tree + view has smaller icons. The Tabbed view locates the icons and labels across + the top of the window. - Synchronize account selection of ledger and investment - view + Show title bar on each page - If this option is selected, selecting an account on the investments - view will change the account selection on the ledger view, and - viceversa. + Checking this box will display the blue &kmymoney; title bar across the + top of each page. Unchecking it will remove the title bar and provide a larger + area for viewing the content of each page - -Filter options +Filter tab + +Accounts / Categories Do not show unused categories - Hide unused categories. + Checking this box hides unused categories in the Categories View. A + hidden category is still available should a transaction require it. By typing + a hidden category in the ledger it will become available for use. Do not show closed accounts - Hide closed accounts. + Checking this box hides closed accounts from display in the Accounts, + Ledger, and Investment Views. Hiding closed accounts makes viewing and working + with open accounts easier. Unchecking this box will show closed accounts with + a line drawn through them allowing viewing and reopening if needed. Show equity accounts - Equity accounts are normally hidden. Select this option to show - them. + By default, equity accounts are hidden in the Accounts View. Selecting + this option will display the equity accounts in the Accounts View. Equity + accounts are used to track individual equities or investments held in an + Investment Account. This should not be confused with the top level + Equity group of accounts, which includes the account(s) + which contain the opening balances for other accounts. - + + + Show categories in the accounts list view + + By default, Categories are hidden in the Accounts View. Selecting this + option will display the Categories in the Accounts View. + + + + Do not show zero balance equities + + Checking this option will remove any zero balance equity accounts from + appearing in the Accounts View. + + + + + + +Schedules + + Do not show finished schedules - Hide finished schedules. + Checking this box will hide all finished schedules from appearing in the + Scheduled transaction View. + + + +Transactions + Do not show transaction prior to - Hide transactions prior to the selected date. - Setting this option to January 1, 1900 is a good way to debug - date-related file import problems. + Transactions prior to the date entered here will not be displayed in the + Ledger View. This can be useful if your data includes many years of + transactions; setting this to one or two years ago hides older + transactions. + Setting this option to January 1, 1900 (the default value) is a good way + to debug date-related data import problems, in case some dates were erroneously + very far in the past. Do not show reconciled transactions - Filter transactions by reconciliation state. Transactions that are - marked as Reconciled will not be shown. + Checking this box will filter transactions by reconciliation state. + Transactions that are marked as Reconciled will not be + shown in the Ledger View. + -Home +Home + +Home page display sections - These options allow you to customize the appearance of the Home page by - choosing which entries to show on the Home page and the order in which they - appear. + These options allow you to customize the appearance of the Home View (also called the Home Page or Main + Window) by choosing which information to display there and the order in which it + appears. Selected sections can be reordered by clicking the respective + Up or Down button. + Assets and Liabilities - It shows the balance of all your assets and liabilities + This will display the asset and liability accounts that are open along + with their current balances. This and other account lists on the Home Page are + sorted in alphabetical order, unless otherwise noted. Payments - It shows the first 5 occurrences of your scheduled - transactions + This will display upcoming occurrences of Scheduled transactions, + distinguishing Overdue payments from Future payments. It shows the date, + schedule name, account and amount of the payment, and the account balance after + the payment. By default it will show up to six transaction. If six are + displayed, there is a link you can select to show more, up to 30 days in + advance. Preferred accounts - It shows the balance and credit of your favorite accounts + This will display the accounts that are identified as Preferred accounts + within the AccountEdit account... + Configuration dialog. + For this and all other account list sections addressed by this set of + options, the display will include the account name and current balance, and + other columns specific to that type of report, and in some cases, as configured + elsewhere. + Payment accounts - It shows the balance of your asset and liability accounts, except - for the investment accounts + This will display all Payment accounts; these include any account into + which income can be deposited, and/or out of which bills can be paid. Favorite Reports - It shows a list of your favorite reports + This will display a list of all reports that have been identified as a + favorite in the Configuration dialog of the Report. The list ordered + chronologically by when each report became a favorite. Forecast (schedule) - It shows a forecast for your asset and liability Accounts + This will display a 30, 60, and 90 day forecast for asset and liability accounts with non zero + balances and with non reconciled transactions. + Net worth forecast - It shows a chart of your forecast net worth for the next 90 - days + This will display chart of your forecast net worth for the next 90 + days. - + + + Budget - It shows the budget items for the current month which have a - negative difference compared to the actual income or expense + This will display a current month summary and overruns for any defined + budget(s). If no budget is defined, it will display no budget or overruns, with + the actual current month spending. - CashFlow + Cash Flow - It shows an analysis of your cash flow for the current month + This will display a Cash Flow Summary analysis for the current month. + + + + +Homepage/Summary page scaling + - Percentage of default font size + Zoom factor: - Select the size of the font on the Home page + This option allows for the selection of the font size as a percentage to + fit the Home View based on the user preference. - Remember font size when leaving the program if manually - changed with mouse-wheel + Remember zoom factor when leaving the program if manually changed + with mouse-wheel - Selecting this will save your font size preference if you have - changed it from the Home page by using the mouse-wheel + Selecting this will save your font size preference if you have changed it + on the Home page by using the mouse-wheel. + + + +Account information display +These options allow you to customize what information is included in any list +of accounts. + Show account limit information - Shows the limit of the accounts on the Payment Accounts and - Preferred Accounts sections of the Home page + Displays the account limit as set in the Account Edit dialog. It affects + any list which includes Payment accounts. Show balance-status of mapped online accounts - Shows the balance-status of mapped accounts on the Assets and Liabilities - Summary, the Payment Accounts and the Preferred Accounts sections of the Home - page. The balance-status is symbolized by an icon in the columns preceding - the account names. A green checkmark is displayed when online-balance and local - file balance are in sync. If the local file has transactions newer than the - previously online-updated ones a mailbox with a green arrow gets shown. This alerts - the user to start an online-update. Mismatching local file and online-balances are - highlighted by a red warning sign. - + Shows the balance-status of mapped accounts, symbolized by an icon in the + columns preceding the account name. A green checkmark indicates the online + balance and local balance are the same. A mailbox with a green arrow indicates + that the local file has transactions newer than the most recent online update. + This alerts the user to start an online-update. A red warning sign indicates a + mismatch between the local file and online-balance. + Show number of not marked transactions per account [!M] - Insert an additional column between account name and current balance showing - the number of not marked transactions for the accounts on the Assets and Liabilities - Summary, the Payment Accounts and Preferred Accounts sections of the Home page - + Inserts an additional column next to the account name, showing the number + of not marked transactions in the account. Show number of cleared transactions per account [C] - Insert an additional column between account name and current balance showing - the number of cleared transactions for the accounts on the Assets and Liabilities - Summary, the Payment Accounts and Preferred Accounts sections of the Home page - + Inserts an additional column showing the number of cleared transactions + for the account. - Show number of not reconciled (not marked + cleared) transactions per account [!R] + Show number of not reconciled (not marked + cleared) transactions + per account [!R] - Insert an additional column between account name and current balance showing - the number of not reconciled transactions (i.e. the sum of all not marked and - cleared transactions) for the accounts on the Assets and Liabilities Summary, the - Payment Accounts and Preferred Accounts sections of the Home page + Inserts an additional column showing the number of not reconciled + transactions for the account, &ie;, the sum of all not marked and cleared + transactions. + + Show date of last reconciliation + + Inserts an additional column showing the last reconciliation date for the + account. + + + -Ledger +Ledger - These options allow you to modify the behavior of the transaction registers. + The following options outlined within this section allow for the modification of the + transaction register contained within the Ledger view tab. -Display tab + +Display tab Show a grid in the register - Show grid lines in the transaction register. + Selecting this option displays grid lines in the transaction + register or Ledger View. It provides an additional viewing option that + may make it easier to visualize separate transactions and their component + parts. - + + + Show all register entries in full detail + + Selecting this option makes the register show all the information + for all displayed transactions, with separate lines in the Detail Column + for Category and Tags and for the Memo. If it is not selected, Category, + Tags, and Memo will not be displayed. + This has the same effect as selecting the Show Transaction + Detail menu option. + + + Use the ledger lens - With this option enabled, the ledger will show all lines of detail - for the transaction you have currently selected, even if you have set - the Show - Transaction Detail menu option turned off. + Selecting this option displays all the detail lines for the selected + transaction, even if Show all register entries in full + detail is not selected. Show transaction form - Show the transaction - form at the bottom of the transaction register for entering and - editing transactions. With this option turned off, you edit the - transaction within the register itself. + Selecting this option displays the transaction form at the + bottom of the register, and that is where you edit a transaction. If the + transaction form is not displayed, you edit a transaction within the + ledger itself. With this option enabled fewer transactions are visible in + the ledger, compared to when it is disabled. Always show a No. field - Always have the number (No.) field when entering or - editing a transaction. + Always display the number (No.) field when entering or + editing a transaction. With this enabled you'll be able to check numbers + entered in this field for checking accounts. Show group header between transactions - Display group headers in the register + Selecting this option will display group headers in the register. + These refer to time periods or milestones such as Last month, Last week, + This month, Last reconciliation, Next week, and Next Month. - Show header for the previous and current fiscal - year + Show header for the previous and current fiscal year + - Display group headers for the current and the previous fiscal - year + Selecting this option will display group headers in the register for + the current and the previous fiscal year. + + + + + Display overdue schedules with planned date + + Scheduled transactions with future dates are shown in the Ledger + View for the relevant account. When the date passes, and a scheduled + transaction becomes overdue, it is no longer shown in the ledger, unless + this option is selected. Sorting tab + + These three sections allow you to set the default sort order of transactions + in the Ledger View. In all cases, the list on the left shows all the fields + available for sorting. To sort on an item, select it (single mouse click) and + click the right arrow between the two lists. The list on the right shows all + the fields which have been selected as part of the sort order for that view. + You can adjust the relative order of the fields by selecting one and adjusting + its position in the list using the up and down arrows to the right of the + list. You can can switch between ascending and descending sort for any item + by double clicking on it. + + + The fields available for sorting are the same for all three sections: Amount, + Category, Entry Order (date the transaction was created,) Number, Payee, Post + date, Reconcile state, Security, and Type. + + Normal view tab - - Choose the sort order of the ledger while in the Normal view. + This tab controls the default sort order of the regular Ledger View. Reconciliation view tab - - Choose the sort order of the ledger while in the Reconciliation view. + This tab controls the default sort order of the ledger while in the + Reconciliation view. This is the display of transactions shown during the + process of Reconciliation Search view tab - Choose the sort order of the ledger while in the Search view. + This tab controls the default sort order of the ledger while in the Search view. Data Entry tab +These options affect the process of creating new transactions. Insert transaction type into No. field for new - transactions + transactions - Automatically place the type of the transaction, ⪚., Deposit, - Withdrawal, etc., in the No. field. + Enabling this will automatically place the type of the transaction, + ⪚, Deposit, Withdrawal, &etc;, in the No. field. Auto increment check number - Have the check number automatically increment whenever entering a - new check transaction. + Enabling this will automatically increment the check number whenever + entering a new check transaction. They have the same effect whether you + use the Ledger Lens or the Transaction Form. Keep changes when selecting a different - transaction/split + transaction/split When enabled, selecting a different transaction or split will save - edits to the current transaction. - When disabled, selecting a different transaction will cancel them. - Instead, you must explicitly accept the changes. + edits to the current transaction. When disabled, selecting a different + transaction split will prompt you whether or not to save any + changes. Use Enter to move between fields - When enabled, pressing Enter will change the focus to the - next field of the transaction form. + + When enabled, pressing &Enter; will change the focus to the next field + of the transaction. When in the last field, the transaction will be + saved. When disabled, navigation between fields would occur by using the + key or the mouse. Pressing the &Enter; key would save the transaction, + no matter which field is currently in focus. + Match names from start - Mark this option if you always want to match names ⪚, for payees - from the start. If unset, any substring is matched + This option controls payee + matching. When enabled, payee names will always be matched from the + start. When disabled, any substring will be matched. Also, when enabled + additional options for matching can be established in the Matching Tab of + the Payees View. + + + Automatic reconciliation + + This option needs to be documented. + + + + Default reconciliation state - Select the default reconciliation state for new - transactions. + Select the default reconciliation state for new transactions: Not + reconciled, Cleared, and Reconciled. Autofill - Select the desired behavior for autofill or deactivate it + This option allows you to select the desired behavior for autofill. + This controls which, if any, previous transactions for the same payee are + displayed, and which, if any, of those transactions are used to auto-fill + details of the new transaction. No Autofill + This will deactivate autofill, and you must manually enter all + details for the new transaction. - Same transaction if amount differs less than X percent - When entering a new transaction with this option, &kmymoney; - will display a list of previous transaction for the payee and - account. If previous transactions differ in less than X percent, - they will be considered identical. If the percentage is 0, all - previous transactions for the payee and account will be - displayed. When selecting a transaction, all transactions details - except the date will be autofilled with those of the selected - transaction. + Same transaction if amount differs less than percentage set below + With this option selected, when you enter a new transaction, + &kmymoney; will display a list of previous transactions for the same + payee and account. If the amount of a previous transactions differ + less than X percent from the amount you have entered, they will be + considered identical, and details from the previous transaction will + be used to autofill details of the new transaction. The "X" above is + specified in the box next to Same transaction if amount + differs less than, and can range from 0 to 100%. If the + percentage is 0, all previous transactions for the payee and account + will be displayed, and you can select one of those transactions to use + for autofill by clicking on it. With previously most often used transaction for the payee - When entering a new transaction with this option enabled, - &kmymoney; will fill in all the transaction details based on the - previously most often used transaction with the selected - payee. + With this option selected, when you enter new transaction, + &kmymoney; will fill in all the transaction details based on the + previously most often used transaction for the selected payee. + + + Use memos from previous transaction + + When selected, the memo from the previously entered transaction will + be automatically entered as the memo for the new transaction. + + + Import tab Match transaction within days Search for matching transactions within the range of the imported - transaction +/- given days + transaction +/- the given number of days, which can be set from 0 to + 99. Ask for a new payee's default category - Whenever a new payee is detected during an import, the user will be - asked for the default category for the payee. + If this option is selected, whenever a new payee is detected during + an import, the user will be asked for the default category for the + payee. -Scheduled Transactions +Scheduled Transactions - These options allow you to modify the behavior of - the scheduling features. + These options allow you to modify the behavior of Scheduling Transactions. - -Schedule startup options - + +Startup options Check schedules upon startup - Enables checking the schedule upon startup of &kmymoney;. + Enables checking the schedule upon startup of &kmymoney;. If + a scheduled transaction matches the schedule criteria set then &kmymoney; + will produce a notification requiring handling of the transaction. Enter transactions this number of days in - advance - Specify how many days in advance should the scheduled - transactions be entered. + advance + Specify how many days in advance a scheduled transaction + should be entered. + + + + + + +Processing Days + + + Use holiday calendar for region + Select the region of the world that best suits your needs. - + + + + + Number of days to preview schedules in ledger - Specify how many days should the schedules show up in the + Specify how many days should the scheduled transactions show up in the ledger. The scheduled transactions will be listed in the ledger. You can enter or skip the scheduled transactions by right-clicking on them. -Online Quotes +Online Quotes - These settings modify how online quotes are retrieved from different online - sources. + These settings modify how financial quotes are retrieved from different online + sources. These include both stock, bond, and other equity prices, as well as + prices for precious metals and currency conversion. This tab allows you to change the online quote sources for &kmymoney;. Generally speaking, this is an advanced feature, and should only be attempted by the most technically adept users. - For more details, please refer to the Online Price - Quotes section. + For more details, please refer to the Online Price Quotes section. - - -Forecast + +Colors - These options allow you to modify how the forecast is calculated. + These section allows you to customize the colors used within &kmymoney;. If + Custom colors is not checked, then default colors will + be used. - - - Number of days to forecast - - Select how many days you want to forecast. The default is 90 - days - - - - - Number of days of account cycle - - The normal number of days that your commonly-used accounts go - through an income-expense cycle. For example, if you receive your salary - on a monthly basis, you should select 30 days here. - - - - - Day of month to start forecast - - Select the day of the month when you want to start the - forecast. This would usually be the day when you receive your salary or - other income. - - - - - Forecast Method - - The forecast method to be used. - Scheduled and future transaction forecasts your balance based on the - information of your scheduled and future transactions. - History-based uses your past transactions to extrapolate a - forecast. - - + + To customize colors check the box Custom colors. + - - Number of cycles to use in forecast - - If using a history-based method, how many previous account cycles - should &kmymoney; use to calculate the forecast. - - + + Modify any of the colors by clicking on the color button, which will open + color select palette. This will provide unlimited options in color selection. + You can select a defined basic color, pick a color by clicking anywhere on the + screen, input an &HTML; defined color, specify values for Hue, Saturation, + Value, Red, Green, and Blue. - - History forecast method - - Select the method to use when calculating a forecast based on - historic information - Simple moving average averages your past transactions to calculate a - forecast. - Weighted moving average averages your past transactions, but it will - give more importance to the most recent transactions. - Linear regression uses a linear-regression formula based on your - past transactions to calculate the forecast - - - + - -Encryption + +Fonts - These options allow you to modify the data encryption settings. + These options allow you to modify the fonts within the Cell or Header fields. - Use GPG encryption - - Enables the use of GPG to encrypt data file. See the section - on GPG Encryption for - more details. - - - - - -GPG Encryption - - - - Your key + Use system fonts - Enter the email address or the hexadecimal key id to use for data - encryption. + Select this to use system fonts. Deselecting this option allows the select + the fonts of your choice. - Additional keys + Cell font - Select additional keys to use for data encryption. + Specify the custom font to use within each cell of the registers. - Also encrypt with kmymoney-recover key + Header font - Encrypts data with the &kmymoney; recover key. Selecting this - option will allow core &kmymoney; developers to decrypt your data file - in case you lose your encryption key. + Specify the custom font to use for the headers of each view within the register. - - - - -Colors - - - These options allow you to change colors within &kmymoney;. - - - -Foreground tab - - - Modify any of the foreground colors by clicking on the color button and - opening a color select palette. - - - - -Background tab - - - Modify any of the background colors by clicking on the color button and - opening a color select palette. - - - - Note the Use system colors checkbox at the top of the - tab. If this is checked, &kmymoney; uses colors provided by the system, such - as those associated with a selected theme, although the details depend on - which operating system and possibly which window system or display manager is - running. Very often, if you experience odd colors or odd combination of - colors, toggling this checkbox will help. - - - -Fonts + +Icons - These options allow you to modify the fonts. + This option allows for the selection of icons that will be displayed. - Use system fonts + Individual icon settings - Select this to use system fonts. Deselecting this option allows you - to customize fonts. - - - - - Cell font - - Specify the custom font to use within each cell. - - - - - Header font - - Specify the custom font to use for the headers of each view. + Using the pull-down menu there is a selection of available icons available for use. + Additional icons may be available for downloading and use depending on the operating + system installed. Plugins - These options allow you to modify the behavior of any installed plugins. Note - that the list of plugins displayed will depend on which plugins &kmymoney; - found when it started. It does not matter whether the plugin was compiled as - part of the application or added as a separate package. + This section provides information about the available plugins, and allow you + to modify the behavior of installed plugins. Note that the list of plugins + displayed will depend on which plugins &kmymoney; found when it started. It + does not matter whether the plugin was compiled as part of the application or + added as a separate package. Configure Plugins Configure Plugins - For most plugins, the only choice is to enable or disable it, as indicated by - the checkbox next to the name of the plugin. In addition, there is also an - information (&infoicon;) button for each, which will - show the plugin version and author. Further information on configuring and - using these plugins may be found in the separate sections for CSV Import and OFX Import plugins. It is also true for - the Reconciliation report, although - that report is only a minor part of the larger reconciliation process. The - one plugin that does require a configuration step is the Print Check plugin. + Every plugin can be enabled or disabled, according to the state of the + checkbox next to the name of the plugin. In addition, at the right, there is + also an information (&infoicon;) button for each, which + will show the plugin version and author. For some plugins, further + information on configuring and using it may be found by clicking the + configure, or tool icon just to the left of the information button. There is + only one plugin that requires configuration, and that is the Check printing + plugin. -Print check plugin +Check printing plugin - This plugin is included with the source of &kmymoney;, and should always be + As one example, there is a plugin for printing checks, which + is included with the source of &kmymoney;, and should always be enabled. It allows printing of a check based on the data from a selected - transaction, with the layout controlled by an html template. You need to use a + transaction, with the layout controlled by an &HTML; template. You need to use a template which is matched to your pre-printed checks. When you click on the - configure (&configicon;) button , the Print - check configuration dialog is displayed. At the top is a text box + configure (&configicon;) button, the Check + printing configuration dialog is displayed. At the top is a text box for the path to the selected template. To the right of that is a button which - brings up a file chooser, to select an alternative template file. + brings up a file chooser, to select an alternate template file. - A template file is an html file, in which specific strings are used to + A template file is an &HTML; file, in which specific strings are used to reference the fields from the transaction and from the current account and institution to show where they will be printed on the check. &kmymoney; is shipped with some sample template files. You should either be able to use one of them, or modify one to suit your needs. When the default template is selected, the configuration dialog will show you the directory which contains the sample files. - + diff --git a/doc/firsttime.docbook b/doc/firsttime.docbook index f984e1932..4556795b8 100644 --- a/doc/firsttime.docbook +++ b/doc/firsttime.docbook @@ -1,1035 +1,1035 @@ &Michael.T.Edwardes; &Michael.T.Edwardes.mail; &Jack.H.Ostroff; &Jack.H.Ostroff.mail; 2018-02-01 5.0.0 Using &kmymoney; for the first time Running &kmymoney; for the first time Once &kmymoney; has fully loaded two windows will be opened. The top window, 'Tip of the Day' offers one of a series of important or useful pieces of information about the application. Tip of the Day Tip of the Day You are recommended to quickly read through these tips by pressing on the Previous and Next buttons at the bottom of the window. Once you have read the tips the window can be closed; it will re-open each time &kmymoney; is started. To stop this default behavior, uncheck the appropriate option on the Tip of the Day window. Tips can be read at anytime by selecting Show tip of the day from the Help menu within &kmymoney;. All tips have been extracted from the Frequently Asked Questions list (FAQ) that provides more detailed help and can be found on the project website and in this manual. Please click on Close to close the Tip of the Day dialog. When you run &kmymoney;, it might not look exactly like the screenshots in this manual. Many details, such as fonts, colors, and icons, can be customized, either using the SettingsConfigure &kmymoney;... dialog or the KDE System Settings application, depending on your operating system. The main window The Main Window The Main Window The &kmymoney; main window consists of four major parts A. The menu bar B. The toolbar C. The View selector D. The view The menu and toolbars provide access to the features of &kmymoney; and allow you to Create files or configure how &kmymoney; operates. - The view selector consists of at least eleven icons in the left side of the - main window, depending upon the software provided by your system. More icons - could be added by various plugins. By clicking on an icon, the view window on - the right hand side is loaded with the appropriate view of your financial - data. Each view provides a unique representation of your financial situation - and allows you to view or edit the information shown. + On the left side of the main window, the view selector consists of at least + eleven icons with optional labels, depending upon the software provided by + your system. More icons could be added by various plugins. By clicking on an + icon, the view window on the right hand side is loaded with the appropriate + view of your financial data. Each view provides a unique representation of + your financial situation and allows you to view or edit the information shown. The view selector may contain the following icons Home View Home: A configurable overview of your current financial situation (or an introduction page when no data file is open). Institutions View Institutions: All of your institutions and accounts displayed in a hierarchy. Accounts View Accounts: All of your accounts displayed in a hierarchy. Schedules View Scheduled Transactions: Your recurring bills and deposits. Categories View Categories: Special accounts that provide a means for you to group associated transactions. Tags View Tags: Special accounts that provide an additional means to Categories for you to group associated transactions. Payees View Payees: All Payees used by &kmymoney;. These are all the people or organizations you pay money to or receive money from. Ledgers View Ledgers: Your account transactions. Investments View Investments: Your portfolio summary. Reports View Reports: A collection of useful reports, providing alternate, customizable views of your financial data. Budgets View Budgets: Your budgets Forecast View Forecast: This view provides a forecast of your accounts and categories Outbox View Outbox: Used by the online banking module to provide feedback on running processes. This view is provided by the KBanking plugin and may not be present on your system. Creating a new file &kmymoney; keeps all the data about your finances in a file. As was stated in Defining the accounts (personal records), you can keep more than one set of accounts, but the data for each will be kept in a separate file, and &kmymoney; can only have one file open at a time. See the chapter on File Formats for more details about &kmymoney;'s data files. To create a new file you can either select FileNew from the menu or choose the New icon from the toolbar. In addition, if the main welcome page is displayed, you can also select the first link: "Get started and setup accounts." &kmymoney; will then open the New File Setup Wizard which will guide you through the process of creating the file. Personal Data Page Personal Data Page Since all fields are optional, you can use &kmymoney; without entering any information now. Any information entered here is used only to personalize your file. In the future, it may be used in some reports and for online banking. If &kmymoney; requires any personal information in future releases and finds none, you will be prompted to enter the relevant data then. Please enter your name and address or nothing and press Next. In case you have an address for yourself stored in the &kaddressbook;, the button Load from Addressbook is enabled and pressing it imports all information into &kmymoney;. &kmymoney; supports multiple currencies. To set your base currency (the currency you use day to day and which will be used for your reports), select the appropriate entry from the list provided and press Next. A default will be selected from &kmymoney; based upon your locale settings. In the following example, US Dollar has been selected as the base currency. Base currency selection Page Base currency selection page The following page allows you to create an initial account. For the typical &kmymoney; user this is a checking account. Checking account creation Page Checking account creation page Enter the name of the account and a possible account or reference number assigned by the institution managing the account. Enter the opening date of the account and the opening balance of the account. For example, you want to use &kmymoney; to manage all your finances as of 2008-01-01, then enter the balance of the account at the beginning of this date. The number can possibly be obtained from a paper statement. Also enter the name and routing number of the institution, though this information is optional and only required for online banking purposes. In case you don't want to setup an initial account, please uncheck the check box. Once finished, press Next to proceed. Account template selection Page Account template selection page Choose the appropriate country and account template(s). Each template provides a different set of categories for organizing your finances. Multiple templates, even from different countries, can be selected using a combination of &Ctrl; and &Shift; together with the left mouse button. The account hierarchy to be setup will be shown in a preview part of the wizard. In case you cannot find a template that suits your needs, don't select a template at this time, and you can manually set up categories later. Once finished with the template selection press Next to proceed. The next page of the wizard allows you to setup user preferences. If you select the checkbox, the application settings dialog will be displayed after you have completed the account setup wizard. Press Next if you are done and want to proceed. Preference Page Preference page The last page of the wizard allows you to select the location and filename that will be used to store your financial data. Initial values will be constructed out of your HOME folder and user name. In case the file exists, a warning will be displayed which reminds you to select a name of a non-existing file. Filename selection Page Filename selection page Now you have entered all relevant information to create the file. Press Finish and &kmymoney; will create the file for you and open it. Creating accounts There are multiple ways you can create an account, which are described here: Using the menu To create an account using the menu select AccountsNew account. Using the Accounts view To create an account using the accounts view click on the Accounts icon in the view selector to display the Accounts View. Right click on the either the Asset tree icon or the Liability tree icon in the right hand view and select New account from the popup menu. The Asset tree holds all of your accounts that are an asset, such as savings and investments. Liabilities are accounts that represent money you owe, such as loans and credit cards. These and all the other account types are discussed in more detail later in this manual. Using the toolbar If the New Account... button is displayed on the toolbar, you can click it to create an account. Note that &kmymoney; uses context menus a lot throughout the whole application. If you find yourself stuck trying to figure out how to do something, try right-clicking where you would expect to see a feature. Creating a new account by either method opens the New Account Wizard. This wizard will take you step by step through the process of creating an account, gathering the required information at each step. To navigate through the pages click on the Next and Back buttons. Create an account using any of the previously mentioned methods to continue. A full description of creating accounts can be found in the Accounts section. Now that one or more accounts exist in &kmymoney;, your account summary is shown in the Home view. You can create transactions manually in the Ledgers view, or set up automatic transactions through the Schedules view. Schedules Sometimes called a recurring transaction, a Schedule is a transaction that occurs one or more times over a certain period of time. A typical example is your pay check that you receive every month or week. Rather than have to enter the transaction details each time you get paid, you can instruct &kmymoney; to create a schedule that will enter a transaction for you when you get paid. To maintain schedules click on the Schedules icon in the view selector. Categories To maintain categories click on the Categories icon in the view selector. A full description of this topic can be found in the Categories section. Tags To maintain tags click on the Tags icon in the view selector. A full description of this topic can be found in the Tags section. Payees To maintain your list of payees click on the Payees icon in the view selector. A full description of this topic can be found in the Payees section. Quicken Interchange Format (QIF) Import If you currently use another financial manager application, &kmymoney; can import an account's transactions as long as they can be exported or saved in a format &kmymoney; understands. The most common of these is the Quicken Interchange Format (QIF). Most financial programs can export this format, although the different applications' methods of exporting QIF is not discussed here for brevity. A full description of this topic can be found in the QIF Import section. Searching for transactions &kmymoney; provides a useful and powerful transaction search facility in the form of the search dialog. The search dialog can be opened from the toolbar using the Find icon or by selecting Find Transaction from the Edit menu. A full description of this topic can be found in the Search section. Reconciliation At some point you will want to reconcile the transactions you have entered with the transactions listed in a statement, be it a bank statement, credit card statement, loan statement, or any other statement. Reconciliation involves matching what you have entered with what the statement lists. If any discrepancies are found then either you or the person who created the statement have made a mistake. To reconcile an account, first select the account, either from Accounts view or from Ledgers view. Then, either click on the Reconcile icon on the Toolbar, or select Reconcile from the Account menu. A full description of this topic can be found in the Reconciliation section. Backing up Now that you have created some accounts and entered transactions it's a good idea to perform a backup. Because the file used by &kmymoney; is a standard Unix file you can just copy the file to another location to back it up or use the dialog provided by &kmymoney;. Using the dialog is the preferred method because if &kmymoney; ever starts using other file formats, such as SQL, then it would be much harder to copy the file manually. To backup your file through &kmymoney; Select Backup from the File menu and enter the folder where you wish the file to be saved. If the folder needs mounting first and you have the sufficient privileges then check the tick box labeled Mount this directory before backing up and click on OK. The file will be saved to the folder specified with the current date appended to the filename so it is easy to see the date the file was backed up. Launching &kmymoney; Most commonly, you will launch &kmymoney; from an icon or a start menu of your window manager. In this case, the default behavior is that &kmymoney; will open the last file you had open. However, in some circumstances, you will want or need to launch &kmymoney; from the command line. For a complete description of all the available command line options, see the man page for &kmymoney;. However, there are two special cases which we describe here. <command>kmymoney -n</command> The -n option tells &kmymoney; not to open the last file it had open. This can be useful if the program crashes on startup. <command>kmymoney <path to file></command> Giving &kmymoney; a filename on the command line tells it to open that file instead of the last file it had open. You can use the name of any valid &kmymoney; file, as described in the chapter on file formats. In addition, this can be used to import a GnuCash file. How to move &kmymoney; to a new computer Moving your data If you get a new computer, it's very easy to move your data. In fact, all you have to do is to copy your &kmymoney; data file. The name of the file is shown in the title bar when &kmymoney; is running, it usually ends in .kmy. The first time you run the application on the new computer you will have to tell it where you have put the data file, either by running from the command line, or from the FileOpen... menu. You should even be able to move between &Windows;, &MacOS;, and &Linux; systems. If you have encrypted your data file, be sure the appropriate software and keys have also been set up on the new computer. If your data is in an SQL database, refer to documentation for your database software for guidance on migrating your data. Note that this assumes you have installed the same version of &kmymoney; on the new computer as you were running on the old computer. Installing a newer version should also be safe, but you need to be aware of any issues related to that upgrade. Any such issues should be noted in the release notes for the new version. Moving your settings If you only move your data file to the new machine, as described in the previous section, &kmymoney; will work correctly, but you will have lost any settings you changed from the default on your old machine. To maintain these settings, you also need to copy your configuration file. This file is called kmymoneyrc. This file will be in a directory with other &kde; application configuration files. On a &Linux; system, this will be in $KDEHOME/share/config where $KDEHOME is usually .config within your home directory. If you are migrating from a version of &kmymoney; prior to 5.0 to 5.0 or later, the old location of $KDEHOME was .kde4. Moving plugin settings If you have run any &kmymoney; plugin, such as the csv importer or exporter, it may also have created a configuration file in that same directory, such as csvimporterrc or csvexporterrc. These should also be copied to the new computer. As with the main data file, this assumes the same version of the plugin on both computers. Upgrades are generally safe, but if the functionality of the plugin has changed greatly, it is advisable to carefully test using the old configuration file, to be sure the plugin behaves as you expect. Note that when upgrading from a previous version to version 5.0 or higher, the location of the template files used by the plugin for printing checks has changed. Please consult the release notes or the project website for more details. These instructions are solely for moving your &kmymoney; data and settings. However, there may be important items which are stored by &kde; and not by &kmymoney;, such as passwords stored in kwallet. Moving other parts of your &kde; setup is beyond the scope of this manual, although many of the relevant files are also stored under $KDEHOME. Contacting the Developers / Reporting Bugs Contacting the developers For general questions or comments about &kmymoney;, there is a users' mailing list &userlist;. (more details here.) In addition, you can contact the developers through their mailing list &devlist;. (more details here.) Since replies are often sent only to the list, you may not get any response unless you subscribe to one of the lists before sending to it. We're happy to hear about your experiences using &kmymoney;. Reporting bugs To report a bug please use the interface provided by &kmymoney; by selecting HelpReport Bug and filling in the required information. However, in case you have any difficulties with that method, you can report a bug (or file a wish-list or enhancement request) directly at the &kde; bug reporting web site. Reports should be filed against the product kmymoney. In any case, before reporting a bug, please read through the guidelines below. Following them will make it more likely that the developers will be able to quickly identify the problem without having to ask you for more information. Writing High Quality Bug Reports We encourage users to take extra time to write high-quality bug reports when submitting them. This reduces wasted effort on all sides, as the reporter and the developer who will fix it trade emails until this information is really in place. Eli Goldberg wrote the canonical Bug-Writing Guideline, available at http://issues.apache.org/bugwritinghelp.html. Generally, you'll be well-served by following these guidelines: Include your system information The HelpReport Bug menu choice will fill this in automatically, but if you would prefer not to use this interface, there are several items you need to include in the report. We will need to know what operating system you are using (&Linux;, &MacOS;, or &Windows;), the version of &kde;, and the version of &kmymoney; you're running, and either the version of &MacOS; or &Windows; or the name and version of your &Linux; distribution and the version of your kernel. Include a backtrace for crashes The backtrace is often the single most useful piece of information in solving crashes. Try to reproduce it Let us know if you were able to do so. It's useful to know whether the bug only happened once and you couldn't get it to happen again or happens every time or happens sometimes but not others. Include specific steps This is a bad bug report: I entered a transaction, and &kmymoney; crashed. A much better bug report is: Using the transaction form, I entered a new transaction in my Credit Card account. I selected the Transfer tab, entered in an amount, then changed to the Deposit tab, and back to the Transfer tab. When I returned to the Transfer tab, &kmymoney; crashed. This bug is fully reproducible for me. Tell us what you expected to happen In some cases, this is obvious, but in more subtle bugs, sometimes you'll see a bug report where it is not obvious what the reporter expected to happen. Always tell us what you think should have happened. Consider running with debugging enabled By compiling a version configured with --enable-debug=full you can ensure that the best possible backtraces are generated. If you encounter a crash, run &kmymoney; again from within gdb, and reproduce the crash. When it crashes, type bt to generate a backtrace. Running this way is a bit more difficult, so we don't expect all users to do this. However, if you're interested in helping &kmymoney; become as stable as possible, this is the best way to do it short of finding the bug in the code and sending in a patch. diff --git a/kmymoney/CMakeLists.txt b/kmymoney/CMakeLists.txt index 2abacf927..5ca45c76b 100644 --- a/kmymoney/CMakeLists.txt +++ b/kmymoney/CMakeLists.txt @@ -1,206 +1,208 @@ include(ECMAddAppIcon) include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/settings/ ${CMAKE_CURRENT_BINARY_DIR}/settings/ ${CMAKE_CURRENT_BINARY_DIR}/dialogs/ ${CMAKE_CURRENT_SOURCE_DIR}/widgets/ ${CMAKE_CURRENT_BINARY_DIR}/widgets/ ${CMAKE_CURRENT_SOURCE_DIR}/mymoney/ ${CMAKE_CURRENT_SOURCE_DIR}/mymoney/storage/ ${CMAKE_CURRENT_SOURCE_DIR}/plugins/ ${CMAKE_CURRENT_BINARY_DIR}/plugins/ ${CMAKE_CURRENT_SOURCE_DIR}/views/ ${CMAKE_CURRENT_SOURCE_DIR}/dialogs/ ${CMAKE_CURRENT_SOURCE_DIR}/converter/ ${CMAKE_CURRENT_BINARY_DIR}/dialogs/settings/ ${CMAKE_CURRENT_BINARY_DIR}/mymoney/storage/ ${CMAKE_CURRENT_BINARY_DIR}/mymoney/ ${CMAKE_CURRENT_SOURCE_DIR}/wizards/endingbalancedlg/ ${CMAKE_CURRENT_BINARY_DIR}/wizards/endingbalancedlg/ ${CMAKE_CURRENT_SOURCE_DIR}/wizards/newinvestmentwizard/ ${CMAKE_CURRENT_BINARY_DIR}/wizards/newinvestmentwizard/ ${CMAKE_CURRENT_SOURCE_DIR}/wizards/newloanwizard/ ${CMAKE_CURRENT_BINARY_DIR}/wizards/newloanwizard/ ${CMAKE_CURRENT_SOURCE_DIR}/wizards/wizardpages/ ${CMAKE_CURRENT_SOURCE_DIR}/models/ ${CMAKE_CURRENT_BINARY_DIR}/models/ ${CMAKE_CURRENT_SOURCE_DIR}/icons/ ${CMAKE_CURRENT_BINARY_DIR}/icons/ ${CMAKE_CURRENT_SOURCE_DIR}/menus/ ${CMAKE_CURRENT_BINARY_DIR}/menus/ + ${CMAKE_CURRENT_SOURCE_DIR}/misc/ + ${CMAKE_CURRENT_BINARY_DIR}/misc/ ) add_subdirectory( mymoney ) add_subdirectory( settings ) add_subdirectory( models ) add_subdirectory( plugins ) add_subdirectory( widgets ) add_subdirectory( dialogs ) add_subdirectory( views ) add_subdirectory( converter ) add_subdirectory( wizards ) add_subdirectory( pics ) add_subdirectory( html ) add_subdirectory( templates ) add_subdirectory( misc ) add_subdirectory( icons ) add_subdirectory( menus ) if(BUILD_TESTING) add_subdirectory( tests ) endif() set( _HEADERS kmymoneyutils.h ) ########### common code (kmymoney_common) STATIC ############### # will be linked into kmymoney, kmymoneytest, and libkmymoney.so set( kmymoney_common_SRCS kmymoneyutils.cpp kstartuplogo.cpp kcreditswindow.cpp ) add_library(kmymoney_common STATIC ${kmymoney_common_SRCS}) target_link_libraries(kmymoney_common PUBLIC Qt5::Core KF5::ConfigGui KF5::WidgetsAddons KF5::KIOCore KF5::KIOFileWidgets KF5::KIOWidgets KF5::KIONTLM Alkimia::alkimia kmm_mymoney kmm_utils_webconnect kmm_utils_platformtools PRIVATE KF5::I18n kmm_settings ) # must build kmymoney/transactionsortoption.h # from transactionsortoption.ui first add_dependencies(kmymoney_common generate_base_ui_srcs kmm_settings) add_dependencies(wizardpages widgets) add_dependencies(dialogs widgets) if(USE_MODELTEST) set( kmymoney_common_LIBS ${kmymoney_common_LIBS} ${QT_QTTEST_LIBRARY}) endif(USE_MODELTEST) # remove these generated files, they are in the way and leftovers from 5.0 if(EXISTS ${CMAKE_CURRENT_BINARY_DIR}/kmymoneysettings.h) file(REMOVE ${CMAKE_CURRENT_BINARY_DIR}/kmymoneysettings.h) endif() if(EXISTS ${CMAKE_CURRENT_BINARY_DIR}/kmymoneysettings.cpp) file(REMOVE ${CMAKE_CURRENT_BINARY_DIR}/kmymoneysettings.cpp) endif() ########### kmymoney executable ############### set( kmymoney_SRCS main.cpp kmymoney.cpp pluginloader.cpp ) qt5_add_dbus_adaptor(kmymoney_SRCS org.kde.kmymoney.xml kmymoney.h KMyMoneyApp) qt5_add_resources(kmymoney_SRCS kmymoney.qrc) # collect application icons file(GLOB_RECURSE KMYMONEY_APP_ICONS "${CMAKE_CURRENT_SOURCE_DIR}/icons/kmymoney/apps/*.png") # add icons to application sources, to have them bundled ecm_add_app_icon(kmymoney_SRCS ICONS ${KMYMONEY_APP_ICONS}) add_executable( kmymoney ${kmymoney_SRCS} ) if (ENABLE_SQLCIPHER) message( STATUS " SQLCIPHER INCLUDE : " ${SQLCIPHER_INCLUDE_DIRS}) #just for testing it on MS Windows target_compile_definitions(kmymoney PRIVATE SQLITE_HAS_CODEC SQLITE_TEMP_STORE=2) #otherwise "fatal error: 'sqlite3.h' file not found" if(CMAKE_SYSTEM_NAME MATCHES "FreeBSD" OR CMAKE_SYSTEM_NAME MATCHES "Windows") target_include_directories(kmymoney PRIVATE ${SQLCIPHER_INCLUDE_DIRS}) endif() endif() target_link_libraries(kmymoney PUBLIC views kmymoney_base kmymoney_common newuserwizard newaccountwizard newinvestmentwizard newloanwizard endingbalancedlg wizardpages dialogs widgets settings converter kmm_models kmm_settings kmm_menus kmm_widgets kmm_mymoney interfaces kmm_plugin Qt5::Core KF5::ConfigGui KF5::WidgetsAddons KF5::CoreAddons $<$:Qt5::Test> $<$:KF5::Holidays> $<$:KF5::Activities> PRIVATE $<$:sqlcipher> ) # own plist magic for mac os if(APPLE) # own plist template set_target_properties(kmymoney PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/MacOSXBundleInfo.plist.in) # the MacOSX bundle display name property (CFBundleDisplayName) is not currently supported by cmake, # so has to be set for all targets in this cmake file set(MACOSX_BUNDLE_DISPLAY_NAME KMyMoney) set_target_properties(kmymoney PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.KMyMoney") set_target_properties(kmymoney PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KMyMoney") set_target_properties(kmymoney PROPERTIES MACOSX_BUNDLE_DISPLAY_NAME "KMyMoney") set_target_properties(kmymoney PROPERTIES MACOSX_BUNDLE_INFO_STRING "KMyMoney - Personal Finances Manager") set_target_properties(kmymoney PROPERTIES MACOSX_BUNDLE_LONG_VERSION_STRING "KMyMoney ${KDE_APPLICATIONS_VERSION}") set_target_properties(kmymoney PROPERTIES MACOSX_BUNDLE_SHORT_VERSION_STRING "${KDE_APPLICATIONS_VERSION_MAJOR}.${KDE_APPLICATIONS_VERSION_MINOR}") set_target_properties(kmymoney PROPERTIES MACOSX_BUNDLE_BUNDLE_VERSION "${KDE_APPLICATIONS_VERSION}") set_target_properties(kmymoney PROPERTIES MACOSX_BUNDLE_COPYRIGHT "2000-2016 The KMyMoney Authors") endif() ########### install files ############### install(TARGETS kmymoney ${INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES ${_HEADERS} DESTINATION ${INCLUDE_INSTALL_DIR}/kmymoney ) install(FILES org.kde.kmymoney.desktop DESTINATION ${XDG_APPS_INSTALL_DIR} ) install(FILES org.kde.kmymoney.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR} ) install(FILES x-kmymoney.xml DESTINATION ${XDG_MIME_INSTALL_DIR}) install(FILES tips DESTINATION ${CMAKE_INSTALL_DATADIR}/kmymoney) #UPDATE_XDG_MIMETYPES(${XDG_MIME_INSTALL_DIR}) diff --git a/kmymoney/dialogs/kmymoneysplittable.cpp b/kmymoney/dialogs/kmymoneysplittable.cpp index 55f4ccfe7..b9faa2497 100644 --- a/kmymoney/dialogs/kmymoneysplittable.cpp +++ b/kmymoney/dialogs/kmymoneysplittable.cpp @@ -1,1095 +1,1137 @@ /* * Copyright 2008-2018 Thomas Baumgart * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "kmymoneysplittable.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyaccount.h" #include "mymoneyfile.h" #include "mymoneyprice.h" #include "kmymoneyedit.h" #include "kmymoneycategory.h" #include "kmymoneyaccountselector.h" #include "kmymoneylineedit.h" #include "mymoneysecurity.h" #include "kmymoneysettings.h" +#include "kmymoneymvccombo.h" +#include "mymoneytag.h" +#include "kmymoneytagcombo.h" +#include "ktagcontainer.h" #include "kcurrencycalculator.h" #include "mymoneyutils.h" #include "mymoneytracer.h" #include "mymoneyexception.h" #include "icons.h" #include "mymoneyenums.h" using namespace Icons; class KMyMoneySplitTablePrivate { Q_DISABLE_COPY(KMyMoneySplitTablePrivate) public: KMyMoneySplitTablePrivate() : m_currentRow(0), m_maxRows(0), m_precision(2), m_contextMenu(nullptr), m_contextMenuDelete(nullptr), m_contextMenuDuplicate(nullptr), m_editCategory(0), + m_editTag(0), m_editMemo(0), m_editAmount(0) { } ~KMyMoneySplitTablePrivate() { } /// the currently selected row (will be printed as selected) int m_currentRow; /// the number of rows filled with data int m_maxRows; MyMoneyTransaction m_transaction; MyMoneyAccount m_account; MyMoneySplit m_split; MyMoneySplit m_hiddenSplit; /** * This member keeps the precision for the values */ int m_precision; /** * This member keeps a pointer to the context menu */ QMenu* m_contextMenu; /// keeps the QAction of the delete entry in the context menu QAction* m_contextMenuDelete; /// keeps the QAction of the duplicate entry in the context menu QAction* m_contextMenuDuplicate; /** * This member contains a pointer to the input widget for the category. * The widget will be created and destroyed dynamically in createInputWidgets() * and destroyInputWidgets(). */ QPointer m_editCategory; + /** + * This member contains a pointer to the tag widget for the memo. + */ + QPointer m_editTag; + /** * This member contains a pointer to the input widget for the memo. * The widget will be created and destroyed dynamically in createInputWidgets() * and destroyInputWidgets(). */ QPointer m_editMemo; /** * This member contains a pointer to the input widget for the amount. * The widget will be created and destroyed dynamically in createInputWidgets() * and destroyInputWidgets(). */ QPointer m_editAmount; /** * This member keeps the tab order for the above widgets */ QWidgetList m_tabOrderWidgets; QPointer m_registerButtonFrame; QPointer m_registerEnterButton; QPointer m_registerCancelButton; QMap m_priceInfo; }; KMyMoneySplitTable::KMyMoneySplitTable(QWidget *parent) : QTableWidget(parent), d_ptr(new KMyMoneySplitTablePrivate) { Q_D(KMyMoneySplitTable); // used for custom coloring with the help of the application's stylesheet setObjectName(QLatin1String("splittable")); // setup the transactions table setRowCount(1); - setColumnCount(3); + setColumnCount(4); QStringList labels; - labels << i18n("Category") << i18n("Memo") << i18n("Amount"); + labels << i18n("Category") << i18n("Memo") << i18n("Tag") << i18n("Amount"); setHorizontalHeaderLabels(labels); setSelectionMode(QAbstractItemView::SingleSelection); setSelectionBehavior(QAbstractItemView::SelectRows); int left, top, right, bottom; getContentsMargins(&left, &top, &right, &bottom); setContentsMargins(0, top, right, bottom); setFont(KMyMoneySettings::listCellFontEx()); setAlternatingRowColors(true); verticalHeader()->hide(); horizontalHeader()->setSectionsMovable(false); horizontalHeader()->setFont(KMyMoneySettings::listHeaderFontEx()); KConfigGroup grp = KSharedConfig::openConfig()->group("SplitTable"); QByteArray columns; columns = grp.readEntry("HeaderState", columns); horizontalHeader()->restoreState(columns); horizontalHeader()->setStretchLastSection(true); setShowGrid(KMyMoneySettings::showGrid()); setEditTriggers(QAbstractItemView::NoEditTriggers); // setup the context menu d->m_contextMenu = new QMenu(this); d->m_contextMenu->setTitle(i18n("Split Options")); d->m_contextMenu->setIcon(Icons::get(Icon::ViewFinancialTransfer)); d->m_contextMenu->addAction(Icons::get(Icon::DocumentEdit), i18n("Edit..."), this, SLOT(slotStartEdit())); d->m_contextMenuDuplicate = d->m_contextMenu->addAction(Icons::get(Icon::EditCopy), i18nc("To duplicate a split", "Duplicate"), this, SLOT(slotDuplicateSplit())); d->m_contextMenuDelete = d->m_contextMenu->addAction(Icons::get(Icon::EditDelete), i18n("Delete..."), this, SLOT(slotDeleteSplit())); connect(this, &QAbstractItemView::clicked, this, static_cast(&KMyMoneySplitTable::slotSetFocus)); connect(this, &KMyMoneySplitTable::transactionChanged, this, &KMyMoneySplitTable::slotUpdateData); installEventFilter(this); } KMyMoneySplitTable::~KMyMoneySplitTable() { Q_D(KMyMoneySplitTable); auto grp = KSharedConfig::openConfig()->group("SplitTable"); QByteArray columns = horizontalHeader()->saveState(); grp.writeEntry("HeaderState", columns); grp.sync(); delete d; } int KMyMoneySplitTable::currentRow() const { Q_D(const KMyMoneySplitTable); return d->m_currentRow; } void KMyMoneySplitTable::setup(const QMap& priceInfo, int precision) { Q_D(KMyMoneySplitTable); d->m_priceInfo = priceInfo; d->m_precision = precision; } bool KMyMoneySplitTable::eventFilter(QObject *o, QEvent *e) { Q_D(KMyMoneySplitTable); // MYMONEYTRACER(tracer); QKeyEvent *k = static_cast(e); bool rc = false; int row = currentRow(); int lines = viewport()->height() / rowHeight(0); if (e->type() == QEvent::KeyPress && !isEditMode()) { rc = true; switch (k->key()) { case Qt::Key_Up: if (row) slotSetFocus(model()->index(row - 1, 0)); break; case Qt::Key_Down: if (row < d->m_transaction.splits().count() - 1) slotSetFocus(model()->index(row + 1, 0)); break; case Qt::Key_Home: slotSetFocus(model()->index(0, 0)); break; case Qt::Key_End: slotSetFocus(model()->index(d->m_transaction.splits().count() - 1, 0)); break; case Qt::Key_PageUp: if (lines) { while (lines-- > 0 && row) --row; slotSetFocus(model()->index(row, 0)); } break; case Qt::Key_PageDown: if (row < d->m_transaction.splits().count() - 1) { while (lines-- > 0 && row < d->m_transaction.splits().count() - 1) ++row; slotSetFocus(model()->index(row, 0)); } break; case Qt::Key_Delete: slotDeleteSplit(); break; case Qt::Key_Return: case Qt::Key_Enter: if (row < d->m_transaction.splits().count() - 1 && KMyMoneySettings::enterMovesBetweenFields()) { slotStartEdit(); } else emit returnPressed(); break; case Qt::Key_Escape: emit escapePressed(); break; case Qt::Key_F2: slotStartEdit(); break; default: rc = true; // duplicate split if (Qt::Key_C == k->key() && Qt::ControlModifier == k->modifiers()) { slotDuplicateSplit(); // new split } else if (Qt::Key_Insert == k->key() && Qt::ControlModifier == k->modifiers()) { slotSetFocus(model()->index(d->m_transaction.splits().count() - 1, 0)); slotStartEdit(); } else if (k->text()[ 0 ].isPrint()) { KMyMoneyCategory* cat = createEditWidgets(false); if (cat) { KMyMoneyLineEdit *le = qobject_cast(cat->lineEdit()); if (le) { // make sure, the widget receives the key again // and does not select the text this time le->setText(k->text()); le->end(false); le->deselect(); le->skipSelectAll(true); le->setFocus(); } } } break; } } else if (e->type() == QEvent::KeyPress && isEditMode()) { bool terminate = true; rc = true; switch (k->key()) { // suppress the F2 functionality to start editing in inline edit mode case Qt::Key_F2: // suppress the cursor movement in inline edit mode case Qt::Key_Up: case Qt::Key_Down: case Qt::Key_PageUp: case Qt::Key_PageDown: break; case Qt::Key_Return: case Qt::Key_Enter: // we cannot call the slot directly, as it destroys the caller of // this method :-( So we let the event handler take care of calling // the respective slot using a timeout. For a KLineEdit derived object // it could be, that at this point the user selected a value from // a completion list. In this case, we close the completion list and // do not end editing of the transaction. if (o->inherits("KLineEdit")) { if (auto le = dynamic_cast(o)) { KCompletionBox* box = le->completionBox(false); if (box && box->isVisible()) { terminate = false; le->completionBox(false)->hide(); } } } // in case we have the 'enter moves focus between fields', we need to simulate // a TAB key when the object 'o' points to the category or memo field. if (KMyMoneySettings::enterMovesBetweenFields()) { - if (o == d->m_editCategory->lineEdit() || o == d->m_editMemo) { + if (o == d->m_editCategory->lineEdit() || o == d->m_editMemo || o == d->m_editTag) { terminate = false; QKeyEvent evt(e->type(), Qt::Key_Tab, k->modifiers(), QString(), k->isAutoRepeat(), k->count()); QApplication::sendEvent(o, &evt); } } if (terminate) { QTimer::singleShot(0, this, SLOT(slotEndEditKeyboard())); } break; case Qt::Key_Escape: // we cannot call the slot directly, as it destroys the caller of // this method :-( So we let the event handler take care of calling // the respective slot using a timeout. QTimer::singleShot(0, this, SLOT(slotCancelEdit())); break; default: rc = false; break; } } else if (e->type() == QEvent::KeyRelease && !isEditMode()) { // for some reason, we only see a KeyRelease event of the Menu key // here. In other locations (e.g. Register::eventFilter()) we see // a KeyPress event. Strange. (ipwizard - 2008-05-10) switch (k->key()) { case Qt::Key_Menu: // if the very last entry is selected, the delete // operation is not available otherwise it is d->m_contextMenuDelete->setEnabled( row < d->m_transaction.splits().count() - 1); d->m_contextMenuDuplicate->setEnabled( row < d->m_transaction.splits().count() - 1); d->m_contextMenu->exec(QCursor::pos()); rc = true; break; default: break; } } // if the event has not been processed here, forward it to // the base class implementation if it's not a key event if (rc == false) { if (e->type() != QEvent::KeyPress && e->type() != QEvent::KeyRelease) { rc = QTableWidget::eventFilter(o, e); } } return rc; } void KMyMoneySplitTable::slotSetFocus(const QModelIndex& index) { slotSetFocus(index, Qt::LeftButton); } void KMyMoneySplitTable::slotSetFocus(const QModelIndex& index, int button) { Q_D(KMyMoneySplitTable); MYMONEYTRACER(tracer); auto row = index.row(); // adjust row to used area if (row > d->m_transaction.splits().count() - 1) row = d->m_transaction.splits().count() - 1; if (row < 0) row = 0; // make sure the row will be on the screen scrollTo(model()->index(row, 0)); if (isEditMode()) { // in edit mode? if (isEditSplitValid() && KMyMoneySettings::focusChangeIsEnter()) endEdit(false/*keyboard driven*/, false/*set focus to next row*/); else slotCancelEdit(); } if (button == Qt::LeftButton) { // left mouse button if (row != currentRow()) { // setup new current row and update visible selection selectRow(row); slotUpdateData(d->m_transaction); } } else if (button == Qt::RightButton) { // context menu is only available when cursor is on // an existing transaction or the first line after this area if (row == index.row()) { // setup new current row and update visible selection selectRow(row); slotUpdateData(d->m_transaction); // if the very last entry is selected, the delete // operation is not available otherwise it is d->m_contextMenuDelete->setEnabled( row < d->m_transaction.splits().count() - 1); d->m_contextMenuDuplicate->setEnabled( row < d->m_transaction.splits().count() - 1); d->m_contextMenu->exec(QCursor::pos()); } } } void KMyMoneySplitTable::mousePressEvent(QMouseEvent* e) { slotSetFocus(indexAt(e->pos()), e->button()); } /* turn off QTable behaviour */ void KMyMoneySplitTable::mouseReleaseEvent(QMouseEvent* /* e */) { } void KMyMoneySplitTable::mouseDoubleClickEvent(QMouseEvent *e) { Q_D(KMyMoneySplitTable); MYMONEYTRACER(tracer); int col = columnAt(e->pos().x()); slotSetFocus(model()->index(rowAt(e->pos().y()), col), e->button()); createEditWidgets(false); QLineEdit* editWidget = 0; //krazy:exclude=qmethods switch (col) { case 0: editWidget = d->m_editCategory->lineEdit(); break; case 1: editWidget = d->m_editMemo; break; case 2: + d->m_editTag->tagCombo()->setFocus(); + break; + + case 3: editWidget = d->m_editAmount->lineedit(); break; default: break; } if (editWidget) { editWidget->setFocus(); editWidget->selectAll(); } } void KMyMoneySplitTable::selectRow(int row) { Q_D(KMyMoneySplitTable); MYMONEYTRACER(tracer); if (row > d->m_maxRows) row = d->m_maxRows; d->m_currentRow = row; QTableWidget::selectRow(row); QList list = getSplits(d->m_transaction); if (row < list.count()) d->m_split = list[row]; else d->m_split = MyMoneySplit(); } void KMyMoneySplitTable::setRowCount(int irows) { QTableWidget::setRowCount(irows); // determine row height according to the edit widgets // we use the category widget as the base QFontMetrics fm(KMyMoneySettings::listCellFontEx()); int height = fm.lineSpacing() + 6; #if 0 // recalculate row height hint KMyMoneyCategory cat; height = qMax(cat.sizeHint().height(), height); #endif verticalHeader()->setUpdatesEnabled(false); for (auto i = 0; i < irows; ++i) verticalHeader()->resizeSection(i, height); verticalHeader()->setUpdatesEnabled(true); } void KMyMoneySplitTable::setTransaction(const MyMoneyTransaction& t, const MyMoneySplit& s, const MyMoneyAccount& acc) { Q_D(KMyMoneySplitTable); MYMONEYTRACER(tracer); d->m_transaction = t; d->m_account = acc; d->m_hiddenSplit = s; selectRow(0); slotUpdateData(d->m_transaction); } MyMoneyTransaction KMyMoneySplitTable::transaction() const { Q_D(const KMyMoneySplitTable); return d->m_transaction; } QList KMyMoneySplitTable::getSplits(const MyMoneyTransaction& t) const { Q_D(const KMyMoneySplitTable); // get list of splits QList list = t.splits(); // and ignore the one that should be hidden QList::Iterator it; for (it = list.begin(); it != list.end(); ++it) { if ((*it).id() == d->m_hiddenSplit.id()) { list.erase(it); break; } } return list; } void KMyMoneySplitTable::slotUpdateData(const MyMoneyTransaction& t) { Q_D(KMyMoneySplitTable); MYMONEYTRACER(tracer); unsigned long numRows = 0; QTableWidgetItem* textItem; QList list = getSplits(t); updateTransactionTableSize(); // fill the part that is used by transactions QList::Iterator it; for (it = list.begin(); it != list.end(); ++it) { QString colText; MyMoneyMoney value = (*it).value(); if (!(*it).accountId().isEmpty()) { try { colText = MyMoneyFile::instance()->accountToCategory((*it).accountId()); } catch (const MyMoneyException &) { qDebug("Unexpected exception in KMyMoneySplitTable::slotUpdateData()"); } } QString amountTxt = value.formatMoney(d->m_account.fraction()); if (value == MyMoneyMoney::autoCalc) { amountTxt = i18n("will be calculated"); } if (colText.isEmpty() && (*it).memo().isEmpty() && value.isZero()) amountTxt.clear(); unsigned width = fontMetrics().width(amountTxt); KMyMoneyEdit* valfield = new KMyMoneyEdit(); valfield->setMinimumWidth(width); width = valfield->minimumSizeHint().width(); delete valfield; textItem = item(numRows, 0); if (textItem) textItem->setText(colText); else setItem(numRows, 0, new QTableWidgetItem(colText)); textItem = item(numRows, 1); if (textItem) textItem->setText((*it).memo()); else setItem(numRows, 1, new QTableWidgetItem((*it).memo())); - textItem = item(numRows, 2); + QList tl = (*it).tagIdList(); + QStringList tagNames; + if (!tl.isEmpty()) { + for (int i = 0; i < tl.size(); i++) + tagNames.append(MyMoneyFile::instance()->tag(tl[i]).name()); + } + setItem(numRows, 2, new QTableWidgetItem(tagNames.join(", "))); + + textItem = item(numRows, 3); if (textItem) textItem->setText(amountTxt); else - setItem(numRows, 2, new QTableWidgetItem(amountTxt)); + setItem(numRows, 3, new QTableWidgetItem(amountTxt)); - item(numRows, 2)->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); + item(numRows, 3)->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); ++numRows; } // now clean out the remainder of the table while (numRows < static_cast(rowCount())) { - for (auto i = 0 ; i < 3; ++i) { + for (auto i = 0 ; i < 4; ++i) { textItem = item(numRows, i); if (textItem) textItem->setText(""); else setItem(numRows, i, new QTableWidgetItem("")); } - item(numRows, 2)->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); + item(numRows, 3)->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); ++numRows; } } void KMyMoneySplitTable::updateTransactionTableSize() { Q_D(KMyMoneySplitTable); // get current size of transactions table int tableHeight = height(); int splitCount = d->m_transaction.splits().count() - 1; if (splitCount < 0) splitCount = 0; // see if we need some extra lines to fill the current size with the grid int numExtraLines = (tableHeight / rowHeight(0)) - splitCount; if (numExtraLines < 2) numExtraLines = 2; setRowCount(splitCount + numExtraLines); d->m_maxRows = splitCount; } void KMyMoneySplitTable::resizeEvent(QResizeEvent* ev) { QTableWidget::resizeEvent(ev); if (!isEditMode()) { // update the size of the transaction table only if a split is not being edited // otherwise the height of the editors would be altered in an undesired way updateTransactionTableSize(); } } void KMyMoneySplitTable::slotDuplicateSplit() { Q_D(KMyMoneySplitTable); MYMONEYTRACER(tracer); QList list = getSplits(d->m_transaction); if (d->m_currentRow < list.count()) { MyMoneySplit split = list[d->m_currentRow]; split.clearId(); try { d->m_transaction.addSplit(split); emit transactionChanged(d->m_transaction); } catch (const MyMoneyException &e) { qDebug("Cannot duplicate split: %s", e.what()); } } } void KMyMoneySplitTable::slotDeleteSplit() { Q_D(KMyMoneySplitTable); MYMONEYTRACER(tracer); QList list = getSplits(d->m_transaction); if (d->m_currentRow < list.count()) { if (KMessageBox::warningContinueCancel(this, i18n("You are about to delete the selected split. " "Do you really want to continue?"), i18n("KMyMoney") ) == KMessageBox::Continue) { try { d->m_transaction.removeSplit(list[d->m_currentRow]); // if we removed the last split, select the previous if (d->m_currentRow && d->m_currentRow == list.count() - 1) selectRow(d->m_currentRow - 1); else selectRow(d->m_currentRow); emit transactionChanged(d->m_transaction); } catch (const MyMoneyException &e) { qDebug("Cannot remove split: %s", e.what()); } } } } KMyMoneyCategory* KMyMoneySplitTable::slotStartEdit() { MYMONEYTRACER(tracer); return createEditWidgets(true); } void KMyMoneySplitTable::slotEndEdit() { endEdit(false); } void KMyMoneySplitTable::slotEndEditKeyboard() { endEdit(true); } void KMyMoneySplitTable::endEdit(bool keyboardDriven, bool setFocusToNextRow) { Q_D(KMyMoneySplitTable); auto file = MyMoneyFile::instance(); MYMONEYTRACER(tracer); MyMoneySplit s1 = d->m_split; if (!isEditSplitValid()) { KMessageBox::information(this, i18n("You need to assign a category to this split before it can be entered."), i18n("Enter split"), "EnterSplitWithEmptyCategory"); d->m_editCategory->setFocus(); return; } bool needUpdate = false; if (d->m_editCategory->selectedItem() != d->m_split.accountId()) { s1.setAccountId(d->m_editCategory->selectedItem()); needUpdate = true; } if (d->m_editMemo->text() != d->m_split.memo()) { s1.setMemo(d->m_editMemo->text()); needUpdate = true; } + if (d->m_editTag->selectedTags() != d->m_split.tagIdList()) { + s1.setTagIdList(d->m_editTag->selectedTags()); + needUpdate = true; + } if (d->m_editAmount->value() != d->m_split.value()) { s1.setValue(d->m_editAmount->value()); needUpdate = true; } if (needUpdate) { if (!s1.value().isZero()) { MyMoneyAccount cat = file->account(s1.accountId()); if (cat.currencyId() != d->m_transaction.commodity()) { MyMoneySecurity fromCurrency, toCurrency; MyMoneyMoney fromValue, toValue; fromCurrency = file->security(d->m_transaction.commodity()); toCurrency = file->security(cat.currencyId()); // determine the fraction required for this category int fract = toCurrency.smallestAccountFraction(); if (cat.accountType() == eMyMoney::Account::Type::Cash) fract = toCurrency.smallestCashFraction(); // display only positive values to the user fromValue = s1.value().abs(); // if we had a price info in the beginning, we use it here if (d->m_priceInfo.find(cat.currencyId()) != d->m_priceInfo.end()) { toValue = (fromValue * d->m_priceInfo[cat.currencyId()]).convert(fract); } // if the shares are still 0, we need to change that if (toValue.isZero()) { const MyMoneyPrice &price = MyMoneyFile::instance()->price(fromCurrency.id(), toCurrency.id()); // if the price is valid calculate the shares. If it is invalid // assume a conversion rate of 1.0 if (price.isValid()) { toValue = (price.rate(toCurrency.id()) * fromValue).convert(fract); } else { toValue = fromValue; } } // now present all that to the user QPointer calc = new KCurrencyCalculator(fromCurrency, toCurrency, fromValue, toValue, d->m_transaction.postDate(), fract, this); if (calc->exec() == QDialog::Rejected) { delete calc; return; } else { s1.setShares((s1.value() * calc->price()).convert(fract)); delete calc; } } else { s1.setShares(s1.value()); } } else s1.setShares(s1.value()); d->m_split = s1; try { if (d->m_split.id().isEmpty()) { d->m_transaction.addSplit(d->m_split); } else { d->m_transaction.modifySplit(d->m_split); } emit transactionChanged(d->m_transaction); } catch (const MyMoneyException &e) { qDebug("Cannot add/modify split: %s", e.what()); } } this->setFocus(); destroyEditWidgets(); if (setFocusToNextRow) { slotSetFocus(model()->index(currentRow() + 1, 0)); } // if we still have more splits, we start editing right away // in case we have selected 'enter moves between fields' if (keyboardDriven && currentRow() < d->m_transaction.splits().count() - 1 && KMyMoneySettings::enterMovesBetweenFields()) { slotStartEdit(); } } void KMyMoneySplitTable::slotCancelEdit() { Q_D(const KMyMoneySplitTable); MYMONEYTRACER(tracer); if (isEditMode()) { /* * Prevent asking to add a new category which happens if the user entered any text * caused by emitting signals in KMyMoneyCombo::focusOutEvent() on focus out event. * (see bug 344409) */ if (d->m_editCategory) d->m_editCategory->lineEdit()->setText(QString()); destroyEditWidgets(); this->setFocus(); } } bool KMyMoneySplitTable::isEditMode() const { Q_D(const KMyMoneySplitTable); // while the edit widgets exist we're in edit mode - return d->m_editAmount || d->m_editMemo || d->m_editCategory; + return d->m_editAmount || d->m_editMemo || d->m_editCategory || d->m_editTag; } bool KMyMoneySplitTable::isEditSplitValid() const { Q_D(const KMyMoneySplitTable); - return isEditMode() && !d->m_editCategory->selectedItem().isEmpty(); + return isEditMode() && !(d->m_editCategory && d->m_editCategory->selectedItem().isEmpty()); } void KMyMoneySplitTable::destroyEditWidgets() { MYMONEYTRACER(tracer); Q_D(KMyMoneySplitTable); emit editFinished(); disconnect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, this, &KMyMoneySplitTable::slotLoadEditWidgets); destroyEditWidget(d->m_currentRow, 0); destroyEditWidget(d->m_currentRow, 1); destroyEditWidget(d->m_currentRow, 2); + destroyEditWidget(d->m_currentRow, 3); destroyEditWidget(d->m_currentRow + 1, 0); } void KMyMoneySplitTable::destroyEditWidget(int r, int c) { if (QWidget* cw = cellWidget(r, c)) cw->hide(); removeCellWidget(r, c); } KMyMoneyCategory* KMyMoneySplitTable::createEditWidgets(bool setFocus) { MYMONEYTRACER(tracer); emit editStarted(); Q_D(KMyMoneySplitTable); auto cellFont = KMyMoneySettings::listCellFontEx(); d->m_tabOrderWidgets.clear(); // create the widgets d->m_editAmount = new KMyMoneyEdit(0); d->m_editAmount->setFont(cellFont); d->m_editAmount->setResetButtonVisible(false); d->m_editAmount->setPrecision(d->m_precision); d->m_editCategory = new KMyMoneyCategory(); d->m_editCategory->setPlaceholderText(i18n("Category")); d->m_editCategory->setFont(cellFont); connect(d->m_editCategory, SIGNAL(createItem(QString,QString&)), this, SIGNAL(createCategory(QString,QString&))); connect(d->m_editCategory, SIGNAL(objectCreation(bool)), this, SIGNAL(objectCreation(bool))); d->m_editMemo = new KMyMoneyLineEdit(0, false, Qt::AlignLeft | Qt::AlignVCenter); d->m_editMemo->setPlaceholderText(i18n("Memo")); d->m_editMemo->setFont(cellFont); + d->m_editTag = new KTagContainer; + d->m_editTag->tagCombo()->setPlaceholderText(i18n("Tag")); + d->m_editTag->tagCombo()->setFont(cellFont); + d->m_editTag->loadTags(MyMoneyFile::instance()->tagList()); + connect(d->m_editTag->tagCombo(), SIGNAL(createItem(QString,QString&)), this, SIGNAL(createTag(QString,QString&))); + connect(d->m_editTag->tagCombo(), SIGNAL(objectCreation(bool)), this, SIGNAL(objectCreation(bool))); + // create buttons for the mouse users d->m_registerButtonFrame = new QFrame(this); d->m_registerButtonFrame->setContentsMargins(0, 0, 0, 0); d->m_registerButtonFrame->setAutoFillBackground(true); QHBoxLayout* l = new QHBoxLayout(d->m_registerButtonFrame); l->setContentsMargins(0, 0, 0, 0); l->setSpacing(0); d->m_registerEnterButton = new QPushButton(Icons::get(Icon::DialogOK) , QString(), d->m_registerButtonFrame); d->m_registerCancelButton = new QPushButton(Icons::get(Icon::DialogCancel) , QString(), d->m_registerButtonFrame); l->addWidget(d->m_registerEnterButton); l->addWidget(d->m_registerCancelButton); l->addStretch(2); connect(d->m_registerEnterButton.data(), &QAbstractButton::clicked, this, &KMyMoneySplitTable::slotEndEdit); connect(d->m_registerCancelButton.data(), &QAbstractButton::clicked, this, &KMyMoneySplitTable::slotCancelEdit); // setup tab order addToTabOrder(d->m_editCategory); addToTabOrder(d->m_editMemo); + addToTabOrder(d->m_editTag); addToTabOrder(d->m_editAmount); addToTabOrder(d->m_registerEnterButton); addToTabOrder(d->m_registerCancelButton); if (!d->m_split.accountId().isEmpty()) { d->m_editCategory->setSelectedItem(d->m_split.accountId()); } else { // check if the transaction is balanced or not. If not, // assign the remainder to the amount. MyMoneyMoney diff; QList list = d->m_transaction.splits(); QList::ConstIterator it_s; for (it_s = list.constBegin(); it_s != list.constEnd(); ++it_s) { if (!(*it_s).accountId().isEmpty()) diff += (*it_s).value(); } d->m_split.setValue(-diff); } + QList t = d->m_split.tagIdList(); + if (!t.isEmpty()) { + for (int i = 0; i < t.size(); i++) + d->m_editTag->addTagWidget(t[i]); + } + d->m_editMemo->loadText(d->m_split.memo()); // don't allow automatically calculated values to be modified if (d->m_split.value() == MyMoneyMoney::autoCalc) { d->m_editAmount->setEnabled(false); d->m_editAmount->loadText("will be calculated"); } else d->m_editAmount->setValue(d->m_split.value()); setCellWidget(d->m_currentRow, 0, d->m_editCategory); setCellWidget(d->m_currentRow, 1, d->m_editMemo); - setCellWidget(d->m_currentRow, 2, d->m_editAmount); + setCellWidget(d->m_currentRow, 2, d->m_editTag); + setCellWidget(d->m_currentRow, 3, d->m_editAmount); setCellWidget(d->m_currentRow + 1, 0, d->m_registerButtonFrame); // load e.g. the category widget with the account list slotLoadEditWidgets(); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, this, &KMyMoneySplitTable::slotLoadEditWidgets); foreach (QWidget* w, d->m_tabOrderWidgets) { if (w) { w->installEventFilter(this); } } if (setFocus) { d->m_editCategory->lineEdit()->setFocus(); d->m_editCategory->lineEdit()->selectAll(); } // resize the rows so the added edit widgets would fit appropriately resizeRowsToContents(); return d->m_editCategory; } void KMyMoneySplitTable::slotLoadEditWidgets() { Q_D(KMyMoneySplitTable); // reload category widget auto categoryId = d->m_editCategory->selectedItem(); AccountSet aSet; aSet.addAccountGroup(eMyMoney::Account::Type::Asset); aSet.addAccountGroup(eMyMoney::Account::Type::Liability); aSet.addAccountGroup(eMyMoney::Account::Type::Income); aSet.addAccountGroup(eMyMoney::Account::Type::Expense); if (KMyMoneySettings::expertMode()) aSet.addAccountGroup(eMyMoney::Account::Type::Equity); // remove the accounts with invalid types at this point aSet.removeAccountType(eMyMoney::Account::Type::CertificateDep); aSet.removeAccountType(eMyMoney::Account::Type::Investment); aSet.removeAccountType(eMyMoney::Account::Type::Stock); aSet.removeAccountType(eMyMoney::Account::Type::MoneyMarket); aSet.load(d->m_editCategory->selector()); // if an account is specified then remove it from the widget so that the user // cannot create a transfer with from and to account being the same account if (!d->m_account.id().isEmpty()) d->m_editCategory->selector()->removeItem(d->m_account.id()); if (!categoryId.isEmpty()) d->m_editCategory->setSelectedItem(categoryId); } void KMyMoneySplitTable::addToTabOrder(QWidget* w) { Q_D(KMyMoneySplitTable); if (w) { while (w->focusProxy()) w = w->focusProxy(); d->m_tabOrderWidgets.append(w); } } bool KMyMoneySplitTable::focusNextPrevChild(bool next) { MYMONEYTRACER(tracer); Q_D(KMyMoneySplitTable); auto rc = false; if (isEditMode()) { QWidget *w = 0; w = qApp->focusWidget(); int currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); while (w && currentWidgetIndex == -1) { // qDebug("'%s' not in list, use parent", w->className()); w = w->parentWidget(); currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); } if (currentWidgetIndex != -1) { // if(w) qDebug("tab order is at '%s'", w->className()); currentWidgetIndex += next ? 1 : -1; if (currentWidgetIndex < 0) currentWidgetIndex = d->m_tabOrderWidgets.size() - 1; else if (currentWidgetIndex >= d->m_tabOrderWidgets.size()) currentWidgetIndex = 0; w = d->m_tabOrderWidgets[currentWidgetIndex]; if (((w->focusPolicy() & Qt::TabFocus) == Qt::TabFocus) && w->isVisible() && w->isEnabled()) { // qDebug("Selecting '%s' as focus", w->className()); w->setFocus(); rc = true; } } } else rc = QTableWidget::focusNextPrevChild(next); return rc; } diff --git a/kmymoney/dialogs/kmymoneysplittable.h b/kmymoney/dialogs/kmymoneysplittable.h index f35835bbd..0409d09ff 100644 --- a/kmymoney/dialogs/kmymoneysplittable.h +++ b/kmymoney/dialogs/kmymoneysplittable.h @@ -1,204 +1,214 @@ /* * Copyright 2008-2018 Thomas Baumgart * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef KMYMONEYSPLITTABLE_H #define KMYMONEYSPLITTABLE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class KMyMoneyCategory; class MyMoneyMoney; class MyMoneySplit; class MyMoneyTransaction; class MyMoneyAccount; +class KTagContainer; template class QMap; /** * @author Thomas Baumgart */ class KMyMoneySplitTablePrivate; class KMyMoneySplitTable : public QTableWidget { Q_OBJECT Q_DISABLE_COPY(KMyMoneySplitTable) public: explicit KMyMoneySplitTable(QWidget *parent = nullptr); ~KMyMoneySplitTable(); /** * This method is used to load the widget with the information about * the transaction @p t. The split referencing the account @p acc is * not shown in the widget. * * @param t reference to transaction to be shown/modified * @param s reference to split that is to be hidden * @param acc reference to account */ void setTransaction(const MyMoneyTransaction& t, const MyMoneySplit& s, const MyMoneyAccount& acc); /** * This method is used to retrieve the transaction from the widget. */ MyMoneyTransaction transaction() const; /** * Returns a list of MyMoneySplit objects. It contains all but the one * referencing the account passed in setTransaction(). * * @param t reference to transaction * @return list of splits */ QList getSplits(const MyMoneyTransaction& t) const; void setup(const QMap& priceInfo, int precision); int currentRow() const; protected: void mousePressEvent(QMouseEvent* e) override; void mouseReleaseEvent(QMouseEvent* e) override; void mouseDoubleClickEvent(QMouseEvent* e) override; bool eventFilter(QObject *o, QEvent *e) override; void resizeEvent(QResizeEvent*) override; KMyMoneyCategory* createEditWidgets(bool setFocus); void destroyEditWidgets(); void destroyEditWidget(int r, int c); /** * This method handles the focus of the keyboard. When in edit mode * (m_editCategory widget is visible) the keyboard focus is handled * according to the widgets that are referenced in m_tabOrderWidgets. * If not in edit mode, the base class functionality is provided. * * @param next true if forward-tab, false if backward-tab was * pressed by the user */ bool focusNextPrevChild(bool next) override; void addToTabOrder(QWidget* w); void updateTransactionTableSize(); /** * This method returns the current state of the inline editing mode * * @return true if inline edit mode is on, false otherwise */ bool isEditMode() const; /** * This method returns true if the currently edited split is valid * and can be entered. * * @return true if the split can be entered, false otherwise */ bool isEditSplitValid() const; void endEdit(bool keyboardDriven, bool setFocusToNextRow = true); public Q_SLOTS: /** No descriptions */ virtual void setRowCount(int r); void selectRow(int row); KMyMoneyCategory* slotStartEdit(); void slotEndEdit(); void slotEndEditKeyboard(); void slotDeleteSplit(); void slotCancelEdit(); void slotDuplicateSplit(); protected Q_SLOTS: /// move the focus to the selected @p row. void slotSetFocus(const QModelIndex& index); void slotSetFocus(const QModelIndex& index, int button); /** * Calling this slot refills the widget with the data * passed in the argument @p t. * * @param t reference to transaction data */ void slotUpdateData(const MyMoneyTransaction& t); void slotLoadEditWidgets(); Q_SIGNALS: /** * This signal is emitted whenever the widget goes into edit mode. */ void editStarted(); /** * This signal is emitted whenever the widget ends edit mode. */ void editFinished(); /** * This signal is emitted whenever the return key is pressed * and the widget is not in edit mode. */ void escapePressed(); /** * This signal is emitted whenever the return key is pressed * and the widget is not in edit mode. */ void returnPressed(); /** * This signal is emitted whenever the transaction data has been changed * * @param t modified transaction data */ void transactionChanged(const MyMoneyTransaction& t); /** * This signal is sent out, when a new category needs to be created * @sa KMyMoneyCombo::createItem() * * @param txt The name of the category to be created * @param id A connected slot should store the id of the created object in this variable */ void createCategory(const QString& txt, QString& id); + /** + * This signal is sent out, when a new tag needs to be created + * @sa KMyMoneyCombo::createItem() + * + * @param txt The name of the tag to be created + * @param id A connected slot should store the id of the created object in this variable + */ + void createTag(const QString& txt, QString& id); + /** * Signal is emitted, if any of the widgets enters (@a state equals @a true) * or leaves (@a state equals @a false) object creation mode. * * @param state Enter (@a true) or leave (@a false) object creation */ void objectCreation(bool state); private: KMyMoneySplitTablePrivate * const d_ptr; Q_DECLARE_PRIVATE(KMyMoneySplitTable) }; #endif diff --git a/kmymoney/dialogs/ksplittransactiondlg.cpp b/kmymoney/dialogs/ksplittransactiondlg.cpp index 0ebb237d8..d9c795a2d 100644 --- a/kmymoney/dialogs/ksplittransactiondlg.cpp +++ b/kmymoney/dialogs/ksplittransactiondlg.cpp @@ -1,567 +1,574 @@ /* * Copyright 2002 Michael Edwardes * Copyright 2002-2011 Thomas Baumgart * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ksplittransactiondlg.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_ksplittransactiondlg.h" #include "ui_ksplitcorrectiondlg.h" +#include "kmymoneyutils.h" #include "mymoneyfile.h" #include "kmymoneysplittable.h" #include "mymoneymoney.h" #include "mymoneyexception.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "icons/icons.h" using namespace Icons; KSplitCorrectionDlg::KSplitCorrectionDlg(QWidget *parent) : QDialog(parent), ui(new Ui::KSplitCorrectionDlg) { ui->setupUi(this); } KSplitCorrectionDlg::~KSplitCorrectionDlg() { delete ui; } class KSplitTransactionDlgPrivate { Q_DISABLE_COPY(KSplitTransactionDlgPrivate) Q_DECLARE_PUBLIC(KSplitTransactionDlg) public: explicit KSplitTransactionDlgPrivate(KSplitTransactionDlg *qq) : q_ptr(qq), ui(new Ui::KSplitTransactionDlg), m_buttonBox(nullptr), m_precision(2), m_amountValid(false), m_isDeposit(false) { } ~KSplitTransactionDlgPrivate() { delete ui; } void init(const MyMoneyTransaction& t, const QMap& priceInfo) { Q_Q(KSplitTransactionDlg); ui->setupUi(q); q->setModal(true); auto okButton = ui->buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); auto user1Button = new QPushButton; ui->buttonBox->addButton(user1Button, QDialogButtonBox::ActionRole); auto user2Button = new QPushButton; ui->buttonBox->addButton(user2Button, QDialogButtonBox::ActionRole); auto user3Button = new QPushButton; ui->buttonBox->addButton(user3Button, QDialogButtonBox::ActionRole); //set custom buttons //clearAll button user1Button->setText(i18n("Clear &All")); user1Button->setToolTip(i18n("Clear all splits")); user1Button->setWhatsThis(i18n("Use this to clear all splits of this transaction")); user1Button->setIcon(Icons::get(Icon::EditClear)); //clearZero button user2Button->setText(i18n("Clear &Zero")); user2Button->setToolTip(i18n("Removes all splits that have a value of zero")); user2Button->setIcon(Icons::get(Icon::EditClear)); //merge button user3Button->setText(i18n("&Merge")); user3Button->setToolTip(i18n("Merges splits with the same category to one split")); user3Button->setWhatsThis(i18n("In case you have multiple split entries to the same category and you like to keep them as a single split")); // make finish the default ui->buttonBox->button(QDialogButtonBox::Cancel)->setDefault(true); // setup the focus ui->buttonBox->button(QDialogButtonBox::Cancel)->setFocusPolicy(Qt::NoFocus); okButton->setFocusPolicy(Qt::NoFocus); user1Button->setFocusPolicy(Qt::NoFocus); // q->connect signals with slots q->connect(ui->transactionsTable, &KMyMoneySplitTable::transactionChanged, q, &KSplitTransactionDlg::slotSetTransaction); q->connect(ui->transactionsTable, &KMyMoneySplitTable::createCategory, q, &KSplitTransactionDlg::slotCreateCategory); + q->connect(ui->transactionsTable, &KMyMoneySplitTable::createTag, q, &KSplitTransactionDlg::slotCreateTag); q->connect(ui->transactionsTable, &KMyMoneySplitTable::objectCreation, q, &KSplitTransactionDlg::objectCreation); q->connect(ui->transactionsTable, &KMyMoneySplitTable::returnPressed, q, &KSplitTransactionDlg::accept); q->connect(ui->transactionsTable, &KMyMoneySplitTable::escapePressed, q, &KSplitTransactionDlg::reject); q->connect(ui->transactionsTable, &KMyMoneySplitTable::editStarted, q, &KSplitTransactionDlg::slotEditStarted); q->connect(ui->transactionsTable, &KMyMoneySplitTable::editFinished, q, &KSplitTransactionDlg::slotUpdateButtons); q->connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QAbstractButton::clicked, q, &KSplitTransactionDlg::reject); q->connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked, q, &KSplitTransactionDlg::accept); q->connect(user1Button, &QAbstractButton::clicked, q, &KSplitTransactionDlg::slotClearAllSplits); q->connect(user3Button, &QAbstractButton::clicked, q, &KSplitTransactionDlg::slotMergeSplits); q->connect(user2Button, &QAbstractButton::clicked, q, &KSplitTransactionDlg::slotClearUnusedSplits); // setup the precision try { auto currency = MyMoneyFile::instance()->currency(t.commodity()); m_precision = MyMoneyMoney::denomToPrec(m_account.fraction(currency)); } catch (const MyMoneyException &) { } q->slotSetTransaction(t); // pass on those vars ui->transactionsTable->setup(priceInfo, m_precision); QSize size(q->width(), q->height()); KConfigGroup grp = KSharedConfig::openConfig()->group("SplitTransactionEditor"); size = grp.readEntry("Geometry", size); size.setHeight(size.height() - 1); q->resize(size.expandedTo(q->minimumSizeHint())); // Trick: it seems, that the initial sizing of the dialog does // not work correctly. At least, the columns do not get displayed // correct. Reason: the return value of ui->transactionsTable->visibleWidth() // is incorrect. If the widget is visible, resizing works correctly. // So, we let the dialog show up and resize it then. It's not really // clean, but the only way I got the damned thing working. QTimer::singleShot(10, q, SLOT(initSize())); } /** * This method updates the display of the sums below the register */ void updateSums() { Q_Q(KSplitTransactionDlg); MyMoneyMoney splits(q->splitsValue()); if (m_amountValid == false) { m_split.setValue(-splits); m_transaction.modifySplit(m_split); } ui->splitSum->setText("" + splits.formatMoney(QString(), m_precision) + ' '); ui->splitUnassigned->setText("" + q->diffAmount().formatMoney(QString(), m_precision) + ' '); ui->transactionAmount->setText("" + (-m_split.value()).formatMoney(QString(), m_precision) + ' '); } KSplitTransactionDlg *q_ptr; Ui::KSplitTransactionDlg *ui; QDialogButtonBox *m_buttonBox; /** * This member keeps a copy of the current selected transaction */ MyMoneyTransaction m_transaction; /** * This member keeps a copy of the currently selected account */ MyMoneyAccount m_account; /** * This member keeps a copy of the currently selected split */ MyMoneySplit m_split; /** * This member keeps the precision for the values */ int m_precision; /** * flag that shows that the amount specified in the constructor * should be used as fix value (true) or if it can be changed (false) */ bool m_amountValid; /** * This member keeps track if the current transaction is of type * deposit (true) or withdrawal (false). */ bool m_isDeposit; /** * This member keeps the amount that will be assigned to all the * splits that are marked 'will be calculated'. */ MyMoneyMoney m_calculatedValue; }; KSplitTransactionDlg::KSplitTransactionDlg(const MyMoneyTransaction& t, const MyMoneySplit& s, const MyMoneyAccount& acc, const bool amountValid, const bool deposit, const MyMoneyMoney& calculatedValue, const QMap& priceInfo, QWidget* parent) : QDialog(parent), d_ptr(new KSplitTransactionDlgPrivate(this)) { Q_D(KSplitTransactionDlg); d->ui->buttonBox = nullptr; d->m_account = acc; d->m_split = s; d->m_precision = 2; d->m_amountValid = amountValid; d->m_isDeposit = deposit; d->m_calculatedValue = calculatedValue; d->init(t, priceInfo); } KSplitTransactionDlg::~KSplitTransactionDlg() { Q_D(KSplitTransactionDlg); auto grp = KSharedConfig::openConfig()->group("SplitTransactionEditor"); grp.writeEntry("Geometry", size()); delete d; } int KSplitTransactionDlg::exec() { Q_D(KSplitTransactionDlg); // for deposits, we invert the sign of all splits. // don't forget to revert when we're done ;-) if (d->m_isDeposit) { for (auto i = 0; i < d->m_transaction.splits().count(); ++i) { MyMoneySplit split = d->m_transaction.splits()[i]; split.setValue(-split.value()); split.setShares(-split.shares()); d->m_transaction.modifySplit(split); } } int rc; do { d->ui->transactionsTable->setFocus(); // initialize the display d->ui->transactionsTable->setTransaction(d->m_transaction, d->m_split, d->m_account); d->updateSums(); rc = QDialog::exec(); if (rc == Accepted) { if (!diffAmount().isZero()) { QPointer corrDlg = new KSplitCorrectionDlg(this); connect(corrDlg->ui->buttonBox, &QDialogButtonBox::accepted, corrDlg.data(), &QDialog::accept); connect(corrDlg->ui->buttonBox, &QDialogButtonBox::rejected, corrDlg.data(), &QDialog::reject); corrDlg->ui->buttonGroup->setId(corrDlg->ui->continueBtn, 0); corrDlg->ui->buttonGroup->setId(corrDlg->ui->changeBtn, 1); corrDlg->ui->buttonGroup->setId(corrDlg->ui->distributeBtn, 2); corrDlg->ui->buttonGroup->setId(corrDlg->ui->leaveBtn, 3); MyMoneySplit split = d->m_transaction.splits()[0]; QString total = (-split.value()).formatMoney(QString(), d->m_precision); QString sums = splitsValue().formatMoney(QString(), d->m_precision); QString diff = diffAmount().formatMoney(QString(), d->m_precision); // now modify the text items of the dialog to contain the correct values QString q = i18n("The total amount of this transaction is %1 while " "the sum of the splits is %2. The remaining %3 are " "unassigned.", total, sums, diff); corrDlg->ui->explanation->setText(q); q = i18n("Change &total amount of transaction to %1.", sums); corrDlg->ui->changeBtn->setText(q); q = i18n("&Distribute difference of %1 among all splits.", diff); corrDlg->ui->distributeBtn->setText(q); // FIXME remove the following line once distribution among // all splits is implemented corrDlg->ui->distributeBtn->hide(); // if we have only two splits left, we don't allow leaving sth. unassigned. if (d->m_transaction.splitCount() < 3) { q = i18n("&Leave total amount of transaction at %1.", total); } else { q = i18n("&Leave %1 unassigned.", diff); } corrDlg->ui->leaveBtn->setText(q); if ((rc = corrDlg->exec()) == Accepted) { switch (corrDlg->ui->buttonGroup->checkedId()) { case 0: // continue to edit rc = Rejected; break; case 1: // modify total split.setValue(-splitsValue()); split.setShares(-splitsValue()); d->m_transaction.modifySplit(split); break; case 2: // distribute difference qDebug("distribution of difference not yet supported in KSplitTransactionDlg::slotFinishClicked()"); break; case 3: // leave unassigned break; } } delete corrDlg; } } else break; } while (rc != Accepted); // for deposits, we inverted the sign of all splits. // now we revert it back, so that things are left correct if (d->m_isDeposit) { for (auto i = 0; i < d->m_transaction.splits().count(); ++i) { auto split = d->m_transaction.splits()[i]; split.setValue(-split.value()); split.setShares(-split.shares()); d->m_transaction.modifySplit(split); } } return rc; } void KSplitTransactionDlg::initSize() { QDialog::resize(width(), height() + 1); } void KSplitTransactionDlg::accept() { Q_D(KSplitTransactionDlg); d->ui->transactionsTable->slotCancelEdit(); QDialog::accept(); } void KSplitTransactionDlg::reject() { Q_D(KSplitTransactionDlg); // cancel any edit activity in the split register d->ui->transactionsTable->slotCancelEdit(); QDialog::reject(); } void KSplitTransactionDlg::slotClearAllSplits() { Q_D(KSplitTransactionDlg); int answer; answer = KMessageBox::warningContinueCancel(this, i18n("You are about to delete all splits of this transaction. " "Do you really want to continue?"), i18n("KMyMoney")); if (answer == KMessageBox::Continue) { d->ui->transactionsTable->slotCancelEdit(); QList list = d->ui->transactionsTable->getSplits(d->m_transaction); QList::ConstIterator it; // clear all but the one referencing the account for (it = list.constBegin(); it != list.constEnd(); ++it) { d->m_transaction.removeSplit(*it); } d->ui->transactionsTable->setTransaction(d->m_transaction, d->m_split, d->m_account); slotSetTransaction(d->m_transaction); } } void KSplitTransactionDlg::slotClearUnusedSplits() { Q_D(KSplitTransactionDlg); QList list = d->ui->transactionsTable->getSplits(d->m_transaction); QList::ConstIterator it; try { // remove all splits that don't have a value assigned for (it = list.constBegin(); it != list.constEnd(); ++it) { if ((*it).shares().isZero()) { d->m_transaction.removeSplit(*it); } } d->ui->transactionsTable->setTransaction(d->m_transaction, d->m_split, d->m_account); slotSetTransaction(d->m_transaction); } catch (const MyMoneyException &) { } } void KSplitTransactionDlg::slotMergeSplits() { Q_D(KSplitTransactionDlg); try { // collect all splits, merge them if needed and remove from transaction QList splits; foreach (const auto lsplit, d->ui->transactionsTable->getSplits(d->m_transaction)) { auto found = false; for (auto& split : splits) { if (split.accountId() == lsplit.accountId() && split.memo().isEmpty() && lsplit.memo().isEmpty()) { split.setShares(lsplit.shares() + split.shares()); split.setValue(lsplit.value() + split.value()); found = true; break; } } if (!found) splits << lsplit; d->m_transaction.removeSplit(lsplit); } // now add them back to the transaction for (auto& split : splits) { split.clearId(); d->m_transaction.addSplit(split); } d->ui->transactionsTable->setTransaction(d->m_transaction, d->m_split, d->m_account); slotSetTransaction(d->m_transaction); } catch (const MyMoneyException &) { } } void KSplitTransactionDlg::slotSetTransaction(const MyMoneyTransaction& t) { Q_D(KSplitTransactionDlg); d->m_transaction = t; slotUpdateButtons(); d->updateSums(); } void KSplitTransactionDlg::slotUpdateButtons() { Q_D(KSplitTransactionDlg); QList list = d->ui->transactionsTable->getSplits(d->m_transaction); // check if we can merge splits or not, have zero splits or not QMap splits; bool haveZeroSplit = false; for (QList::const_iterator it = list.constBegin(); it != list.constEnd(); ++it) { splits[(*it).accountId()]++; if (((*it).id() != d->m_split.id()) && ((*it).shares().isZero())) haveZeroSplit = true; } QMap::const_iterator it_s; for (it_s = splits.constBegin(); it_s != splits.constEnd(); ++it_s) { if ((*it_s) > 1) break; } d->ui->buttonBox->buttons().at(4)->setEnabled(it_s != splits.constEnd()); d->ui->buttonBox->buttons().at(3)->setEnabled(haveZeroSplit); } void KSplitTransactionDlg::slotEditStarted() { Q_D(KSplitTransactionDlg); d->ui->buttonBox->buttons().at(4)->setEnabled(false); d->ui->buttonBox->buttons().at(3)->setEnabled(false); } MyMoneyMoney KSplitTransactionDlg::splitsValue() { Q_D(KSplitTransactionDlg); MyMoneyMoney splitsValue(d->m_calculatedValue); QList list = d->ui->transactionsTable->getSplits(d->m_transaction); QList::ConstIterator it; // calculate the current sum of all split parts for (it = list.constBegin(); it != list.constEnd(); ++it) { if ((*it).value() != MyMoneyMoney::autoCalc) splitsValue += (*it).value(); } return splitsValue; } MyMoneyTransaction KSplitTransactionDlg::transaction() const { Q_D(const KSplitTransactionDlg); return d->m_transaction; } MyMoneyMoney KSplitTransactionDlg::diffAmount() { Q_D(KSplitTransactionDlg); MyMoneyMoney diff; // if there is an amount specified in the transaction, we need to calculate the // difference, otherwise we display the difference as 0 and display the same sum. if (d->m_amountValid) { MyMoneySplit split = d->m_transaction.splits()[0]; diff = -(splitsValue() + split.value()); } return diff; } void KSplitTransactionDlg::slotCreateCategory(const QString& name, QString& id) { Q_D(KSplitTransactionDlg); MyMoneyAccount acc, parent; acc.setName(name); if (d->m_isDeposit) parent = MyMoneyFile::instance()->income(); else parent = MyMoneyFile::instance()->expense(); // TODO extract possible first part of a hierarchy and check if it is one // of our top categories. If so, remove it and select the parent // according to this information. emit createCategory(acc, parent); // return id id = acc.id(); } +void KSplitTransactionDlg::slotCreateTag(const QString& txt, QString& id) +{ + KMyMoneyUtils::newTag(txt, id); + emit createTag(txt, id); +} diff --git a/kmymoney/dialogs/ksplittransactiondlg.h b/kmymoney/dialogs/ksplittransactiondlg.h index 79d57f3e0..28ef0d448 100644 --- a/kmymoney/dialogs/ksplittransactiondlg.h +++ b/kmymoney/dialogs/ksplittransactiondlg.h @@ -1,143 +1,152 @@ /* * Copyright 2002 Michael Edwardes * Copyright 2002-2011 Thomas Baumgart * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef KSPLITTRANSACTIONDLG_H #define KSPLITTRANSACTIONDLG_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class MyMoneyMoney; class MyMoneySplit; class MyMoneyTransaction; class MyMoneyAccount; +class MyMoneyTag; namespace Ui { class KSplitCorrectionDlg; } class KSplitCorrectionDlg : public QDialog { Q_OBJECT Q_DISABLE_COPY(KSplitCorrectionDlg) public: explicit KSplitCorrectionDlg(QWidget *parent = nullptr); ~KSplitCorrectionDlg(); Ui::KSplitCorrectionDlg *ui; }; /** * @author Thomas Baumgart */ class KSplitTransactionDlgPrivate; class KSplitTransactionDlg : public QDialog { Q_OBJECT Q_DISABLE_COPY(KSplitTransactionDlg) public: explicit KSplitTransactionDlg(const MyMoneyTransaction& t, const MyMoneySplit& s, const MyMoneyAccount& acc, const bool amountValid, const bool deposit, const MyMoneyMoney& calculatedValue, const QMap& priceInfo, QWidget* parent = nullptr); ~KSplitTransactionDlg(); /** * Using this method, an external object can retrieve the result * of the dialog. * * @return MyMoneyTransaction based on the transaction passes during * the construction of this object and modified using the * dialog. */ MyMoneyTransaction transaction() const; /** * This method calculates the difference between the split that references * the account passed as argument to the constructor of this object and * all the other splits shown in the register of this dialog. * * @return difference as MyMoneyMoney object */ MyMoneyMoney diffAmount(); /** * This method calculates the sum of the splits shown in the register * of this dialog. * * @return sum of splits as MyMoneyMoney object */ MyMoneyMoney splitsValue(); public Q_SLOTS: int exec() override; protected Q_SLOTS: void accept() override; void reject() override; void slotClearAllSplits(); void slotClearUnusedSplits(); void slotSetTransaction(const MyMoneyTransaction& t); void slotCreateCategory(const QString& txt, QString& id); + void slotCreateTag(const QString &txt, QString &id); void slotUpdateButtons(); void slotMergeSplits(); void slotEditStarted(); /// used internally to setup the initial size of all widgets void initSize(); Q_SIGNALS: /** * This signal is sent out, when a new category needs to be created * Depending on the setting of either a payment or deposit, the parent * account will be preset to Expense or Income. * * @param account reference to account info. Will be filled by called slot * @param parent reference to parent account */ void createCategory(MyMoneyAccount& account, const MyMoneyAccount& parent); + /** + * This signal is sent out, when a new tag needs to be created + * @param txt The name of the tag to be created + * @param id A connected slot should store the id of the created object in this variable + */ + void createTag(const QString& txt, QString& id); + /** * Signal is emitted, if any of the widgets enters (@a state equals @a true) * or leaves (@a state equals @a false) object creation mode. * * @param state Enter (@a true) or leave (@a false) object creation */ void objectCreation(bool state); private: KSplitTransactionDlgPrivate * const d_ptr; Q_DECLARE_PRIVATE(KSplitTransactionDlg) }; #endif diff --git a/kmymoney/dialogs/ksplittransactiondlg.ui b/kmymoney/dialogs/ksplittransactiondlg.ui index 067aa31f9..6e334a955 100644 --- a/kmymoney/dialogs/ksplittransactiondlg.ui +++ b/kmymoney/dialogs/ksplittransactiondlg.ui @@ -1,364 +1,364 @@ KSplitTransactionDlg 0 0 - 656 + 752 408 Split transaction true 6 0 0 0 0 6 0 0 0 0 ArrowCursor 0 0 QFrame::StyledPanel QFrame::Raised 6 1 1 1 1 Qt::Horizontal QSizePolicy::Expanding 0 16 0 0 0 0 1 0 0 120 15 <b>11,00<b> Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter false 0 0 120 15 <b>111,00<b> Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter false 75 true Unassigned Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter false 75 true Sum of splits Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter false 0 0 120 15 100,00 Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter false 150 0 75 true Transaction amount Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter false QFrame::HLine QFrame::Sunken 0 0 15 0 32767 80 QFrame::NoFrame QFrame::Raised Qt::Vertical QDialogButtonBox::Cancel|QDialogButtonBox::Ok KMyMoneySplitTable QWidget
../dialogs/kmymoneysplittable.h
buttonBox accepted() KSplitTransactionDlg accept() 607 203 327 203 buttonBox rejected() KSplitTransactionDlg reject() 607 203 327 203
diff --git a/kmymoney/dialogs/settings/ksettingshome.cpp b/kmymoney/dialogs/settings/ksettingshome.cpp index 50f0e426c..0489081e0 100644 --- a/kmymoney/dialogs/settings/ksettingshome.cpp +++ b/kmymoney/dialogs/settings/ksettingshome.cpp @@ -1,200 +1,202 @@ /* * Copyright 2005-2018 Thomas Baumgart * Copyright 2017 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ksettingshome.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_ksettingshome.h" #include "kmymoneysettings.h" #include "kmymoney/kmymoneyutils.h" #include "icons/icons.h" using namespace Icons; class KSettingsHomePrivate { Q_DISABLE_COPY(KSettingsHomePrivate) public: KSettingsHomePrivate() : ui(new Ui::KSettingsHome), m_noNeedToUpdateList(false) { } ~KSettingsHomePrivate() { delete ui; } Ui::KSettingsHome *ui; bool m_noNeedToUpdateList; }; KSettingsHome::KSettingsHome(QWidget* parent) : QWidget(parent), d_ptr(new KSettingsHomePrivate) { Q_D(KSettingsHome); d->ui->setupUi(this); d->ui->m_homePageList->setSortingEnabled(false); d->ui->m_upButton->setIcon(Icons::get(Icon::ArrowUp)); d->ui->m_downButton->setIcon(Icons::get(Icon::ArrowDown)); d->ui->m_upButton->setEnabled(false); d->ui->m_downButton->setEnabled(false); // connect this, so that the list gets loaded once the edit field is filled connect(d->ui->kcfg_ItemList, &QLineEdit::textChanged, this, &KSettingsHome::slotLoadItems); connect(d->ui->m_homePageList, &QListWidget::itemSelectionChanged, this, &KSettingsHome::slotSelectHomePageItem); connect(d->ui->m_homePageList, &QAbstractItemView::clicked, this, &KSettingsHome::slotUpdateItemList); connect(d->ui->m_upButton, &QAbstractButton::clicked, this, &KSettingsHome::slotMoveUp); connect(d->ui->m_downButton, &QAbstractButton::clicked, this, &KSettingsHome::slotMoveDown); // Don't show it to the user, we only need it to load and save the settings d->ui->kcfg_ItemList->hide(); } KSettingsHome::~KSettingsHome() { Q_D(KSettingsHome); delete d; } void KSettingsHome::slotLoadItems() { Q_D(KSettingsHome); if (d->m_noNeedToUpdateList) return; QStringList list = KMyMoneySettings::listOfItems(); QStringList::ConstIterator it; d->ui->m_homePageList->clear(); QListWidgetItem *sel = 0; for (it = list.constBegin(); it != list.constEnd(); ++it) { int idx = (*it).toInt(); // skip over unknown item entries if (idx == 0) continue; + + // "Forecast (history)" is currently unused (s.a. KHomeViewPrivate::loadView() + // and KMyMoneyUtils::homePageItems so we don't present it to the user. + // We cannot remove it since the settings value is the index into the list. + if (idx == 7) + continue; + bool enabled = idx > 0; if (!enabled) idx = -idx; QListWidgetItem* item = new QListWidgetItem(d->ui->m_homePageList); item->setText(KMyMoneyUtils::homePageItemToString(idx)); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - // qDebug("Adding %s", item->text(0).toLatin1()); - if (enabled) { - item->setCheckState(Qt::Checked); - } else { - item->setCheckState(Qt::Unchecked); - } + item->setCheckState(enabled ? Qt::Checked : Qt::Unchecked); if (sel == 0) sel = item; } if (sel) { d->ui->m_homePageList->setCurrentItem(sel); slotSelectHomePageItem(); } } void KSettingsHome::slotUpdateItemList() { Q_D(KSettingsHome); QString list; QListWidgetItem *it; for (it = d->ui->m_homePageList->item(0); it;) { int item = KMyMoneyUtils::stringToHomePageItem(it->text()); if (it->checkState() == Qt::Unchecked) item = -item; list += QString::number(item); if (d->ui->m_homePageList->count() > (d->ui->m_homePageList->row(it) + 1)) { it = d->ui->m_homePageList->item(d->ui->m_homePageList->row(it) + 1); if (it) { list += ','; } } else { break; } } // don't update the list d->m_noNeedToUpdateList = true; d->ui->kcfg_ItemList->setText(list); d->m_noNeedToUpdateList = false; } void KSettingsHome::slotSelectHomePageItem() { Q_D(KSettingsHome); auto item = d->ui->m_homePageList->currentItem(); d->ui->m_upButton->setEnabled(d->ui->m_homePageList->item(0) != item); d->ui->m_downButton->setEnabled(d->ui->m_homePageList->count() > (d->ui->m_homePageList->row(item) + 1)); } void KSettingsHome::slotMoveUp() { Q_D(KSettingsHome); auto item = d->ui->m_homePageList->currentItem(); auto prev = d->ui->m_homePageList->item(d->ui->m_homePageList->row(item) - 1); int prevRow = d->ui->m_homePageList->row(prev); if (prev) { d->ui->m_homePageList->takeItem(d->ui->m_homePageList->row(item)); d->ui->m_homePageList->insertItem(prevRow, item); d->ui->m_homePageList->setCurrentRow(d->ui->m_homePageList->row(item)); slotSelectHomePageItem(); slotUpdateItemList(); } } void KSettingsHome::slotMoveDown() { Q_D(KSettingsHome); auto item = d->ui->m_homePageList->currentItem(); auto next = d->ui->m_homePageList->item(d->ui->m_homePageList->row(item) + 1); int nextRow = d->ui->m_homePageList->row(next); if (next) { d->ui->m_homePageList->takeItem(d->ui->m_homePageList->row(item)); d->ui->m_homePageList->insertItem(nextRow, item); d->ui->m_homePageList->setCurrentRow(d->ui->m_homePageList->row(item)); slotSelectHomePageItem(); slotUpdateItemList(); } } diff --git a/kmymoney/dialogs/settings/ksettingsregister.ui b/kmymoney/dialogs/settings/ksettingsregister.ui index bbd451849..358c81d4b 100644 --- a/kmymoney/dialogs/settings/ksettingsregister.ui +++ b/kmymoney/dialogs/settings/ksettingsregister.ui @@ -1,554 +1,554 @@ KSettingsRegister 0 0 632 500 Register settings 2 Display Show a grid in the register Show all register entries in full detail Using the ledger lens shows the details for the transaction that has focus in the ledger. Usually, when using the transaction form, only a one line summary is displayed for each transaction as the details are shown in the form. Use the ledger lens Show transaction form Always show a No. field Draws a larger header above each group of transactions. The grouping depends on the current sort order. Show group header between transactions Show header for the previous and current fiscal year If a planned transaction is overdue, today's date is used by default as the posting date. With this option, the originally planned date is used instead. Display overdue schedules with planned date Qt::Vertical QSizePolicy::Expanding 20 40 Sorting Normal view Reconciliation view Search view Use the <i>left</i> and <i>right</i> buttons to add and remove sort options. Use the <i>up</i> and <i>down</i> buttons to modify the sort order. Double-Click a selected entry to toggle the sort order between <i>ascending</i> and <i>descending</i>. true Data entry Insert transaction type into No. field for new transactions Auto increment check number Keep changes when selecting a different transaction/split Use Enter to move between fields Mark this option, if you always want to match names e.g. for payees from the start. If unset, any substring is matched. Match names from start After entering the reconciliation data automatically detect the transactions that match that data (in some cases it might be not possible to do that). Automatic reconciliation Default reconciliation state false Default reconciliation state for transactions entered during reconciliation of an account Not reconciled Cleared Reconciled Qt::Horizontal QSizePolicy::Expanding 31 20 Autofill -1 Do not auto-fill transaction data at all. No Autofill Collect all transactions for the given payee. Treat all transactions that refer to the same category and have an amount with +/- X % as identical. If more than one transaction is found, a list of them is presented to the user. Selecting 0% will list all transactions. Same transaction if amount differs less than percentage set below true The data of the last transaction assigned to the category used most often for this payee is autofilled into the transaction editor. With previously most often used transaction for the payee 0 0 - Same transaction if amount differes less than + Same transaction if amount differs less than Two transactions are usually treated identical for autofill, if they refer the same accounts. They are treated as different transactions though, when their amount varies by more than the percentage given here. % 100 10 Qt::Horizontal 40 20 If this option is checked the memos from the previous transaction will be used otherwise the memos will not be considered when the transaction is autofilled. Use memos from previous transaction Qt::Vertical QSizePolicy::Expanding 20 20 Import Match transactions within days false Search for matching transactions within the range of the posting date of the imported transaction +/- the number of given days. QAbstractSpinBox::UpDownArrows 0 99 4 Qt::Horizontal QSizePolicy::Expanding 61 20 Whenever a new payee is detected during import of a statement, the user will be asked to assign a default category for this user when this option is selected. Ask for a new payee's default category Qt::Vertical QSizePolicy::Expanding 20 60 KComboBox QComboBox
kcombobox.h
KLineEdit QLineEdit
klineedit.h
TransactionSortOption QWidget
transactionsortoption.h
radioButton2 toggled(bool) percentageWidget setEnabled(bool) 147 323 171 378
diff --git a/kmymoney/kmymoney.cpp b/kmymoney/kmymoney.cpp index fa0682086..39d02eafb 100644 --- a/kmymoney/kmymoney.cpp +++ b/kmymoney/kmymoney.cpp @@ -1,3649 +1,3653 @@ /*************************************************************************** kmymoney.cpp ------------------- copyright : (C) 2000 by Michael Edwardes - (C) 2007 by Thomas Baumgart - (C) 2017, 2018 by Łukasz Wojniłowicz + (C) 2002-2019 by Thomas Baumgart + (C) 2017-2018 by Łukasz Wojniłowicz ****************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include #include "kmymoney.h" // ---------------------------------------------------------------------------- // Std C++ / STL Includes #include #include #include // ---------------------------------------------------------------------------- // QT Includes #include #include // only for performance tests #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef ENABLE_HOLIDAYS #include #include #endif #ifdef ENABLE_ACTIVITIES #include #endif // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneysettings.h" #include "kmymoneyadaptor.h" #include "dialogs/settings/ksettingskmymoney.h" #include "dialogs/kbackupdlg.h" #include "dialogs/kconfirmmanualenterdlg.h" #include "dialogs/kmymoneypricedlg.h" #include "dialogs/kcurrencyeditdlg.h" #include "dialogs/kequitypriceupdatedlg.h" #include "dialogs/kmymoneyfileinfodlg.h" #include "dialogs/knewbankdlg.h" #include "dialogs/ksaveasquestion.h" #include "wizards/newinvestmentwizard/knewinvestmentwizard.h" #include "dialogs/knewaccountdlg.h" #include "dialogs/editpersonaldatadlg.h" #include "dialogs/kcurrencycalculator.h" #include "dialogs/keditscheduledlg.h" #include "wizards/newloanwizard/keditloanwizard.h" #include "dialogs/kpayeereassigndlg.h" #include "dialogs/kcategoryreassigndlg.h" #include "wizards/endingbalancedlg/kendingbalancedlg.h" #include "dialogs/kloadtemplatedlg.h" #include "dialogs/ktemplateexportdlg.h" #include "dialogs/transactionmatcher.h" #include "wizards/newuserwizard/knewuserwizard.h" #include "wizards/newaccountwizard/knewaccountwizard.h" #include "dialogs/kbalancewarning.h" #include "widgets/kmymoneyaccountselector.h" #include "widgets/kmymoneypayeecombo.h" #include "widgets/amountedit.h" #include "widgets/kmymoneyedit.h" #include "widgets/kmymoneymvccombo.h" #include "views/kmymoneyview.h" #include "models/models.h" #include "models/accountsmodel.h" #include "models/equitiesmodel.h" #include "models/securitiesmodel.h" #ifdef ENABLE_UNFINISHEDFEATURES #include "models/ledgermodel.h" #endif #include "mymoney/mymoneyobject.h" #include "mymoney/mymoneyfile.h" #include "mymoney/mymoneyinstitution.h" #include "mymoney/mymoneyaccount.h" #include "mymoney/mymoneyaccountloan.h" #include "mymoney/mymoneysecurity.h" #include "mymoney/mymoneypayee.h" #include "mymoney/mymoneyprice.h" #include "mymoney/mymoneytag.h" #include "mymoney/mymoneybudget.h" #include "mymoney/mymoneyreport.h" #include "mymoney/mymoneysplit.h" #include "mymoney/mymoneyutils.h" #include "mymoney/mymoneystatement.h" #include "mymoney/mymoneyforecast.h" #include "mymoney/mymoneytransactionfilter.h" #include "mymoneyexception.h" #include "converter/mymoneystatementreader.h" #include "converter/mymoneytemplate.h" #include "plugins/interfaces/kmmappinterface.h" #include "plugins/interfaces/kmmviewinterface.h" #include "plugins/interfaces/kmmstatementinterface.h" #include "plugins/interfaces/kmmimportinterface.h" #include "plugins/interfaceloader.h" #include "plugins/onlinepluginextended.h" #include "pluginloader.h" #include "kmymoneyplugin.h" #include "tasks/credittransfer.h" #include "icons/icons.h" #include "misc/webconnect.h" #include "storage/mymoneystoragemgr.h" #include "imymoneystorageformat.h" #include "transactioneditor.h" #include #include #include "kmymoneyutils.h" #include "kcreditswindow.h" #include "ledgerdelegate.h" #include "storageenums.h" #include "mymoneyenums.h" #include "dialogenums.h" #include "viewenums.h" #include "menuenums.h" #include "kmymoneyenums.h" -#include "misc/platformtools.h" +#include "platformtools.h" +#include "kmm_printer.h" #ifdef ENABLE_SQLCIPHER #include "sqlcipher/sqlite3.h" #endif #ifdef KMM_DEBUG #include "mymoney/storage/mymoneystoragedump.h" #include "mymoneytracer.h" #endif using namespace Icons; using namespace eMenu; enum backupStateE { BACKUP_IDLE = 0, BACKUP_MOUNTING, BACKUP_COPYING, BACKUP_UNMOUNTING }; class KMyMoneyApp::Private { public: explicit Private(KMyMoneyApp *app) : q(app), m_backupState(backupStateE::BACKUP_IDLE), m_backupResult(0), m_backupMount(0), m_ignoreBackupExitCode(false), m_myMoneyView(nullptr), m_startDialog(false), m_progressBar(nullptr), m_statusLabel(nullptr), m_autoSaveEnabled(true), m_autoSaveTimer(nullptr), m_progressTimer(nullptr), m_autoSavePeriod(0), m_inAutoSaving(false), m_recentFiles(nullptr), #ifdef ENABLE_HOLIDAYS m_holidayRegion(nullptr), #endif #ifdef ENABLE_ACTIVITIES m_activityResourceInstance(nullptr), #endif m_applicationIsReady(true), m_webConnect(new WebConnect(app)) { // since the days of the week are from 1 to 7, // and a day of the week is used to index this bit array, // resize the array to 8 elements (element 0 is left unused) m_processingDays.resize(8); } void unlinkStatementXML(); void moveInvestmentTransaction(const QString& fromId, const QString& toId, const MyMoneyTransaction& t); struct storageInfo { eKMyMoney::StorageType type {eKMyMoney::StorageType::None}; bool isOpened {false}; QUrl url; }; storageInfo m_storageInfo; /** * The public interface. */ KMyMoneyApp * const q; /** the configuration object of the application */ KSharedConfigPtr m_config; /** * The following variable represents the state while crafting a backup. * It can have the following values * * - IDLE: the default value if not performing a backup * - MOUNTING: when a mount command has been issued * - COPYING: when a copy command has been issued * - UNMOUNTING: when an unmount command has been issued */ backupStateE m_backupState; /** * This variable keeps the result of the backup operation. */ int m_backupResult; /** * This variable is set, when the user selected to mount/unmount * the backup volume. */ bool m_backupMount; /** * Flag for internal run control */ bool m_ignoreBackupExitCode; KProcess m_proc; /// A pointer to the view holding the tabs. KMyMoneyView *m_myMoneyView; bool m_startDialog; QString m_mountpoint; QProgressBar* m_progressBar; QTime m_lastUpdate; QLabel* m_statusLabel; // allows multiple imports to be launched trough web connect and to be executed sequentially QQueue m_importUrlsQueue; // This is Auto Saving related bool m_autoSaveEnabled; QTimer* m_autoSaveTimer; QTimer* m_progressTimer; int m_autoSavePeriod; bool m_inAutoSaving; // id's that need to be remembered QString m_accountGoto, m_payeeGoto; KRecentFilesAction* m_recentFiles; #ifdef ENABLE_HOLIDAYS // used by the calendar interface for schedules KHolidays::HolidayRegion* m_holidayRegion; #endif #ifdef ENABLE_ACTIVITIES KActivities::ResourceInstance * m_activityResourceInstance; #endif QBitArray m_processingDays; QMap m_holidayMap; QStringList m_consistencyCheckResult; bool m_applicationIsReady; WebConnect* m_webConnect; // methods void consistencyCheck(bool alwaysDisplayResults); static void setThemedCSS(); void copyConsistencyCheckResults(); void saveConsistencyCheckResults(); void checkAccountName(const MyMoneyAccount& _acc, const QString& name) const { auto file = MyMoneyFile::instance(); if (_acc.name() != name) { MyMoneyAccount acc(_acc); acc.setName(name); file->modifyAccount(acc); } } /** * This method updates names of currencies from file to localized names */ void updateCurrencyNames() { auto file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; QList storedCurrencies = MyMoneyFile::instance()->currencyList(); QList availableCurrencies = MyMoneyFile::instance()->availableCurrencyList(); QStringList currencyIDs; foreach (auto currency, availableCurrencies) currencyIDs.append(currency.id()); try { foreach (auto currency, storedCurrencies) { int i = currencyIDs.indexOf(currency.id()); if (i != -1 && availableCurrencies.at(i).name() != currency.name()) { currency.setName(availableCurrencies.at(i).name()); file->modifyCurrency(currency); } } ft.commit(); } catch (const MyMoneyException &e) { qDebug("Error %s updating currency names", e.what()); } } void updateAccountNames() { // make sure we setup the name of the base accounts in translated form try { MyMoneyFileTransaction ft; const auto file = MyMoneyFile::instance(); checkAccountName(file->asset(), i18n("Asset")); checkAccountName(file->liability(), i18n("Liability")); checkAccountName(file->income(), i18n("Income")); checkAccountName(file->expense(), i18n("Expense")); checkAccountName(file->equity(), i18n("Equity")); ft.commit(); } catch (const MyMoneyException &) { } } void ungetString(QIODevice *qfile, char *buf, int len) { buf = &buf[len-1]; while (len--) { qfile->ungetChar(*buf--); } } bool applyFileFixes() { const auto blocked = MyMoneyFile::instance()->blockSignals(true); KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("General Options"); // For debugging purposes, we can turn off the automatic fix manually // by setting the entry in kmymoneyrc to true if (grp.readEntry("SkipFix", false) != true) { MyMoneyFileTransaction ft; try { // Check if we have to modify the file before we allow to work with it auto s = MyMoneyFile::instance()->storage(); while (s->fileFixVersion() < s->currentFixVersion()) { qDebug("%s", qPrintable((QString("testing fileFixVersion %1 < %2").arg(s->fileFixVersion()).arg(s->currentFixVersion())))); switch (s->fileFixVersion()) { case 0: fixFile_0(); s->setFileFixVersion(1); break; case 1: fixFile_1(); s->setFileFixVersion(2); break; case 2: fixFile_2(); s->setFileFixVersion(3); break; case 3: fixFile_3(); s->setFileFixVersion(4); break; case 4: fixFile_4(); s->setFileFixVersion(5); break; // add new levels above. Don't forget to increase currentFixVersion() for all // the storage backends this fix applies to default: throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown fix level in input file")); } } ft.commit(); } catch (const MyMoneyException &) { MyMoneyFile::instance()->blockSignals(blocked); return false; } } else { qDebug("Skipping automatic transaction fix!"); } MyMoneyFile::instance()->blockSignals(blocked); return true; } void connectStorageToModels() { const auto file = MyMoneyFile::instance(); const auto accountsModel = Models::instance()->accountsModel(); q->connect(file, &MyMoneyFile::objectAdded, accountsModel, &AccountsModel::slotObjectAdded); q->connect(file, &MyMoneyFile::objectModified, accountsModel, &AccountsModel::slotObjectModified); q->connect(file, &MyMoneyFile::objectRemoved, accountsModel, &AccountsModel::slotObjectRemoved); q->connect(file, &MyMoneyFile::balanceChanged, accountsModel, &AccountsModel::slotBalanceOrValueChanged); q->connect(file, &MyMoneyFile::valueChanged, accountsModel, &AccountsModel::slotBalanceOrValueChanged); const auto institutionsModel = Models::instance()->institutionsModel(); q->connect(file, &MyMoneyFile::objectAdded, institutionsModel, &InstitutionsModel::slotObjectAdded); q->connect(file, &MyMoneyFile::objectModified, institutionsModel, &InstitutionsModel::slotObjectModified); q->connect(file, &MyMoneyFile::objectRemoved, institutionsModel, &InstitutionsModel::slotObjectRemoved); q->connect(file, &MyMoneyFile::balanceChanged, institutionsModel, &AccountsModel::slotBalanceOrValueChanged); q->connect(file, &MyMoneyFile::valueChanged, institutionsModel, &AccountsModel::slotBalanceOrValueChanged); const auto equitiesModel = Models::instance()->equitiesModel(); q->connect(file, &MyMoneyFile::objectAdded, equitiesModel, &EquitiesModel::slotObjectAdded); q->connect(file, &MyMoneyFile::objectModified, equitiesModel, &EquitiesModel::slotObjectModified); q->connect(file, &MyMoneyFile::objectRemoved, equitiesModel, &EquitiesModel::slotObjectRemoved); q->connect(file, &MyMoneyFile::balanceChanged, equitiesModel, &EquitiesModel::slotBalanceOrValueChanged); q->connect(file, &MyMoneyFile::valueChanged, equitiesModel, &EquitiesModel::slotBalanceOrValueChanged); const auto securitiesModel = Models::instance()->securitiesModel(); q->connect(file, &MyMoneyFile::objectAdded, securitiesModel, &SecuritiesModel::slotObjectAdded); q->connect(file, &MyMoneyFile::objectModified, securitiesModel, &SecuritiesModel::slotObjectModified); q->connect(file, &MyMoneyFile::objectRemoved, securitiesModel, &SecuritiesModel::slotObjectRemoved); #ifdef ENABLE_UNFINISHEDFEATURES const auto ledgerModel = Models::instance()->ledgerModel(); q->connect(file, &MyMoneyFile::objectAdded, ledgerModel, &LedgerModel::slotAddTransaction); q->connect(file, &MyMoneyFile::objectModified, ledgerModel, &LedgerModel::slotModifyTransaction); q->connect(file, &MyMoneyFile::objectRemoved, ledgerModel, &LedgerModel::slotRemoveTransaction); q->connect(file, &MyMoneyFile::objectAdded, ledgerModel, &LedgerModel::slotAddSchedule); q->connect(file, &MyMoneyFile::objectModified, ledgerModel, &LedgerModel::slotModifySchedule); q->connect(file, &MyMoneyFile::objectRemoved, ledgerModel, &LedgerModel::slotRemoveSchedule); #endif } void disconnectStorageFromModels() { const auto file = MyMoneyFile::instance(); q->disconnect(file, nullptr, Models::instance()->accountsModel(), nullptr); q->disconnect(file, nullptr, Models::instance()->institutionsModel(), nullptr); q->disconnect(file, nullptr, Models::instance()->equitiesModel(), nullptr); q->disconnect(file, nullptr, Models::instance()->securitiesModel(), nullptr); #ifdef ENABLE_UNFINISHEDFEATURES q->disconnect(file, nullptr, Models::instance()->ledgerModel(), nullptr); #endif } bool askAboutSaving() { const auto isFileNotSaved = q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->isEnabled(); const auto isNewFileNotSaved = m_storageInfo.isOpened && m_storageInfo.url.isEmpty(); auto fileNeedsToBeSaved = false; if (isFileNotSaved && KMyMoneySettings::autoSaveOnClose()) { fileNeedsToBeSaved = true; } else if (isFileNotSaved || isNewFileNotSaved) { switch (KMessageBox::warningYesNoCancel(q, i18n("The file has been changed, save it?"))) { case KMessageBox::ButtonCode::Yes: fileNeedsToBeSaved = true; break; case KMessageBox::ButtonCode::No: fileNeedsToBeSaved = false; break; case KMessageBox::ButtonCode::Cancel: default: return false; break; } } if (fileNeedsToBeSaved) { if (isFileNotSaved) return q->slotFileSave(); else if (isNewFileNotSaved) return q->slotFileSaveAs(); } return true; } /** * This method attaches an empty storage object to the MyMoneyFile * object. It calls removeStorage() to remove a possibly attached * storage object. */ void newStorage() { removeStorage(); auto file = MyMoneyFile::instance(); file->attachStorage(new MyMoneyStorageMgr); } /** * This method removes an attached storage from the MyMoneyFile * object. */ void removeStorage() { auto file = MyMoneyFile::instance(); auto p = file->storage(); if (p) { file->detachStorage(p); delete p; } } /** * if no base currency is defined, start the dialog and force it to be set */ void selectBaseCurrency() { auto file = MyMoneyFile::instance(); // check if we have a base currency. If not, we need to select one QString baseId; try { baseId = MyMoneyFile::instance()->baseCurrency().id(); } catch (const MyMoneyException &e) { qDebug("%s", e.what()); } if (baseId.isEmpty()) { QPointer dlg = new KCurrencyEditDlg(q); // connect(dlg, SIGNAL(selectBaseCurrency(MyMoneySecurity)), this, SLOT(slotSetBaseCurrency(MyMoneySecurity))); dlg->exec(); delete dlg; } try { baseId = MyMoneyFile::instance()->baseCurrency().id(); } catch (const MyMoneyException &e) { qDebug("%s", e.what()); } if (!baseId.isEmpty()) { // check that all accounts have a currency QList list; file->accountList(list); QList::Iterator it; // don't forget those standard accounts list << file->asset(); list << file->liability(); list << file->income(); list << file->expense(); list << file->equity(); for (it = list.begin(); it != list.end(); ++it) { QString cid; try { if (!(*it).currencyId().isEmpty() || (*it).currencyId().length() != 0) cid = MyMoneyFile::instance()->currency((*it).currencyId()).id(); } catch (const MyMoneyException &e) { qDebug() << QLatin1String("Account") << (*it).id() << (*it).name() << e.what(); } if (cid.isEmpty()) { (*it).setCurrencyId(baseId); MyMoneyFileTransaction ft; try { file->modifyAccount(*it); ft.commit(); } catch (const MyMoneyException &e) { qDebug("Unable to setup base currency in account %s (%s): %s", qPrintable((*it).name()), qPrintable((*it).id()), e.what()); } } } } } /** * Call this to see if the MyMoneyFile contains any unsaved data. * * @retval true if any data has been modified but not saved * @retval false otherwise */ bool dirty() { if (!m_storageInfo.isOpened) return false; return MyMoneyFile::instance()->dirty(); } /* DO NOT ADD code to this function or any of it's called ones. Instead, create a new function, fixFile_n, and modify the initializeStorage() logic above to call it */ void fixFile_4() { auto file = MyMoneyFile::instance(); QList currencies = file->currencyList(); static const QStringList symbols = { QStringLiteral("XAU"), QStringLiteral("XPD"), QStringLiteral("XPT"), QStringLiteral("XAG") }; foreach(auto currency, currencies) { if (symbols.contains(currency.id())) { if (currency.smallestAccountFraction() != currency.smallestCashFraction()) { currency.setSmallestAccountFraction(currency.smallestCashFraction()); file->modifyCurrency(currency); } } } } void fixFile_3() { // make sure each storage object contains a (unique) id MyMoneyFile::instance()->storageId(); } void fixFile_2() { auto file = MyMoneyFile::instance(); MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); QList transactionList; file->transactionList(transactionList, filter); // scan the transactions and modify transactions with two splits // which reference an account and a category to have the memo text // of the account. auto count = 0; foreach (const auto transaction, transactionList) { if (transaction.splitCount() == 2) { QString accountId; QString categoryId; QString accountMemo; QString categoryMemo; foreach (const auto split, transaction.splits()) { auto acc = file->account(split.accountId()); if (acc.isIncomeExpense()) { categoryId = split.id(); categoryMemo = split.memo(); } else { accountId = split.id(); accountMemo = split.memo(); } } if (!accountId.isEmpty() && !categoryId.isEmpty() && accountMemo != categoryMemo) { MyMoneyTransaction t(transaction); MyMoneySplit s(t.splitById(categoryId)); s.setMemo(accountMemo); t.modifySplit(s); file->modifyTransaction(t); ++count; } } } qDebug("%d transactions fixed in fixFile_2", count); } void fixFile_1() { // we need to fix reports. If the account filter list contains // investment accounts, we need to add the stock accounts to the list // as well if we don't have the expert mode enabled if (!KMyMoneySettings::expertMode()) { try { QList reports = MyMoneyFile::instance()->reportList(); QList::iterator it_r; for (it_r = reports.begin(); it_r != reports.end(); ++it_r) { QStringList list; (*it_r).accounts(list); QStringList missing; QStringList::const_iterator it_a, it_b; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { auto acc = MyMoneyFile::instance()->account(*it_a); if (acc.accountType() == eMyMoney::Account::Type::Investment) { foreach (const auto accountID, acc.accountList()) { if (!list.contains(accountID)) { missing.append(accountID); } } } } if (!missing.isEmpty()) { (*it_r).addAccount(missing); MyMoneyFile::instance()->modifyReport(*it_r); } } } catch (const MyMoneyException &) { } } } #if 0 if (!m_accountsView->allItemsSelected()) { // retrieve a list of selected accounts QStringList list; m_accountsView->selectedItems(list); // if we're not in expert mode, we need to make sure // that all stock accounts for the selected investment // account are also selected if (!KMyMoneySettings::expertMode()) { QStringList missing; QStringList::const_iterator it_a, it_b; for (it_a = list.begin(); it_a != list.end(); ++it_a) { auto acc = MyMoneyFile::instance()->account(*it_a); if (acc.accountType() == Account::Type::Investment) { foreach (const auto accountID, acc.accountList()) { if (!list.contains(accountID)) { missing.append(accountID); } } } } list += missing; } m_filter.addAccount(list); } #endif void fixFile_0() { /* (Ace) I am on a crusade against file fixups. Whenever we have to fix the * file, it is really a warning. So I'm going to print a debug warning, and * then go track them down when I see them to figure out how they got saved * out needing fixing anyway. */ auto file = MyMoneyFile::instance(); QList accountList; file->accountList(accountList); QList::Iterator it_a; QList scheduleList = file->scheduleList(); QList::Iterator it_s; MyMoneyAccount equity = file->equity(); MyMoneyAccount asset = file->asset(); bool equityListEmpty = equity.accountList().count() == 0; for (it_a = accountList.begin(); it_a != accountList.end(); ++it_a) { if ((*it_a).accountType() == eMyMoney::Account::Type::Loan || (*it_a).accountType() == eMyMoney::Account::Type::AssetLoan) { fixLoanAccount_0(*it_a); } // until early before 0.8 release, the equity account was not saved to // the file. If we have an equity account with no sub-accounts but // find and equity account that has equity() as it's parent, we reparent // this account. Need to move it to asset() first, because otherwise // MyMoneyFile::reparent would act as NOP. if (equityListEmpty && (*it_a).accountType() == eMyMoney::Account::Type::Equity) { if ((*it_a).parentAccountId() == equity.id()) { auto acc = *it_a; // tricky, force parent account to be empty so that we really // can re-parent it acc.setParentAccountId(QString()); file->reparentAccount(acc, equity); qDebug() << Q_FUNC_INFO << " fixed account " << acc.id() << " reparented to " << equity.id(); } } } for (it_s = scheduleList.begin(); it_s != scheduleList.end(); ++it_s) { fixSchedule_0(*it_s); } fixTransactions_0(); } void fixSchedule_0(MyMoneySchedule sched) { MyMoneyTransaction t = sched.transaction(); QList splitList = t.splits(); QList::ConstIterator it_s; try { bool updated = false; // Check if the splits contain valid data and set it to // be valid. for (it_s = splitList.constBegin(); it_s != splitList.constEnd(); ++it_s) { // the first split is always the account on which this transaction operates // and if the transaction commodity is not set, we take this if (it_s == splitList.constBegin() && t.commodity().isEmpty()) { qDebug() << Q_FUNC_INFO << " " << t.id() << " has no commodity"; try { auto acc = MyMoneyFile::instance()->account((*it_s).accountId()); t.setCommodity(acc.currencyId()); updated = true; } catch (const MyMoneyException &) { } } // make sure the account exists. If not, remove the split try { MyMoneyFile::instance()->account((*it_s).accountId()); } catch (const MyMoneyException &) { qDebug() << Q_FUNC_INFO << " " << sched.id() << " " << (*it_s).id() << " removed, because account '" << (*it_s).accountId() << "' does not exist."; t.removeSplit(*it_s); updated = true; } if ((*it_s).reconcileFlag() != eMyMoney::Split::State::NotReconciled) { qDebug() << Q_FUNC_INFO << " " << sched.id() << " " << (*it_s).id() << " should be 'not reconciled'"; MyMoneySplit split = *it_s; split.setReconcileDate(QDate()); split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); t.modifySplit(split); updated = true; } // the schedule logic used to operate only on the value field. // This is now obsolete. if ((*it_s).shares().isZero() && !(*it_s).value().isZero()) { MyMoneySplit split = *it_s; split.setShares(split.value()); t.modifySplit(split); updated = true; } } // If there have been changes, update the schedule and // the engine data. if (updated) { sched.setTransaction(t); MyMoneyFile::instance()->modifySchedule(sched); } } catch (const MyMoneyException &e) { qWarning("Unable to update broken schedule: %s", e.what()); } } void fixLoanAccount_0(MyMoneyAccount acc) { if (acc.value("final-payment").isEmpty() || acc.value("term").isEmpty() || acc.value("periodic-payment").isEmpty() || acc.value("loan-amount").isEmpty() || acc.value("interest-calculation").isEmpty() || acc.value("schedule").isEmpty() || acc.value("fixed-interest").isEmpty()) { KMessageBox::information(q, i18n("

The account \"%1\" was previously created as loan account but some information is missing.

The new loan wizard will be started to collect all relevant information.

Please use KMyMoney version 0.8.7 or later and earlier than version 0.9 to correct the problem.

" , acc.name()), i18n("Account problem")); throw MYMONEYEXCEPTION_CSTRING("Fix LoanAccount0 not supported anymore"); } } void fixTransactions_0() { auto file = MyMoneyFile::instance(); QList scheduleList = file->scheduleList(); MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); QList transactionList; file->transactionList(transactionList, filter); QList::Iterator it_x; QStringList interestAccounts; KMSTATUS(i18n("Fix transactions")); q->slotStatusProgressBar(0, scheduleList.count() + transactionList.count()); int cnt = 0; // scan the schedules to find interest accounts for (it_x = scheduleList.begin(); it_x != scheduleList.end(); ++it_x) { MyMoneyTransaction t = (*it_x).transaction(); QList::ConstIterator it_s; QStringList accounts; bool hasDuplicateAccounts = false; foreach (const auto split, t.splits()) { if (accounts.contains(split.accountId())) { hasDuplicateAccounts = true; qDebug() << Q_FUNC_INFO << " " << t.id() << " has multiple splits with account " << split.accountId(); } else { accounts << split.accountId(); } if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { if (interestAccounts.contains(split.accountId()) == 0) { interestAccounts << split.accountId(); } } } if (hasDuplicateAccounts) { fixDuplicateAccounts_0(t); } ++cnt; if (!(cnt % 10)) q->slotStatusProgressBar(cnt); } // scan the transactions and modify loan transactions for (auto& transaction : transactionList) { QString defaultAction; QList splits = transaction.splits(); QStringList accounts; // check if base commodity is set. if not, set baseCurrency if (transaction.commodity().isEmpty()) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " has no base currency"; transaction.setCommodity(file->baseCurrency().id()); file->modifyTransaction(transaction); } bool isLoan = false; // Determine default action if (transaction.splitCount() == 2) { // check for transfer int accountCount = 0; MyMoneyMoney val; foreach (const auto split, splits) { auto acc = file->account(split.accountId()); if (acc.accountGroup() == eMyMoney::Account::Type::Asset || acc.accountGroup() == eMyMoney::Account::Type::Liability) { val = split.value(); accountCount++; if (acc.accountType() == eMyMoney::Account::Type::Loan || acc.accountType() == eMyMoney::Account::Type::AssetLoan) isLoan = true; } else break; } if (accountCount == 2) { if (isLoan) defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization); else defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer); } else { if (val.isNegative()) defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal); else defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit); } } isLoan = false; foreach (const auto split, splits) { auto acc = file->account(split.accountId()); MyMoneyMoney val = split.value(); if (acc.accountGroup() == eMyMoney::Account::Type::Asset || acc.accountGroup() == eMyMoney::Account::Type::Liability) { if (!val.isPositive()) { defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal); break; } else { defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit); break; } } } #if 0 // Check for correct actions in transactions referencing credit cards bool needModify = false; // The action fields are actually not used anymore in the ledger view logic // so we might as well skip this whole thing here! for (it_s = splits.begin(); needModify == false && it_s != splits.end(); ++it_s) { auto acc = file->account((*it_s).accountId()); MyMoneyMoney val = (*it_s).value(); if (acc.accountType() == Account::Type::CreditCard) { if (val < 0 && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal) && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) needModify = true; if (val >= 0 && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit) && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) needModify = true; } } // (Ace) Extended the #endif down to cover this conditional, because as-written // it will ALWAYS be skipped. if (needModify == true) { for (it_s = splits.begin(); it_s != splits.end(); ++it_s) { (*it_s).setAction(defaultAction); transaction.modifySplit(*it_s); file->modifyTransaction(transaction); } splits = transaction.splits(); // update local copy qDebug("Fixed credit card assignment in %s", transaction.id().data()); } #endif // Check for correct assignment of ActionInterest in all splits // and check if there are any duplicates in this transactions for (auto& split : splits) { MyMoneyAccount splitAccount = file->account(split.accountId()); if (!accounts.contains(split.accountId())) { accounts << split.accountId(); } // if this split references an interest account, the action // must be of type ActionInterest if (interestAccounts.contains(split.accountId())) { if (split.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " contains an interest account (" << split.accountId() << ") but does not have ActionInterest"; split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)); transaction.modifySplit(split); file->modifyTransaction(transaction); qDebug("Fixed interest action in %s", qPrintable(transaction.id())); } // if it does not reference an interest account, it must not be // of type ActionInterest } else { if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " does not contain an interest account so it should not have ActionInterest"; split.setAction(defaultAction); transaction.modifySplit(split); file->modifyTransaction(transaction); qDebug("Fixed interest action in %s", qPrintable(transaction.id())); } } // check that for splits referencing an account that has // the same currency as the transactions commodity the value // and shares field are the same. if (transaction.commodity() == splitAccount.currencyId() && split.value() != split.shares()) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " " << split.id() << " uses the transaction currency, but shares != value"; split.setShares(split.value()); transaction.modifySplit(split); file->modifyTransaction(transaction); } // fix the shares and values to have the correct fraction if (!splitAccount.isInvest()) { try { int fract = splitAccount.fraction(); if (split.shares() != split.shares().convert(fract)) { qDebug("adjusting fraction in %s,%s", qPrintable(transaction.id()), qPrintable(split.id())); split.setShares(split.shares().convert(fract)); split.setValue(split.value().convert(fract)); transaction.modifySplit(split); file->modifyTransaction(transaction); } } catch (const MyMoneyException &) { qDebug("Missing security '%s', split not altered", qPrintable(splitAccount.currencyId())); } } } ++cnt; if (!(cnt % 10)) q->slotStatusProgressBar(cnt); } q->slotStatusProgressBar(-1, -1); } void fixDuplicateAccounts_0(MyMoneyTransaction& t) { qDebug("Duplicate account in transaction %s", qPrintable(t.id())); } /** * This method is used to update the caption of the application window. * It sets the caption to "filename [modified] - KMyMoney". * * @param skipActions if true, the actions will not be updated. This * is usually onyl required by some early calls when * these widgets are not yet created (the default is false). */ void updateCaption(); void updateActions(); bool canFileSaveAs() const; bool canUpdateAllAccounts() const; void fileAction(eKMyMoney::FileAction action); }; KMyMoneyApp::KMyMoneyApp(QWidget* parent) : KXmlGuiWindow(parent), d(new Private(this)) { #ifdef KMM_DBUS new KmymoneyAdaptor(this); QDBusConnection::sessionBus().registerObject("/KMymoney", this); QDBusConnection::sessionBus().interface()->registerService( "org.kde.kmymoney-" + QString::number(platformTools::processId()), QDBusConnectionInterface::DontQueueService); #endif // Register the main engine types used as meta-objects qRegisterMetaType("MyMoneyMoney"); qRegisterMetaType("MyMoneySecurity"); #ifdef ENABLE_SQLCIPHER /* Issues: * 1) libsqlite3 loads implicitly before libsqlcipher * thus making the second one loaded but non-functional, * 2) libsqlite3 gets linked into kmymoney target implicitly * and it's not possible to unload or unlink it explicitly * * Solution: * Use e.g. dummy sqlite3_key call, so that libsqlcipher gets loaded implicitly before libsqlite3 * thus making the first one functional. * * Additional info: * 1) loading libsqlcipher explicitly doesn't solve the issue, * 2) using sqlite3_key only in sqlstorage plugin doesn't solve the issue, * 3) in a separate, minimal test case, loading libsqlite3 explicitly * with QLibrary::ExportExternalSymbolsHint makes libsqlcipher non-functional */ sqlite3_key(nullptr, nullptr, 0); #endif // preset the pointer because we need it during the course of this constructor kmymoney = this; d->m_config = KSharedConfig::openConfig(); d->setThemedCSS(); MyMoneyTransactionFilter::setFiscalYearStart(KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()); QFrame* frame = new QFrame; frame->setFrameStyle(QFrame::NoFrame); // values for margin (11) and spacing(6) taken from KDialog implementation QBoxLayout* layout = new QBoxLayout(QBoxLayout::TopToBottom, frame); layout->setContentsMargins(2, 2, 2, 2); layout->setSpacing(6); { const auto customIconRelativePath = QString(QStringLiteral("icons/hicolor/16x16/actions/account-add.png")); #ifndef IS_APPIMAGE // find where our custom icons were installed based on an custom icon that we know should exist after installation auto customIconAbsolutePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, customIconRelativePath); if (customIconAbsolutePath.isEmpty()) { qWarning("Custom icons were not found in any of the following QStandardPaths::AppDataLocation:"); for (const auto &standardPath : QStandardPaths::standardLocations(QStandardPaths::AppDataLocation)) qWarning() << standardPath; } #else // according to https://docs.appimage.org/packaging-guide/ingredients.html#open-source-applications // QStandardPaths::AppDataLocation is unreliable on AppImages, so apply workaround here in case we fail to find icons QString customIconAbsolutePath; const auto appImageAppDataLocation = QString("%1%2%3").arg(QCoreApplication::applicationDirPath(), QString("/../share/kmymoney/"), customIconRelativePath); if (QFile::exists(appImageAppDataLocation )) { customIconAbsolutePath = appImageAppDataLocation ; } else { qWarning("Custom icons were not found in the following location:"); qWarning() << appImageAppDataLocation ; } #endif // add our custom icons path to icons search path if (!customIconAbsolutePath.isEmpty()) { customIconAbsolutePath.chop(customIconRelativePath.length()); customIconAbsolutePath.append(QLatin1String("icons")); auto paths = QIcon::themeSearchPaths(); paths.append(customIconAbsolutePath); QIcon::setThemeSearchPaths(paths); } #if defined(Q_OS_WIN) || defined(Q_OS_MACOS) QString themeName = QLatin1Literal("system"); // using QIcon::setThemeName on Craft build system causes icons to disappear #else QString themeName = KMyMoneySettings::iconsTheme(); // get theme user wants #endif if (!themeName.isEmpty() && themeName != QLatin1Literal("system")) // if it isn't default theme then set it QIcon::setThemeName(themeName); Icons::setIconThemeNames(QIcon::themeName()); // get whatever theme user ends up with and hope our icon names will fit that theme } initStatusBar(); pActions = initActions(); pMenus = initMenus(); d->m_myMoneyView = new KMyMoneyView; layout->addWidget(d->m_myMoneyView, 10); connect(d->m_myMoneyView, &KMyMoneyView::statusMsg, this, &KMyMoneyApp::slotStatusMsg); connect(d->m_myMoneyView, &KMyMoneyView::statusProgress, this, &KMyMoneyApp::slotStatusProgressBar); // Initialize kactivities resource instance #ifdef ENABLE_ACTIVITIES d->m_activityResourceInstance = new KActivities::ResourceInstance(window()->winId(), this); #endif const auto viewActions = d->m_myMoneyView->actionsToBeConnected(); actionCollection()->addActions(viewActions.values()); for (auto it = viewActions.cbegin(); it != viewActions.cend(); ++it) pActions.insert(it.key(), it.value()); /////////////////////////////////////////////////////////////////// // call inits to invoke all other construction parts readOptions(); // now initialize the plugin structure createInterfaces(); KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Load, pPlugins, this, guiFactory()); onlineJobAdministration::instance()->setOnlinePlugins(pPlugins.extended); d->m_myMoneyView->setOnlinePlugins(pPlugins.online); setCentralWidget(frame); connect(&d->m_proc, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotBackupHandleEvents())); // force to show the home page if the file is closed connect(pActions[Action::ViewTransactionDetail], &QAction::toggled, d->m_myMoneyView, &KMyMoneyView::slotShowTransactionDetail); d->m_backupState = BACKUP_IDLE; QLocale locale; for (auto const& weekDay: locale.weekdays()) { d->m_processingDays.setBit(static_cast(weekDay)); } d->m_autoSaveTimer = new QTimer(this); d->m_progressTimer = new QTimer(this); connect(d->m_autoSaveTimer, SIGNAL(timeout()), this, SLOT(slotAutoSave())); connect(d->m_progressTimer, SIGNAL(timeout()), this, SLOT(slotStatusProgressDone())); // connect the WebConnect server connect(d->m_webConnect, &WebConnect::gotUrl, this, &KMyMoneyApp::webConnectUrl); // setup the initial configuration slotUpdateConfiguration(QString()); // kickstart date change timer slotDateChanged(); d->fileAction(eKMyMoney::FileAction::Closed); } KMyMoneyApp::~KMyMoneyApp() { // delete cached objects since they are in the way // when unloading the plugins onlineJobAdministration::instance()->clearCaches(); // we need to unload all plugins before we destroy anything else KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Unload, pPlugins, this, guiFactory()); d->removeStorage(); #ifdef ENABLE_HOLIDAYS delete d->m_holidayRegion; #endif #ifdef ENABLE_ACTIVITIES delete d->m_activityResourceInstance; #endif + // destroy printer object + KMyMoneyPrinter::cleanup(); + // make sure all settings are written to disk KMyMoneySettings::self()->save(); delete d; } QUrl KMyMoneyApp::lastOpenedURL() { QUrl url = d->m_startDialog ? QUrl() : d->m_storageInfo.url; if (!url.isValid()) { url = QUrl::fromUserInput(readLastUsedFile()); } ready(); return url; } void KMyMoneyApp::slotInstallConsistencyCheckContextMenu() { // this code relies on the implementation of KMessageBox::informationList to add a context menu to that list, // please adjust it if it's necessary or rewrite the way the consistency check results are displayed if (QWidget* dialog = QApplication::activeModalWidget()) { if (QListWidget* widget = dialog->findChild()) { // give the user a hint that the data can be saved widget->setToolTip(i18n("This is the consistency check log, use the context menu to copy or save it.")); widget->setWhatsThis(widget->toolTip()); widget->setContextMenuPolicy(Qt::CustomContextMenu); connect(widget, SIGNAL(customContextMenuRequested(QPoint)), SLOT(slotShowContextMenuForConsistencyCheck(QPoint))); } } } void KMyMoneyApp::slotShowContextMenuForConsistencyCheck(const QPoint &pos) { // allow the user to save the consistency check results if (QWidget* widget = qobject_cast< QWidget* >(sender())) { QMenu contextMenu(widget); QAction* copy = new QAction(i18n("Copy to clipboard"), widget); QAction* save = new QAction(i18n("Save to file"), widget); contextMenu.addAction(copy); contextMenu.addAction(save); QAction *result = contextMenu.exec(widget->mapToGlobal(pos)); if (result == copy) { // copy the consistency check results to the clipboard d->copyConsistencyCheckResults(); } else if (result == save) { // save the consistency check results to a file d->saveConsistencyCheckResults(); } } } QHash KMyMoneyApp::initMenus() { QHash lutMenus; const QHash menuNames { {Menu::Institution, QStringLiteral("institution_context_menu")}, {Menu::Account, QStringLiteral("account_context_menu")}, {Menu::Schedule, QStringLiteral("schedule_context_menu")}, {Menu::Category, QStringLiteral("category_context_menu")}, {Menu::Tag, QStringLiteral("tag_context_menu")}, {Menu::Payee, QStringLiteral("payee_context_menu")}, {Menu::Investment, QStringLiteral("investment_context_menu")}, {Menu::Transaction, QStringLiteral("transaction_context_menu")}, {Menu::MoveTransaction, QStringLiteral("transaction_move_menu")}, {Menu::MarkTransaction, QStringLiteral("transaction_mark_menu")}, {Menu::MarkTransactionContext, QStringLiteral("transaction_context_mark_menu")}, {Menu::OnlineJob, QStringLiteral("onlinejob_context_menu")} }; for (auto it = menuNames.cbegin(); it != menuNames.cend(); ++it) lutMenus.insert(it.key(), qobject_cast(factory()->container(it.value(), this))); return lutMenus; } QHash KMyMoneyApp::initActions() { auto aC = actionCollection(); /* Look-up table for all custom and standard actions. It's required for: 1) building QList with QActions to be added to ActionCollection 2) adding custom features to QActions like e.g. keyboard shortcut */ QHash lutActions; // ************* // Adding standard actions // ************* KStandardAction::openNew(this, &KMyMoneyApp::slotFileNew, aC); KStandardAction::open(this, &KMyMoneyApp::slotFileOpen, aC); d->m_recentFiles = KStandardAction::openRecent(this, &KMyMoneyApp::slotFileOpenRecent, aC); KStandardAction::save(this, &KMyMoneyApp::slotFileSave, aC); KStandardAction::saveAs(this, &KMyMoneyApp::slotFileSaveAs, aC); KStandardAction::close(this, &KMyMoneyApp::slotFileClose, aC); KStandardAction::quit(this, &KMyMoneyApp::slotFileQuit, aC); lutActions.insert(Action::Print, KStandardAction::print(this, &KMyMoneyApp::slotPrintView, aC)); KStandardAction::preferences(this, &KMyMoneyApp::slotSettings, aC); // ************* // Adding all actions // ************* { // struct for creating useless (unconnected) QAction struct actionInfo { Action action; QString name; QString text; Icon icon; }; const QVector actionInfos { // ************* // The File menu // ************* {Action::FileBackup, QStringLiteral("file_backup"), i18n("Backup..."), Icon::Empty}, {Action::FileImportStatement, QStringLiteral("file_import_statement"), i18n("Statement file..."), Icon::Empty}, {Action::FileImportTemplate, QStringLiteral("file_import_template"), i18n("Account Template..."), Icon::Empty}, {Action::FileExportTemplate, QStringLiteral("file_export_template"), i18n("Account Template..."), Icon::Empty}, {Action::FilePersonalData, QStringLiteral("view_personal_data"), i18n("Personal Data..."), Icon::UserProperties}, #ifdef KMM_DEBUG {Action::FileDump, QStringLiteral("file_dump"), i18n("Dump Memory"), Icon::Empty}, #endif {Action::FileInformation, QStringLiteral("view_file_info"), i18n("File-Information..."), Icon::DocumentProperties}, // ************* // The Edit menu // ************* {Action::EditFindTransaction, QStringLiteral("edit_find_transaction"), i18n("Find transaction..."), Icon::EditFindTransaction}, // ************* // The View menu // ************* {Action::ViewTransactionDetail, QStringLiteral("view_show_transaction_detail"), i18n("Show Transaction Detail"), Icon::ViewTransactionDetail}, {Action::ViewHideReconciled, QStringLiteral("view_hide_reconciled_transactions"), i18n("Hide reconciled transactions"), Icon::HideReconciled}, {Action::ViewHideCategories, QStringLiteral("view_hide_unused_categories"), i18n("Hide unused categories"), Icon::HideCategories}, {Action::ViewShowAll, QStringLiteral("view_show_all_accounts"), i18n("Show all accounts"), Icon::Empty}, // ********************* // The institutions menu // ********************* {Action::NewInstitution, QStringLiteral("institution_new"), i18n("New institution..."), Icon::InstitutionNew}, {Action::EditInstitution, QStringLiteral("institution_edit"), i18n("Edit institution..."), Icon::InstitutionEdit}, {Action::DeleteInstitution, QStringLiteral("institution_delete"), i18n("Delete institution..."), Icon::InstitutionDelete}, // ***************** // The accounts menu // ***************** {Action::NewAccount, QStringLiteral("account_new"), i18n("New account..."), Icon::AccountNew}, {Action::OpenAccount, QStringLiteral("account_open"), i18n("Open ledger"), Icon::ViewFinancialList}, {Action::StartReconciliation, QStringLiteral("account_reconcile"), i18n("Reconcile..."), Icon::Reconcile}, {Action::FinishReconciliation, QStringLiteral("account_reconcile_finish"), i18nc("Finish reconciliation", "Finish"), Icon::AccountFinishReconciliation}, {Action::PostponeReconciliation, QStringLiteral("account_reconcile_postpone"), i18n("Postpone reconciliation"), Icon::MediaPlaybackPause}, {Action::EditAccount, QStringLiteral("account_edit"), i18n("Edit account..."), Icon::AccountEdit}, {Action::DeleteAccount, QStringLiteral("account_delete"), i18n("Delete account..."), Icon::AccountDelete}, {Action::CloseAccount, QStringLiteral("account_close"), i18n("Close account"), Icon::AccountClose}, {Action::ReopenAccount, QStringLiteral("account_reopen"), i18n("Reopen account"), Icon::AccountReopen}, {Action::ReportAccountTransactions, QStringLiteral("account_transaction_report"), i18n("Transaction report"), Icon::ViewFinancialList}, {Action::ChartAccountBalance, QStringLiteral("account_chart"), i18n("Show balance chart..."), Icon::OfficeChartLine}, {Action::MapOnlineAccount, QStringLiteral("account_online_map"), i18n("Map account..."), Icon::NewsSubscribe}, {Action::UnmapOnlineAccount, QStringLiteral("account_online_unmap"), i18n("Unmap account..."), Icon::NewsUnsubscribe}, {Action::UpdateAccount, QStringLiteral("account_online_update"), i18n("Update account..."), Icon::AccountUpdate}, {Action::UpdateAllAccounts, QStringLiteral("account_online_update_all"), i18n("Update all accounts..."), Icon::AccountUpdateAll}, {Action::AccountCreditTransfer, QStringLiteral("account_online_new_credit_transfer"), i18n("New credit transfer"), Icon::AccountCreditTransfer}, // ******************* // The categories menu // ******************* {Action::NewCategory, QStringLiteral("category_new"), i18n("New category..."), Icon::CategoryNew}, {Action::EditCategory, QStringLiteral("category_edit"), i18n("Edit category..."), Icon::CategoryEdit}, {Action::DeleteCategory, QStringLiteral("category_delete"), i18n("Delete category..."), Icon::CategoryDelete}, // ************** // The tools menu // ************** {Action::ToolCurrencies, QStringLiteral("tools_currency_editor"), i18n("Currencies..."), Icon::ViewCurrencyList}, {Action::ToolPrices, QStringLiteral("tools_price_editor"), i18n("Prices..."), Icon::Empty}, {Action::ToolUpdatePrices, QStringLiteral("tools_update_prices"), i18n("Update Stock and Currency Prices..."), Icon::ToolUpdatePrices}, {Action::ToolConsistency, QStringLiteral("tools_consistency_check"), i18n("Consistency Check"), Icon::Empty}, {Action::ToolPerformance, QStringLiteral("tools_performancetest"), i18n("Performance-Test"), Icon::Fork}, {Action::ToolCalculator, QStringLiteral("tools_kcalc"), i18n("Calculator..."), Icon::AccessoriesCalculator}, // ***************** // The settings menu // ***************** {Action::SettingsAllMessages, QStringLiteral("settings_enable_messages"), i18n("Enable all messages"), Icon::Empty}, // ************* // The help menu // ************* {Action::HelpShow, QStringLiteral("help_show_tip"), i18n("&Show tip of the day"), Icon::Tip}, // *************************** // Actions w/o main menu entry // *************************** {Action::NewTransaction, QStringLiteral("transaction_new"), i18nc("New transaction button", "New"), Icon::TransactionNew}, {Action::EditTransaction, QStringLiteral("transaction_edit"), i18nc("Edit transaction button", "Edit"), Icon::TransactionEdit}, {Action::EnterTransaction, QStringLiteral("transaction_enter"), i18nc("Enter transaction", "Enter"), Icon::DialogOK}, {Action::EditSplits, QStringLiteral("transaction_editsplits"), i18nc("Edit split button", "Edit splits"), Icon::Split}, {Action::CancelTransaction, QStringLiteral("transaction_cancel"), i18nc("Cancel transaction edit", "Cancel"), Icon::DialogCancel}, {Action::DeleteTransaction, QStringLiteral("transaction_delete"), i18nc("Delete transaction", "Delete"), Icon::EditDelete}, {Action::DuplicateTransaction, QStringLiteral("transaction_duplicate"), i18nc("Duplicate transaction", "Duplicate"), Icon::EditCopy}, {Action::MatchTransaction, QStringLiteral("transaction_match"), i18nc("Button text for match transaction", "Match"),Icon::TransactionMatch}, {Action::AcceptTransaction, QStringLiteral("transaction_accept"), i18nc("Accept 'imported' and 'matched' transaction", "Accept"), Icon::TransactionAccept}, {Action::ToggleReconciliationFlag, QStringLiteral("transaction_mark_toggle"), i18nc("Toggle reconciliation flag", "Toggle"), Icon::Empty}, {Action::MarkCleared, QStringLiteral("transaction_mark_cleared"), i18nc("Mark transaction cleared", "Cleared"), Icon::Empty}, {Action::MarkReconciled, QStringLiteral("transaction_mark_reconciled"), i18nc("Mark transaction reconciled", "Reconciled"), Icon::Empty}, {Action::MarkNotReconciled, QStringLiteral("transaction_mark_notreconciled"), i18nc("Mark transaction not reconciled", "Not reconciled"), Icon::Empty}, {Action::SelectAllTransactions, QStringLiteral("transaction_select_all"), i18nc("Select all transactions", "Select all"), Icon::Empty}, {Action::GoToAccount, QStringLiteral("transaction_goto_account"), i18n("Go to account"), Icon::GoJump}, {Action::GoToPayee, QStringLiteral("transaction_goto_payee"), i18n("Go to payee"), Icon::GoJump}, {Action::NewScheduledTransaction, QStringLiteral("transaction_create_schedule"), i18n("Create scheduled transaction..."), Icon::AppointmentNew}, {Action::AssignTransactionsNumber, QStringLiteral("transaction_assign_number"), i18n("Assign next number"), Icon::Empty}, {Action::CombineTransactions, QStringLiteral("transaction_combine"), i18nc("Combine transactions", "Combine"), Icon::Empty}, {Action::CopySplits, QStringLiteral("transaction_copy_splits"), i18n("Copy splits"), Icon::Empty}, //Investment {Action::NewInvestment, QStringLiteral("investment_new"), i18n("New investment..."), Icon::InvestmentNew}, {Action::EditInvestment, QStringLiteral("investment_edit"), i18n("Edit investment..."), Icon::InvestmentEdit}, {Action::DeleteInvestment, QStringLiteral("investment_delete"), i18n("Delete investment..."), Icon::InvestmentDelete}, {Action::UpdatePriceOnline, QStringLiteral("investment_online_price_update"), i18n("Online price update..."), Icon::InvestmentOnlinePrice}, {Action::UpdatePriceManually, QStringLiteral("investment_manual_price_update"), i18n("Manual price update..."), Icon::Empty}, //Schedule {Action::NewSchedule, QStringLiteral("schedule_new"), i18n("New scheduled transaction"), Icon::AppointmentNew}, {Action::EditSchedule, QStringLiteral("schedule_edit"), i18n("Edit scheduled transaction"), Icon::DocumentEdit}, {Action::DeleteSchedule, QStringLiteral("schedule_delete"), i18n("Delete scheduled transaction"), Icon::EditDelete}, {Action::DuplicateSchedule, QStringLiteral("schedule_duplicate"), i18n("Duplicate scheduled transaction"), Icon::EditCopy}, {Action::EnterSchedule, QStringLiteral("schedule_enter"), i18n("Enter next transaction..."), Icon::KeyEnter}, {Action::SkipSchedule, QStringLiteral("schedule_skip"), i18n("Skip next transaction..."), Icon::MediaSeekForward}, //Payees {Action::NewPayee, QStringLiteral("payee_new"), i18n("New payee"), Icon::ListAddUser}, {Action::RenamePayee, QStringLiteral("payee_rename"), i18n("Rename payee"), Icon::PayeeRename}, {Action::DeletePayee, QStringLiteral("payee_delete"), i18n("Delete payee"), Icon::ListRemoveUser}, {Action::MergePayee, QStringLiteral("payee_merge"), i18n("Merge payees"), Icon::PayeeMerge}, //Tags {Action::NewTag, QStringLiteral("tag_new"), i18n("New tag"), Icon::ListAddTag}, {Action::RenameTag, QStringLiteral("tag_rename"), i18n("Rename tag"), Icon::TagRename}, {Action::DeleteTag, QStringLiteral("tag_delete"), i18n("Delete tag"), Icon::ListRemoveTag}, //debug actions #ifdef KMM_DEBUG {Action::WizardNewUser, QStringLiteral("new_user_wizard"), i18n("Test new feature"), Icon::Empty}, {Action::DebugTraces, QStringLiteral("debug_traces"), i18n("Debug Traces"), Icon::Empty}, #endif {Action::DebugTimers, QStringLiteral("debug_timers"), i18n("Debug Timers"), Icon::Empty}, // onlineJob actions {Action::DeleteOnlineJob, QStringLiteral("onlinejob_delete"), i18n("Remove credit transfer"), Icon::EditDelete}, {Action::EditOnlineJob, QStringLiteral("onlinejob_edit"), i18n("Edit credit transfer"), Icon::DocumentEdit}, {Action::LogOnlineJob, QStringLiteral("onlinejob_log"), i18n("Show log"), Icon::Empty}, }; for (const auto& info : actionInfos) { auto a = new QAction(this); // KActionCollection::addAction by name sets object name anyways, // so, as better alternative, set it here right from the start a->setObjectName(info.name); a->setText(info.text); if (info.icon != Icon::Empty) // no need to set empty icon a->setIcon(Icons::get(info.icon)); a->setEnabled(false); lutActions.insert(info.action, a); // store QAction's pointer for later processing } } { // List with slots that get connected here. Other slots get connected in e.g. appropriate views typedef void(KMyMoneyApp::*KMyMoneyAppFunc)(); const QHash actionConnections { // ************* // The File menu // ************* // {Action::FileOpenDatabase, &KMyMoneyApp::slotOpenDatabase}, // {Action::FileSaveAsDatabase, &KMyMoneyApp::slotSaveAsDatabase}, {Action::FileBackup, &KMyMoneyApp::slotBackupFile}, {Action::FileImportTemplate, &KMyMoneyApp::slotLoadAccountTemplates}, {Action::FileExportTemplate, &KMyMoneyApp::slotSaveAccountTemplates}, {Action::FilePersonalData, &KMyMoneyApp::slotFileViewPersonal}, #ifdef KMM_DEBUG {Action::FileDump, &KMyMoneyApp::slotFileFileInfo}, #endif {Action::FileInformation, &KMyMoneyApp::slotFileInfoDialog}, // ************* // The View menu // ************* {Action::ViewTransactionDetail, &KMyMoneyApp::slotShowTransactionDetail}, {Action::ViewHideReconciled, &KMyMoneyApp::slotHideReconciledTransactions}, {Action::ViewHideCategories, &KMyMoneyApp::slotHideUnusedCategories}, {Action::ViewShowAll, &KMyMoneyApp::slotShowAllAccounts}, // ************** // The tools menu // ************** {Action::ToolCurrencies, &KMyMoneyApp::slotCurrencyDialog}, {Action::ToolPrices, &KMyMoneyApp::slotPriceDialog}, {Action::ToolUpdatePrices, &KMyMoneyApp::slotEquityPriceUpdate}, {Action::ToolConsistency, &KMyMoneyApp::slotFileConsistencyCheck}, {Action::ToolPerformance, &KMyMoneyApp::slotPerformanceTest}, // {Action::ToolSQL, &KMyMoneyApp::slotGenerateSql}, {Action::ToolCalculator, &KMyMoneyApp::slotToolsStartKCalc}, // ***************** // The settings menu // ***************** {Action::SettingsAllMessages, &KMyMoneyApp::slotEnableMessages}, // ************* // The help menu // ************* {Action::HelpShow, &KMyMoneyApp::slotShowTipOfTheDay}, // *************************** // Actions w/o main menu entry // *************************** //debug actions #ifdef KMM_DEBUG {Action::WizardNewUser, &KMyMoneyApp::slotNewFeature}, {Action::DebugTraces, &KMyMoneyApp::slotToggleTraces}, #endif {Action::DebugTimers, &KMyMoneyApp::slotToggleTimers}, }; for (auto connection = actionConnections.cbegin(); connection != actionConnections.cend(); ++connection) connect(lutActions[connection.key()], &QAction::triggered, this, connection.value()); } // ************* // Setting some of added actions checkable // ************* { // Some actions are checkable, // so set them here const QVector checkableActions { Action::ViewTransactionDetail, Action::ViewHideReconciled, Action::ViewHideCategories, #ifdef KMM_DEBUG Action::DebugTraces, Action::DebugTimers, #endif Action::ViewShowAll }; for (const auto& it : checkableActions) { lutActions[it]->setCheckable(true); lutActions[it]->setEnabled(true); } } // ************* // Setting actions that are always enabled // ************* { const QVector alwaysEnabled { Action::HelpShow, Action::SettingsAllMessages, Action::ToolPerformance, Action::ToolCalculator }; for (const auto& action : alwaysEnabled) { lutActions[action]->setEnabled(true); } } // ************* // Setting keyboard shortcuts for some of added actions // ************* { const QVector> actionShortcuts { {qMakePair(Action::EditFindTransaction, Qt::CTRL + Qt::Key_F)}, {qMakePair(Action::ViewTransactionDetail, Qt::CTRL + Qt::Key_T)}, {qMakePair(Action::ViewHideReconciled, Qt::CTRL + Qt::Key_R)}, {qMakePair(Action::ViewHideCategories, Qt::CTRL + Qt::Key_U)}, {qMakePair(Action::ViewShowAll, Qt::CTRL + Qt::SHIFT + Qt::Key_A)}, {qMakePair(Action::StartReconciliation, Qt::CTRL + Qt::SHIFT + Qt::Key_R)}, {qMakePair(Action::NewTransaction, Qt::CTRL + Qt::Key_Insert)}, {qMakePair(Action::ToggleReconciliationFlag, Qt::CTRL + Qt::Key_Space)}, {qMakePair(Action::MarkCleared, Qt::CTRL + Qt::ALT+ Qt::Key_Space)}, {qMakePair(Action::MarkReconciled, Qt::CTRL + Qt::SHIFT + Qt::Key_Space)}, {qMakePair(Action::SelectAllTransactions, Qt::CTRL + Qt::Key_A)}, #ifdef KMM_DEBUG {qMakePair(Action::WizardNewUser, Qt::CTRL + Qt::Key_G)}, #endif {qMakePair(Action::AssignTransactionsNumber, Qt::CTRL + Qt::SHIFT + Qt::Key_N)} }; for(const auto& it : actionShortcuts) aC->setDefaultShortcut(lutActions[it.first], it.second); } // ************* // Misc settings // ************* connect(onlineJobAdministration::instance(), &onlineJobAdministration::canSendCreditTransferChanged, lutActions.value(Action::AccountCreditTransfer), &QAction::setEnabled); // Setup transaction detail switch lutActions[Action::ViewTransactionDetail]->setChecked(KMyMoneySettings::showRegisterDetailed()); lutActions[Action::ViewHideReconciled]->setChecked(KMyMoneySettings::hideReconciledTransactions()); lutActions[Action::ViewHideCategories]->setChecked(KMyMoneySettings::hideUnusedCategory()); lutActions[Action::ViewShowAll]->setChecked(KMyMoneySettings::showAllAccounts()); // ************* // Adding actions to ActionCollection // ************* actionCollection()->addActions(lutActions.values()); // ************************ // Currently unused actions // ************************ #if 0 new KToolBarPopupAction(i18n("View back"), "go-previous", 0, this, SLOT(slotShowPreviousView()), actionCollection(), "go_back"); new KToolBarPopupAction(i18n("View forward"), "go-next", 0, this, SLOT(slotShowNextView()), actionCollection(), "go_forward"); action("go_back")->setEnabled(false); action("go_forward")->setEnabled(false); #endif // use the absolute path to your kmymoneyui.rc file for testing purpose in createGUI(); setupGUI(); // reconnect about app entry to dialog with full credits information auto aboutApp = aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::AboutApp))); aboutApp->disconnect(); connect(aboutApp, &QAction::triggered, this, &KMyMoneyApp::slotShowCredits); QMenu *menuContainer; menuContainer = static_cast(factory()->container(QStringLiteral("import"), this)); menuContainer->setIcon(Icons::get(Icon::DocumentImport)); menuContainer = static_cast(factory()->container(QStringLiteral("export"), this)); menuContainer->setIcon(Icons::get(Icon::DocumentExport)); return lutActions; } #ifdef KMM_DEBUG void KMyMoneyApp::dumpActions() const { const QList list = actionCollection()->actions(); foreach (const auto it, list) std::cout << qPrintable(it->objectName()) << ": " << qPrintable(it->text()) << std::endl; } #endif bool KMyMoneyApp::isActionToggled(const Action _a) { return pActions[_a]->isChecked(); } void KMyMoneyApp::initStatusBar() { /////////////////////////////////////////////////////////////////// // STATUSBAR d->m_statusLabel = new QLabel; statusBar()->addWidget(d->m_statusLabel); ready(); // Initialization of progress bar taken from KDevelop ;-) d->m_progressBar = new QProgressBar; statusBar()->addWidget(d->m_progressBar); d->m_progressBar->setFixedHeight(d->m_progressBar->sizeHint().height() - 8); // hide the progress bar for now slotStatusProgressBar(-1, -1); } void KMyMoneyApp::saveOptions() { KConfigGroup grp = d->m_config->group("General Options"); grp.writeEntry("Geometry", size()); grp.writeEntry("Show Statusbar", actionCollection()->action(KStandardAction::name(KStandardAction::ShowStatusbar))->isChecked()); KConfigGroup toolbarGrp = d->m_config->group("mainToolBar"); toolBar("mainToolBar")->saveSettings(toolbarGrp); d->m_recentFiles->saveEntries(d->m_config->group("Recent Files")); } void KMyMoneyApp::readOptions() { KConfigGroup grp = d->m_config->group("General Options"); pActions[Action::ViewHideReconciled]->setChecked(KMyMoneySettings::hideReconciledTransactions()); pActions[Action::ViewHideCategories]->setChecked(KMyMoneySettings::hideUnusedCategory()); d->m_recentFiles->loadEntries(d->m_config->group("Recent Files")); // Startdialog is written in the settings dialog d->m_startDialog = grp.readEntry("StartDialog", true); } #ifdef KMM_DEBUG void KMyMoneyApp::resizeEvent(QResizeEvent* ev) { KMainWindow::resizeEvent(ev); d->updateCaption(); } #endif bool KMyMoneyApp::queryClose() { if (!isReady()) return false; if (!slotFileClose()) return false; saveOptions(); return true; } ///////////////////////////////////////////////////////////////////// // SLOT IMPLEMENTATION ///////////////////////////////////////////////////////////////////// void KMyMoneyApp::slotFileInfoDialog() { QPointer dlg = new KMyMoneyFileInfoDlg(0); dlg->exec(); delete dlg; } void KMyMoneyApp::slotPerformanceTest() { // dump performance report to stderr int measurement[2]; QTime timer; MyMoneyAccount acc; qDebug("--- Starting performance tests ---"); // AccountList // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; timer.start(); for (int i = 0; i < 1000; ++i) { QList list; MyMoneyFile::instance()->accountList(list); measurement[i != 0] = timer.elapsed(); } std::cerr << "accountList()" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // Balance of asset account(s) // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->asset(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->balance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "balance(Asset)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // total balance of asset account // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->asset(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->totalBalance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "totalBalance(Asset)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // Balance of expense account(s) // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->expense(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->balance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "balance(Expense)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // total balance of expense account // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->expense(); timer.start(); for (int i = 0; i < 1000; ++i) { MyMoneyMoney result = MyMoneyFile::instance()->totalBalance(acc.id()); measurement[i != 0] = timer.elapsed(); } std::cerr << "totalBalance(Expense)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // transaction list // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; if (MyMoneyFile::instance()->asset().accountCount()) { MyMoneyTransactionFilter filter(MyMoneyFile::instance()->asset().accountList()[0]); filter.setDateFilter(QDate(), QDate::currentDate()); QList list; timer.start(); for (int i = 0; i < 100; ++i) { list = MyMoneyFile::instance()->transactionList(filter); measurement[i != 0] = timer.elapsed(); } std::cerr << "transactionList()" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 100 << " msec" << std::endl; } // transaction list // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; if (MyMoneyFile::instance()->asset().accountCount()) { MyMoneyTransactionFilter filter(MyMoneyFile::instance()->asset().accountList()[0]); filter.setDateFilter(QDate(), QDate::currentDate()); QList list; timer.start(); for (int i = 0; i < 100; ++i) { MyMoneyFile::instance()->transactionList(list, filter); measurement[i != 0] = timer.elapsed(); } std::cerr << "transactionList(list)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 100 << " msec" << std::endl; } // MyMoneyFile::instance()->preloadCache(); } bool KMyMoneyApp::isDatabase() { return (d->m_storageInfo.isOpened && ((d->m_storageInfo.type == eKMyMoney::StorageType::SQL))); } bool KMyMoneyApp::isNativeFile() { return (d->m_storageInfo.isOpened && (d->m_storageInfo.type == eKMyMoney::StorageType::SQL || d->m_storageInfo.type == eKMyMoney::StorageType::XML)); } bool KMyMoneyApp::fileOpen() const { return d->m_storageInfo.isOpened; } KMyMoneyAppCallback KMyMoneyApp::progressCallback() { return &KMyMoneyApp::progressCallback; } void KMyMoneyApp::consistencyCheck(bool alwaysDisplayResult) { d->consistencyCheck(alwaysDisplayResult); } bool KMyMoneyApp::isImportableFile(const QUrl &url) { bool result = false; // Iterate through the plugins and see if there's a loaded plugin who can handle it QMap::const_iterator it_plugin = pPlugins.importer.constBegin(); while (it_plugin != pPlugins.importer.constEnd()) { if ((*it_plugin)->isMyFormat(url.toLocalFile())) { result = true; break; } ++it_plugin; } // If we did not find a match, try importing it as a KMM statement file, // which is really just for testing. the statement file is not exposed // to users. if (it_plugin == pPlugins.importer.constEnd()) if (MyMoneyStatement::isStatementFile(url.path())) result = true; // Place code here to test for QIF and other locally-supported formats // (i.e. not a plugin). If you add them here, be sure to add it to // the webConnect function. return result; } bool KMyMoneyApp::isFileOpenedInAnotherInstance(const QUrl &url) { const auto instances = instanceList(); #ifdef KMM_DBUS // check if there are other instances which might have this file open for (const auto& instance : instances) { QDBusInterface remoteApp(instance, "/KMymoney", "org.kde.kmymoney"); QDBusReply reply = remoteApp.call("filename"); if (!reply.isValid()) qDebug("D-Bus error while calling app->filename()"); else if (reply.value() == url.url()) return true; } #else Q_UNUSED(url) #endif return false; } void KMyMoneyApp::slotShowTransactionDetail() { } void KMyMoneyApp::slotHideReconciledTransactions() { KMyMoneySettings::setHideReconciledTransactions(pActions[Action::ViewHideReconciled]->isChecked()); d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::slotHideUnusedCategories() { KMyMoneySettings::setHideUnusedCategory(pActions[Action::ViewHideCategories]->isChecked()); d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::slotShowAllAccounts() { KMyMoneySettings::setShowAllAccounts(pActions[Action::ViewShowAll]->isChecked()); d->m_myMoneyView->slotRefreshViews(); } #ifdef KMM_DEBUG void KMyMoneyApp::slotFileFileInfo() { if (!d->m_storageInfo.isOpened) { KMessageBox::information(this, i18n("No KMyMoneyFile open")); return; } QFile g("kmymoney.dump"); g.open(QIODevice::WriteOnly); QDataStream st(&g); MyMoneyStorageDump dumper; dumper.writeStream(st, MyMoneyFile::instance()->storage()); g.close(); } void KMyMoneyApp::slotToggleTraces() { MyMoneyTracer::onOff(pActions[Action::DebugTraces]->isChecked() ? 1 : 0); } #endif void KMyMoneyApp::slotToggleTimers() { extern bool timersOn; // main.cpp timersOn = pActions[Action::DebugTimers]->isChecked(); } QString KMyMoneyApp::slotStatusMsg(const QString &text) { /////////////////////////////////////////////////////////////////// // change status message permanently QString previousMessage = d->m_statusLabel->text(); d->m_applicationIsReady = false; QString currentMessage = text; if (currentMessage.isEmpty() || currentMessage == i18nc("Application is ready to use", "Ready.")) { d->m_applicationIsReady = true; currentMessage = i18nc("Application is ready to use", "Ready."); } statusBar()->clearMessage(); d->m_statusLabel->setText(currentMessage); return previousMessage; } void KMyMoneyApp::ready() { slotStatusMsg(QString()); } bool KMyMoneyApp::isReady() { return d->m_applicationIsReady; } void KMyMoneyApp::slotStatusProgressBar(int current, int total) { if (total == -1 && current == -1) { // reset if (d->m_progressTimer) { d->m_progressTimer->start(500); // remove from screen in 500 msec d->m_progressBar->setValue(d->m_progressBar->maximum()); } } else if (total != 0) { // init d->m_progressTimer->stop(); d->m_progressBar->setMaximum(total); d->m_progressBar->setValue(0); d->m_progressBar->show(); d->m_lastUpdate = QTime::currentTime(); } else { // update const auto currentTime = QTime::currentTime(); // only process painting if last update is at least 200 ms ago if (abs(d->m_lastUpdate.msecsTo(currentTime)) > 200) { d->m_progressBar->setValue(current); d->m_lastUpdate = currentTime; } } } void KMyMoneyApp::slotStatusProgressDone() { d->m_progressTimer->stop(); d->m_progressBar->reset(); d->m_progressBar->hide(); d->m_progressBar->setValue(0); } void KMyMoneyApp::progressCallback(int current, int total, const QString& msg) { if (!msg.isEmpty()) kmymoney->slotStatusMsg(msg); kmymoney->slotStatusProgressBar(current, total); } void KMyMoneyApp::slotFileViewPersonal() { if (!d->m_storageInfo.isOpened) { KMessageBox::information(this, i18n("No KMyMoneyFile open")); return; } KMSTATUS(i18n("Viewing personal data...")); MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyPayee user = file->user(); QPointer editPersonalDataDlg = new EditPersonalDataDlg(user.name(), user.address(), user.city(), user.state(), user.postcode(), user.telephone(), user.email(), this, i18n("Edit Personal Data")); if (editPersonalDataDlg->exec() == QDialog::Accepted && editPersonalDataDlg != 0) { user.setName(editPersonalDataDlg->userName()); user.setAddress(editPersonalDataDlg->userStreet()); user.setCity(editPersonalDataDlg->userTown()); user.setState(editPersonalDataDlg->userCountry()); user.setPostcode(editPersonalDataDlg->userPostcode()); user.setTelephone(editPersonalDataDlg->userTelephone()); user.setEmail(editPersonalDataDlg->userEmail()); MyMoneyFileTransaction ft; try { file->setUser(user); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to store user information: %1", QString::fromLatin1(e.what()))); } } delete editPersonalDataDlg; } void KMyMoneyApp::slotLoadAccountTemplates() { KMSTATUS(i18n("Importing account templates.")); QPointer dlg = new KLoadTemplateDlg(); if (dlg->exec() == QDialog::Accepted && dlg != 0) { MyMoneyFileTransaction ft; try { // import the account templates QList templates = dlg->templates(); QList::iterator it_t; for (it_t = templates.begin(); it_t != templates.end(); ++it_t) { (*it_t).importTemplate(progressCallback); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to import template(s)"), QString::fromLatin1(e.what())); } } delete dlg; } void KMyMoneyApp::slotSaveAccountTemplates() { KMSTATUS(i18n("Exporting account templates.")); QString savePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/templates/" + QLocale().name(); QDir templatesDir(savePath); if (!templatesDir.exists()) templatesDir.mkpath(savePath); QString newName = QFileDialog::getSaveFileName(this, i18n("Save as..."), savePath, i18n("KMyMoney template files (*.kmt);;All files (*)")); // // If there is no file extension, then append a .kmt at the end of the file name. // If there is a file extension, make sure it is .kmt, delete any others. // if (!newName.isEmpty()) { // find last . delimiter int nLoc = newName.lastIndexOf('.'); if (nLoc != -1) { QString strExt, strTemp; strTemp = newName.left(nLoc + 1); strExt = newName.right(newName.length() - (nLoc + 1)); if ((strExt.indexOf("kmt", 0, Qt::CaseInsensitive) == -1)) { strTemp.append("kmt"); //append to make complete file name newName = strTemp; } } else { newName.append(".kmt"); } if (okToWriteFile(QUrl::fromLocalFile(newName))) { QPointer dlg = new KTemplateExportDlg(this); if (dlg->exec() == QDialog::Accepted && dlg) { MyMoneyTemplate templ; templ.setTitle(dlg->title()); templ.setShortDescription(dlg->shortDescription()); templ.setLongDescription(dlg->longDescription()); templ.exportTemplate(progressCallback); templ.saveTemplate(QUrl::fromLocalFile(newName)); } delete dlg; } } } bool KMyMoneyApp::okToWriteFile(const QUrl &url) { Q_UNUSED(url) // check if the file exists and warn the user bool reallySaveFile = true; if (KMyMoneyUtils::fileExists(url)) { if (KMessageBox::warningYesNo(this, QLatin1String("") + i18n("The file %1 already exists. Do you really want to overwrite it?", url.toDisplayString(QUrl::PreferLocalFile)) + QLatin1String(""), i18n("File already exists")) != KMessageBox::Yes) reallySaveFile = false; } return reallySaveFile; } void KMyMoneyApp::slotSettings() { // if we already have an instance of the settings dialog, then use it if (KConfigDialog::showDialog("KMyMoney-Settings")) return; // otherwise, we have to create it auto dlg = new KSettingsKMyMoney(this, "KMyMoney-Settings", KMyMoneySettings::self()); connect(dlg, &KSettingsKMyMoney::settingsChanged, this, &KMyMoneyApp::slotUpdateConfiguration); dlg->show(); } void KMyMoneyApp::slotShowCredits() { KAboutData aboutData = initializeCreditsData(); KAboutApplicationDialog dlg(aboutData, this); dlg.exec(); } void KMyMoneyApp::slotUpdateConfiguration(const QString &dialogName) { if(dialogName.compare(QLatin1String("Plugins")) == 0) { KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Reorganize, pPlugins, this, guiFactory()); actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::SaveAs)))->setEnabled(d->canFileSaveAs()); onlineJobAdministration::instance()->updateActions(); onlineJobAdministration::instance()->setOnlinePlugins(pPlugins.extended); d->m_myMoneyView->setOnlinePlugins(pPlugins.online); d->updateActions(); d->m_myMoneyView->slotRefreshViews(); return; } MyMoneyTransactionFilter::setFiscalYearStart(KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()); #ifdef ENABLE_UNFINISHEDFEATURES LedgerSeparator::setFirstFiscalDate(KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()); #endif d->m_myMoneyView->updateViewType(); // update the sql storage module settings // MyMoneyStorageSql::setStartDate(KMyMoneySettings::startDate().date()); // update the report module settings MyMoneyReport::setLineWidth(KMyMoneySettings::lineWidth()); // update the holiday region configuration setHolidayRegion(KMyMoneySettings::holidayRegion()); d->m_myMoneyView->slotRefreshViews(); // re-read autosave configuration d->m_autoSaveEnabled = KMyMoneySettings::autoSaveFile(); d->m_autoSavePeriod = KMyMoneySettings::autoSavePeriod(); // stop timer if turned off but running if (d->m_autoSaveTimer->isActive() && !d->m_autoSaveEnabled) { d->m_autoSaveTimer->stop(); } // start timer if turned on and needed but not running if (!d->m_autoSaveTimer->isActive() && d->m_autoSaveEnabled && d->dirty()) { d->m_autoSaveTimer->setSingleShot(true); d->m_autoSaveTimer->start(d->m_autoSavePeriod * 60 * 1000); } d->setThemedCSS(); } void KMyMoneyApp::slotBackupFile() { // Save the file first so isLocalFile() works if (d->m_myMoneyView && d->dirty()) { if (KMessageBox::questionYesNo(this, i18n("The file must be saved first " "before it can be backed up. Do you want to continue?")) == KMessageBox::No) { return; } slotFileSave(); } if (d->m_storageInfo.url.isEmpty()) return; if (!d->m_storageInfo.url.isLocalFile()) { KMessageBox::sorry(this, i18n("The current implementation of the backup functionality only supports local files as source files. Your current source file is '%1'.", d->m_storageInfo.url.url()), i18n("Local files only")); return; } QPointer backupDlg = new KBackupDlg(this); int returncode = backupDlg->exec(); if (returncode == QDialog::Accepted && backupDlg != 0) { d->m_backupMount = backupDlg->mountCheckBoxChecked(); d->m_proc.clearProgram(); d->m_backupState = BACKUP_MOUNTING; d->m_mountpoint = backupDlg->mountPoint(); if (d->m_backupMount) { slotBackupMount(); } else { progressCallback(0, 300, ""); #ifdef Q_OS_WIN d->m_ignoreBackupExitCode = true; QTimer::singleShot(0, this, SLOT(slotBackupHandleEvents())); #else // If we don't have to mount a device, we just issue // a dummy command to start the copy operation d->m_proc.setProgram("true"); d->m_proc.start(); #endif } } delete backupDlg; } void KMyMoneyApp::slotBackupMount() { progressCallback(0, 300, i18n("Mounting %1", d->m_mountpoint)); d->m_proc.setProgram("mount"); d->m_proc << d->m_mountpoint; d->m_proc.start(); } bool KMyMoneyApp::slotBackupWriteFile() { QFileInfo fi(d->m_storageInfo.url.fileName()); QString today = QDate::currentDate().toString("-yyyy-MM-dd.") + fi.suffix(); QString backupfile = d->m_mountpoint + '/' + d->m_storageInfo.url.fileName(); KMyMoneyUtils::appendCorrectFileExt(backupfile, today); // check if file already exists and ask what to do QFile f(backupfile); if (f.exists()) { int answer = KMessageBox::warningContinueCancel(this, i18n("Backup file for today exists on that device. Replace?"), i18n("Backup"), KGuiItem(i18n("&Replace"))); if (answer == KMessageBox::Cancel) { return false; } } progressCallback(50, 0, i18n("Writing %1", backupfile)); d->m_proc.clearProgram(); #ifdef Q_OS_WIN d->m_proc << "cmd.exe" << "/c" << "copy" << "/b" << "/y"; d->m_proc << QDir::toNativeSeparators(d->m_storageInfo.url.toLocalFile()) << "+" << "nul" << QDir::toNativeSeparators(backupfile); #else d->m_proc << "cp" << "-f"; d->m_proc << d->m_storageInfo.url.toLocalFile() << backupfile; #endif d->m_backupState = BACKUP_COPYING; qDebug() << "Backup cmd:" << d->m_proc.program(); d->m_proc.start(); return true; } void KMyMoneyApp::slotBackupUnmount() { progressCallback(250, 0, i18n("Unmounting %1", d->m_mountpoint)); d->m_proc.clearProgram(); d->m_proc.setProgram("umount"); d->m_proc << d->m_mountpoint; d->m_backupState = BACKUP_UNMOUNTING; d->m_proc.start(); } void KMyMoneyApp::slotBackupFinish() { d->m_backupState = BACKUP_IDLE; progressCallback(-1, -1, QString()); ready(); } void KMyMoneyApp::slotBackupHandleEvents() { switch (d->m_backupState) { case BACKUP_MOUNTING: if (d->m_ignoreBackupExitCode || (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0)) { d->m_ignoreBackupExitCode = false; d->m_backupResult = 0; if (!slotBackupWriteFile()) { d->m_backupResult = 1; if (d->m_backupMount) slotBackupUnmount(); else slotBackupFinish(); } } else { KMessageBox::information(this, i18n("Error mounting device"), i18n("Backup")); d->m_backupResult = 1; if (d->m_backupMount) slotBackupUnmount(); else slotBackupFinish(); } break; case BACKUP_COPYING: if (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0) { if (d->m_backupMount) { slotBackupUnmount(); } else { progressCallback(300, 0, i18nc("Backup done", "Done")); KMessageBox::information(this, i18n("File successfully backed up"), i18n("Backup")); slotBackupFinish(); } } else { qDebug("copy exit code is %d", d->m_proc.exitCode()); d->m_backupResult = 1; KMessageBox::information(this, i18n("Error copying file to device"), i18n("Backup")); if (d->m_backupMount) slotBackupUnmount(); else slotBackupFinish(); } break; case BACKUP_UNMOUNTING: if (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0) { progressCallback(300, 0, i18nc("Backup done", "Done")); if (d->m_backupResult == 0) KMessageBox::information(this, i18n("File successfully backed up"), i18n("Backup")); } else { KMessageBox::information(this, i18n("Error unmounting device"), i18n("Backup")); } slotBackupFinish(); break; default: qWarning("Unknown state for backup operation!"); progressCallback(-1, -1, QString()); ready(); break; } } void KMyMoneyApp::slotShowTipOfTheDay() { KTipDialog::showTip(d->m_myMoneyView, "", true); } void KMyMoneyApp::slotShowPreviousView() { } void KMyMoneyApp::slotShowNextView() { } void KMyMoneyApp::slotViewSelected(View view) { KMyMoneySettings::setLastViewSelected(static_cast(view)); } void KMyMoneyApp::slotGenerateSql() { // QPointer editor = new KGenerateSqlDlg(this); // editor->setObjectName("Generate Database SQL"); // editor->exec(); // delete editor; } void KMyMoneyApp::slotToolsStartKCalc() { QString cmd = KMyMoneySettings::externalCalculator(); // if none is present, we fall back to the default if (cmd.isEmpty()) { #if defined(Q_OS_WIN32) cmd = QLatin1String("calc"); #elif defined(Q_OS_MAC) cmd = QLatin1String("open -a Calculator"); #else cmd = QLatin1String("kcalc"); #endif } KRun::runCommand(cmd, this); } void KMyMoneyApp::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal) { MyMoneyFile *file = MyMoneyFile::instance(); try { const MyMoneySecurity& sec = file->security(newAccount.currencyId()); // Check the opening balance if (openingBal.isPositive() && newAccount.accountGroup() == eMyMoney::Account::Type::Liability) { QString message = i18n("This account is a liability and if the " "opening balance represents money owed, then it should be negative. " "Negate the amount?\n\n" "Please click Yes to change the opening balance to %1,\n" "Please click No to leave the amount as %2,\n" "Please click Cancel to abort the account creation." , MyMoneyUtils::formatMoney(-openingBal, newAccount, sec) , MyMoneyUtils::formatMoney(openingBal, newAccount, sec)); int ans = KMessageBox::questionYesNoCancel(this, message); if (ans == KMessageBox::Yes) { openingBal = -openingBal; } else if (ans == KMessageBox::Cancel) return; } file->createAccount(newAccount, parentAccount, brokerageAccount, openingBal); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add account: %1", QString::fromLatin1(e.what()))); } } void KMyMoneyApp::slotInvestmentNew(MyMoneyAccount& account, const MyMoneyAccount& parent) { KNewInvestmentWizard::newInvestment(account, parent); } void KMyMoneyApp::slotCategoryNew(MyMoneyAccount& account, const MyMoneyAccount& parent) { KNewAccountDlg::newCategory(account, parent); } void KMyMoneyApp::slotCategoryNew(MyMoneyAccount& account) { KNewAccountDlg::newCategory(account, MyMoneyAccount()); } void KMyMoneyApp::slotAccountNew(MyMoneyAccount& account) { NewAccountWizard::Wizard::newAccount(account); } void KMyMoneyApp::createSchedule(MyMoneySchedule newSchedule, MyMoneyAccount& newAccount) { MyMoneyFile* file = MyMoneyFile::instance(); // Add the schedule only if one exists // // Remember to modify the first split to reference the newly created account if (!newSchedule.name().isEmpty()) { try { // We assume at least 2 splits in the transaction MyMoneyTransaction t = newSchedule.transaction(); if (t.splitCount() < 2) { throw MYMONEYEXCEPTION_CSTRING("Transaction for schedule has less than 2 splits!"); } MyMoneyFileTransaction ft; try { file->addSchedule(newSchedule); // in case of a loan account, we keep a reference to this // schedule in the account if (newAccount.accountType() == eMyMoney::Account::Type::Loan || newAccount.accountType() == eMyMoney::Account::Type::AssetLoan) { newAccount.setValue("schedule", newSchedule.id()); file->modifyAccount(newAccount); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add scheduled transaction: %1", QString::fromLatin1(e.what()))); } } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add scheduled transaction: %1", QString::fromLatin1(e.what()))); } } } void KMyMoneyApp::slotReparentAccount(const MyMoneyAccount& _src, const MyMoneyInstitution& _dst) { MyMoneyAccount src(_src); src.setInstitutionId(_dst.id()); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(src); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("

%1 cannot be moved to institution %2. Reason: %3

", src.name(), _dst.name(), QString::fromLatin1(e.what()))); } } void KMyMoneyApp::slotReparentAccount(const MyMoneyAccount& _src, const MyMoneyAccount& _dst) { MyMoneyAccount src(_src); MyMoneyAccount dst(_dst); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->reparentAccount(src, dst); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("

%1 cannot be moved to %2. Reason: %3

", src.name(), dst.name(), QString::fromLatin1(e.what()))); } } void KMyMoneyApp::slotScheduleNew(const MyMoneyTransaction& _t, eMyMoney::Schedule::Occurrence occurrence) { KEditScheduleDlg::newSchedule(_t, occurrence); } void KMyMoneyApp::slotPayeeNew(const QString& newnameBase, QString& id) { KMyMoneyUtils::newPayee(newnameBase, id); } void KMyMoneyApp::slotNewFeature() { } // move a stock transaction from one investment account to another void KMyMoneyApp::Private::moveInvestmentTransaction(const QString& /*fromId*/, const QString& toId, const MyMoneyTransaction& tx) { MyMoneyAccount toInvAcc = MyMoneyFile::instance()->account(toId); MyMoneyTransaction t(tx); // first determine which stock we are dealing with. // fortunately, investment transactions have only one stock involved QString stockAccountId; QString stockSecurityId; MyMoneySplit s; foreach (const auto split, t.splits()) { stockAccountId = split.accountId(); stockSecurityId = MyMoneyFile::instance()->account(stockAccountId).currencyId(); if (!MyMoneyFile::instance()->security(stockSecurityId).isCurrency()) { s = split; break; } } // Now check the target investment account to see if it // contains a stock with this id QString newStockAccountId; foreach (const auto sAccount, toInvAcc.accountList()) { if (MyMoneyFile::instance()->account(sAccount).currencyId() == stockSecurityId) { newStockAccountId = sAccount; break; } } // if it doesn't exist, we need to add it as a copy of the old one // no 'copyAccount()' function?? if (newStockAccountId.isEmpty()) { MyMoneyAccount stockAccount = MyMoneyFile::instance()->account(stockAccountId); MyMoneyAccount newStock; newStock.setName(stockAccount.name()); newStock.setNumber(stockAccount.number()); newStock.setDescription(stockAccount.description()); newStock.setInstitutionId(stockAccount.institutionId()); newStock.setOpeningDate(stockAccount.openingDate()); newStock.setAccountType(stockAccount.accountType()); newStock.setCurrencyId(stockAccount.currencyId()); newStock.setClosed(stockAccount.isClosed()); MyMoneyFile::instance()->addAccount(newStock, toInvAcc); newStockAccountId = newStock.id(); } // now update the split and the transaction s.setAccountId(newStockAccountId); t.modifySplit(s); MyMoneyFile::instance()->modifyTransaction(t); } void KMyMoneyApp::showContextMenu(const QString& containerName) { QWidget* w = factory()->container(containerName, this); if (auto menu = dynamic_cast(w)) menu->exec(QCursor::pos()); else qDebug("menu '%s' not found: w = %p, menu = %p", qPrintable(containerName), w, menu); } void KMyMoneyApp::slotPrintView() { d->m_myMoneyView->slotPrintView(); } void KMyMoneyApp::Private::updateCaption() { auto caption = m_storageInfo.url.isEmpty() && m_myMoneyView && m_storageInfo.isOpened ? i18n("Untitled") : m_storageInfo.url.fileName(); #ifdef KMM_DEBUG caption += QString(" (%1 x %2)").arg(q->width()).arg(q->height()); #endif q->setCaption(caption, MyMoneyFile::instance()->dirty()); } void KMyMoneyApp::Private::updateActions() { const QVector actions { Action::FilePersonalData, Action::FileInformation, Action::FileImportTemplate, Action::FileExportTemplate, #ifdef KMM_DEBUG Action::FileDump, #endif Action::EditFindTransaction, Action::NewCategory, Action::ToolCurrencies, Action::ToolPrices, Action::ToolUpdatePrices, Action::ToolConsistency, Action::ToolPerformance, Action::NewAccount, Action::NewInstitution, Action::NewSchedule }; for (const auto &action : actions) pActions[action]->setEnabled(m_storageInfo.isOpened); pActions[Action::FileBackup]->setEnabled(m_storageInfo.isOpened && m_storageInfo.type == eKMyMoney::StorageType::XML); auto aC = q->actionCollection(); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::SaveAs)))->setEnabled(canFileSaveAs()); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Close)))->setEnabled(m_storageInfo.isOpened); pActions[eMenu::Action::UpdateAllAccounts]->setEnabled(KMyMoneyUtils::canUpdateAllAccounts()); } bool KMyMoneyApp::Private::canFileSaveAs() const { return (m_storageInfo.isOpened && (!pPlugins.storage.isEmpty() && !(pPlugins.storage.count() == 1 && pPlugins.storage.first()->storageType() == eKMyMoney::StorageType::GNC))); } void KMyMoneyApp::slotDataChanged() { d->fileAction(eKMyMoney::FileAction::Changed); } void KMyMoneyApp::slotCurrencyDialog() { QPointer dlg = new KCurrencyEditDlg(this); dlg->exec(); delete dlg; } void KMyMoneyApp::slotPriceDialog() { QPointer dlg = new KMyMoneyPriceDlg(this); dlg->exec(); delete dlg; } void KMyMoneyApp::slotFileConsistencyCheck() { d->consistencyCheck(true); } void KMyMoneyApp::Private::consistencyCheck(bool alwaysDisplayResult) { KMSTATUS(i18n("Running consistency check...")); MyMoneyFileTransaction ft; try { m_consistencyCheckResult = MyMoneyFile::instance()->consistencyCheck(); ft.commit(); } catch (const MyMoneyException &e) { m_consistencyCheckResult.append(i18n("Consistency check failed: %1", e.what())); // always display the result if the check failed alwaysDisplayResult = true; } // in case the consistency check was OK, we get a single line as result // in all erroneous cases, we get more than one line and force the // display of them. if (alwaysDisplayResult || m_consistencyCheckResult.size() > 1) { QString msg = i18n("The consistency check has found no issues in your data. Details are presented below."); if (m_consistencyCheckResult.size() > 1) msg = i18n("The consistency check has found some issues in your data. Details are presented below. Those issues that could not be corrected automatically need to be solved by the user."); // install a context menu for the list after the dialog is displayed QTimer::singleShot(500, q, SLOT(slotInstallConsistencyCheckContextMenu())); KMessageBox::informationList(0, msg, m_consistencyCheckResult, i18n("Consistency check result")); } // this data is no longer needed m_consistencyCheckResult.clear(); } void KMyMoneyApp::Private::copyConsistencyCheckResults() { QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(m_consistencyCheckResult.join(QLatin1String("\n"))); } void KMyMoneyApp::Private::saveConsistencyCheckResults() { QUrl fileUrl = QFileDialog::getSaveFileUrl(q); if (!fileUrl.isEmpty()) { QFile file(fileUrl.toLocalFile()); if (file.open(QFile::WriteOnly | QFile::Append | QFile::Text)) { QTextStream out(&file); out << m_consistencyCheckResult.join(QLatin1String("\n")); file.close(); } } } void KMyMoneyApp::Private::setThemedCSS() { const QStringList CSSnames {QStringLiteral("kmymoney.css"), QStringLiteral("welcome.css")}; const QString rcDir("/html/"); QStringList defaultCSSDirs; #ifndef IS_APPIMAGE defaultCSSDirs = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, rcDir, QStandardPaths::LocateDirectory); #else // according to https://docs.appimage.org/packaging-guide/ingredients.html#open-source-applications // QStandardPaths::AppDataLocation is unreliable on AppImages, so apply workaround here in case we fail to find icons // watch out for QStringBuilder here; for yet unknown reason it causes segmentation fault on startup const auto appImageAppDataLocation = QString("%1%2%3").arg(QCoreApplication::applicationDirPath(), QString("/../share/kmymoney"), rcDir); if (QFile::exists(appImageAppDataLocation + CSSnames.first())) { defaultCSSDirs.append(appImageAppDataLocation); } else { qWarning("CSS file was not found in the following location:"); qWarning() << appImageAppDataLocation; } #endif // scan the list of directories to find the ones that really // contains all files we look for QString defaultCSSDir; foreach (const auto dir, defaultCSSDirs) { defaultCSSDir = dir; foreach (const auto CSSname, CSSnames) { QFileInfo fileInfo(defaultCSSDir + CSSname); if (!fileInfo.exists()) { defaultCSSDir.clear(); break; } } if (!defaultCSSDir.isEmpty()) { break; } } // make sure we have the local directory where the themed version is stored const QString themedCSSDir = QStandardPaths::standardLocations(QStandardPaths::AppConfigLocation).first() + rcDir; QDir().mkpath(themedCSSDir); foreach (const auto CSSname, CSSnames) { const QString defaultCSSFilename = defaultCSSDir + CSSname; QFileInfo fileInfo(defaultCSSFilename); if (fileInfo.exists()) { const QString themedCSSFilename = themedCSSDir + CSSname; QFile::remove(themedCSSFilename); if (QFile::copy(defaultCSSFilename, themedCSSFilename)) { QFile cssFile (themedCSSFilename); if (cssFile.open(QIODevice::ReadWrite)) { QTextStream cssStream(&cssFile); auto cssText = cssStream.readAll(); cssText.replace(QLatin1String("./"), defaultCSSDir, Qt::CaseSensitive); cssText.replace(QLatin1String("WindowText"), KMyMoneySettings::schemeColor(SchemeColor::WindowText).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("Window"), KMyMoneySettings::schemeColor(SchemeColor::WindowBackground).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("HighlightText"), KMyMoneySettings::schemeColor(SchemeColor::ListHighlightText).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("Highlight"), KMyMoneySettings::schemeColor(SchemeColor::ListHighlight).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("black"), KMyMoneySettings::schemeColor(SchemeColor::ListGrid).name(), Qt::CaseSensitive); cssStream.seek(0); cssStream << cssText; cssFile.close(); } } } } } void KMyMoneyApp::slotCheckSchedules() { if (KMyMoneySettings::checkSchedule() == true) { KMSTATUS(i18n("Checking for overdue scheduled transactions...")); MyMoneyFile *file = MyMoneyFile::instance(); QDate checkDate = QDate::currentDate().addDays(KMyMoneySettings::checkSchedulePreview()); QList scheduleList = file->scheduleList(); QList::Iterator it; eDialogs::ScheduleResultCode rc = eDialogs::ScheduleResultCode::Enter; for (it = scheduleList.begin(); (it != scheduleList.end()) && (rc != eDialogs::ScheduleResultCode::Cancel); ++it) { // Get the copy in the file because it might be modified by commitTransaction MyMoneySchedule schedule = file->schedule((*it).id()); if (schedule.autoEnter()) { try { while (!schedule.isFinished() && (schedule.adjustedNextDueDate() <= checkDate) && rc != eDialogs::ScheduleResultCode::Ignore && rc != eDialogs::ScheduleResultCode::Cancel) { rc = d->m_myMoneyView->enterSchedule(schedule, true, true); schedule = file->schedule((*it).id()); // get a copy of the modified schedule } } catch (const MyMoneyException &) { } } if (rc == eDialogs::ScheduleResultCode::Ignore) { // if the current schedule was ignored then we must make sure that the user can still enter the next scheduled transaction rc = eDialogs::ScheduleResultCode::Enter; } } } } void KMyMoneyApp::writeLastUsedDir(const QString& directory) { //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = kconfig->group("General Options"); //write path entry, no error handling since its void. grp.writeEntry("LastUsedDirectory", directory); } } void KMyMoneyApp::writeLastUsedFile(const QString& fileName) { //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); // write path entry, no error handling since its void. // use a standard string, as fileName could contain a protocol // e.g. file:/home/thb/.... grp.writeEntry("LastUsedFile", fileName); } } QString KMyMoneyApp::readLastUsedDir() const { QString str; //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); //read path entry. Second parameter is the default if the setting is not found, which will be the default document path. str = grp.readEntry("LastUsedDirectory", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); // if the path stored is empty, we use the default nevertheless if (str.isEmpty()) str = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); } return str; } QString KMyMoneyApp::readLastUsedFile() const { QString str; // get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); // read filename entry. str = grp.readEntry("LastUsedFile", ""); } return str; } QString KMyMoneyApp::filename() const { return d->m_storageInfo.url.url(); } QUrl KMyMoneyApp::filenameURL() const { return d->m_storageInfo.url; } void KMyMoneyApp::writeFilenameURL(const QUrl &url) { d->m_storageInfo.url = url; } void KMyMoneyApp::addToRecentFiles(const QUrl& url) { d->m_recentFiles->addUrl(url); } QTimer* KMyMoneyApp::autosaveTimer() { return d->m_autoSaveTimer; } WebConnect* KMyMoneyApp::webConnect() const { return d->m_webConnect; } QList KMyMoneyApp::instanceList() const { QList list; #ifdef KMM_DBUS QDBusReply reply = QDBusConnection::sessionBus().interface()->registeredServiceNames(); if (reply.isValid()) { QStringList apps = reply.value(); QStringList::ConstIterator it; // build a list of service names of all running kmymoney applications without this one for (it = apps.constBegin(); it != apps.constEnd(); ++it) { // please change this method of creating a list of 'all the other kmymoney instances that are running on the system' // since assuming that D-Bus creates service names with org.kde.kmymoney-PID is an observation I don't think that it's documented somewhere if ((*it).indexOf("org.kde.kmymoney-") == 0) { uint thisProcPid = platformTools::processId(); if ((*it).indexOf(QString("org.kde.kmymoney-%1").arg(thisProcPid)) != 0) list += (*it); } } } else { qDebug("D-Bus returned the following error while obtaining instances: %s", qPrintable(reply.error().message())); } #endif return list; } void KMyMoneyApp::slotEquityPriceUpdate() { QPointer dlg = new KEquityPriceUpdateDlg(this); if (dlg->exec() == QDialog::Accepted && dlg != 0) dlg->storePrices(); delete dlg; } void KMyMoneyApp::webConnectUrl(const QUrl url) { QMetaObject::invokeMethod(this, "webConnect", Qt::QueuedConnection, Q_ARG(QString, url.toLocalFile()), Q_ARG(QByteArray, QByteArray())); } void KMyMoneyApp::webConnect(const QString& sourceUrl, const QByteArray& asn_id) { // // Web connect attempts to go through the known importers and see if the file // can be importing using that method. If so, it will import it using that // plugin // Q_UNUSED(asn_id) d->m_importUrlsQueue.enqueue(sourceUrl); // only start processing if this is the only import so far if (d->m_importUrlsQueue.count() == 1) { MyMoneyStatementReader::clearResultMessages(); auto statementCount = 0; while (!d->m_importUrlsQueue.isEmpty()) { ++statementCount; // get the value of the next item from the queue // but leave it on the queue for now QString url = d->m_importUrlsQueue.head(); // Bring this window to the forefront. This method was suggested by // Lubos Lunak of the KDE core development team. //KStartupInfo::setNewStartupId(this, asn_id); // Make sure we have an open file if (! d->m_storageInfo.isOpened && KMessageBox::warningContinueCancel(this, i18n("You must first select a KMyMoney file before you can import a statement.")) == KMessageBox::Continue) slotFileOpen(); // only continue if the user really did open a file. if (d->m_storageInfo.isOpened) { KMSTATUS(i18n("Importing a statement via Web Connect")); // remove the statement files d->unlinkStatementXML(); QMap::const_iterator it_plugin = pPlugins.importer.constBegin(); while (it_plugin != pPlugins.importer.constEnd()) { if ((*it_plugin)->isMyFormat(url)) { if (!(*it_plugin)->import(url) && !(*it_plugin)->lastError().isEmpty()) { KMessageBox::error(this, i18n("Unable to import %1 using %2 plugin. The plugin returned the following error: %3", url, (*it_plugin)->formatName(), (*it_plugin)->lastError()), i18n("Importing error")); } break; } ++it_plugin; } // If we did not find a match, try importing it as a KMM statement file, // which is really just for testing. the statement file is not exposed // to users. if (it_plugin == pPlugins.importer.constEnd()) if (MyMoneyStatement::isStatementFile(url)) MyMoneyStatementReader::importStatement(url, false, progressCallback); } // remove the current processed item from the queue d->m_importUrlsQueue.dequeue(); } KMyMoneyUtils::showStatementImportResult(MyMoneyStatementReader::resultMessages(), statementCount); } } void KMyMoneyApp::slotEnableMessages() { KMessageBox::enableAllMessages(); KMessageBox::information(this, i18n("All messages have been enabled."), i18n("All messages")); } void KMyMoneyApp::createInterfaces() { // Sets up the plugin interface KMyMoneyPlugin::pluginInterfaces().appInterface = new KMyMoneyPlugin::KMMAppInterface(this, this); KMyMoneyPlugin::pluginInterfaces().importInterface = new KMyMoneyPlugin::KMMImportInterface(this); KMyMoneyPlugin::pluginInterfaces().statementInterface = new KMyMoneyPlugin::KMMStatementInterface(this); KMyMoneyPlugin::pluginInterfaces().viewInterface = new KMyMoneyPlugin::KMMViewInterface(d->m_myMoneyView, this); // setup the calendar interface for schedules MyMoneySchedule::setProcessingCalendar(this); } void KMyMoneyApp::slotAutoSave() { if (!d->m_inAutoSaving) { // store the focus widget so we can restore it after save QPointer focusWidget = qApp->focusWidget(); d->m_inAutoSaving = true; KMSTATUS(i18n("Auto saving...")); //calls slotFileSave if needed, and restart the timer //it the file is not saved, reinitializes the countdown. if (d->dirty() && d->m_autoSaveEnabled) { if (!slotFileSave() && d->m_autoSavePeriod > 0) { d->m_autoSaveTimer->setSingleShot(true); d->m_autoSaveTimer->start(d->m_autoSavePeriod * 60 * 1000); } } d->m_inAutoSaving = false; if (focusWidget && focusWidget != qApp->focusWidget()) { // we have a valid focus widget so restore it focusWidget->setFocus(); } } } void KMyMoneyApp::slotDateChanged() { QDateTime dt = QDateTime::currentDateTime(); QDateTime nextDay(QDate(dt.date().addDays(1)), QTime(0, 0, 0)); // +1 is to make sure that we're already in the next day when the // signal is sent (this way we also avoid setting the timer to 0) QTimer::singleShot((static_cast(dt.secsTo(nextDay)) + 1)*1000, this, SLOT(slotDateChanged())); d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::setHolidayRegion(const QString& holidayRegion) { #ifdef ENABLE_HOLIDAYS //since the cost of updating the cache is now not negligible //check whether the region has been modified if (!d->m_holidayRegion || d->m_holidayRegion->regionCode() != holidayRegion) { // Delete the previous holidayRegion before creating a new one. delete d->m_holidayRegion; // Create a new holidayRegion. d->m_holidayRegion = new KHolidays::HolidayRegion(holidayRegion); //clear and update the holiday cache preloadHolidays(); } #else Q_UNUSED(holidayRegion); #endif } bool KMyMoneyApp::isProcessingDate(const QDate& date) const { if (!d->m_processingDays.testBit(date.dayOfWeek())) return false; #ifdef ENABLE_HOLIDAYS if (!d->m_holidayRegion || !d->m_holidayRegion->isValid()) return true; //check first whether it's already in cache if (d->m_holidayMap.contains(date)) { return d->m_holidayMap.value(date, true); } else { bool processingDay = !d->m_holidayRegion->isHoliday(date); d->m_holidayMap.insert(date, processingDay); return processingDay; } #else return true; #endif } void KMyMoneyApp::preloadHolidays() { #ifdef ENABLE_HOLIDAYS //clear the cache before loading d->m_holidayMap.clear(); //only do this if it is a valid region if (d->m_holidayRegion && d->m_holidayRegion->isValid()) { //load holidays for the forecast days plus 1 cycle, to be on the safe side auto forecastDays = KMyMoneySettings::forecastDays() + KMyMoneySettings::forecastAccountCycle(); QDate endDate = QDate::currentDate().addDays(forecastDays); //look for holidays for the next 2 years as a minimum. That should give a good margin for the cache if (endDate < QDate::currentDate().addYears(2)) endDate = QDate::currentDate().addYears(2); KHolidays::Holiday::List holidayList = d->m_holidayRegion->holidays(QDate::currentDate(), endDate); KHolidays::Holiday::List::const_iterator holiday_it; for (holiday_it = holidayList.constBegin(); holiday_it != holidayList.constEnd(); ++holiday_it) { for (QDate holidayDate = (*holiday_it).observedStartDate(); holidayDate <= (*holiday_it).observedEndDate(); holidayDate = holidayDate.addDays(1)) d->m_holidayMap.insert(holidayDate, false); } for (QDate date = QDate::currentDate(); date <= endDate; date = date.addDays(1)) { //if it is not a processing day, set it to false if (!d->m_processingDays.testBit(date.dayOfWeek())) { d->m_holidayMap.insert(date, false); } else if (!d->m_holidayMap.contains(date)) { //if it is not a holiday nor a weekend, it is a processing day d->m_holidayMap.insert(date, true); } } } #endif } bool KMyMoneyApp::slotFileNew() { KMSTATUS(i18n("Creating new document...")); if (!slotFileClose()) return false; NewUserWizard::Wizard wizard; if (wizard.exec() != QDialog::Accepted) return false; d->m_storageInfo.isOpened = true; d->m_storageInfo.type = eKMyMoney::StorageType::None; d->m_storageInfo.url = QUrl(); try { auto storage = new MyMoneyStorageMgr; MyMoneyFile::instance()->attachStorage(storage); MyMoneyFileTransaction ft; auto file = MyMoneyFile::instance(); // store the user info file->setUser(wizard.user()); // create and setup base currency file->addCurrency(wizard.baseCurrency()); file->setBaseCurrency(wizard.baseCurrency()); // create a possible institution MyMoneyInstitution inst = wizard.institution(); if (inst.name().length()) { file->addInstitution(inst); } // create a possible checking account auto acc = wizard.account(); if (acc.name().length()) { acc.setInstitutionId(inst.id()); MyMoneyAccount asset = file->asset(); file->addAccount(acc, asset); // create possible opening balance transaction if (!wizard.openingBalance().isZero()) { file->createOpeningBalanceTransaction(acc, wizard.openingBalance()); } } // import the account templates for (auto &tmpl : wizard.templates()) tmpl.importTemplate(progressCallback); ft.commit(); KMyMoneySettings::setFirstTimeRun(false); d->fileAction(eKMyMoney::FileAction::Opened); if (actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::SaveAs)))->isEnabled()) slotFileSaveAs(); } catch (const MyMoneyException & e) { slotFileClose(); d->removeStorage(); KMessageBox::detailedError(this, i18n("Couldn't create a new file."), e.what()); return false; } if (wizard.startSettingsAfterFinished()) slotSettings(); return true; } void KMyMoneyApp::slotFileOpen() { KMSTATUS(i18n("Open a file.")); const QVector desiredFileExtensions {eKMyMoney::StorageType::XML, eKMyMoney::StorageType::GNC}; QString fileExtensions; for (const auto &extension : desiredFileExtensions) { for (const auto &plugin : pPlugins.storage) { if (plugin->storageType() == extension) { fileExtensions += plugin->fileExtension() + QLatin1String(";;"); break; } } } if (fileExtensions.isEmpty()) { KMessageBox::error(this, i18n("Couldn't find any plugin for opening storage.")); return; } fileExtensions.append(i18n("All files (*)")); QPointer dialog = new QFileDialog(this, QString(), readLastUsedDir(), fileExtensions); dialog->setFileMode(QFileDialog::ExistingFile); dialog->setAcceptMode(QFileDialog::AcceptOpen); if (dialog->exec() == QDialog::Accepted && dialog != nullptr) slotFileOpenRecent(dialog->selectedUrls().first()); delete dialog; } bool KMyMoneyApp::slotFileOpenRecent(const QUrl &url) { KMSTATUS(i18n("Loading file...")); if (!url.isValid()) throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid URL %1").arg(qPrintable(url.url()))); if (isFileOpenedInAnotherInstance(url)) { KMessageBox::sorry(this, i18n("

File %1 is already opened in another instance of KMyMoney

", url.toDisplayString(QUrl::PreferLocalFile)), i18n("Duplicate open")); return false; } if (url.scheme() != QLatin1String("sql") && !KMyMoneyUtils::fileExists(url)) { KMessageBox::sorry(this, i18n("

%1 is either an invalid filename or the file does not exist. You can open another file or create a new one.

", url.toDisplayString(QUrl::PreferLocalFile)), i18n("File not found")); return false; } if (d->m_storageInfo.isOpened) if (!slotFileClose()) return false; // open the database d->m_storageInfo.type = eKMyMoney::StorageType::None; for (auto &plugin : pPlugins.storage) { try { if (auto pStorage = plugin->open(url)) { MyMoneyFile::instance()->attachStorage(pStorage); d->m_storageInfo.type = plugin->storageType(); if (plugin->storageType() != eKMyMoney::StorageType::GNC) { d->m_storageInfo.url = plugin->openUrl(); writeLastUsedFile(url.toDisplayString(QUrl::PreferLocalFile)); /* Don't use url variable after KRecentFilesAction::addUrl * as it might delete it. * More in API reference to this method */ d->m_recentFiles->addUrl(url); } d->m_storageInfo.isOpened = true; break; } } catch (const MyMoneyException &e) { KMessageBox::detailedError(this, i18n("Cannot open file as requested."), QString::fromLatin1(e.what())); return false; } } if(d->m_storageInfo.type == eKMyMoney::StorageType::None) { KMessageBox::error(this, i18n("Could not read your data source. Please check the KMyMoney settings that the necessary plugin is enabled.")); return false; } d->fileAction(eKMyMoney::FileAction::Opened); return true; } bool KMyMoneyApp::slotFileSave() { KMSTATUS(i18n("Saving file...")); for (const auto& plugin : pPlugins.storage) { if (plugin->storageType() == d->m_storageInfo.type) { d->consistencyCheck(false); try { if (plugin->save(d->m_storageInfo.url)) { d->fileAction(eKMyMoney::FileAction::Saved); return true; } return false; } catch (const MyMoneyException &e) { KMessageBox::detailedError(this, i18n("Failed to save your storage."), e.what()); return false; } } } KMessageBox::error(this, i18n("Couldn't find suitable plugin to save your storage.")); return false; } bool KMyMoneyApp::slotFileSaveAs() { KMSTATUS(i18n("Saving file as....")); QVector availableFileTypes; for (const auto& plugin : pPlugins.storage) { switch (plugin->storageType()) { case eKMyMoney::StorageType::GNC: break; default: availableFileTypes.append(plugin->storageType()); break; } } auto chosenFileType = eKMyMoney::StorageType::None; switch (availableFileTypes.count()) { case 0: KMessageBox::error(this, i18n("Couldn't find any plugin for saving storage.")); return false; case 1: chosenFileType = availableFileTypes.first(); break; default: { QPointer dlg = new KSaveAsQuestion(availableFileTypes, this); auto rc = dlg->exec(); if (dlg) { auto fileType = dlg->fileType(); delete dlg; if (rc != QDialog::Accepted) return false; chosenFileType = fileType; } } } for (const auto &plugin : pPlugins.storage) { if (chosenFileType == plugin->storageType()) { try { d->consistencyCheck(false); if (plugin->saveAs()) { d->fileAction(eKMyMoney::FileAction::Saved); d->m_storageInfo.type = plugin->storageType(); return true; } } catch (const MyMoneyException &e) { KMessageBox::detailedError(this, i18n("Failed to save your storage."), e.what()); } } } return false; } bool KMyMoneyApp::slotFileClose() { if (!d->m_storageInfo.isOpened) return true; if (!d->askAboutSaving()) return false; d->fileAction(eKMyMoney::FileAction::Closing); d->removeStorage(); d->m_storageInfo = KMyMoneyApp::Private::storageInfo(); d->fileAction(eKMyMoney::FileAction::Closed); return true; } void KMyMoneyApp::slotFileQuit() { // don't modify the status message here as this will prevent quit from working!! // See the beginning of queryClose() and isReady() why. Thomas Baumgart 2005-10-17 bool quitApplication = true; QList memberList = KMainWindow::memberList(); if (!memberList.isEmpty()) { QList::const_iterator w_it = memberList.constBegin(); for (; w_it != memberList.constEnd(); ++w_it) { // only close the window if the closeEvent is accepted. If the user presses Cancel on the saveModified() dialog, // the window and the application stay open. if (!(*w_it)->close()) { quitApplication = false; break; } } } // We will only quit if all windows were processed and not cancelled if (quitApplication) { QCoreApplication::quit(); } } void KMyMoneyApp::Private::fileAction(eKMyMoney::FileAction action) { switch(action) { case eKMyMoney::FileAction::Opened: q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(false); updateAccountNames(); updateCurrencyNames(); selectBaseCurrency(); // setup the standard precision AmountEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); KMyMoneyEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); applyFileFixes(); Models::instance()->fileOpened(); connectStorageToModels(); // inform everyone about new data MyMoneyFile::instance()->forceDataChanged(); // Enable save in case the fix changed the contents q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(dirty()); updateActions(); m_myMoneyView->slotFileOpened(); onlineJobAdministration::instance()->updateActions(); m_myMoneyView->enableViewsIfFileOpen(m_storageInfo.isOpened); m_myMoneyView->slotRefreshViews(); onlineJobAdministration::instance()->updateOnlineTaskProperties(); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); #ifdef ENABLE_ACTIVITIES { // make sure that we don't store the DB password in activity QUrl url(m_storageInfo.url); url.setPassword(QString()); m_activityResourceInstance->setUri(url); } #endif // start the check for scheduled transactions that need to be // entered as soon as the event loop becomes active. QMetaObject::invokeMethod(q, "slotCheckSchedules", Qt::QueuedConnection); // make sure to catch view activations connect(m_myMoneyView, &KMyMoneyView::viewActivated, q, &KMyMoneyApp::slotViewSelected); break; case eKMyMoney::FileAction::Saved: q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(false); m_autoSaveTimer->stop(); break; case eKMyMoney::FileAction::Closing: disconnect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); // make sure to not catch view activations anymore disconnect(m_myMoneyView, &KMyMoneyView::viewActivated, q, &KMyMoneyApp::slotViewSelected); m_myMoneyView->slotFileClosed(); // notify the models that the file is going to be closed (we should have something like dataChanged that reaches the models first) Models::instance()->fileClosed(); break; case eKMyMoney::FileAction::Closed: q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); disconnectStorageFromModels(); q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(false); m_myMoneyView->enableViewsIfFileOpen(m_storageInfo.isOpened); updateActions(); break; case eKMyMoney::FileAction::Changed: q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(true && !m_storageInfo.url.isEmpty()); // As this method is called every time the MyMoneyFile instance // notifies a modification, it's the perfect place to start the timer if needed if (m_autoSaveEnabled && !m_autoSaveTimer->isActive()) { m_autoSaveTimer->setSingleShot(true); m_autoSaveTimer->start(m_autoSavePeriod * 60 * 1000); //miliseconds } pActions[eMenu::Action::UpdateAllAccounts]->setEnabled(KMyMoneyUtils::canUpdateAllAccounts()); break; default: break; } updateCaption(); } KMStatus::KMStatus(const QString &text) : m_prevText(kmymoney->slotStatusMsg(text)) { } KMStatus::~KMStatus() { kmymoney->slotStatusMsg(m_prevText); } void KMyMoneyApp::Private::unlinkStatementXML() { QDir d(KMyMoneySettings::logPath(), "kmm-statement*"); for (uint i = 0; i < d.count(); ++i) { qDebug("Remove %s", qPrintable(d[i])); d.remove(KMyMoneySettings::logPath() + QString("/%1").arg(d[i])); } } diff --git a/kmymoney/kmymoneyutils.cpp b/kmymoney/kmymoneyutils.cpp index a69844a06..390bfe922 100644 --- a/kmymoney/kmymoneyutils.cpp +++ b/kmymoney/kmymoneyutils.cpp @@ -1,829 +1,836 @@ /*************************************************************************** kmymoneyutils.cpp - description ------------------- begin : Wed Feb 5 2003 copyright : (C) 2000-2003 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kmymoneyutils.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #include +#include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneymoney.h" #include "mymoneyexception.h" #include "mymoneytransactionfilter.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyschedule.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneyprice.h" #include "mymoneystatement.h" #include "mymoneyforecast.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "kmymoneysettings.h" #include "icons.h" #include "storageenums.h" #include "mymoneyenums.h" #include "kmymoneyplugin.h" using namespace Icons; KMyMoneyUtils::KMyMoneyUtils() { } KMyMoneyUtils::~KMyMoneyUtils() { } const QString KMyMoneyUtils::occurrenceToString(const eMyMoney::Schedule::Occurrence occurrence) { return i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(occurrence).toLatin1()); } const QString KMyMoneyUtils::paymentMethodToString(eMyMoney::Schedule::PaymentType paymentType) { return i18nc("Scheduled Transaction payment type", MyMoneySchedule::paymentMethodToString(paymentType).toLatin1()); } const QString KMyMoneyUtils::weekendOptionToString(eMyMoney::Schedule::WeekendOption weekendOption) { return i18n(MyMoneySchedule::weekendOptionToString(weekendOption).toLatin1()); } const QString KMyMoneyUtils::scheduleTypeToString(eMyMoney::Schedule::Type type) { return i18nc("Scheduled transaction type", MyMoneySchedule::scheduleTypeToString(type).toLatin1()); } KGuiItem KMyMoneyUtils::scheduleNewGuiItem() { KGuiItem splitGuiItem(i18n("&New Schedule..."), Icons::get(Icon::DocumentNew), i18n("Create a new schedule."), i18n("Use this to create a new schedule.")); return splitGuiItem; } KGuiItem KMyMoneyUtils::accountsFilterGuiItem() { KGuiItem splitGuiItem(i18n("&Filter"), Icons::get(Icon::ViewFilter), i18n("Filter out accounts"), i18n("Use this to filter out accounts")); return splitGuiItem; } const char* homePageItems[] = { I18N_NOOP("Payments"), I18N_NOOP("Preferred accounts"), I18N_NOOP("Payment accounts"), I18N_NOOP("Favorite reports"), I18N_NOOP("Forecast (schedule)"), I18N_NOOP("Net worth forecast"), - I18N_NOOP("Forecast (history)"), + I18N_NOOP("Forecast (history)"), // unused, s.a. KSettingsHome::slotLoadItems() I18N_NOOP("Assets and Liabilities"), I18N_NOOP("Budget"), I18N_NOOP("CashFlow"), // insert new items above this comment 0 }; const QString KMyMoneyUtils::homePageItemToString(const int idx) { QString rc; if (abs(idx) > 0 && abs(idx) < static_cast(sizeof(homePageItems) / sizeof(homePageItems[0]))) { rc = i18n(homePageItems[abs(idx-1)]); } return rc; } int KMyMoneyUtils::stringToHomePageItem(const QString& txt) { int idx = 0; for (idx = 0; homePageItems[idx] != 0; ++idx) { if (txt == i18n(homePageItems[idx])) return idx + 1; } return 0; } bool KMyMoneyUtils::appendCorrectFileExt(QString& str, const QString& strExtToUse) { bool rc = false; if (!str.isEmpty()) { //find last . deliminator int nLoc = str.lastIndexOf('.'); if (nLoc != -1) { QString strExt, strTemp; strTemp = str.left(nLoc + 1); strExt = str.right(str.length() - (nLoc + 1)); if (strExt.indexOf(strExtToUse, 0, Qt::CaseInsensitive) == -1) { // if the extension given contains a period, we remove ours if (strExtToUse.indexOf('.') != -1) strTemp = strTemp.left(strTemp.length() - 1); //append extension to make complete file name strTemp.append(strExtToUse); str = strTemp; rc = true; } } else { str.append(QLatin1Char('.')); str.append(strExtToUse); rc = true; } } return rc; } void KMyMoneyUtils::checkConstants() { // TODO: port to kf5 #if 0 Q_ASSERT(static_cast(KLocale::ParensAround) == static_cast(MyMoneyMoney::ParensAround)); Q_ASSERT(static_cast(KLocale::BeforeQuantityMoney) == static_cast(MyMoneyMoney::BeforeQuantityMoney)); Q_ASSERT(static_cast(KLocale::AfterQuantityMoney) == static_cast(MyMoneyMoney::AfterQuantityMoney)); Q_ASSERT(static_cast(KLocale::BeforeMoney) == static_cast(MyMoneyMoney::BeforeMoney)); Q_ASSERT(static_cast(KLocale::AfterMoney) == static_cast(MyMoneyMoney::AfterMoney)); #endif } QString KMyMoneyUtils::variableCSS() { QColor tcolor = KColorScheme(QPalette::Active).foreground(KColorScheme::NormalText).color(); QColor link = KColorScheme(QPalette::Active).foreground(KColorScheme::LinkText).color(); QString css; css += "\n"; return css; } QString KMyMoneyUtils::findResource(QStandardPaths::StandardLocation type, const QString& filename) { QLocale locale; QString country; QString localeName = locale.bcp47Name(); QString language = localeName; // extract language and country from the bcp47name QRegularExpression regExp(QLatin1String("(\\w+)_(\\w+)")); QRegularExpressionMatch match = regExp.match(localeName); if (match.hasMatch()) { language = match.captured(1); country = match.captured(2); } QString rc; // check that the placeholder is present and set things up if (filename.indexOf("%1") != -1) { /// @fixme somehow I have the impression, that language and country /// mappings to the filename are not correct. This certainly must /// be overhauled at some point in time (ipwizard, 2017-10-22) QString mask = filename.arg("_%1.%2"); rc = QStandardPaths::locate(type, mask.arg(country, language)); // search the given resource if (rc.isEmpty()) { mask = filename.arg("_%1"); rc = QStandardPaths::locate(type, mask.arg(language)); } if (rc.isEmpty()) { // qDebug(QString("html/home_%1.html not found").arg(country).toLatin1()); rc = QStandardPaths::locate(type, mask.arg(country)); } if (rc.isEmpty()) { rc = QStandardPaths::locate(type, filename.arg("")); } } else { rc = QStandardPaths::locate(type, filename); } if (rc.isEmpty()) { qWarning("No resource found for (%s,%s)", qPrintable(QStandardPaths::displayName(type)), qPrintable(filename)); } return rc; } const MyMoneySplit KMyMoneyUtils::stockSplit(const MyMoneyTransaction& t) { MyMoneySplit investmentAccountSplit; foreach (const auto split, t.splits()) { if (!split.accountId().isEmpty()) { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (acc.isInvest()) { return split; } // if we have a reference to an investment account, we remember it here if (acc.accountType() == eMyMoney::Account::Type::Investment) investmentAccountSplit = split; } } // if we haven't found a stock split, we see if we've seen // an investment account on the way. If so, we return it. if (!investmentAccountSplit.id().isEmpty()) return investmentAccountSplit; // if none was found, we return an empty split. return MyMoneySplit(); } KMyMoneyUtils::transactionTypeE KMyMoneyUtils::transactionType(const MyMoneyTransaction& t) { if (!stockSplit(t).id().isEmpty()) return InvestmentTransaction; if (t.splitCount() < 2) { return Unknown; } else if (t.splitCount() > 2) { // FIXME check for loan transaction here return SplitTransaction; } QString ida, idb; const auto & splits = t.splits(); if (splits.size() > 0) ida = splits[0].accountId(); if (splits.size() > 1) idb = splits[1].accountId(); if (ida.isEmpty() || idb.isEmpty()) return Unknown; MyMoneyAccount a, b; a = MyMoneyFile::instance()->account(ida); b = MyMoneyFile::instance()->account(idb); if ((a.accountGroup() == eMyMoney::Account::Type::Asset || a.accountGroup() == eMyMoney::Account::Type::Liability) && (b.accountGroup() == eMyMoney::Account::Type::Asset || b.accountGroup() == eMyMoney::Account::Type::Liability)) return Transfer; return Normal; } void KMyMoneyUtils::calculateAutoLoan(const MyMoneySchedule& schedule, MyMoneyTransaction& transaction, const QMap& balances) { try { MyMoneyForecast::calculateAutoLoan(schedule, transaction, balances); } catch (const MyMoneyException &e) { KMessageBox::detailedError(0, i18n("Unable to load schedule details"), QString::fromLatin1(e.what())); } } QString KMyMoneyUtils::nextCheckNumber(const MyMoneyAccount& acc) { return getAdjacentNumber(acc.value("lastNumberUsed"), 1); } QString KMyMoneyUtils::nextFreeCheckNumber(const MyMoneyAccount& acc) { auto file = MyMoneyFile::instance(); auto num = acc.value("lastNumberUsed"); if (num.isEmpty()) num = QStringLiteral("1"); // now check if this number has been used already if (file->checkNoUsed(acc.id(), num)) { // if a number has been entered which is immediately prior to // an existing number, the next new number produced would clash // so need to look ahead for free next number // we limit that to a number of tries which depends on the // number of splits in that account (we can't have more) MyMoneyTransactionFilter filter(acc.id()); QList transactions; file->transactionList(transactions, filter); const int maxNumber = transactions.count(); for (int i = 0; i < maxNumber; i++) { if (file->checkNoUsed(acc.id(), num)) { // increment and try again num = getAdjacentNumber(num); } else { // found a free number break; } } } return num; } QString KMyMoneyUtils::getAdjacentNumber(const QString& number, int offset) { // make sure the offset is either -1 or 1 offset = (offset >= 0) ? 1 : -1; QString num = number; // +-#1--+ +#2++-#3-++-#4--+ QRegExp exp(QString("(.*\\D)?(0*)(\\d+)(\\D.*)?")); if (exp.indexIn(num) != -1) { QString arg1 = exp.cap(1); QString arg2 = exp.cap(2); QString arg3 = QString::number(exp.cap(3).toULong() + offset); QString arg4 = exp.cap(4); num = QString("%1%2%3%4").arg(arg1, arg2, arg3, arg4); } else { num = QStringLiteral("1"); } return num; } quint64 KMyMoneyUtils::numericPart(const QString & num) { quint64 num64 = 0; QRegExp exp(QString("(.*\\D)?(0*)(\\d+)(\\D.*)?")); if (exp.indexIn(num) != -1) { // QString arg1 = exp.cap(1); QString arg2 = exp.cap(2); QString arg3 = QString::number(exp.cap(3).toULongLong()); // QString arg4 = exp.cap(4); num64 = QString("%2%3").arg(arg2, arg3).toULongLong(); } return num64; } QString KMyMoneyUtils::reconcileStateToString(eMyMoney::Split::State flag, bool text) { QString txt; if (text) { switch (flag) { case eMyMoney::Split::State::NotReconciled: txt = i18nc("Reconciliation state 'Not reconciled'", "Not reconciled"); break; case eMyMoney::Split::State::Cleared: txt = i18nc("Reconciliation state 'Cleared'", "Cleared"); break; case eMyMoney::Split::State::Reconciled: txt = i18nc("Reconciliation state 'Reconciled'", "Reconciled"); break; case eMyMoney::Split::State::Frozen: txt = i18nc("Reconciliation state 'Frozen'", "Frozen"); break; default: txt = i18nc("Unknown reconciliation state", "Unknown"); break; } } else { switch (flag) { case eMyMoney::Split::State::NotReconciled: break; case eMyMoney::Split::State::Cleared: txt = i18nc("Reconciliation flag C", "C"); break; case eMyMoney::Split::State::Reconciled: txt = i18nc("Reconciliation flag R", "R"); break; case eMyMoney::Split::State::Frozen: txt = i18nc("Reconciliation flag F", "F"); break; default: txt = i18nc("Flag for unknown reconciliation state", "?"); break; } } return txt; } MyMoneyTransaction KMyMoneyUtils::scheduledTransaction(const MyMoneySchedule& schedule) { MyMoneyTransaction t = schedule.transaction(); try { if (schedule.type() == eMyMoney::Schedule::Type::LoanPayment) { calculateAutoLoan(schedule, t, QMap()); } } catch (const MyMoneyException &e) { qDebug("Unable to load schedule details for '%s' during transaction match: %s", qPrintable(schedule.name()), e.what()); } t.clearId(); t.setEntryDate(QDate()); return t; } KXmlGuiWindow* KMyMoneyUtils::mainWindow() { foreach (QWidget *widget, QApplication::topLevelWidgets()) { KXmlGuiWindow* result = dynamic_cast(widget); if (result) return result; } return 0; } void KMyMoneyUtils::updateWizardButtons(QWizard* wizard) { // setup text on buttons wizard->setButtonText(QWizard::NextButton, i18nc("Go to next page of the wizard", "&Next")); wizard->setButtonText(QWizard::BackButton, KStandardGuiItem::back().text()); // setup icons wizard->button(QWizard::FinishButton)->setIcon(KStandardGuiItem::ok().icon()); wizard->button(QWizard::CancelButton)->setIcon(KStandardGuiItem::cancel().icon()); wizard->button(QWizard::NextButton)->setIcon(KStandardGuiItem::forward(KStandardGuiItem::UseRTL).icon()); wizard->button(QWizard::BackButton)->setIcon(KStandardGuiItem::back(KStandardGuiItem::UseRTL).icon()); } void KMyMoneyUtils::dissectTransaction(const MyMoneyTransaction& transaction, const MyMoneySplit& split, MyMoneySplit& assetAccountSplit, QList& feeSplits, QList& interestSplits, MyMoneySecurity& security, MyMoneySecurity& currency, eMyMoney::Split::InvestmentTransactionType& transactionType) { // collect the splits. split references the stock account and should already // be set up. assetAccountSplit references the corresponding asset account (maybe // empty), feeSplits is the list of all expenses and interestSplits // the list of all incomes assetAccountSplit = MyMoneySplit(); // set to none to check later if it was assigned auto file = MyMoneyFile::instance(); foreach (const auto tsplit, transaction.splits()) { auto acc = file->account(tsplit.accountId()); if (tsplit.id() == split.id()) { security = file->security(acc.currencyId()); } else if (acc.accountGroup() == eMyMoney::Account::Type::Expense) { feeSplits.append(tsplit); // feeAmount += tsplit.value(); } else if (acc.accountGroup() == eMyMoney::Account::Type::Income) { interestSplits.append(tsplit); // interestAmount += tsplit.value(); } else { if (assetAccountSplit == MyMoneySplit()) // first asset Account should be our requested brokerage account assetAccountSplit = tsplit; else if (tsplit.value().isNegative()) // the rest (if present) is handled as fee or interest feeSplits.append(tsplit); // and shouldn't be allowed to override assetAccountSplit else if (tsplit.value().isPositive()) interestSplits.append(tsplit); } } // determine transaction type if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::AddShares)) { transactionType = (!split.shares().isNegative()) ? eMyMoney::Split::InvestmentTransactionType::AddShares : eMyMoney::Split::InvestmentTransactionType::RemoveShares; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) { transactionType = (!split.value().isNegative()) ? eMyMoney::Split::InvestmentTransactionType::BuyShares : eMyMoney::Split::InvestmentTransactionType::SellShares; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend)) { transactionType = eMyMoney::Split::InvestmentTransactionType::Dividend; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend)) { transactionType = eMyMoney::Split::InvestmentTransactionType::ReinvestDividend; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Yield)) { transactionType = eMyMoney::Split::InvestmentTransactionType::Yield; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)) { transactionType = eMyMoney::Split::InvestmentTransactionType::SplitShares; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::InterestIncome)) { transactionType = eMyMoney::Split::InvestmentTransactionType::InterestIncome; } else transactionType = eMyMoney::Split::InvestmentTransactionType::BuyShares; currency.setTradingSymbol("???"); try { currency = file->security(transaction.commodity()); } catch (const MyMoneyException &) { } } void KMyMoneyUtils::processPriceList(const MyMoneyStatement &st) { auto file = MyMoneyFile::instance(); QHash secBySymbol; QHash secByName; const auto securityList = file->securityList(); for (const auto& sec : securityList) { secBySymbol[sec.tradingSymbol()] = sec; secByName[sec.name()] = sec; } for (const auto& stPrice : st.m_listPrices) { auto currency = file->baseCurrency().id(); QString security; if (!stPrice.m_strCurrency.isEmpty()) { security = stPrice.m_strSecurity; currency = stPrice.m_strCurrency; } else if (secBySymbol.contains(stPrice.m_strSecurity)) { security = secBySymbol[stPrice.m_strSecurity].id(); currency = file->security(file->security(security).tradingCurrency()).id(); } else if (secByName.contains(stPrice.m_strSecurity)) { security = secByName[stPrice.m_strSecurity].id(); currency = file->security(file->security(security).tradingCurrency()).id(); } else return; MyMoneyPrice price(security, currency, stPrice.m_date, stPrice.m_amount, stPrice.m_sourceName.isEmpty() ? i18n("Prices Importer") : stPrice.m_sourceName); file->addPrice(price); } } void KMyMoneyUtils::deleteSecurity(const MyMoneySecurity& security, QWidget* parent) { QString msg, msg2; QString dontAsk, dontAsk2; if (security.isCurrency()) { msg = i18n("

Do you really want to remove the currency %1 from the file?

", security.name()); msg2 = i18n("

All exchange rates for currency %1 will be lost.

Do you still want to continue?

", security.name()); dontAsk = "DeleteCurrency"; dontAsk2 = "DeleteCurrencyRates"; } else { msg = i18n("

Do you really want to remove the %1 %2 from the file?

", MyMoneySecurity::securityTypeToString(security.securityType()), security.name()); msg2 = i18n("

All price quotes for %1 %2 will be lost.

Do you still want to continue?

", MyMoneySecurity::securityTypeToString(security.securityType()), security.name()); dontAsk = "DeleteSecurity"; dontAsk2 = "DeleteSecurityPrices"; } if (KMessageBox::questionYesNo(parent, msg, i18n("Delete security"), KStandardGuiItem::yes(), KStandardGuiItem::no(), dontAsk) == KMessageBox::Yes) { MyMoneyFileTransaction ft; auto file = MyMoneyFile::instance(); QBitArray skip((int)eStorage::Reference::Count); skip.fill(true); skip.clearBit((int)eStorage::Reference::Price); if (file->isReferenced(security, skip)) { if (KMessageBox::questionYesNo(parent, msg2, i18n("Delete prices"), KStandardGuiItem::yes(), KStandardGuiItem::no(), dontAsk2) == KMessageBox::Yes) { try { QString secID = security.id(); foreach (auto priceEntry, file->priceList()) { const MyMoneyPrice& price = priceEntry.first(); if (price.from() == secID || price.to() == secID) file->removePrice(price); } ft.commit(); ft.restart(); } catch (const MyMoneyException &) { qDebug("Cannot delete price"); return; } } else return; } try { if (security.isCurrency()) file->removeCurrency(security); else file->removeSecurity(security); ft.commit(); } catch (const MyMoneyException &) { } } } bool KMyMoneyUtils::fileExists(const QUrl &url) { bool fileExists = false; if (url.isValid()) { - short int detailLevel = 0; // Lowest level: file/dir/symlink/none - KIO::StatJob* statjob = KIO::stat(url, KIO::StatJob::SourceSide, detailLevel); - bool noerror = statjob->exec(); - if (noerror) { - // We want a file - fileExists = !statjob->statResult().isDir(); + if (url.isLocalFile() || url.scheme().isEmpty()) { + QFileInfo check_file(url.toLocalFile()); + fileExists = check_file.exists() && check_file.isFile(); + + } else { + short int detailLevel = 0; // Lowest level: file/dir/symlink/none + KIO::StatJob* statjob = KIO::stat(url, KIO::StatJob::SourceSide, detailLevel); + bool noerror = statjob->exec(); + if (noerror) { + // We want a file + fileExists = !statjob->statResult().isDir(); + } + statjob->kill(); } - statjob->kill(); } return fileExists; } QString KMyMoneyUtils::downloadFile(const QUrl &url) { QString filename; KIO::StoredTransferJob *transferjob = KIO::storedGet (url); // KJobWidgets::setWindow(transferjob, this); if (! transferjob->exec()) { KMessageBox::detailedError(nullptr, i18n("Error while loading file '%1'.", url.url()), transferjob->errorString(), i18n("File access error")); return filename; } QTemporaryFile file; file.setAutoRemove(false); file.open(); file.write(transferjob->data()); filename = file.fileName(); file.close(); return filename; } bool KMyMoneyUtils::newPayee(const QString& newnameBase, QString& id) { bool doit = true; if (newnameBase != i18n("New Payee")) { // Ask the user if that is what he intended to do? const auto msg = i18n("Do you want to add %1 as payer/receiver?", newnameBase); if (KMessageBox::questionYesNo(nullptr, msg, i18n("New payee/receiver"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "NewPayee") == KMessageBox::No) { doit = false; // we should not keep the 'no' setting because that can confuse people like // I have seen in some usability tests. So we just delete it right away. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("NewPayee")); } } } if (doit) { MyMoneyFileTransaction ft; try { QString newname(newnameBase); // adjust name until a unique name has been created int count = 0; for (;;) { try { MyMoneyFile::instance()->payeeByName(newname); newname = QString::fromLatin1("%1 [%2]").arg(newnameBase).arg(++count); } catch (const MyMoneyException &) { break; } } MyMoneyPayee p; p.setName(newname); MyMoneyFile::instance()->addPayee(p); id = p.id(); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(nullptr, i18n("Unable to add payee"), QString::fromLatin1(e.what())); doit = false; } } return doit; } void KMyMoneyUtils::newTag(const QString& newnameBase, QString& id) { bool doit = true; if (newnameBase != i18n("New Tag")) { // Ask the user if that is what he intended to do? const auto msg = i18n("Do you want to add %1 as tag?", newnameBase); if (KMessageBox::questionYesNo(nullptr, msg, i18n("New tag"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "NewTag") == KMessageBox::No) { doit = false; // we should not keep the 'no' setting because that can confuse people like // I have seen in some usability tests. So we just delete it right away. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("NewTag")); } } } if (doit) { MyMoneyFileTransaction ft; try { QString newname(newnameBase); // adjust name until a unique name has been created int count = 0; for (;;) { try { MyMoneyFile::instance()->tagByName(newname); newname = QString::fromLatin1("%1 [%2]").arg(newnameBase, ++count); } catch (const MyMoneyException &) { break; } } MyMoneyTag ta; ta.setName(newname); MyMoneyFile::instance()->addTag(ta); id = ta.id(); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(nullptr, i18n("Unable to add tag"), QString::fromLatin1(e.what())); } } } void KMyMoneyUtils::newInstitution(MyMoneyInstitution& institution) { auto file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; try { file->addInstitution(institution); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(nullptr, i18n("Cannot add institution: %1", QString::fromLatin1(e.what()))); } } QDebug KMyMoneyUtils::debug() { return qDebug() << QDateTime::currentDateTime().toString(QStringLiteral("HH:mm:ss.zzz")); } MyMoneyForecast KMyMoneyUtils::forecast() { MyMoneyForecast forecast; // override object defaults with those of the application forecast.setForecastCycles(KMyMoneySettings::forecastCycles()); forecast.setAccountsCycle(KMyMoneySettings::forecastAccountCycle()); forecast.setHistoryStartDate(QDate::currentDate().addDays(-forecast.forecastCycles()*forecast.accountsCycle())); forecast.setHistoryEndDate(QDate::currentDate().addDays(-1)); forecast.setForecastDays(KMyMoneySettings::forecastDays()); forecast.setBeginForecastDay(KMyMoneySettings::beginForecastDay()); forecast.setForecastMethod(KMyMoneySettings::forecastMethod()); forecast.setHistoryMethod(KMyMoneySettings::historyMethod()); forecast.setIncludeFutureTransactions(KMyMoneySettings::includeFutureTransactions()); forecast.setIncludeScheduledTransactions(KMyMoneySettings::includeScheduledTransactions()); return forecast; } bool KMyMoneyUtils::canUpdateAllAccounts() { const auto file = MyMoneyFile::instance(); auto rc = false; if (!file->storageAttached()) return rc; QList accList; file->accountList(accList); QList::const_iterator it_a; auto it_p = pPlugins.online.constEnd(); for (it_a = accList.constBegin(); (it_p == pPlugins.online.constEnd()) && (it_a != accList.constEnd()); ++it_a) { if ((*it_a).hasOnlineMapping()) { // check if provider is available it_p = pPlugins.online.constFind((*it_a).onlineBankingSettings().value("provider").toLower()); if (it_p != pPlugins.online.constEnd()) { QStringList protocols; (*it_p)->protocols(protocols); if (!protocols.isEmpty()) { rc = true; break; } } } } return rc; } void KMyMoneyUtils::showStatementImportResult(const QStringList& resultMessages, uint statementCount) { KMessageBox::informationList(nullptr, i18np("One statement has been processed with the following results:", "%1 statements have been processed with the following results:", statementCount), !resultMessages.isEmpty() ? resultMessages : QStringList { i18np("No new transaction has been imported.", "No new transactions have been imported.", statementCount) }, i18n("Statement import statistics")); } diff --git a/kmymoney/misc/CMakeLists.txt b/kmymoney/misc/CMakeLists.txt index 3f09c7d63..080e82905 100644 --- a/kmymoney/misc/CMakeLists.txt +++ b/kmymoney/misc/CMakeLists.txt @@ -1,57 +1,80 @@ install(PROGRAMS financequote.pl DESTINATION ${DATA_INSTALL_DIR}/kmymoney/misc) set( kmm_utils_validators_SRCS charvalidator.cpp validators.cpp ) set( kmm_utils_webconnect_SRCS webconnect.cpp ) if( "${HAVE_UNISTD_H}" AND "${HAVE_PWD_H}" ) set( kmm_utils_platformtools_SRCS platformtools_gnu.cpp ) elseif( "${HAVE_WINDOWS_H}" AND "${HAVE_LMCONS_H}" AND "${HAVE_PROCESS_H}" ) set( kmm_utils_platformtools_SRCS platformtools_nognu.cpp ) else() message(FATAL_ERROR "make sure either windows.h and lmcons.h (on msvc platforms) or unistd.h and pwd.h (on other platforms) are present.") endif() set( kmm_utils_validators_HEADER charvalidator.h validators.h ) set( kmm_utils_webconnect_HEADER webconnect.h ) set( kmm_utils_platformtools_HEADER platformtools.h ) +set( kmm_printer_SRCS + kmm_printer.cpp +) + + add_library(kmm_utils_validators STATIC ${kmm_utils_validators_SRCS}) add_library(kmm_utils_webconnect STATIC ${kmm_utils_webconnect_SRCS}) add_library(kmm_utils_platformtools STATIC ${kmm_utils_platformtools_SRCS}) +add_library(kmm_printer SHARED ${kmm_printer_SRCS}) target_link_libraries( kmm_utils_validators PUBLIC Qt5::Core Qt5::Gui ) target_link_libraries( kmm_utils_webconnect PUBLIC Qt5::Core Qt5::Network ) target_link_libraries( kmm_utils_platformtools PUBLIC Qt5::Core ) + +target_link_libraries(kmm_printer + PUBLIC + Qt5::Core + Qt5::PrintSupport +) + +set_target_properties(kmm_printer PROPERTIES + VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR} +) + +generate_export_header(kmm_printer) + +install(TARGETS kmm_printer ${INSTALL_TARGETS_DEFAULT_ARGS} ) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/kmm_printer_export.h + DESTINATION ${INCLUDE_INSTALL_DIR}/kmymoney COMPONENT Devel) + diff --git a/kmymoney/misc/kmm_printer.cpp b/kmymoney/misc/kmm_printer.cpp new file mode 100644 index 000000000..a391048f7 --- /dev/null +++ b/kmymoney/misc/kmm_printer.cpp @@ -0,0 +1,71 @@ +/* + * This file is part of KMyMoney, A Personal Finance Manager by KDE + * + * Copyright (C) 2019 Thomas Baumgart + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "kmm_printer.h" + +#include +#include +#include +#include +#include + +// Q_LOGGING_CATEGORY(Print, "Printing") + +KMyMoneyPrinter::KMyMoneyPrinter() +{ +} + +QPrinter* KMyMoneyPrinter::instance(QPrinter::PrinterMode mode) +{ + static QPrinter* printer(nullptr); + + if (printer == nullptr) { + printer = new QPrinter(mode); + } + return printer; +} + +QPrintDialog* KMyMoneyPrinter::dialog() +{ + static QPrintDialog* dialog(nullptr); + + if (dialog == nullptr) { + dialog = new QPrintDialog(instance()); + dialog->setWindowTitle(QString()); + } + return dialog; +} + +QPrinter* KMyMoneyPrinter::startPrint(QPrinter::PrinterMode mode) +{ + QPrinter *printer = instance(mode); + + if (dialog()->exec() != QDialog::Accepted) + return nullptr; + return printer; +} + +void KMyMoneyPrinter::cleanup() +{ + auto printer = instance(); + auto dlg = dialog(); + + delete dlg; + delete printer; +} diff --git a/kmymoney/misc/kmm_printer.h b/kmymoney/misc/kmm_printer.h new file mode 100644 index 000000000..9ae75adf7 --- /dev/null +++ b/kmymoney/misc/kmm_printer.h @@ -0,0 +1,40 @@ +/* + * This file is part of KMyMoney, A Personal Finance Manager by KDE + * + * Copyright (C) 2019 Thomas Baumgart + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KMM_PRINTER +#define KMM_PRINTER + +#include +#include + +class QPrintDialog; + +class KMM_PRINTER_EXPORT KMyMoneyPrinter +{ + KMyMoneyPrinter(); +protected: + static QPrintDialog* dialog(); + static QPrinter* instance(QPrinter::PrinterMode mode = QPrinter::ScreenResolution); + +public: + static QPrinter* startPrint(QPrinter::PrinterMode mode = QPrinter::ScreenResolution); + static void cleanup(); +}; + +#endif diff --git a/kmymoney/plugins/checkprinting/CMakeLists.txt b/kmymoney/plugins/checkprinting/CMakeLists.txt index a806852ee..7b80ef667 100644 --- a/kmymoney/plugins/checkprinting/CMakeLists.txt +++ b/kmymoney/plugins/checkprinting/CMakeLists.txt @@ -1,82 +1,83 @@ # patch the version with the version defined in the build system configure_file(${CMAKE_CURRENT_SOURCE_DIR}/checkprinting.json.cmake ${CMAKE_CURRENT_BINARY_DIR}/checkprinting.json @ONLY) set(checkprinting_PART_SRCS numbertowords.cpp checkprinting.cpp ../../widgets/selectedtransaction.cpp ) kconfig_add_kcfg_files(checkprinting_PART_SRCS pluginsettings.kcfgc) kcoreaddons_add_plugin(checkprinting SOURCES ${checkprinting_PART_SRCS} JSON "${CMAKE_CURRENT_BINARY_DIR}/checkprinting.json" INSTALL_NAMESPACE "kmymoney") #kcoreaddons_add_plugin sets LIBRARY_OUTPUT_DIRECTORY to ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${INSTALL_NAMESPACE} set_target_properties(checkprinting PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") target_link_libraries(checkprinting Qt5::PrintSupport KF5::I18n kmm_mymoney kmm_plugin + kmm_printer ) if(ENABLE_WEBENGINE) target_link_libraries(checkprinting Qt5::WebEngineWidgets) else(ENABLE_WEBENGINE) target_link_libraries(checkprinting KF5::WebKit) endif(ENABLE_WEBENGINE) install(FILES checkprinting.rc DESTINATION "${KXMLGUI_INSTALL_DIR}/checkprinting") install(FILES check_template.html DESTINATION "${DATA_INSTALL_DIR}/checkprinting") install(FILES check_template_green_linen.html DESTINATION "${DATA_INSTALL_DIR}/checkprinting") # the KCM module set(kcm_checkprinting_PART_SRCS kcm_checkprinting.cpp ) kconfig_add_kcfg_files(kcm_checkprinting_PART_SRCS pluginsettings.kcfgc) ki18n_wrap_ui(kcm_checkprinting_PART_SRCS pluginsettingsdecl.ui) kcoreaddons_add_plugin(kcm_checkprinting SOURCES ${kcm_checkprinting_PART_SRCS} JSON "${CMAKE_CURRENT_BINARY_DIR}/kcm_checkprinting.json" INSTALL_NAMESPACE "kmymoney") kcoreaddons_desktop_to_json(kcm_checkprinting kcm_checkprinting.desktop) #kcoreaddons_add_plugin sets LIBRARY_OUTPUT_DIRECTORY to ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${INSTALL_NAMESPACE} set_target_properties(kcm_checkprinting PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") target_link_libraries(kcm_checkprinting Qt5::PrintSupport KF5::I18n KF5::ConfigWidgets KF5::Completion KF5::KIOWidgets KF5::CoreAddons ) if(ENABLE_WEBENGINE) target_link_libraries(kcm_checkprinting Qt5::WebEngineWidgets) else(ENABLE_WEBENGINE) target_link_libraries(kcm_checkprinting KF5::WebKit) endif(ENABLE_WEBENGINE) install(FILES kcm_checkprinting.desktop DESTINATION "${SERVICES_INSTALL_DIR}") diff --git a/kmymoney/plugins/checkprinting/checkprinting.cpp b/kmymoney/plugins/checkprinting/checkprinting.cpp index 071b33a7b..7ad43d1e1 100644 --- a/kmymoney/plugins/checkprinting/checkprinting.cpp +++ b/kmymoney/plugins/checkprinting/checkprinting.cpp @@ -1,262 +1,256 @@ /*************************************************************************** - * Copyright 2009 Cristian Onet onet.cristian@gmail.com * + * This file is part of KMyMoney, A Personal Finance Manager by KDE * + * * + * Copyright (C) 2009 Cristian Onet * + * Copyright (C) 2019 Thomas Baumgart * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see * ***************************************************************************/ #include #include "checkprinting.h" // QT includes #include #include #include #ifdef ENABLE_WEBENGINE #include #else #include #endif -#include -#include #include // KDE includes #include #include #include // KMyMoney includes #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyinstitution.h" #include "mymoneymoney.h" #include "mymoneypayee.h" #include "mymoneysecurity.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyutils.h" #include "viewinterface.h" #include "selectedtransactions.h" #include "numbertowords.h" #include "pluginsettings.h" #include "mymoneyenums.h" #ifdef IS_APPIMAGE #include #include #endif +#include "kmm_printer.h" struct CheckPrinting::Private { QAction* m_action; QString m_checkTemplateHTML; QStringList m_printedTransactionIdList; KMyMoneyRegister::SelectedTransactions m_transactions; }; CheckPrinting::CheckPrinting(QObject *parent, const QVariantList &args) : - KMyMoneyPlugin::Plugin(parent, "checkprinting"/*must be the same as X-KDE-PluginInfo-Name*/), - m_currentPrinter(nullptr) + KMyMoneyPlugin::Plugin(parent, "checkprinting"/*must be the same as X-KDE-PluginInfo-Name*/) { Q_UNUSED(args); const auto componentName = QLatin1String("checkprinting"); const auto rcFileName = QLatin1String("checkprinting.rc"); // Tell the host application to load my GUI component setComponentName(componentName, i18nc("It's about printing bank checks", "Check printing")); #ifdef IS_APPIMAGE const QString rcFilePath = QCoreApplication::applicationDirPath() + QLatin1String("/../share/kxmlgui5/") + componentName + QLatin1Char('/') + rcFileName; setXMLFile(rcFilePath); const QString localRcFilePath = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).first() + QLatin1Char('/') + componentName + QLatin1Char('/') + rcFileName; setLocalXMLFile(localRcFilePath); #else setXMLFile(rcFileName); #endif // For ease announce that we have been loaded. qDebug("Plugins: checkprinting loaded"); d = std::unique_ptr(new Private); // Create the actions of this plugin QString actionName = i18n("Print check"); d->m_action = actionCollection()->addAction("transaction_checkprinting", this, SLOT(slotPrintCheck())); d->m_action->setText(actionName); // wait until a transaction is selected before enabling the action d->m_action->setEnabled(false); d->m_printedTransactionIdList = PluginSettings::printedChecks(); readCheckTemplate(); //! @todo Christian: Replace #if 0 connect(KMyMoneyPlugin::PluginLoader::instance(), SIGNAL(configChanged(Plugin*)), this, SLOT(slotUpdateConfig())); #endif } /** * @internal Destructor is needed because destructor call of unique_ptr must be in this compile unit */ CheckPrinting::~CheckPrinting() { qDebug("Plugins: checkprinting unloaded"); } void CheckPrinting::plug() { connect(viewInterface(), &KMyMoneyPlugin::ViewInterface::transactionsSelected, this, &CheckPrinting::slotTransactionsSelected); } void CheckPrinting::unplug() { disconnect(viewInterface(), &KMyMoneyPlugin::ViewInterface::transactionsSelected, this, &CheckPrinting::slotTransactionsSelected); } void CheckPrinting::readCheckTemplate() { QString checkTemplateHTMLPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "checkprinting/check_template.html"); if (PluginSettings::checkTemplateFile().isEmpty()) { PluginSettings::setCheckTemplateFile(checkTemplateHTMLPath); PluginSettings::self()->save(); } QFile checkTemplateHTMLFile(PluginSettings::checkTemplateFile()); checkTemplateHTMLFile.open(QIODevice::ReadOnly); QTextStream stream(&checkTemplateHTMLFile); d->m_checkTemplateHTML = stream.readAll(); checkTemplateHTMLFile.close(); } bool CheckPrinting::canBePrinted(const KMyMoneyRegister::SelectedTransaction & selectedTransaction) const { MyMoneyFile* file = MyMoneyFile::instance(); bool isACheck = file->account(selectedTransaction.split().accountId()).accountType() == eMyMoney::Account::Type::Checkings && selectedTransaction.split().shares().isNegative(); return isACheck && d->m_printedTransactionIdList.contains(selectedTransaction.transaction().id()) == 0; } void CheckPrinting::markAsPrinted(const KMyMoneyRegister::SelectedTransaction & selectedTransaction) { d->m_printedTransactionIdList.append(selectedTransaction.transaction().id()); } void CheckPrinting::slotPrintCheck() { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyMoneyToWordsConverter converter; #ifdef ENABLE_WEBENGINE auto htmlPart = new QWebEngineView(); #else auto htmlPart = new KWebView(); #endif KMyMoneyRegister::SelectedTransactions::const_iterator it; for (it = d->m_transactions.constBegin(); it != d->m_transactions.constEnd(); ++it) { if (!canBePrinted(*it)) continue; // skip this check since it was already printed QString checkHTML = d->m_checkTemplateHTML; const MyMoneyAccount account = file->account((*it).split().accountId()); const MyMoneySecurity currency = file->currency(account.currencyId()); const MyMoneyInstitution institution = file->institution(file->account((*it).split().accountId()).institutionId()); // replace the predefined tokens // data about the user checkHTML.replace("$OWNER_NAME", file->user().name()); checkHTML.replace("$OWNER_ADDRESS", file->user().address()); checkHTML.replace("$OWNER_CITY", file->user().city()); checkHTML.replace("$OWNER_STATE", file->user().state()); // data about the account institution checkHTML.replace("$INSTITUTION_NAME", institution.name()); checkHTML.replace("$INSTITUTION_STREET", institution.street()); checkHTML.replace("$INSTITUTION_TELEPHONE", institution.telephone()); checkHTML.replace("$INSTITUTION_TOWN", institution.town()); checkHTML.replace("$INSTITUTION_CITY", institution.city()); checkHTML.replace("$INSTITUTION_POSTCODE", institution.postcode()); checkHTML.replace("$INSTITUTION_MANAGER", institution.manager()); // data about the transaction checkHTML.replace("$DATE", QLocale().toString((*it).transaction().postDate(), QLocale::ShortFormat)); checkHTML.replace("$CHECK_NUMBER", (*it).split().number()); checkHTML.replace("$PAYEE_NAME", file->payee((*it).split().payeeId()).name()); checkHTML.replace("$PAYEE_ADDRESS", file->payee((*it).split().payeeId()).address()); checkHTML.replace("$PAYEE_CITY", file->payee((*it).split().payeeId()).city()); checkHTML.replace("$PAYEE_POSTCODE", file->payee((*it).split().payeeId()).postcode()); checkHTML.replace("$PAYEE_STATE", file->payee((*it).split().payeeId()).state()); checkHTML.replace("$AMOUNT_STRING", converter.convert((*it).split().shares().abs(), currency.smallestAccountFraction())); checkHTML.replace("$AMOUNT_DECIMAL", MyMoneyUtils::formatMoney((*it).split().shares().abs(), currency)); checkHTML.replace("$MEMO", (*it).split().memo()); // print the check htmlPart->setHtml(checkHTML, QUrl("file://")); - m_currentPrinter = new QPrinter(); - QPointer dialog = new QPrintDialog(m_currentPrinter); - dialog->setWindowTitle(QString()); - if (dialog->exec() != QDialog::Accepted) { - delete m_currentPrinter; - m_currentPrinter = nullptr; - continue; - } else { + auto printer = KMyMoneyPrinter::startPrint(); + if (printer != nullptr) { #ifdef ENABLE_WEBENGINE - htmlPart->page()->print(m_currentPrinter, [=] (bool) {delete m_currentPrinter; m_currentPrinter = nullptr;}); + htmlPart->page()->print(printer, [=] (bool) {}); #else - htmlPart->print(m_currentPrinter); + htmlPart->print(printer); #endif } - delete dialog; // mark the transaction as printed markAsPrinted(*it); } PluginSettings::setPrintedChecks(d->m_printedTransactionIdList); delete htmlPart; } void CheckPrinting::slotTransactionsSelected(const KMyMoneyRegister::SelectedTransactions& transactions) { d->m_transactions = transactions; bool actionEnabled = false; // enable/disable the action depending if there are transactions selected or not // and whether they can be printed or not KMyMoneyRegister::SelectedTransactions::const_iterator it; for (it = d->m_transactions.constBegin(); it != d->m_transactions.constEnd(); ++it) { if (canBePrinted(*it)) { actionEnabled = true; break; } } d->m_action->setEnabled(actionEnabled); } // the plugin's configurations has changed void CheckPrinting::configurationChanged() { PluginSettings::self()->load(); // re-read the data because the configuration has changed readCheckTemplate(); d->m_printedTransactionIdList = PluginSettings::printedChecks(); } K_PLUGIN_FACTORY_WITH_JSON(CheckPrintingFactory, "checkprinting.json", registerPlugin();) #include "checkprinting.moc" diff --git a/kmymoney/plugins/checkprinting/checkprinting.h b/kmymoney/plugins/checkprinting/checkprinting.h index 10a072b50..3b1a49de4 100644 --- a/kmymoney/plugins/checkprinting/checkprinting.h +++ b/kmymoney/plugins/checkprinting/checkprinting.h @@ -1,61 +1,59 @@ /*************************************************************************** * Copyright 2009 Cristian Onet onet.cristian@gmail.com * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see * ***************************************************************************/ #ifndef CHECKPRINTING_H #define CHECKPRINTING_H #include #include "kmymoneyplugin.h" #include "selectedtransactions.h" class KPluginInfo; -class QPrinter; class QObject; class CheckPrinting : public KMyMoneyPlugin::Plugin { Q_OBJECT public: explicit CheckPrinting(QObject *parent, const QVariantList &args); ~CheckPrinting() override; public Q_SLOTS: void plug() override; void unplug() override; void configurationChanged() override; private: void readCheckTemplate(); bool canBePrinted(const KMyMoneyRegister::SelectedTransaction & selectedTransaction) const; void markAsPrinted(const KMyMoneyRegister::SelectedTransaction & selectedTransaction); protected Q_SLOTS: void slotPrintCheck(); void slotTransactionsSelected(const KMyMoneyRegister::SelectedTransactions& transactions); private: struct Private; std::unique_ptr d; - QPrinter *m_currentPrinter; }; #endif diff --git a/kmymoney/plugins/csv/import/core/csvimportercore.cpp b/kmymoney/plugins/csv/import/core/csvimportercore.cpp index 67c888c3e..075d01419 100644 --- a/kmymoney/plugins/csv/import/core/csvimportercore.cpp +++ b/kmymoney/plugins/csv/import/core/csvimportercore.cpp @@ -1,1775 +1,1777 @@ /* * Copyright 2010 Allan Anderson * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "csvimportercore.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneytransaction.h" #include "csvutil.h" #include "convdate.h" #include "mymoneyenums.h" const QHash CSVImporterCore::m_profileConfPrefix { {Profile::Banking, QStringLiteral("Bank")}, {Profile::Investment, QStringLiteral("Invest")}, {Profile::CurrencyPrices, QStringLiteral("CPrices")}, {Profile::StockPrices, QStringLiteral("SPrices")} }; const QHash CSVImporterCore::m_colTypeConfName { {Column::Date, QStringLiteral("DateCol")}, {Column::Memo, QStringLiteral("MemoCol")}, {Column::Number, QStringLiteral("NumberCol")}, {Column::Payee, QStringLiteral("PayeeCol")}, {Column::Amount, QStringLiteral("AmountCol")}, {Column::Credit, QStringLiteral("CreditCol")}, {Column::Debit, QStringLiteral("DebitCol")}, {Column::Category, QStringLiteral("CategoryCol")}, {Column::Type, QStringLiteral("TypeCol")}, {Column::Price, QStringLiteral("PriceCol")}, {Column::Quantity, QStringLiteral("QuantityCol")}, {Column::Fee, QStringLiteral("FeeCol")}, {Column::Symbol, QStringLiteral("SymbolCol")}, {Column::Name, QStringLiteral("NameCol")}, }; const QHash CSVImporterCore::m_miscSettingsConfName { {ConfDirectory, QStringLiteral("Directory")}, {ConfEncoding, QStringLiteral("Encoding")}, {ConfDateFormat, QStringLiteral("DateFormat")}, {ConfFieldDelimiter, QStringLiteral("FieldDelimiter")}, {ConfTextDelimiter, QStringLiteral("TextDelimiter")}, {ConfDecimalSymbol, QStringLiteral("DecimalSymbol")}, {ConfStartLine, QStringLiteral("StartLine")}, {ConfTrailerLines, QStringLiteral("TrailerLines")}, {ConfOppositeSigns, QStringLiteral("OppositeSigns")}, {ConfFeeIsPercentage, QStringLiteral("FeeIsPercentage")}, {ConfFeeRate, QStringLiteral("FeeRate")}, {ConfMinFee, QStringLiteral("MinFee")}, {ConfSecurityName, QStringLiteral("SecurityName")}, {ConfSecuritySymbol, QStringLiteral("SecuritySymbol")}, {ConfCurrencySymbol, QStringLiteral("CurrencySymbol")}, {ConfPriceFraction, QStringLiteral("PriceFraction")}, {ConfDontAsk, QStringLiteral("DontAsk")}, {ConfHeight, QStringLiteral("Height")}, {ConfWidth, QStringLiteral("Width")} }; const QHash CSVImporterCore::m_transactionConfName { {eMyMoney::Transaction::Action::Buy, QStringLiteral("BuyParam")}, {eMyMoney::Transaction::Action::Sell, QStringLiteral("SellParam")}, {eMyMoney::Transaction::Action::ReinvestDividend, QStringLiteral("ReinvdivParam")}, {eMyMoney::Transaction::Action::CashDividend, QStringLiteral("DivXParam")}, {eMyMoney::Transaction::Action::Interest, QStringLiteral("IntIncParam")}, {eMyMoney::Transaction::Action::Shrsin, QStringLiteral("ShrsinParam")}, {eMyMoney::Transaction::Action::Shrsout, QStringLiteral("ShrsoutParam")} }; const QString CSVImporterCore::m_confProfileNames = QStringLiteral("ProfileNames"); const QString CSVImporterCore::m_confPriorName = QStringLiteral("Prior"); const QString CSVImporterCore::m_confMiscName = QStringLiteral("Misc"); CSVImporterCore::CSVImporterCore() : m_profile(0), m_isActionTypeValidated(false) { m_convertDate = new ConvertDate; m_file = new CSVFile; m_priceFractions << MyMoneyMoney(0.01) << MyMoneyMoney(0.1) << MyMoneyMoney::ONE << MyMoneyMoney(10) << MyMoneyMoney(100); validateConfigFile(); readMiscSettings(); } CSVImporterCore::~CSVImporterCore() { delete m_convertDate; delete m_file; } MyMoneyStatement CSVImporterCore::unattendedImport(const QString &filename, CSVProfile *profile) { MyMoneyStatement st; m_profile = profile; m_convertDate->setDateFormatIndex(m_profile->m_dateFormat); if (m_file->getInFileName(filename)) { m_file->readFile(m_profile); m_file->setupParser(m_profile); if (profile->m_decimalSymbol == DecimalSymbol::Auto) { auto columns = getNumericalColumns(); if (detectDecimalSymbols(columns) != -2) return st; } if (!createStatement(st)) st = MyMoneyStatement(); } return st; } KSharedConfigPtr CSVImporterCore::configFile() { return KSharedConfig::openConfig(QStringLiteral("kmymoney/csvimporterrc")); } void CSVImporterCore::profileFactory(const Profile type, const QString &name) { // delete current profile if (m_profile) { delete m_profile; m_profile = nullptr; } switch (type) { default: case Profile::Investment: m_profile = new InvestmentProfile; break; case Profile::Banking: m_profile = new BankingProfile; break; case Profile::CurrencyPrices: case Profile::StockPrices: m_profile = new PricesProfile(type); break; } m_profile->m_profileName = name; } void CSVImporterCore::readMiscSettings() { KConfigGroup miscGroup(configFile(), m_confMiscName); m_autodetect.clear(); m_autodetect.insert(AutoFieldDelimiter, miscGroup.readEntry(QStringLiteral("AutoFieldDelimiter"), true)); m_autodetect.insert(AutoDecimalSymbol, miscGroup.readEntry(QStringLiteral("AutoDecimalSymbol"), true)); m_autodetect.insert(AutoDateFormat, miscGroup.readEntry(QStringLiteral("AutoDateFormat"), true)); m_autodetect.insert(AutoAccountInvest, miscGroup.readEntry(QStringLiteral("AutoAccountInvest"), true)); m_autodetect.insert(AutoAccountBank, miscGroup.readEntry(QStringLiteral("AutoAccountBank"), true)); } void CSVImporterCore::validateConfigFile() { const KSharedConfigPtr config = configFile(); KConfigGroup profileNamesGroup(config, m_confProfileNames); if (!profileNamesGroup.exists()) { profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::Banking), QStringList()); profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::Investment), QStringList()); profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::CurrencyPrices), QStringList()); profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::StockPrices), QStringList()); profileNamesGroup.writeEntry(m_confPriorName + m_profileConfPrefix.value(Profile::Banking), int()); profileNamesGroup.writeEntry(m_confPriorName + m_profileConfPrefix.value(Profile::Investment), int()); profileNamesGroup.writeEntry(m_confPriorName + m_profileConfPrefix.value(Profile::CurrencyPrices), int()); profileNamesGroup.writeEntry(m_confPriorName + m_profileConfPrefix.value(Profile::StockPrices), int()); profileNamesGroup.sync(); } KConfigGroup miscGroup(config, m_confMiscName); if (!miscGroup.exists()) { miscGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfHeight), "400"); miscGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfWidth), "800"); miscGroup.sync(); } QList confVer = miscGroup.readEntry("KMMVer", QList {0, 0, 0}); if (updateConfigFile(confVer)) // write kmmVer only if there were no errors miscGroup.writeEntry("KMMVer", confVer); } bool CSVImporterCore::updateConfigFile(QList &confVer) { bool ret = true; QList kmmVer = QList {5, 0, 0}; int kmmVersion = kmmVer.at(0) * 100 + kmmVer.at(1) * 10 + kmmVer.at(2); int confVersion = confVer.at(0) * 100 + confVer.at(1) * 10 + confVer.at(2); if (confVersion > kmmVersion) { KMessageBox::information(0, i18n("Version of your CSV config file is %1.%2.%3 and is newer than supported version %4.%5.%6. Expect troubles.", confVer.at(0), confVer.at(1), confVer.at(2), kmmVer.at(0), kmmVer.at(1), kmmVer.at(2))); ret = false; return ret; } else if (confVersion == kmmVersion) return true; confVer = kmmVer; const KSharedConfigPtr config = configFile(); QString configFilePath = config.constData()->name(); QFile::copy(configFilePath, configFilePath + QLatin1String(".bak")); KConfigGroup profileNamesGroup(config, m_confProfileNames); QStringList bankProfiles = profileNamesGroup.readEntry(m_profileConfPrefix.value(Profile::Banking), QStringList()); QStringList investProfiles = profileNamesGroup.readEntry(m_profileConfPrefix.value(Profile::Investment), QStringList()); QStringList invalidBankProfiles = profileNamesGroup.readEntry(QLatin1String("Invalid") + m_profileConfPrefix.value(Profile::Banking), QStringList()); // get profiles that was marked invalid during last update QStringList invalidInvestProfiles = profileNamesGroup.readEntry(QLatin1String("Invalid") + m_profileConfPrefix.value(Profile::Investment), QStringList()); QString bankPrefix = m_profileConfPrefix.value(Profile::Banking) + QLatin1Char('-'); QString investPrefix = m_profileConfPrefix.value(Profile::Investment) + QLatin1Char('-'); // for kmm < 5.0.0 change 'BankNames' to 'ProfileNames' and remove 'MainWindow' group if (confVersion < 500 && bankProfiles.isEmpty()) { KConfigGroup oldProfileNamesGroup(config, "BankProfiles"); bankProfiles = oldProfileNamesGroup.readEntry("BankNames", QStringList()); // profile names are under 'BankNames' entry for kmm < 5.0.0 bankPrefix = QLatin1String("Profiles-"); // needed to remove non-existent profiles in first run oldProfileNamesGroup.deleteGroup(); KConfigGroup oldMainWindowGroup(config, "MainWindow"); oldMainWindowGroup.deleteGroup(); KConfigGroup oldSecuritiesGroup(config, "Securities"); oldSecuritiesGroup.deleteGroup(); } bool firstTry = false; if (invalidBankProfiles.isEmpty() && invalidInvestProfiles.isEmpty()) // if there is no invalid profiles then this might be first update try firstTry = true; int invalidProfileResponse = QDialogButtonBox::No; for (auto profileName = bankProfiles.begin(); profileName != bankProfiles.end();) { KConfigGroup bankProfile(config, bankPrefix + *profileName); if (!bankProfile.exists() && !invalidBankProfiles.contains(*profileName)) { // if there is reference to profile but no profile then remove this reference profileName = bankProfiles.erase(profileName); continue; } // for kmm < 5.0.0 remove 'FileType' and 'ProfileName' and assign them to either "Bank=" or "Invest=" if (confVersion < 500) { QString lastUsedDirectory; KConfigGroup oldBankProfile(config, QLatin1String("Profiles-") + *profileName); // if half of configuration is updated and the other one untouched this is needed QString oldProfileType = oldBankProfile.readEntry("FileType", QString()); KConfigGroup newProfile; if (oldProfileType == QLatin1String("Invest")) { oldBankProfile.deleteEntry("BrokerageParam"); oldBankProfile.writeEntry(m_colTypeConfName.value(Column::Type), oldBankProfile.readEntry("PayeeCol")); oldBankProfile.deleteEntry("PayeeCol"); oldBankProfile.deleteEntry("Filter"); oldBankProfile.deleteEntry("SecurityName"); lastUsedDirectory = oldBankProfile.readEntry("InvDirectory"); newProfile = KConfigGroup(config, m_profileConfPrefix.value(Profile::Investment) + QLatin1Char('-') + *profileName); investProfiles.append(*profileName); profileName = bankProfiles.erase(profileName); } else if (oldProfileType == QLatin1String("Banking")) { lastUsedDirectory = oldBankProfile.readEntry("CsvDirectory"); newProfile = KConfigGroup(config, m_profileConfPrefix.value(Profile::Banking) + QLatin1Char('-') + *profileName); ++profileName; } else { if (invalidProfileResponse != QDialogButtonBox::YesToAll && invalidProfileResponse != QDialogButtonBox::NoToAll) { if (!firstTry && !invalidBankProfiles.contains(*profileName)) { // if it isn't first update run and profile isn't on the list of invalid ones then don't bother ++profileName; continue; } invalidProfileResponse = KMessageBox::createKMessageBox(nullptr, new QDialogButtonBox(QDialogButtonBox::Yes | QDialogButtonBox::YesToAll | QDialogButtonBox::No | QDialogButtonBox::NoToAll), QMessageBox::Warning, i18n("
During update of %1
" "the profile type for %2 could not be recognized.
" "The profile cannot be used because of that.
" "Do you want to delete it?
", configFilePath, *profileName), QStringList(), QString(), nullptr, KMessageBox::Dangerous); } switch (invalidProfileResponse) { case QDialogButtonBox::YesToAll: case QDialogButtonBox::Yes: oldBankProfile.deleteGroup(); invalidBankProfiles.removeOne(*profileName); profileName = bankProfiles.erase(profileName); break; case QDialogButtonBox::NoToAll: case QDialogButtonBox::No: if (!invalidBankProfiles.contains(*profileName)) // on user request: don't delete profile but keep eye on it invalidBankProfiles.append(*profileName); ret = false; ++profileName; break; } continue; } oldBankProfile.deleteEntry("FileType"); oldBankProfile.deleteEntry("ProfileName"); oldBankProfile.deleteEntry("DebitFlag"); oldBankProfile.deleteEntry("InvDirectory"); oldBankProfile.deleteEntry("CsvDirectory"); oldBankProfile.sync(); oldBankProfile.copyTo(&newProfile); oldBankProfile.deleteGroup(); newProfile.writeEntry(m_miscSettingsConfName.value(ConfDirectory), lastUsedDirectory); newProfile.writeEntry(m_miscSettingsConfName.value(ConfEncoding), "106" /*UTF-8*/ ); // in 4.8 encoding wasn't supported well so set it to utf8 by default newProfile.sync(); } } for (auto profileName = investProfiles.begin(); profileName != investProfiles.end();) { KConfigGroup investProfile(config, investPrefix + *profileName); if (!investProfile.exists() && !invalidInvestProfiles.contains(*profileName)) { // if there is reference to profile but no profile then remove this reference profileName = investProfiles.erase(profileName); continue; } ++profileName; } profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::Banking), bankProfiles); // update profile names as some of them might have been changed profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::Investment), investProfiles); if (invalidBankProfiles.isEmpty()) // if no invalid profiles then we don't need this variable anymore profileNamesGroup.deleteEntry("InvalidBank"); else profileNamesGroup.writeEntry("InvalidBank", invalidBankProfiles); if (invalidInvestProfiles.isEmpty()) profileNamesGroup.deleteEntry("InvalidInvest"); else profileNamesGroup.writeEntry("InvalidInvest", invalidInvestProfiles); if (ret) QFile::remove(configFilePath + ".bak"); // remove backup if all is ok return ret; } bool CSVImporterCore::profilesAction(const Profile type, const ProfileAction action, const QString &name, const QString &newname) { bool ret = false; const KSharedConfigPtr config = configFile(); KConfigGroup profileNamesGroup(config, m_confProfileNames); QString profileTypeStr = m_profileConfPrefix.value(type); QStringList profiles = profileNamesGroup.readEntry(profileTypeStr, QStringList()); KConfigGroup profileName(config, profileTypeStr + QLatin1Char('-') + name); switch (action) { case ProfileAction::UpdateLastUsed: profileNamesGroup.writeEntry(m_confPriorName + profileTypeStr, profiles.indexOf(name)); break; case ProfileAction::Add: if (!profiles.contains(newname)) { profiles.append(newname); ret = true; } break; case ProfileAction::Remove: { profiles.removeOne(name); profileName.deleteGroup(); profileName.sync(); ret = true; break; } case ProfileAction::Rename: { if (!newname.isEmpty() && name != newname) { int idx = profiles.indexOf(name); if (idx != -1) { profiles[idx] = newname; KConfigGroup newProfileName(config, profileTypeStr + QLatin1Char('-') + newname); if (profileName.exists() && !newProfileName.exists()) { profileName.copyTo(&newProfileName); profileName.deleteGroup(); profileName.sync(); newProfileName.sync(); ret = true; } } } break; } } profileNamesGroup.writeEntry(profileTypeStr, profiles); profileNamesGroup.sync(); return ret; } bool CSVImporterCore::validateDateFormat(const int col) { bool isOK = true; for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) { QStandardItem* item = m_file->m_model->item(row, col); QDate dat = m_convertDate->convertDate(item->text()); if (dat == QDate()) { isOK = false; break; } } return isOK; } bool CSVImporterCore::validateDecimalSymbols(const QList &columns) { bool isOK = true; foreach (const auto column, columns) { m_file->m_parse->setDecimalSymbol(m_decimalSymbolIndexMap.value(column)); for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) { QStandardItem *item = m_file->m_model->item(row, column); QString rawNumber = item->text(); m_file->m_parse->possiblyReplaceSymbol(rawNumber); if (m_file->m_parse->invalidConversion() && !rawNumber.isEmpty()) { // empty strings are welcome isOK = false; break; } } } return isOK; } bool CSVImporterCore::validateCurrencies(const PricesProfile *profile) { if (profile->m_securitySymbol.isEmpty() || profile->m_currencySymbol.isEmpty()) return false; return true; } bool CSVImporterCore::validateSecurity(const PricesProfile *profile) { if (profile->m_securitySymbol.isEmpty() || profile->m_securityName.isEmpty()) return false; return true; } bool CSVImporterCore::validateSecurity(const InvestmentProfile *profile) { if (profile->m_securitySymbol.isEmpty() || profile->m_securityName.isEmpty()) return false; return true; } bool CSVImporterCore::validateSecurities() { QSet onlySymbols; QSet onlyNames; sortSecurities(onlySymbols, onlyNames, m_mapSymbolName); if (!onlySymbols.isEmpty() || !onlyNames.isEmpty()) return false; return true; } eMyMoney::Transaction::Action CSVImporterCore::processActionTypeField(const InvestmentProfile *profile, const int row, const int col) { if (col == -1) return eMyMoney::Transaction::Action::None; QString type = m_file->m_model->item(row, col)->text(); QList actions; actions << eMyMoney::Transaction::Action::Buy << eMyMoney::Transaction::Action::Sell << // first and second most frequent action eMyMoney::Transaction::Action::ReinvestDividend << eMyMoney::Transaction::Action::CashDividend << // we don't want "reinv-dividend" to be accidentally caught by "dividend" eMyMoney::Transaction::Action::Interest << eMyMoney::Transaction::Action::Shrsin << eMyMoney::Transaction::Action::Shrsout; foreach (const auto action, actions) { if (profile->m_transactionNames.value(action).contains(type, Qt::CaseInsensitive)) return action; } return eMyMoney::Transaction::Action::None; } validationResultE CSVImporterCore::validateActionType(MyMoneyStatement::Transaction &tr) { validationResultE ret = ValidActionType; QList validActionTypes = createValidActionTypes(tr); if (validActionTypes.isEmpty()) ret = InvalidActionValues; else if (!validActionTypes.contains(tr.m_eAction)) ret = NoActionType; return ret; } bool CSVImporterCore::calculateFee() { auto profile = dynamic_cast(m_profile); if (!profile) return false; if ((profile->m_feeRate.isEmpty() || // check whether feeRate... profile->m_colTypeNum.value(Column::Amount) == -1)) // ...and amount is in place return false; QString decimalSymbol; if (profile->m_decimalSymbol == DecimalSymbol::Auto) { DecimalSymbol detectedSymbol = detectDecimalSymbol(profile->m_colTypeNum.value(Column::Amount), QString()); if (detectedSymbol == DecimalSymbol::Auto) return false; m_file->m_parse->setDecimalSymbol(detectedSymbol); decimalSymbol = m_file->m_parse->decimalSymbol(detectedSymbol); } else decimalSymbol = m_file->m_parse->decimalSymbol(profile->m_decimalSymbol); MyMoneyMoney feePercent(m_file->m_parse->possiblyReplaceSymbol(profile->m_feeRate)); // convert 0.67% ... feePercent /= MyMoneyMoney(100); // ... to 0.0067 if (profile->m_minFee.isEmpty()) profile->m_minFee = QString::number(0.00, 'f', 2); MyMoneyMoney minFee(m_file->m_parse->possiblyReplaceSymbol(profile->m_minFee)); QList items; for (int row = 0; row < profile->m_startLine; ++row) // fill rows above with whitespace for nice effect with markUnwantedRows items.append(new QStandardItem(QString())); for (int row = profile->m_startLine; row <= profile->m_endLine; ++row) { QString txt, numbers; bool ok = false; numbers = txt = m_file->m_model->item(row, profile->m_colTypeNum.value(Column::Amount))->text(); numbers.remove(QRegularExpression(QStringLiteral("[,. ]"))).toInt(&ok); if (!ok) { // check if it's numerical string... items.append(new QStandardItem(QString())); continue; // ...and skip if not (TODO: allow currency symbols and IDs) } if (txt.startsWith(QLatin1Char('('))) { txt.remove(QRegularExpression(QStringLiteral("[()]"))); txt.prepend(QLatin1Char('-')); } txt = m_file->m_parse->possiblyReplaceSymbol(txt); MyMoneyMoney fee(txt); fee *= feePercent; if (fee < minFee) fee = minFee; txt.setNum(fee.toDouble(), 'f', 4); txt.replace(QLatin1Char('.'), decimalSymbol); //make sure decimal symbol is uniform in whole line items.append(new QStandardItem(txt)); } for (int row = profile->m_endLine + 1; row < m_file->m_rowCount; ++row) // fill rows below with whitespace for nice effect with markUnwantedRows items.append(new QStandardItem(QString())); int col = profile->m_colTypeNum.value(Column::Fee, -1); if (col == -1) { // fee column isn't present m_file->m_model->appendColumn(items); ++m_file->m_columnCount; } else if (col >= m_file->m_columnCount) { // column number must have been stored in profile m_file->m_model->appendColumn(items); ++m_file->m_columnCount; } else { // fee column is present and has been recalculated m_file->m_model->removeColumn(m_file->m_columnCount - 1); m_file->m_model->appendColumn(items); } profile->m_colTypeNum[Column::Fee] = m_file->m_columnCount - 1; return true; } DecimalSymbol CSVImporterCore::detectDecimalSymbol(const int col, const QString &exclude) { DecimalSymbol detectedSymbol = DecimalSymbol::Auto; QString pattern; QRegularExpression re("^[\\(+-]?\\d+[\\)]?$"); // matches '0' ; '+12' ; '-345' ; '(6789)' bool dotIsDecimalSeparator = false; bool commaIsDecimalSeparator = false; for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) { QString txt = m_file->m_model->item(row, col)->text(); if (txt.isEmpty()) // nothing to process, so go to next row continue; int dotPos = txt.lastIndexOf(QLatin1Char('.')); // get last positions of decimal/thousand separator... int commaPos = txt.lastIndexOf(QLatin1Char(',')); // ...to be able to determine which one is the last if (dotPos != -1 && commaPos != -1) { if (dotPos > commaPos && commaIsDecimalSeparator == false) // following case 1,234.56 dotIsDecimalSeparator = true; else if (dotPos < commaPos && dotIsDecimalSeparator == false) // following case 1.234,56 commaIsDecimalSeparator = true; else // following case 1.234,56 and somewhere earlier there was 1,234.56 so unresolvable conflict return detectedSymbol; } else if (dotPos != -1) { // following case 1.23 if (dotIsDecimalSeparator) // it's already know that dotIsDecimalSeparator continue; if (!commaIsDecimalSeparator) // if there is no conflict with comma as decimal separator dotIsDecimalSeparator = true; else { if (txt.count(QLatin1Char('.')) > 1) // following case 1.234.567 so OK continue; else if (txt.length() - 4 == dotPos) // following case 1.234 and somewhere earlier there was 1.234,56 so OK continue; else // following case 1.23 and somewhere earlier there was 1,23 so unresolvable conflict return detectedSymbol; } } else if (commaPos != -1) { // following case 1,23 if (commaIsDecimalSeparator) // it's already know that commaIsDecimalSeparator continue; else if (!dotIsDecimalSeparator) // if there is no conflict with dot as decimal separator commaIsDecimalSeparator = true; else { if (txt.count(QLatin1Char(',')) > 1) // following case 1,234,567 so OK continue; else if (txt.length() - 4 == commaPos) // following case 1,234 and somewhere earlier there was 1,234.56 so OK continue; else // following case 1,23 and somewhere earlier there was 1.23 so unresolvable conflict return detectedSymbol; } } else { // following case 123 if (pattern.isEmpty()) { } txt.remove(QRegularExpression(QLatin1String("[ ") + QRegularExpression::escape(exclude) + QLatin1String("]"))); QRegularExpressionMatch match = re.match(txt); if (match.hasMatch()) // if string is pure numerical then go forward... continue; else // ...if not then it's non-numerical garbage return detectedSymbol; } } if (dotIsDecimalSeparator) detectedSymbol = DecimalSymbol::Dot; else if (commaIsDecimalSeparator) detectedSymbol = DecimalSymbol::Comma; else { // whole column was empty, but we don't want to fail so take OS's decimal symbol if (QLocale().decimalPoint() == QLatin1Char('.')) detectedSymbol = DecimalSymbol::Dot; else detectedSymbol = DecimalSymbol::Comma; } return detectedSymbol; } int CSVImporterCore::detectDecimalSymbols(const QList &columns) { int ret = -2; // get list of used currencies to remove them from col QList accounts; MyMoneyFile *file = MyMoneyFile::instance(); file->accountList(accounts); QList accountTypes; accountTypes << eMyMoney::Account::Type::Checkings << eMyMoney::Account::Type::Savings << eMyMoney::Account::Type::Liability << eMyMoney::Account::Type::Checkings << eMyMoney::Account::Type::Savings << eMyMoney::Account::Type::Cash << eMyMoney::Account::Type::CreditCard << eMyMoney::Account::Type::Loan << eMyMoney::Account::Type::Asset << eMyMoney::Account::Type::Liability; QSet currencySymbols; foreach (const auto account, accounts) { if (accountTypes.contains(account.accountType())) { // account must actually have currency property currencySymbols.insert(account.currencyId()); // add currency id currencySymbols.insert(file->currency(account.currencyId()).tradingSymbol()); // add currency symbol } } QString filteredCurrencies = QStringList(currencySymbols.values()).join(""); QString pattern = QString::fromLatin1("%1%2").arg(QLocale().currencySymbol()).arg(filteredCurrencies); foreach (const auto column, columns) { DecimalSymbol detectedSymbol = detectDecimalSymbol(column, pattern); if (detectedSymbol == DecimalSymbol::Auto) { ret = column; return ret; } m_decimalSymbolIndexMap.insert(column, detectedSymbol); } return ret; } QList CSVImporterCore::findAccounts(const QList &accountTypes, const QString &statementHeader) { MyMoneyFile* file = MyMoneyFile::instance(); QList accountList; file->accountList(accountList); QList filteredTypes; QList filteredAccounts; QRegularExpression filterOutChars(QStringLiteral("[-., ]")); foreach (const auto account, accountList) { if (accountTypes.contains(account.accountType()) && !(account).isClosed()) filteredTypes.append(account); } // filter out accounts whose names aren't in statements header foreach (const auto account, filteredTypes) { QString txt = account.name(); txt.remove(filterOutChars); if (txt.isEmpty() || txt.length() < 3) continue; if (statementHeader.contains(txt, Qt::CaseInsensitive)) filteredAccounts.append(account); } // if filtering returned more results, filter out accounts whose numbers aren't in statements header if (filteredAccounts.count() > 1) { for (auto account = filteredAccounts.begin(); account != filteredAccounts.end();) { QString txt = (*account).number(); txt.remove(filterOutChars); if (txt.isEmpty() || txt.length() < 3) { ++account; continue; } if (statementHeader.contains(txt, Qt::CaseInsensitive)) ++account; else account = filteredAccounts.erase(account); } } // if filtering returned more results, filter out accounts whose numbers are the shortest if (filteredAccounts.count() > 1) { for (auto i = 1; i < filteredAccounts.count();) { auto firstAccNumber = filteredAccounts.at(0).number(); auto secondAccNumber = filteredAccounts.at(i).number(); if (firstAccNumber.length() > secondAccNumber.length()) { filteredAccounts.removeAt(i); } else if (firstAccNumber.length() < secondAccNumber.length()) { filteredAccounts.removeAt(0); --i; } else { ++i; } } } // if filtering returned more results, filter out accounts whose names are the shortest if (filteredAccounts.count() > 1) { for (auto i = 1; i < filteredAccounts.count();) { auto firstAccName = filteredAccounts.at(0).name(); auto secondAccName = filteredAccounts.at(i).name(); if (firstAccName.length() > secondAccName.length()) { filteredAccounts.removeAt(i); } else if (firstAccName.length() < secondAccName.length()) { filteredAccounts.removeAt(0); --i; } else { ++i; } } } // if filtering by name and number didn't return nothing, then try filtering by number only if (filteredAccounts.isEmpty()) { foreach (const auto account, filteredTypes) { QString txt = account.number(); txt.remove(filterOutChars); if (txt.isEmpty() || txt.length() < 3) continue; if (statementHeader.contains(txt, Qt::CaseInsensitive)) filteredAccounts.append(account); } } return filteredAccounts; } bool CSVImporterCore::detectAccount(MyMoneyStatement &st) { QString statementHeader; for (int row = 0; row < m_profile->m_startLine; ++row) // concatenate header for better search for (int col = 0; col < m_file->m_columnCount; ++col) statementHeader.append(m_file->m_model->item(row, col)->text()); statementHeader.remove(QRegularExpression(QStringLiteral("[-., ]"))); QList accounts; QList accountTypes; switch(m_profile->type()) { default: case Profile::Banking: accountTypes << eMyMoney::Account::Type::Checkings << eMyMoney::Account::Type::Savings << eMyMoney::Account::Type::Liability << eMyMoney::Account::Type::Checkings << eMyMoney::Account::Type::Savings << eMyMoney::Account::Type::Cash << eMyMoney::Account::Type::CreditCard << eMyMoney::Account::Type::Loan << eMyMoney::Account::Type::Asset << eMyMoney::Account::Type::Liability; accounts = findAccounts(accountTypes, statementHeader); break; case Profile::Investment: accountTypes << eMyMoney::Account::Type::Investment; // take investment accounts... accounts = findAccounts(accountTypes, statementHeader); //...and search them in statement header break; } if (accounts.count() == 1) { // set account in statement, if it was the only one match st.m_strAccountName = accounts.first().name(); st.m_strAccountNumber = accounts.first().number(); st.m_accountId = accounts.first().id(); switch (accounts.first().accountType()) { case eMyMoney::Account::Type::Checkings: st.m_eType = eMyMoney::Statement::Type::Checkings; break; case eMyMoney::Account::Type::Savings: st.m_eType = eMyMoney::Statement::Type::Savings; break; case eMyMoney::Account::Type::Investment: st.m_eType = eMyMoney::Statement::Type::Investment; break; case eMyMoney::Account::Type::CreditCard: st.m_eType = eMyMoney::Statement::Type::CreditCard; break; default: st.m_eType = eMyMoney::Statement::Type::None; } return true; } return false; } bool CSVImporterCore::processBankRow(MyMoneyStatement &st, const BankingProfile *profile, const int row) { MyMoneyStatement::Transaction tr; QString memo; QString txt; if (!profile) return false; // process date field int col = profile->m_colTypeNum.value(Column::Date, -1); tr.m_datePosted = processDateField(row, col); if (tr.m_datePosted == QDate()) return false; // process number field col = profile->m_colTypeNum.value(Column::Number, -1); if (col != -1) tr.m_strNumber = m_file->m_model->item(row, col)->text(); // process payee field col = profile->m_colTypeNum.value(Column::Payee, -1); if (col != -1) tr.m_strPayee = m_file->m_model->item(row, col)->text(); // process memo field col = profile->m_colTypeNum.value(Column::Memo, -1); if (col != -1) memo.append(m_file->m_model->item(row, col)->text()); for (int i = 0; i < profile->m_memoColList.count(); ++i) { if (profile->m_memoColList.at(i) != col) { if (!memo.isEmpty()) memo.append(QLatin1Char('\n')); if (profile->m_memoColList.at(i) < m_file->m_columnCount) memo.append(m_file->m_model->item(row, profile->m_memoColList.at(i))->text()); } } tr.m_strMemo = memo; // process amount field col = profile->m_colTypeNum.value(Column::Amount, -1); tr.m_amount = processAmountField(profile, row, col); if (col != -1 && profile->m_oppositeSigns) // change signs to opposite if requested by user tr.m_amount *= MyMoneyMoney(-1); // process credit/debit field if (profile->m_colTypeNum.value(Column::Credit, -1) != -1 && profile->m_colTypeNum.value(Column::Debit, -1) != -1) { QString credit = m_file->m_model->item(row, profile->m_colTypeNum.value(Column::Credit))->text(); QString debit = m_file->m_model->item(row, profile->m_colTypeNum.value(Column::Debit))->text(); tr.m_amount = processCreditDebit(credit, debit); if (!credit.isEmpty() && !debit.isEmpty()) return false; } MyMoneyStatement::Split s1; s1.m_amount = tr.m_amount; s1.m_strMemo = tr.m_strMemo; MyMoneyStatement::Split s2 = s1; s2.m_reconcile = tr.m_reconcile; s2.m_amount = -s1.m_amount; // process category field col = profile->m_colTypeNum.value(Column::Category, -1); if (col != -1) { txt = m_file->m_model->item(row, col)->text(); QString accountId = MyMoneyFile::instance()->checkCategory(txt, s1.m_amount, s2.m_amount); if (!accountId.isEmpty()) { s2.m_accountId = accountId; s2.m_strCategoryName = txt; tr.m_listSplits.append(s2); } } // calculate hash txt.clear(); for (int i = 0; i < m_file->m_columnCount; ++i) txt.append(m_file->m_model->item(row, i)->text()); QString hashBase = QString::fromLatin1("%1-%2") .arg(tr.m_datePosted.toString(Qt::ISODate)) .arg(MyMoneyTransaction::hash(txt)); QString hash; for (uchar idx = 0; idx < 0xFF; ++idx) { // assuming threre will be no more than 256 transactions with the same hashBase hash = QString::fromLatin1("%1-%2").arg(hashBase).arg(idx); QSet::const_iterator it = m_hashSet.constFind(hash); if (it == m_hashSet.constEnd()) break; } m_hashSet.insert(hash); tr.m_strBankID = hash; st.m_listTransactions.append(tr); // Add the MyMoneyStatement::Transaction to the statement return true; } bool CSVImporterCore::processInvestRow(MyMoneyStatement &st, const InvestmentProfile *profile, const int row) { MyMoneyStatement::Transaction tr; if (!profile) return false; QString memo; QString txt; // process date field int col = profile->m_colTypeNum.value(Column::Date, -1); tr.m_datePosted = processDateField(row, col); if (tr.m_datePosted == QDate()) return false; // process quantity field col = profile->m_colTypeNum.value(Column::Quantity, -1); tr.m_shares = processQuantityField(profile, row, col); // process price field col = profile->m_colTypeNum.value(Column::Price, -1); tr.m_price = processPriceField(profile, row, col); // process amount field col = profile->m_colTypeNum.value(Column::Amount, -1); tr.m_amount = processAmountField(profile, row, col); // process type field col = profile->m_colTypeNum.value(Column::Type, -1); tr.m_eAction = processActionTypeField(profile, row, col); if (!m_isActionTypeValidated && col != -1 && // if action type wasn't validated in wizard then... validateActionType(tr) != ValidActionType) // ...check if price, amount, quantity is appropriate return false; // process fee field col = profile->m_colTypeNum.value(Column::Fee, -1); if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) { DecimalSymbol decimalSymbol = m_decimalSymbolIndexMap.value(col); m_file->m_parse->setDecimalSymbol(decimalSymbol); } txt = m_file->m_model->item(row, col)->text(); if (txt.startsWith(QLatin1Char('('))) // check if brackets notation is used for negative numbers txt.remove(QRegularExpression(QStringLiteral("[()]"))); if (txt.isEmpty()) tr.m_fees = MyMoneyMoney(); else { MyMoneyMoney fee(m_file->m_parse->possiblyReplaceSymbol(txt)); if (profile->m_feeIsPercentage && profile->m_feeRate.isEmpty()) // fee is percent fee *= tr.m_amount / MyMoneyMoney(100); // as percentage fee.abs(); tr.m_fees = fee; } } // process symbol and name field col = profile->m_colTypeNum.value(Column::Symbol, -1); if (col != -1) tr.m_strSymbol = m_file->m_model->item(row, col)->text(); col = profile->m_colTypeNum.value(Column::Name, -1); if (col != -1 && tr.m_strSymbol.isEmpty()) { // case in which symbol field is empty txt = m_file->m_model->item(row, col)->text(); tr.m_strSymbol = m_mapSymbolName.key(txt); // it's all about getting the right symbol } else if (!profile->m_securitySymbol.isEmpty()) tr.m_strSymbol = profile->m_securitySymbol; else if (tr.m_strSymbol.isEmpty()) return false; tr.m_strSecurity = m_mapSymbolName.value(tr.m_strSymbol); // take name from prepared names to avoid potential name mismatch // process memo field col = profile->m_colTypeNum.value(Column::Memo, -1); if (col != -1) memo.append(m_file->m_model->item(row, col)->text()); for (int i = 0; i < profile->m_memoColList.count(); ++i) { if (profile->m_memoColList.at(i) != col) { if (!memo.isEmpty()) memo.append(QLatin1Char('\n')); if (profile->m_memoColList.at(i) < m_file->m_columnCount) memo.append(m_file->m_model->item(row, profile->m_memoColList.at(i))->text()); } } tr.m_strMemo = memo; tr.m_strInterestCategory.clear(); // no special category tr.m_strBrokerageAccount.clear(); // no brokerage account auto-detection MyMoneyStatement::Split s1; s1.m_amount = tr.m_amount; s1.m_strMemo = tr.m_strMemo; MyMoneyStatement::Split s2 = s1; s2.m_amount = -s1.m_amount; s2.m_accountId = MyMoneyFile::instance()->checkCategory(tr.m_strInterestCategory, s1.m_amount, s2.m_amount); // deduct fees from amount if (tr.m_eAction == eMyMoney::Transaction::Action::CashDividend || tr.m_eAction == eMyMoney::Transaction::Action::Sell || tr.m_eAction == eMyMoney::Transaction::Action::Interest) tr.m_amount -= tr.m_fees; else if (tr.m_eAction == eMyMoney::Transaction::Action::Buy) { if (tr.m_amount.isPositive()) tr.m_amount = -tr.m_amount; //if broker doesn't use minus sings for buy transactions, set it manually here tr.m_amount -= tr.m_fees; } else if (tr.m_eAction == eMyMoney::Transaction::Action::None) tr.m_listSplits.append(s2); st.m_listTransactions.append(tr); // Add the MyMoneyStatement::Transaction to the statement return true; } bool CSVImporterCore::processPriceRow(MyMoneyStatement &st, const PricesProfile *profile, const int row) { MyMoneyStatement::Price pr; if (!profile) return false; // process date field int col = profile->m_colTypeNum.value(Column::Date, -1); pr.m_date = processDateField(row, col); if (pr.m_date == QDate()) return false; // process price field col = profile->m_colTypeNum.value(Column::Price, -1); pr.m_amount = processPriceField(profile, row, col); switch (profile->type()) { case Profile::CurrencyPrices: if (profile->m_securitySymbol.isEmpty() || profile->m_currencySymbol.isEmpty()) return false; pr.m_strSecurity = profile->m_securitySymbol; pr.m_strCurrency = profile->m_currencySymbol; break; case Profile::StockPrices: if (profile->m_securityName.isEmpty()) return false; pr.m_strSecurity = profile->m_securityName; break; default: return false; } pr.m_sourceName = profile->m_profileName; st.m_listPrices.append(pr); // Add price to the statement return true; } QDate CSVImporterCore::processDateField(const int row, const int col) { QDate date; if (col != -1) { QString txt = m_file->m_model->item(row, col)->text(); date = m_convertDate->convertDate(txt); // Date column } return date; } MyMoneyMoney CSVImporterCore::processCreditDebit(QString &credit, QString &debit) { MyMoneyMoney amount; if (m_profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(m_profile->m_colTypeNum.value(Column::Credit)); if (credit.startsWith(QLatin1Char('('))) { // check if brackets notation is used for negative numbers credit.remove(QRegularExpression(QStringLiteral("[()]"))); credit.prepend(QLatin1Char('-')); } if (debit.startsWith(QLatin1Char('('))) { // check if brackets notation is used for negative numbers debit.remove(QRegularExpression(QStringLiteral("[()]"))); debit.prepend(QLatin1Char('-')); } if (!credit.isEmpty() && !debit.isEmpty()) { // we do not expect both fields to be non-zero if (MyMoneyMoney(credit).isZero()) credit = QString(); if (MyMoneyMoney(debit).isZero()) debit = QString(); } if (!debit.startsWith(QLatin1Char('-')) && !debit.isEmpty()) // ensure debit field is negative debit.prepend(QLatin1Char('-')); if (!credit.isEmpty() && debit.isEmpty()) amount = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(credit)); else if (credit.isEmpty() && !debit.isEmpty()) amount = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(debit)); else if (!credit.isEmpty() && !debit.isEmpty()) { // both fields are non-empty and non-zero so let user decide return amount; } else amount = MyMoneyMoney(); // both fields are empty and zero so set amount to zero return amount; } MyMoneyMoney CSVImporterCore::processQuantityField(const CSVProfile *profile, const int row, const int col) { MyMoneyMoney shares; if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(col); QString txt = m_file->m_model->item(row, col)->text(); txt.remove(QRegularExpression(QStringLiteral("-+"))); // remove unwanted sings in quantity if (!txt.isEmpty()) shares = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(txt)); } return shares; } MyMoneyMoney CSVImporterCore::processAmountField(const CSVProfile *profile, const int row, const int col) { MyMoneyMoney amount; if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(col); QString txt = m_file->m_model->item(row, col)->text(); if (txt.startsWith(QLatin1Char('('))) { // check if brackets notation is used for negative numbers txt.remove(QRegularExpression(QStringLiteral("[()]"))); txt.prepend(QLatin1Char('-')); } if (!txt.isEmpty()) amount = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(txt)); } return amount; } MyMoneyMoney CSVImporterCore::processPriceField(const InvestmentProfile *profile, const int row, const int col) { MyMoneyMoney price; if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(col); QString txt = m_file->m_model->item(row, col)->text(); if (!txt.isEmpty()) { price = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(txt)); price *= m_priceFractions.at(profile->m_priceFraction); } } return price; } MyMoneyMoney CSVImporterCore::processPriceField(const PricesProfile *profile, const int row, const int col) { MyMoneyMoney price; if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(col); QString txt = m_file->m_model->item(row, col)->text(); if (!txt.isEmpty()) { price = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(txt)); price *= m_priceFractions.at(profile->m_priceFraction); } } return price; } QList CSVImporterCore::createValidActionTypes(MyMoneyStatement::Transaction &tr) { QList validActionTypes; if (tr.m_shares.isPositive() && tr.m_price.isPositive() && !tr.m_amount.isZero()) validActionTypes << eMyMoney::Transaction::Action::ReinvestDividend << eMyMoney::Transaction::Action::Buy << eMyMoney::Transaction::Action::Sell; else if (tr.m_shares.isZero() && tr.m_price.isZero() && !tr.m_amount.isZero()) validActionTypes << eMyMoney::Transaction::Action::CashDividend << eMyMoney::Transaction::Action::Interest; else if (tr.m_shares.isPositive() && tr.m_price.isZero() && tr.m_amount.isZero()) validActionTypes << eMyMoney::Transaction::Action::Shrsin << eMyMoney::Transaction::Action::Shrsout; return validActionTypes; } bool CSVImporterCore::sortSecurities(QSet& onlySymbols, QSet& onlyNames, QMap& mapSymbolName) { QList securityList = MyMoneyFile::instance()->securityList(); int symbolCol = m_profile->m_colTypeNum.value(Column::Symbol, -1); int nameCol = m_profile->m_colTypeNum.value(Column::Name, -1); // sort by availability of symbol and name for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) { QString symbol; QString name; if (symbolCol != -1) symbol = m_file->m_model->item(row, symbolCol)->text().trimmed(); if (nameCol != -1) name = m_file->m_model->item(row, nameCol)->text().trimmed(); if (!symbol.isEmpty() && !name.isEmpty()) mapSymbolName.insert(symbol, name); else if (!symbol.isEmpty()) onlySymbols.insert(symbol); else if (!name.isEmpty()) onlyNames.insert(name); else return false; } // try to find names for symbols for (QSet::iterator symbol = onlySymbols.begin(); symbol != onlySymbols.end();) { QList filteredSecurities; foreach (const auto sec, securityList) { if ((*symbol).compare(sec.tradingSymbol(), Qt::CaseInsensitive) == 0) filteredSecurities.append(sec); // gather all securities that by matched by symbol } if (filteredSecurities.count() == 1) { // single security matched by the symbol so... mapSymbolName.insert(*symbol, filteredSecurities.first().name()); symbol = onlySymbols.erase(symbol); // ...it's no longer unknown } else if (!filteredSecurities.isEmpty()) { // multiple securities matched by the symbol // TODO: Ask user which security should we match to mapSymbolName.insert(*symbol, filteredSecurities.first().name()); symbol = onlySymbols.erase(symbol); } else // no security matched, so leave it as unknown ++symbol; } // try to find symbols for names for (QSet::iterator name = onlyNames.begin(); name != onlyNames.end();) { QList filteredSecurities; foreach (const auto sec, securityList) { if ((*name).compare(sec.name(), Qt::CaseInsensitive) == 0) filteredSecurities.append(sec); // gather all securities that by matched by name } if (filteredSecurities.count() == 1) { // single security matched by the name so... mapSymbolName.insert(filteredSecurities.first().tradingSymbol(), *name); name = onlyNames.erase(name); // ...it's no longer unknown } else if (!filteredSecurities.isEmpty()) { // multiple securities matched by the name // TODO: Ask user which security should we match to mapSymbolName.insert(filteredSecurities.first().tradingSymbol(), *name); name = onlySymbols.erase(name); } else // no security matched, so leave it as unknown ++name; } return true; } void CSVImporterCore::setupFieldDecimalSymbol(int col) { m_file->m_parse->setDecimalSymbol(m_decimalSymbolIndexMap.value(col)); } QList CSVImporterCore::getNumericalColumns() { QList columns; switch(m_profile->type()) { case Profile::Banking: if (m_profile->m_colTypeNum.value(Column::Amount, -1) != -1) { columns << m_profile->m_colTypeNum.value(Column::Amount); } else { columns << m_profile->m_colTypeNum.value(Column::Debit); columns << m_profile->m_colTypeNum.value(Column::Credit); } break; case Profile::Investment: columns << m_profile->m_colTypeNum.value(Column::Amount); columns << m_profile->m_colTypeNum.value(Column::Price); columns << m_profile->m_colTypeNum.value(Column::Quantity); if (m_profile->m_colTypeNum.value(Column::Fee, -1) != -1) columns << m_profile->m_colTypeNum.value(Column::Fee); break; case Profile::CurrencyPrices: case Profile::StockPrices: columns << m_profile->m_colTypeNum.value(Column::Price); break; default: break; } return columns; } bool CSVImporterCore::createStatement(MyMoneyStatement &st) { switch (m_profile->type()) { case Profile::Banking: { if (!st.m_listTransactions.isEmpty()) // don't create statement if there is one return true; st.m_eType = eMyMoney::Statement::Type::None; if (m_autodetect.value(AutoAccountBank)) detectAccount(st); m_hashSet.clear(); BankingProfile *profile = dynamic_cast(m_profile); for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) if (!processBankRow(st, profile, row)) { // parse fields st = MyMoneyStatement(); return false; } return true; break; } case Profile::Investment: { if (!st.m_listTransactions.isEmpty()) // don't create statement if there is one return true; st.m_eType = eMyMoney::Statement::Type::Investment; if (m_autodetect.value(AutoAccountInvest)) detectAccount(st); auto profile = dynamic_cast(m_profile); if ((m_profile->m_colTypeNum.value(Column::Fee, -1) == -1 || m_profile->m_colTypeNum.value(Column::Fee, -1) >= m_file->m_columnCount) && profile && !profile->m_feeRate.isEmpty()) // fee column has not been calculated so do it now calculateFee(); if (profile) { for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) if (!processInvestRow(st, profile, row)) { // parse fields st = MyMoneyStatement(); return false; } } for (QMap::const_iterator it = m_mapSymbolName.cbegin(); it != m_mapSymbolName.cend(); ++it) { MyMoneyStatement::Security security; security.m_strSymbol = it.key(); security.m_strName = it.value(); st.m_listSecurities.append(security); } return true; break; } default: case Profile::CurrencyPrices: case Profile::StockPrices: { if (!st.m_listPrices.isEmpty()) // don't create statement if there is one return true; st.m_eType = eMyMoney::Statement::Type::None; if (auto profile = dynamic_cast(m_profile)) { for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) if (!processPriceRow(st, profile, row)) { // parse fields st = MyMoneyStatement(); return false; } } for (QMap::const_iterator it = m_mapSymbolName.cbegin(); it != m_mapSymbolName.cend(); ++it) { MyMoneyStatement::Security security; security.m_strSymbol = it.key(); security.m_strName = it.value(); st.m_listSecurities.append(security); } return true; } } return true; } void CSVProfile::readSettings(const KConfigGroup &profilesGroup) { m_lastUsedDirectory = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDirectory), QString()); m_startLine = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfStartLine), 0); m_trailerLines = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfTrailerLines), 0); m_encodingMIBEnum = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfEncoding), 106 /* UTF-8 */); m_dateFormat = static_cast(profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDateFormat), (int)DateFormat::YearMonthDay)); m_textDelimiter = static_cast(profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfTextDelimiter), (int)TextDelimiter::DoubleQuote)); m_fieldDelimiter = static_cast(profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFieldDelimiter), (int)FieldDelimiter::Auto)); m_decimalSymbol = static_cast(profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDecimalSymbol), (int)DecimalSymbol::Auto)); initColNumType(); } void CSVProfile::writeSettings(KConfigGroup &profilesGroup) { QFileInfo fileInfo (m_lastUsedDirectory); if (fileInfo.isFile()) m_lastUsedDirectory = fileInfo.absolutePath(); if (m_lastUsedDirectory.startsWith(QDir::homePath())) // replace /home/user with ~/ for brevity m_lastUsedDirectory.replace(0, QDir::homePath().length(), QLatin1Char('~')); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDirectory), m_lastUsedDirectory); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfEncoding), m_encodingMIBEnum); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDateFormat), (int)m_dateFormat); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFieldDelimiter), (int)m_fieldDelimiter); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfTextDelimiter), (int)m_textDelimiter); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDecimalSymbol), (int)m_decimalSymbol); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfStartLine), m_startLine); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfTrailerLines), m_trailerLines); } bool BankingProfile::readSettings(const KSharedConfigPtr &config) { bool exists = true; KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); if (!profilesGroup.exists()) exists = false; m_colTypeNum[Column::Payee] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Payee), -1); m_colTypeNum[Column::Number] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Number), -1); m_colTypeNum[Column::Amount] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Amount), -1); m_colTypeNum[Column::Debit] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Debit), -1); m_colTypeNum[Column::Credit] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Credit), -1); m_colTypeNum[Column::Date] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), -1); m_colTypeNum[Column::Category] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Category), -1); m_colTypeNum[Column::Memo] = -1; // initialize, otherwise random data may go here m_oppositeSigns = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfOppositeSigns), false); m_memoColList = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Memo), QList()); CSVProfile::readSettings(profilesGroup); return exists; } void BankingProfile::writeSettings(const KSharedConfigPtr &config) { KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); CSVProfile::writeSettings(profilesGroup); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfOppositeSigns), m_oppositeSigns); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Payee), m_colTypeNum.value(Column::Payee)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Number), m_colTypeNum.value(Column::Number)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Amount), m_colTypeNum.value(Column::Amount)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Debit), m_colTypeNum.value(Column::Debit)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Credit), m_colTypeNum.value(Column::Credit)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), m_colTypeNum.value(Column::Date)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Category), m_colTypeNum.value(Column::Category)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Memo), m_memoColList); profilesGroup.config()->sync(); } bool InvestmentProfile::readSettings(const KSharedConfigPtr &config) { bool exists = true; KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); if (!profilesGroup.exists()) exists = false; m_transactionNames[eMyMoney::Transaction::Action::Buy] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Buy), QString(i18nc("Type of operation as in financial statement", "buy")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::Sell] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Sell), QString(i18nc("Type of operation as in financial statement", "sell,repurchase")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::ReinvestDividend] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::ReinvestDividend), QString(i18nc("Type of operation as in financial statement", "reinvest,reinv,re-inv")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::CashDividend] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::CashDividend), QString(i18nc("Type of operation as in financial statement", "dividend")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::Interest] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Interest), QString(i18nc("Type of operation as in financial statement", "interest,income")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::Shrsin] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Shrsin), QString(i18nc("Type of operation as in financial statement", "add,stock dividend,divd reinv,transfer in,re-registration in,journal entry")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::Shrsout] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Shrsout), QString(i18nc("Type of operation as in financial statement", "remove")).split(',', QString::SkipEmptyParts)); m_colTypeNum[Column::Date] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), -1); m_colTypeNum[Column::Type] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Type), -1); //use for type col. m_colTypeNum[Column::Price] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Price), -1); m_colTypeNum[Column::Quantity] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Quantity), -1); m_colTypeNum[Column::Amount] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Amount), -1); m_colTypeNum[Column::Name] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Name), -1); m_colTypeNum[Column::Fee] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Fee), -1); m_colTypeNum[Column::Symbol] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Symbol), -1); m_colTypeNum[Column::Memo] = -1; // initialize, otherwise random data may go here m_feeIsPercentage = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFeeIsPercentage), false); m_feeRate = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFeeRate), QString()); m_minFee = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfMinFee), QString()); m_memoColList = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Memo), QList()); m_securityName = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecurityName), QString()); m_securitySymbol = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecuritySymbol), QString()); m_dontAsk = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDontAsk), 0); m_priceFraction = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfPriceFraction), 2); CSVProfile::readSettings(profilesGroup); return exists; } void InvestmentProfile::writeSettings(const KSharedConfigPtr &config) { KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); CSVProfile::writeSettings(profilesGroup); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Buy), m_transactionNames.value(eMyMoney::Transaction::Action::Buy)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Sell), m_transactionNames.value(eMyMoney::Transaction::Action::Sell)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::ReinvestDividend), m_transactionNames.value(eMyMoney::Transaction::Action::ReinvestDividend)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::CashDividend), m_transactionNames.value(eMyMoney::Transaction::Action::CashDividend)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Interest), m_transactionNames.value(eMyMoney::Transaction::Action::Interest)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Shrsin), m_transactionNames.value(eMyMoney::Transaction::Action::Shrsin)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Shrsout), m_transactionNames.value(eMyMoney::Transaction::Action::Shrsout)); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfPriceFraction), m_priceFraction); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFeeIsPercentage), m_feeIsPercentage); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFeeRate), m_feeRate); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfMinFee), m_minFee); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecurityName), m_securityName); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecuritySymbol), m_securitySymbol); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDontAsk), m_dontAsk); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), m_colTypeNum.value(Column::Date)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Type), m_colTypeNum.value(Column::Type)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Quantity), m_colTypeNum.value(Column::Quantity)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Amount), m_colTypeNum.value(Column::Amount)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Price), m_colTypeNum.value(Column::Price)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Symbol), m_colTypeNum.value(Column::Symbol)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Name), m_colTypeNum.value(Column::Name)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Fee), m_colTypeNum.value(Column::Fee)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Memo), m_memoColList); profilesGroup.config()->sync(); } bool PricesProfile::readSettings(const KSharedConfigPtr &config) { bool exists = true; KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); if (!profilesGroup.exists()) exists = false; m_colTypeNum[Column::Date] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), -1); m_colTypeNum[Column::Price] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Price), -1); m_priceFraction = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfPriceFraction), 2); m_securityName = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecurityName), QString()); m_securitySymbol = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecuritySymbol), QString()); m_currencySymbol = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfCurrencySymbol), QString()); m_dontAsk = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDontAsk), 0); CSVProfile::readSettings(profilesGroup); return exists; } void PricesProfile::writeSettings(const KSharedConfigPtr &config) { KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); CSVProfile::writeSettings(profilesGroup); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), m_colTypeNum.value(Column::Date)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Price), m_colTypeNum.value(Column::Price)); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfPriceFraction), m_priceFraction); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecurityName), m_securityName); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecuritySymbol), m_securitySymbol); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfCurrencySymbol), m_currencySymbol); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDontAsk), m_dontAsk); profilesGroup.config()->sync(); } CSVFile::CSVFile() : m_columnCount(0), m_rowCount(0) { m_parse = new Parse; m_model = new QStandardItemModel; } CSVFile::~CSVFile() { delete m_parse; delete m_model; } void CSVFile::getStartEndRow(CSVProfile *profile) { profile->m_endLine = m_rowCount - 1; - if (profile->m_endLine > profile->m_trailerLines) + + // if trailer lines are specified then remove them here + if (profile->m_trailerLines) profile->m_endLine -= profile->m_trailerLines; if (profile->m_startLine > profile->m_endLine) // Don't allow m_startLine > m_endLine profile->m_startLine = profile->m_endLine; } void CSVFile::getColumnCount(CSVProfile *profile, const QStringList &rows) { if (rows.isEmpty()) return; QVector delimiterIndexes; if (profile->m_fieldDelimiter == FieldDelimiter::Auto) delimiterIndexes = QVector{FieldDelimiter::Comma, FieldDelimiter::Semicolon, FieldDelimiter::Colon, FieldDelimiter::Tab}; // include all delimiters to test or ... else delimiterIndexes = QVector{profile->m_fieldDelimiter}; // ... only the one specified QList totalDelimiterCount({0, 0, 0, 0}); // Total in file for each delimiter QList thisDelimiterCount({0, 0, 0, 0}); // Total in this line for each delimiter int colCount = 0; // Total delimiters in this line FieldDelimiter possibleDelimiter = FieldDelimiter::Comma; m_columnCount = 0; foreach (const auto row, rows) { foreach(const auto delimiterIndex, delimiterIndexes) { m_parse->setFieldDelimiter(delimiterIndex); colCount = m_parse->parseLine(row).count(); // parse each line using each delimiter if (colCount > thisDelimiterCount.at((int)delimiterIndex)) thisDelimiterCount[(int)delimiterIndex] = colCount; if (thisDelimiterCount[(int)delimiterIndex] > m_columnCount) m_columnCount = thisDelimiterCount.at((int)delimiterIndex); totalDelimiterCount[(int)delimiterIndex] += colCount; if (totalDelimiterCount.at((int)delimiterIndex) > totalDelimiterCount.at((int)possibleDelimiter)) possibleDelimiter = delimiterIndex; } } if (delimiterIndexes.count() != 1) // if purpose was to autodetect... profile->m_fieldDelimiter = possibleDelimiter; // ... then change field delimiter m_parse->setFieldDelimiter(profile->m_fieldDelimiter); // restore original field delimiter } bool CSVFile::getInFileName(QString inFileName) { QFileInfo fileInfo; if (!inFileName.isEmpty()) { if (inFileName.startsWith(QLatin1Char('~'))) inFileName.replace(0, 1, QDir::homePath()); fileInfo = QFileInfo(inFileName); if (fileInfo.isFile()) { // if it is file... if (fileInfo.exists()) { // ...and exists... m_inFileName = inFileName; // ...then set as valid filename return true; // ...and return success... } else { // ...but if not... fileInfo.setFile(fileInfo.absolutePath()); //...then set start directory to directory of that file... if (!fileInfo.exists()) //...and if it doesn't exist too... fileInfo.setFile(QDir::homePath()); //...then set start directory to home path } } else if (fileInfo.isDir()) { if (fileInfo.exists()) fileInfo = QFileInfo(inFileName); else fileInfo.setFile(QDir::homePath()); } } else fileInfo = QFileInfo(QDir::homePath()); QPointer dialog = new QFileDialog(nullptr, QString(), fileInfo.absoluteFilePath(), i18n("CSV Files (*.csv)")); dialog->setFileMode(QFileDialog::ExistingFile); QUrl url; if (dialog->exec() == QDialog::Accepted) url = dialog->selectedUrls().first(); delete dialog; if (url.isEmpty()) { m_inFileName.clear(); return false; } else m_inFileName = url.toDisplayString(QUrl::PreferLocalFile); return true; } void CSVFile::setupParser(CSVProfile *profile) { if (profile->m_decimalSymbol != DecimalSymbol::Auto) m_parse->setDecimalSymbol(profile->m_decimalSymbol); m_parse->setFieldDelimiter(profile->m_fieldDelimiter); m_parse->setTextDelimiter(profile->m_textDelimiter); } void CSVFile::readFile(CSVProfile *profile) { QFile inFile(m_inFileName); if (!inFile.exists()) return; inFile.open(QIODevice::ReadOnly); QTextStream inStream(&inFile); QTextCodec* codec = QTextCodec::codecForMib(profile->m_encodingMIBEnum); inStream.setCodec(codec); QString buf = inStream.readAll(); inFile.close(); m_parse->setTextDelimiter(profile->m_textDelimiter); QStringList rows = m_parse->parseFile(buf); // parse the buffer m_rowCount = m_parse->lastLine(); // won't work without above line getColumnCount(profile, rows); getStartEndRow(profile); // prepare model from rows having rowCount and columnCount m_model->clear(); for (int i = 0; i < m_rowCount; ++i) { QList itemList; QStringList columns = m_parse->parseLine(rows.takeFirst()); // take instead of read from rows to preserve memory for (int j = 0; j < m_columnCount; ++j) itemList.append(new QStandardItem(columns.value(j, QString()))); m_model->appendRow(itemList); } } diff --git a/kmymoney/plugins/csv/import/csvwizard.cpp b/kmymoney/plugins/csv/import/csvwizard.cpp index 77a186f6a..a7ce429c3 100644 --- a/kmymoney/plugins/csv/import/csvwizard.cpp +++ b/kmymoney/plugins/csv/import/csvwizard.cpp @@ -1,1119 +1,1119 @@ /* * Copyright 2015-2016 Allan Anderson * Copyright 2016-2018 Łukasz Wojniłowicz * Copyright 2018 Thomas Baumgart * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "csvwizard.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "csvimporter.h" #include "core/csvutil.h" #include "core/convdate.h" #include "core/csvimportercore.h" #include "investmentwizardpage.h" #include "bankingwizardpage.h" #include "priceswizardpage.h" #include "icons/icons.h" #include "ui_csvwizard.h" #include "ui_introwizardpage.h" #include "ui_separatorwizardpage.h" #include "ui_rowswizardpage.h" #include "ui_formatswizardpage.h" using namespace Icons; CSVWizard::CSVWizard(CSVImporter *plugin) : ui(new Ui::CSVWizard), m_skipSetup(false), m_plugin(plugin), m_imp(new CSVImporterCore), m_wiz(new QWizard) { ui->setupUi(this); ui->tableView->setModel(m_imp->m_file->m_model); readWindowSize(CSVImporterCore::configFile()); m_wiz->setWizardStyle(QWizard::ClassicStyle); ui->horizontalLayout->addWidget(m_wiz); m_curId = -1; m_lastId = -1; m_wiz->installEventFilter(this); // event filter for escape key presses m_wiz->button(QWizard::BackButton)->setIcon(Icons::get(Icon::ArrowLeft)); m_wiz->button(QWizard::CancelButton)->setIcon(Icons::get(Icon::DialogCancel)); m_wiz->button(QWizard::FinishButton)->setIcon(Icons::get(Icon::KMyMoney)); m_wiz->button(QWizard::CustomButton1)->setIcon(Icons::get(Icon::FileArchiver)); m_wiz->button(QWizard::CustomButton2)->setIcon(Icons::get(Icon::InvestApplet)); m_wiz->button(QWizard::NextButton)->setIcon(Icons::get(Icon::ArrowRight)); m_pageIntro = new IntroPage(this, m_imp); m_wiz->setPage(PageIntro, m_pageIntro); m_pageSeparator = new SeparatorPage(this, m_imp); m_wiz->setPage(PageSeparator, m_pageSeparator); m_pageRows = new RowsPage(this, m_imp); m_wiz->setPage(PageRows, m_pageRows); m_pageFormats = new FormatsPage(this, m_imp); m_wiz->setPage(PageFormats, m_pageFormats); showStage(); m_wiz->button(QWizard::CustomButton1)->setEnabled(false); m_stageLabels << ui->label_intro << ui->label_separators << ui->label_rows << ui->label_columns << ui->label_columns << ui->label_columns << ui->label_formats; m_pageFormats->setFinalPage(true); connect(m_wiz->button(QWizard::FinishButton), &QAbstractButton::clicked, this, &CSVWizard::importClicked); connect(m_wiz->button(QWizard::CancelButton), &QAbstractButton::clicked, this, &CSVWizard::reject); connect(m_wiz->button(QWizard::CustomButton1), &QAbstractButton::clicked, this, &CSVWizard::fileDialogClicked); connect(m_wiz->button(QWizard::CustomButton2), &QAbstractButton::clicked, this, &CSVWizard::saveAsQIFClicked); connect(m_wiz, SIGNAL(currentIdChanged(int)), this, SLOT(slotIdChanged(int))); ui->tableView->setWordWrap(false); m_vScrollBar = ui->tableView->verticalScrollBar(); m_vScrollBar->setTracking(false); m_clearBrush = KColorScheme(QPalette::Normal).background(KColorScheme::NormalBackground); m_clearBrushText = KColorScheme(QPalette::Normal).foreground(KColorScheme::NormalText); m_colorBrush = KColorScheme(QPalette::Normal).background(KColorScheme::PositiveBackground); m_colorBrushText = KColorScheme(QPalette::Normal).foreground(KColorScheme::PositiveText); m_errorBrush = KColorScheme(QPalette::Normal).background(KColorScheme::NegativeBackground); m_errorBrushText = KColorScheme(QPalette::Normal).foreground(KColorScheme::NegativeText); m_wiz->setSideWidget(ui->wizardBox); show(); } CSVWizard::~CSVWizard() { delete m_imp; delete ui; } void CSVWizard::presetFilename(const QString& name) { m_fileName = name; } void CSVWizard::showStage() { QString str = ui->label_intro->text(); ui->label_intro->setText(QString::fromLatin1("%1").arg(str)); } void CSVWizard::readWindowSize(const KSharedConfigPtr& config) { KConfigGroup miscGroup(config, CSVImporterCore::m_confMiscName); m_initialWidth = miscGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfWidth), 800); m_initialHeight = miscGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfHeight), 400); } void CSVWizard::saveWindowSize(const KSharedConfigPtr& config) { KConfigGroup miscGroup(config, CSVImporterCore::m_confMiscName); m_initialHeight = this->geometry().height(); m_initialWidth = this->geometry().width(); miscGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfWidth), m_initialWidth); miscGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfHeight), m_initialHeight); miscGroup.sync(); } void CSVWizard::slotIdChanged(int id) { QString txt; m_lastId = m_curId; m_curId = id; if ((m_lastId == -1) || (m_curId == -1)) { return; } txt = m_stageLabels[m_lastId]->text(); txt.remove(QRegularExpression(QStringLiteral("[/]"))); m_stageLabels[m_lastId]->setText(txt); txt = m_stageLabels[m_curId]->text(); txt = QString::fromLatin1("%1").arg(txt); m_stageLabels[m_curId]->setText(txt); } void CSVWizard::clearColumnsBackground(const int col) { QList columnList; columnList << col; clearColumnsBackground(columnList); } void CSVWizard::clearColumnsBackground(const QList &columnList) { QStandardItemModel *model = m_imp->m_file->m_model; for (int i = m_imp->m_profile->m_startLine; i <= m_imp->m_profile->m_endLine; ++i) { foreach (const auto j, columnList) { model->item(i, j)->setBackground(m_clearBrush); model->item(i, j)->setForeground(m_clearBrushText); } } } void CSVWizard::clearBackground() { QStandardItemModel *model = m_imp->m_file->m_model; int rowCount = model->rowCount(); int colCount = model->columnCount(); for (int i = 0; i < rowCount; ++i) { for (int j = 0; j < colCount; ++j) { model->item(i, j)->setBackground(m_clearBrush); model->item(i, j)->setForeground(m_clearBrushText); } } } void CSVWizard::markUnwantedRows() { QStandardItemModel *model = m_imp->m_file->m_model; int rowCount = model->rowCount(); int colCount = model->columnCount(); QBrush brush; QBrush brushText; for (int i = 0; i < rowCount; ++i) { if ((i < m_imp->m_profile->m_startLine) || (i > m_imp->m_profile->m_endLine)) { brush = m_errorBrush; brushText = m_errorBrushText; } else { brush = m_clearBrush; brushText = m_clearBrushText; } for (int j = 0; j < colCount; ++j) { model->item(i, j)->setBackground(brush); model->item(i, j)->setForeground(brushText); } } } void CSVWizard::updateWindowSize() { QTableView *table = this->ui->tableView; table->resizeColumnsToContents(); this->repaint(); QRect screen = QApplication::desktop()->availableGeometry(); //get available screen size QRect wizard = this->frameGeometry(); //get current wizard size int newWidth = table->contentsMargins().left() + table->contentsMargins().right() + table->horizontalHeader()->length() + table->verticalHeader()->width() + (wizard.width() - table->width()); if (table->verticalScrollBar()->isEnabled()) { if (!table->verticalScrollBar()->isVisible() && // vertical scrollbar may be not visible after repaint... table->horizontalScrollBar()->isVisible()) // ...so use horizontal scrollbar dimension newWidth += table->horizontalScrollBar()->height(); else newWidth += table->verticalScrollBar()->width(); } int newHeight = table->contentsMargins().top() + table->contentsMargins().bottom() + table->verticalHeader()->length() + table->horizontalHeader()->height() + (wizard.height() - table->height()); if (table->horizontalScrollBar()->isEnabled()) { if (!table->horizontalScrollBar()->isVisible() && // horizontal scrollbar may be not visible after repaint... table->verticalScrollBar()->isVisible()) // ...so use vertical scrollbar dimension newHeight += table->verticalScrollBar()->width(); else newHeight += table->horizontalScrollBar()->height(); } // limit wizard size to screen size if (newHeight > screen.height()) newHeight = screen.height(); if (newWidth > screen.width()) newWidth = screen.width(); // don't shrink wizard if required size is less than initial if (newWidth < this->m_initialWidth) newWidth = this->m_initialWidth; if (newHeight < this->m_initialHeight) newHeight = this->m_initialHeight; newWidth -= (wizard.width() - this->geometry().width()); // remove window frame newHeight -= (wizard.height() - this->geometry().height()); wizard.setWidth(newWidth); wizard.setHeight(newHeight); wizard.moveTo((screen.width() - wizard.width()) / 2, (screen.height() - wizard.height()) / 2); this->setGeometry(wizard); } bool CSVWizard::eventFilter(QObject *object, QEvent *event) { // prevent the QWizard part of CSVWizard window from closing on Escape key press if (object == this->m_wiz) { if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Escape) { close(); return true; } } } return false; } void CSVWizard::saveSettings() const { m_imp->m_profile->m_lastUsedDirectory = m_imp->m_file->m_inFileName; m_imp->m_profile->writeSettings(CSVImporterCore::configFile()); m_imp->profilesAction(m_imp->m_profile->type(), ProfileAction::UpdateLastUsed, m_imp->m_profile->m_profileName, m_imp->m_profile->m_profileName); } void CSVWizard::slotClose() { saveSettings(); m_st = MyMoneyStatement(); // nothing imported accept(); } void CSVWizard::fileDialogClicked() { m_imp->profileFactory(m_pageIntro->m_profileType, m_pageIntro->ui->m_profiles->currentText()); bool profileExists = m_imp->m_profile->readSettings(CSVImporterCore::configFile()); if (!m_fileName.isEmpty()) { if (!m_imp->m_file->getInFileName(m_fileName)) { if (!m_imp->m_file->getInFileName(m_imp->m_profile->m_lastUsedDirectory)) { return; } } } else if (!m_imp->m_file->getInFileName(m_imp->m_profile->m_lastUsedDirectory)) return; saveWindowSize(CSVImporterCore::configFile()); m_imp->m_file->readFile(m_imp->m_profile); m_imp->m_file->setupParser(m_imp->m_profile); m_skipSetup = m_pageIntro->ui->m_skipSetup->isChecked(); switch(m_imp->m_profile->type()) { case Profile::Investment: if (!m_pageInvestment) { m_pageInvestment = new InvestmentPage(this, m_imp); m_wiz->setPage(CSVWizard::PageInvestment, m_pageInvestment); } break; case Profile::Banking: if (!m_pageBanking) { m_pageBanking = new BankingPage(this, m_imp); m_wiz->setPage(CSVWizard::PageBanking, m_pageBanking); } break; case Profile::StockPrices: case Profile::CurrencyPrices: if (!m_pagePrices) { m_pagePrices = new PricesPage(this, m_imp); m_wiz->setPage(CSVWizard::PagePrices, m_pagePrices); } break; default: return; } m_wiz->next(); // go to separator page if (m_skipSetup && profileExists) { // programmatically skip to last page while((m_wiz->currentPage() != m_pageFormats) && (m_wiz->nextId() != -1)) { m_wiz->next(); } } } void CSVWizard::importClicked() { switch (m_imp->m_profile->type()) { case Profile::Banking: if (!m_pageBanking->validateCreditDebit()) return; break; case Profile::Investment: if (!m_pageInvestment->validateActionType()) return; break; default: break; } saveSettings(); if (!m_imp->createStatement(m_st)) { m_st = MyMoneyStatement(); return; } accept(); } const MyMoneyStatement& CSVWizard::statement() const { return m_st; } void CSVWizard::saveAsQIFClicked() { switch (m_imp->m_profile->type()) { case Profile::Banking: if (!m_pageBanking->validateCreditDebit()) return; break; case Profile::Investment: if (!m_pageInvestment->validateActionType()) return; break; default: break; } bool isOK = m_imp->createStatement(m_st); if (!isOK || m_st.m_listTransactions.isEmpty()) return; QString outFileName = m_imp->m_file->m_inFileName; outFileName.truncate(outFileName.lastIndexOf('.')); outFileName.append(QLatin1String(".qif")); outFileName = QFileDialog::getSaveFileName(this, i18n("Save QIF"), outFileName, i18n("QIF Files (*.qif)")); if (outFileName.isEmpty()) return; switch (m_imp->m_profile->type()) { case Profile::Banking: m_pageBanking->makeQIF(m_st, outFileName); break; case Profile::Investment: m_pageInvestment->makeQIF(m_st, outFileName); break; default: break; } } void CSVWizard::initializeComboBoxes(const QHash &columns) { QStringList columnNumbers; for (int i = 0; i < m_imp->m_file->m_columnCount; ++i) columnNumbers.append(QString::number(i + 1)); foreach (const auto column, columns) { // disable widgets allowing their initialization column->blockSignals(true); // clear all existing items before adding new ones column->clear(); // populate comboboxes with col # values column->addItems(columnNumbers); // all comboboxes are set to 0 so set them to -1 column->setCurrentIndex(-1); // enable widgets after their initialization column->blockSignals(false); } } //------------------------------------------------------------------------------------------------------- IntroPage::IntroPage(CSVWizard *dlg, CSVImporterCore *imp) : CSVWizardPage(dlg, imp), m_profileType(Profile::Banking), ui(new Ui::IntroPage) { ui->setupUi(this); } IntroPage::~IntroPage() { delete ui; } int IntroPage::nextId() const { return CSVWizard::PageSeparator; } void IntroPage::initializePage() { m_imp->m_file->m_model->clear(); wizard()->setButtonText(QWizard::CustomButton1, i18n("Select File")); wizard()->button(QWizard::CustomButton1)->setToolTip(i18n("A profile must be selected before selecting a file.")); QList layout; layout << QWizard::Stretch << QWizard::CustomButton1 << QWizard::CancelButton; wizard()->setButtonLayout(layout); ui->m_profiles->lineEdit()->setClearButtonEnabled(true); connect(ui->m_profiles, SIGNAL(currentIndexChanged(int)), this, SLOT(slotComboSourceIndexChanged(int))); connect(ui->m_add, &QAbstractButton::clicked, this, &IntroPage::slotAddProfile); connect(ui->m_remove, &QAbstractButton::clicked, this, &IntroPage::slotRemoveProfile); connect(ui->m_rename, &QAbstractButton::clicked, this, &IntroPage::slotRenameProfile); connect(ui->m_profilesBank, &QAbstractButton::toggled, this, &IntroPage::slotBankRadioToggled); connect(ui->m_profilesInvest, &QAbstractButton::toggled, this, &IntroPage::slotInvestRadioToggled); connect(ui->m_profilesCurrencyPrices, &QAbstractButton::toggled, this, &IntroPage::slotCurrencyPricesRadioToggled); connect(ui->m_profilesStockPrices, &QAbstractButton::toggled, this, &IntroPage::slotStockPricesRadioToggled); if (m_dlg->m_initialHeight == -1 || m_dlg->m_initialWidth == -1) { m_dlg->m_initialHeight = m_dlg->geometry().height(); m_dlg->m_initialWidth = m_dlg->geometry().width(); } else { //resize wizard to its initial size and center it m_dlg->setGeometry( QStyle::alignedRect( Qt::LeftToRight, Qt::AlignCenter, QSize(m_dlg->m_initialWidth, m_dlg->m_initialHeight), QApplication::desktop()->availableGeometry() ) ); } m_dlg->ui->tableView->hide(); } bool IntroPage::validatePage() { return true; } void IntroPage::slotAddProfile() { profileChanged(ProfileAction::Add); } void IntroPage::slotRemoveProfile() { profileChanged(ProfileAction::Remove); } void IntroPage::slotRenameProfile() { profileChanged(ProfileAction::Rename); } void IntroPage::profileChanged(const ProfileAction action) { QString cbText = ui->m_profiles->currentText(); if (cbText.isEmpty()) // you cannot neither add nor remove empty name profile or rename to empty name return; int cbIndex = ui->m_profiles->currentIndex(); switch (action) { case ProfileAction::Rename: case ProfileAction::Add: { int dupIndex = m_profiles.indexOf(QRegularExpression (cbText)); if (dupIndex == cbIndex && cbIndex != -1) // if profile name wasn't changed then return return; else if (dupIndex != -1) { // profile with the same name already exists ui->m_profiles->setItemText(cbIndex, m_profiles.value(cbIndex)); KMessageBox::information(m_dlg, i18n("
Profile %1 already exists.
" "Please enter another name
", cbText)); return; } break; } case ProfileAction::Remove: if (m_profiles.value(cbIndex) != cbText) // user changed name of the profile and tries to remove it return; break; default: break; } if (CSVImporterCore::profilesAction(m_profileType, action, m_profiles.value(cbIndex), cbText)) { switch (action) { case ProfileAction::Add: m_profiles.append(cbText); ui->m_profiles->addItem(cbText); ui->m_profiles->setCurrentIndex(m_profiles.count() - 1); KMessageBox::information(m_dlg, i18n("
Profile %1 has been added.
", cbText)); break; case ProfileAction::Remove: m_profiles.removeAt(cbIndex); ui->m_profiles->removeItem(cbIndex); KMessageBox::information(m_dlg, i18n("
Profile %1 has been removed.
", cbText)); break; case ProfileAction::Rename: ui->m_profiles->setItemText(cbIndex, cbText); KMessageBox::information(m_dlg, i18n("
Profile name has been renamed from %1 to %2.
", m_profiles.value(cbIndex), cbText)); m_profiles[cbIndex] = cbText; break; default: break; } } } void IntroPage::slotComboSourceIndexChanged(int idx) { if (idx == -1) { wizard()->button(QWizard::CustomButton1)->setEnabled(false); ui->m_skipSetup->setEnabled(false); ui->m_remove->setEnabled(false); ui->m_rename->setEnabled(false); } else { wizard()->button(QWizard::CustomButton1)->setEnabled(true); ui->m_skipSetup->setEnabled(true); ui->m_remove->setEnabled(true); ui->m_rename->setEnabled(true); } } void IntroPage::profileTypeChanged(const Profile profileType, bool toggled) { if (!toggled) return; KConfigGroup profilesGroup(CSVImporterCore::configFile(), CSVImporterCore::m_confProfileNames); m_profileType = profileType; QString profileTypeStr; switch (m_profileType) { case Profile::Banking: ui->m_profilesInvest->setChecked(false); ui->m_profilesStockPrices->setChecked(false); ui->m_profilesCurrencyPrices->setChecked(false); break; case Profile::Investment: ui->m_profilesBank->setChecked(false); ui->m_profilesStockPrices->setChecked(false); ui->m_profilesCurrencyPrices->setChecked(false); break; case Profile::StockPrices: ui->m_profilesBank->setChecked(false); ui->m_profilesInvest->setChecked(false); ui->m_profilesCurrencyPrices->setChecked(false); break; case Profile::CurrencyPrices: ui->m_profilesBank->setChecked(false); ui->m_profilesInvest->setChecked(false); ui->m_profilesStockPrices->setChecked(false); break; default: break; } profileTypeStr = CSVImporterCore::m_profileConfPrefix.value(m_profileType); m_profiles = profilesGroup.readEntry(profileTypeStr, QStringList()); int priorProfile = profilesGroup.readEntry(CSVImporterCore::m_confPriorName + profileTypeStr, 0); ui->m_profiles->clear(); ui->m_profiles->addItems(m_profiles); ui->m_profiles->setCurrentIndex(priorProfile); ui->m_profiles->setEnabled(true); ui->m_add->setEnabled(true); } void IntroPage::slotBankRadioToggled(bool toggled) { profileTypeChanged(Profile::Banking, toggled); } void IntroPage::slotInvestRadioToggled(bool toggled) { profileTypeChanged(Profile::Investment, toggled); } void IntroPage::slotCurrencyPricesRadioToggled(bool toggled) { profileTypeChanged(Profile::CurrencyPrices, toggled); } void IntroPage::slotStockPricesRadioToggled(bool toggled) { profileTypeChanged(Profile::StockPrices, toggled); } SeparatorPage::SeparatorPage(CSVWizard *dlg, CSVImporterCore *imp) : CSVWizardPage(dlg, imp), ui(new Ui::SeparatorPage) { ui->setupUi(this); connect(ui->m_encoding, SIGNAL(currentIndexChanged(int)), this, SLOT(encodingChanged(int))); connect(ui->m_fieldDelimiter, SIGNAL(currentIndexChanged(int)), this, SLOT(fieldDelimiterChanged(int))); connect(ui->m_textDelimiter, SIGNAL(currentIndexChanged(int)), this, SLOT(textDelimiterChanged(int))); } SeparatorPage::~SeparatorPage() { delete ui; } void SeparatorPage::initializePage() { m_dlg->ui->tableView->show(); // comboboxes are preset to -1 and, in new profile case, can be set here to -1 as well ... // ... so block their signals until setting them ... ui->m_encoding->blockSignals(true); ui->m_fieldDelimiter->blockSignals(true); ui->m_textDelimiter->blockSignals(true); initializeEncodingCombobox(); ui->m_encoding->setCurrentIndex(ui->m_encoding->findData(m_imp->m_profile->m_encodingMIBEnum)); ui->m_fieldDelimiter->setCurrentIndex((int)m_imp->m_profile->m_fieldDelimiter); ui->m_textDelimiter->setCurrentIndex((int)m_imp->m_profile->m_textDelimiter); ui->m_encoding->blockSignals(false); ui->m_fieldDelimiter->blockSignals(false); ui->m_textDelimiter->blockSignals(false); // ... and ensure that their signal receivers will always be called emit ui->m_encoding->currentIndexChanged(ui->m_encoding->currentIndex()); emit ui->m_fieldDelimiter->currentIndexChanged(ui->m_fieldDelimiter->currentIndex()); emit ui->m_textDelimiter->currentIndexChanged(ui->m_textDelimiter->currentIndex()); m_dlg->updateWindowSize(); QList layout; layout << QWizard::Stretch << QWizard::BackButton << QWizard::NextButton << QWizard::CancelButton; wizard()->setButtonLayout(layout); } void SeparatorPage::initializeEncodingCombobox() { ui->m_encoding->clear(); QList codecs; QMap codecMap; QRegExp iso8859RegExp(QLatin1Literal("ISO[- ]8859-([0-9]+).*")); foreach (const auto mib, QTextCodec::availableMibs()) { QTextCodec *codec = QTextCodec::codecForMib(mib); QString sortKey = codec->name().toUpper(); int rank; if (sortKey.startsWith(QLatin1String("UTF-8"))) { // krazy:exclude=strings rank = 1; } else if (sortKey.startsWith(QLatin1String("UTF-16"))) { // krazy:exclude=strings rank = 2; } else if (iso8859RegExp.exactMatch(sortKey)) { if (iso8859RegExp.cap(1).size() == 1) rank = 3; else rank = 4; } else { rank = 5; } sortKey.prepend(QChar('0' + rank)); codecMap.insert(sortKey, codec); } codecs = codecMap.values(); foreach (const auto codec, codecs) ui->m_encoding->addItem(codec->name(), codec->mibEnum()); } void SeparatorPage::encodingChanged(const int index) { if (index == -1) { ui->m_encoding->setCurrentIndex(ui->m_encoding->findText("UTF-8")); return; } else if (index == ui->m_encoding->findData(m_imp->m_profile->m_encodingMIBEnum)) return; m_imp->m_profile->m_encodingMIBEnum = ui->m_encoding->currentData().toInt(); m_imp->m_file->readFile(m_imp->m_profile); emit completeChanged(); } void SeparatorPage::fieldDelimiterChanged(const int index) { if (index == -1 && // if field delimiter isn't set... !m_imp->m_autodetect.value(AutoFieldDelimiter)) // ... and user disabled autodetecting... return; // ... then wait for him to choose else if (index == (int)m_imp->m_profile->m_fieldDelimiter) return; m_imp->m_profile->m_fieldDelimiter = static_cast(int(index)); m_imp->m_file->readFile(m_imp->m_profile); // get column count, we get with this fieldDelimiter m_imp->m_file->setupParser(m_imp->m_profile); if (index == -1) { ui->m_fieldDelimiter->blockSignals(true); ui->m_fieldDelimiter->setCurrentIndex((int)m_imp->m_profile->m_fieldDelimiter); ui->m_fieldDelimiter->blockSignals(false); } m_dlg->updateWindowSize(); emit completeChanged(); } void SeparatorPage::textDelimiterChanged(const int index) { if (index == -1) { // if text delimiter isn't set... ui->m_textDelimiter->setCurrentIndex(0); // ...then set it to 0, as for now there is no better idea how to detect it return; } m_imp->m_profile->m_textDelimiter = static_cast(index); m_imp->m_file->setupParser(m_imp->m_profile); emit completeChanged(); } bool SeparatorPage::isComplete() const { bool rc = false; if (ui->m_encoding->currentIndex() != -1 && ui->m_fieldDelimiter->currentIndex() != -1 && ui->m_textDelimiter->currentIndex() != -1) { switch(m_imp->m_profile->type()) { case Profile::Banking: if (m_imp->m_file->m_columnCount > 2) rc = true; break; case Profile::Investment: if (m_imp->m_file->m_columnCount > 3) rc = true; break; case Profile::CurrencyPrices: case Profile::StockPrices: if (m_imp->m_file->m_columnCount > 1) rc = true; break; default: break; } } return rc; } bool SeparatorPage::validatePage() { return true; } void SeparatorPage::cleanupPage() { // On completion with error force use of 'Back' button. // ...to allow resetting of 'Skip setup' m_dlg->m_pageIntro->initializePage(); // Need to show button(QWizard::CustomButton1) not 'NextButton' } RowsPage::RowsPage(CSVWizard *dlg, CSVImporterCore *imp) : CSVWizardPage(dlg, imp), ui(new Ui::RowsPage) { ui->setupUi(this); connect(ui->m_startLine, SIGNAL(valueChanged(int)), this, SLOT(startRowChanged(int)));; connect(ui->m_endLine, SIGNAL(valueChanged(int)), this, SLOT(endRowChanged(int))); } RowsPage::~RowsPage() { delete ui; } void RowsPage::initializePage() { ui->m_startLine->blockSignals(true); ui->m_endLine->blockSignals(true); ui->m_startLine->setMaximum(m_imp->m_file->m_rowCount); ui->m_endLine->setMaximum(m_imp->m_file->m_rowCount); ui->m_startLine->setValue(m_imp->m_profile->m_startLine + 1); ui->m_endLine->setValue(m_imp->m_profile->m_endLine + 1); ui->m_startLine->blockSignals(false); ui->m_endLine->blockSignals(false); m_dlg->markUnwantedRows(); m_dlg->m_vScrollBar->setValue(m_imp->m_profile->m_startLine); QList layout; layout << QWizard::Stretch << QWizard::BackButton << QWizard::NextButton << QWizard::CancelButton; wizard()->setButtonLayout(layout); } void RowsPage::cleanupPage() { m_dlg->clearBackground(); } int RowsPage::nextId() const { int ret; switch (m_imp->m_profile->type()) { case Profile::Banking: ret = CSVWizard::PageBanking; break; case Profile::Investment: ret = CSVWizard::PageInvestment; break; case Profile::StockPrices: case Profile::CurrencyPrices: ret = CSVWizard::PagePrices; break; default: ret = CSVWizard::PageRows; break; } return ret; } void RowsPage::startRowChanged(int val) { if (val > m_imp->m_file->m_rowCount) { ui->m_startLine->setValue(m_imp->m_file->m_rowCount - 1); return; } --val; if (val > m_imp->m_profile->m_endLine) { if (m_imp->m_profile->m_endLine <= m_imp->m_file->m_rowCount) ui->m_startLine->setValue(m_imp->m_profile->m_endLine + 1); return; } m_imp->m_profile->m_startLine = val; m_dlg->m_vScrollBar->setValue(val); m_dlg->markUnwantedRows(); } void RowsPage::endRowChanged(int val) { if (val > m_imp->m_file->m_rowCount) { ui->m_endLine->setValue(m_imp->m_file->m_rowCount - 1); return; } --val; if (val < m_imp->m_profile->m_startLine) { if (m_imp->m_profile->m_startLine <= m_imp->m_file->m_rowCount) ui->m_endLine->setValue(m_imp->m_profile->m_startLine + 1); return; } - m_imp->m_profile->m_trailerLines = m_imp->m_file->m_rowCount - val; + m_imp->m_profile->m_trailerLines = m_imp->m_file->m_rowCount - (val + 1); m_imp->m_profile->m_endLine = val; m_dlg->markUnwantedRows(); } FormatsPage::FormatsPage(CSVWizard *dlg, CSVImporterCore *imp) : CSVWizardPage(dlg, imp), ui(new Ui::FormatsPage), m_isDecimalSymbolOK(false), m_isDateFormatOK(false) { ui->setupUi(this); connect(ui->m_dateFormat, SIGNAL(currentIndexChanged(int)), this, SLOT(dateFormatChanged(int))); connect(ui->m_decimalSymbol, SIGNAL(currentIndexChanged(int)), this, SLOT(decimalSymbolChanged(int))); } FormatsPage::~FormatsPage() { delete ui; } void FormatsPage::initializePage() { m_isDecimalSymbolOK = false; m_isDateFormatOK = false; QList layout; layout << QWizard::Stretch << QWizard::CustomButton2 << QWizard::BackButton << QWizard::FinishButton << QWizard::CancelButton; wizard()->setButtonText(QWizard::FinishButton, i18n("Import CSV")); wizard()->setOption(QWizard::HaveCustomButton2, true); wizard()->setButtonText(QWizard::CustomButton2, i18n("Make QIF File")); wizard()->setButtonLayout(layout); wizard()->button(QWizard::CustomButton2)->setEnabled(false); wizard()->button(QWizard::FinishButton)->setEnabled(false); ui->m_thousandsDelimiter->setEnabled(false); ui->m_dateFormat->blockSignals(true); ui->m_dateFormat->setCurrentIndex((int)m_imp->m_profile->m_dateFormat); ui->m_dateFormat->blockSignals(false); emit ui->m_dateFormat->currentIndexChanged((int)m_imp->m_profile->m_dateFormat); // emit update signal manually regardless of change to combobox ui->m_decimalSymbol->blockSignals(true); if (m_imp->m_profile->m_decimalSymbol == DecimalSymbol::Auto && !m_imp->m_autodetect.value(AutoDecimalSymbol)) ui->m_decimalSymbol->setCurrentIndex(-1); else ui->m_decimalSymbol->setCurrentIndex((int)m_imp->m_profile->m_decimalSymbol); ui->m_decimalSymbol->blockSignals(false); emit ui->m_decimalSymbol->currentIndexChanged((int)m_imp->m_profile->m_decimalSymbol); // emit update signal manually regardless of change to combobox } void FormatsPage::decimalSymbolChanged(int index) { const QList columns = m_imp->getNumericalColumns(); switch (index) { case -1: if (!m_imp->m_autodetect.value(AutoDecimalSymbol)) { break; } // intentional fall through case 2: { ui->m_decimalSymbol->blockSignals(true); m_imp->m_profile->m_decimalSymbol = DecimalSymbol::Auto; int failColumn = m_imp->detectDecimalSymbols(columns); if (failColumn != -2) { KMessageBox::sorry(this, i18n("
Autodetect could not detect your decimal symbol in column %1.
" "
Try manual selection to see problematic cells and correct your data.
", failColumn), i18n("CSV import")); ui->m_decimalSymbol->setCurrentIndex(-1); ui->m_thousandsDelimiter->setCurrentIndex(-1); } else if (index == -1) { // if detection went well and decimal symbol was unspecified then we'll be specifying it DecimalSymbol firstDecSymbol = m_imp->m_decimalSymbolIndexMap.first(); bool allSymbolsEqual = true; foreach (const auto mapDecSymbol, m_imp->m_decimalSymbolIndexMap) { if (firstDecSymbol != mapDecSymbol) allSymbolsEqual = false; } if (allSymbolsEqual) { // if symbol in all columns is equal then set it... m_imp->m_profile->m_decimalSymbol = firstDecSymbol; ui->m_decimalSymbol->setCurrentIndex((int)firstDecSymbol); ui->m_thousandsDelimiter->setCurrentIndex((int)firstDecSymbol); } else { // else set to auto m_imp->m_profile->m_decimalSymbol = DecimalSymbol::Auto; ui->m_decimalSymbol->setCurrentIndex((int)DecimalSymbol::Auto); ui->m_thousandsDelimiter->setCurrentIndex((int)DecimalSymbol::Auto); } } ui->m_decimalSymbol->blockSignals(false); break; } default: foreach (const auto column, columns) m_imp->m_decimalSymbolIndexMap.insert(column, static_cast(index)); ui->m_thousandsDelimiter->setCurrentIndex(index); m_imp->m_profile->m_decimalSymbol = static_cast(index); } m_isDecimalSymbolOK = validateDecimalSymbols(columns); emit completeChanged(); } bool FormatsPage::validateDecimalSymbols(const QList &columns) { bool isOK = true; foreach (const auto col, columns) { m_imp->m_file->m_parse->setDecimalSymbol(m_imp->m_decimalSymbolIndexMap.value(col)); m_dlg->clearColumnsBackground(col); for (int row = m_imp->m_profile->m_startLine; row <= m_imp->m_profile->m_endLine; ++row) { QStandardItem *item = m_imp->m_file->m_model->item(row, col); QString rawNumber = item->text(); m_imp->m_file->m_parse->possiblyReplaceSymbol(rawNumber); if (!m_imp->m_file->m_parse->invalidConversion() || rawNumber.isEmpty()) { // empty strings are welcome item->setBackground(m_dlg->m_colorBrush); item->setForeground(m_dlg->m_colorBrushText); } else { isOK = false; m_dlg->ui->tableView->scrollTo(item->index(), QAbstractItemView::EnsureVisible); item->setBackground(m_dlg->m_errorBrush); item->setForeground(m_dlg->m_errorBrushText); } } } return isOK; } void FormatsPage::dateFormatChanged(const int index) { if (index == -1) return; int col = m_imp->m_profile->m_colTypeNum.value(Column::Date); m_imp->m_profile->m_dateFormat = static_cast(index); m_imp->m_convertDate->setDateFormatIndex(static_cast(index)); m_isDateFormatOK = validateDateFormat(col); if (!m_isDateFormatOK) { KMessageBox::sorry(this, i18n("
There are invalid date formats in column '%1'.
" "
Please check your selections.
" , col + 1), i18n("CSV import")); } emit completeChanged(); } bool FormatsPage::validateDateFormat(const int col) { m_dlg->clearColumnsBackground(col); QDate emptyDate; bool isOK = true; for (int row = m_imp->m_profile->m_startLine; row <= m_imp->m_profile->m_endLine; ++row) { QStandardItem* item = m_imp->m_file->m_model->item(row, col); QDate dat = m_imp->m_convertDate->convertDate(item->text()); if (dat == emptyDate) { isOK = false; m_dlg->ui->tableView->scrollTo(item->index(), QAbstractItemView::EnsureVisible); item->setBackground(m_dlg->m_errorBrush); item->setForeground(m_dlg->m_errorBrushText); } else { item->setBackground(m_dlg->m_colorBrush); item->setForeground(m_dlg->m_colorBrushText); } } return isOK; } bool FormatsPage::isComplete() const { const bool enable = m_isDecimalSymbolOK && m_isDateFormatOK; if (m_imp->m_profile->type() != Profile::StockPrices && m_imp->m_profile->type() != Profile::CurrencyPrices) wizard()->button(QWizard::CustomButton2)->setEnabled(enable); return enable; } void FormatsPage::cleanupPage() { QList columns = m_imp->getNumericalColumns(); columns.append(m_imp->m_profile->m_colTypeNum.value(Column::Date)); m_dlg->clearColumnsBackground(columns); m_dlg->m_st = MyMoneyStatement(); // any change on investment/banking page invalidates created statement QList layout; layout << QWizard::Stretch << QWizard::BackButton << QWizard::NextButton << QWizard::CancelButton; wizard()->setButtonLayout(layout); } diff --git a/kmymoney/plugins/reconciliationreport/CMakeLists.txt b/kmymoney/plugins/reconciliationreport/CMakeLists.txt index 5be80463c..9c9d7f5d1 100644 --- a/kmymoney/plugins/reconciliationreport/CMakeLists.txt +++ b/kmymoney/plugins/reconciliationreport/CMakeLists.txt @@ -1,34 +1,35 @@ # patch the version with the version defined in the build system configure_file(${CMAKE_CURRENT_SOURCE_DIR}/reconciliationreport.json.cmake ${CMAKE_CURRENT_BINARY_DIR}/reconciliationreport.json @ONLY ) set(reconciliationreport_PART_SRCS kreconciliationreportdlg.cpp reconciliationreport.cpp ) ki18n_wrap_ui(reconciliationreport_PART_SRCS kreconciliationreportdlgdecl.ui ) kcoreaddons_add_plugin(reconciliationreport SOURCES ${reconciliationreport_PART_SRCS} JSON "${CMAKE_CURRENT_BINARY_DIR}/reconciliationreport.json" INSTALL_NAMESPACE "kmymoney") #kcoreaddons_add_plugin sets LIBRARY_OUTPUT_DIRECTORY to ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${INSTALL_NAMESPACE} set_target_properties(reconciliationreport PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") target_link_libraries(reconciliationreport Qt5::PrintSupport kmm_mymoney kmm_plugin + kmm_printer ) if(ENABLE_WEBENGINE) target_link_libraries(reconciliationreport Qt5::WebEngineWidgets) else(ENABLE_WEBENGINE) target_link_libraries(reconciliationreport KF5::WebKit) endif(ENABLE_WEBENGINE) diff --git a/kmymoney/plugins/reconciliationreport/kreconciliationreportdlg.cpp b/kmymoney/plugins/reconciliationreport/kreconciliationreportdlg.cpp index d347e0285..672258fc5 100644 --- a/kmymoney/plugins/reconciliationreport/kreconciliationreportdlg.cpp +++ b/kmymoney/plugins/reconciliationreport/kreconciliationreportdlg.cpp @@ -1,100 +1,92 @@ /*************************************************************************** * Copyright 2009 Cristian Onet onet.cristian@gmail.com * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see * ***************************************************************************/ #include "kreconciliationreportdlg.h" // Qt includes #include #include -#include -#include #include // KDE includes #include #ifdef ENABLE_WEBENGINE #include #else #include #endif +#include "kmm_printer.h" + KReportDlg::KReportDlg(QWidget* parent, const QString& summaryReportHTML, const QString& detailsReportHTML) : - QDialog(parent), - m_currentPrinter(nullptr) + QDialog(parent) { setupUi(this); #ifdef ENABLE_WEBENGINE m_summaryHTMLPart = new QWebEngineView(m_summaryTab); m_detailsHTMLPart = new QWebEngineView(m_detailsTab); #else m_summaryHTMLPart = new KWebView(m_summaryTab); m_detailsHTMLPart = new KWebView(m_detailsTab); #endif m_summaryLayout->addWidget(m_summaryHTMLPart); m_detailsLayout->addWidget(m_detailsHTMLPart); m_summaryHTMLPart->setHtml(summaryReportHTML, QUrl("file://")); m_detailsHTMLPart->setHtml(detailsReportHTML, QUrl("file://")); QPushButton* printButton = m_buttonBox->addButton(QString(), QDialogButtonBox::ActionRole); KGuiItem::assign(printButton, KStandardGuiItem::print()); // signals and slots connections connect(printButton, SIGNAL(clicked()), this, SLOT(print())); } KReportDlg::~KReportDlg() { } void KReportDlg::print() { - m_currentPrinter = new QPrinter(); - QPointer dialog = new QPrintDialog(m_currentPrinter, this); - dialog->setWindowTitle(QString()); - if (dialog->exec() != QDialog::Accepted) { - delete m_currentPrinter; - m_currentPrinter = nullptr; - return; - } - - // do the actual painting job - switch (m_tabWidget->currentIndex()) { - case 0: - #ifdef ENABLE_WEBENGINE - m_summaryHTMLPart->page()->print(m_currentPrinter, [=] (bool) {delete m_currentPrinter; m_currentPrinter = nullptr;}); - #else - m_summaryHTMLPart->print(m_currentPrinter); - #endif - break; - case 1: - #ifdef ENABLE_WEBENGINE - m_detailsHTMLPart->page()->print(m_currentPrinter, [=] (bool) {delete m_currentPrinter; m_currentPrinter = nullptr;}); - #else - m_detailsHTMLPart->print(m_currentPrinter); - #endif - break; - default: - delete m_currentPrinter; - m_currentPrinter = nullptr; - qDebug("KReportDlg::print() current page index not handled correctly"); + auto printer = KMyMoneyPrinter::startPrint(); + if (printer != nullptr) { + // do the actual painting job + switch (m_tabWidget->currentIndex()) { + case 0: + #ifdef ENABLE_WEBENGINE + m_summaryHTMLPart->page()->print(printer, [=] (bool) {}); + #else + m_summaryHTMLPart->print(printer); + #endif + break; + case 1: + #ifdef ENABLE_WEBENGINE + m_detailsHTMLPart->page()->print(printer, [=] (bool) {}); + #else + m_detailsHTMLPart->print(printer); + #endif + break; + default: + qDebug("KReportDlg::print() current page index not handled correctly"); + break; + } } } diff --git a/kmymoney/plugins/reconciliationreport/kreconciliationreportdlg.h b/kmymoney/plugins/reconciliationreport/kreconciliationreportdlg.h index 12345ff5c..76c423a19 100644 --- a/kmymoney/plugins/reconciliationreport/kreconciliationreportdlg.h +++ b/kmymoney/plugins/reconciliationreport/kreconciliationreportdlg.h @@ -1,57 +1,55 @@ /*************************************************************************** * Copyright 2009 Cristian Onet onet.cristian@gmail.com * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see * ***************************************************************************/ #ifndef KRECONCILIATIONREPORTDLG_H #define KRECONCILIATIONREPORTDLG_H #include #include "ui_kreconciliationreportdlgdecl.h" -class QPrinter; #ifdef ENABLE_WEBENGINE class QWebEngineView; #else class KWebView; #endif class KReportDlg : public QDialog, public Ui::KReconciliationReportDlgDecl { Q_OBJECT public: KReportDlg(QWidget* parent, const QString& summaryReportHTML, const QString& detailsReportHTML); ~KReportDlg(); protected Q_SLOTS: void print(); private: #ifdef ENABLE_WEBENGINE QWebEngineView *m_summaryHTMLPart; QWebEngineView *m_detailsHTMLPart; #else KWebView *m_summaryHTMLPart; KWebView *m_detailsHTMLPart; #endif - QPrinter *m_currentPrinter; }; #endif diff --git a/kmymoney/plugins/views/forecast/kforecastview.cpp b/kmymoney/plugins/views/forecast/kforecastview.cpp index 0c6d67fcd..12d8acb2c 100644 --- a/kmymoney/plugins/views/forecast/kforecastview.cpp +++ b/kmymoney/plugins/views/forecast/kforecastview.cpp @@ -1,133 +1,134 @@ /*************************************************************************** kforecastview.cpp ------------------- copyright : (C) 2007 by Alvaro Soliverez email : asoliverez@gmail.com (C) 2017 Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kforecastview_p.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes using namespace reports; using namespace Icons; KForecastView::KForecastView(QWidget *parent) : KMyMoneyViewBase(*new KForecastViewPrivate(this), parent) { } KForecastView::~KForecastView() { } void KForecastView::slotTabChanged(int index) { Q_D(KForecastView); ForecastViewTab tab = static_cast(index); // remember this setting for startup KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("Last Use Settings"); grp.writeEntry("KForecastView_LastType", QVariant(tab).toString()); + d->loadForecastSettings(); d->loadForecast(tab); } void KForecastView::slotManualForecast() { Q_D(KForecastView); d->m_needReload[SummaryView] = true; d->m_needReload[ListView] = true; d->m_needReload[AdvancedView] = true; d->m_needReload[BudgetView] = true; d->m_needReload[ChartView] = true; if (isVisible()) slotTabChanged(d->ui->m_tab->currentIndex()); } void KForecastView::showEvent(QShowEvent* event) { Q_D(KForecastView); if (d->m_needLoad) { d->init(); d->loadForecastSettings(); } emit customActionRequested(View::Forecast, eView::Action::AboutToShow); slotTabChanged(d->ui->m_tab->currentIndex()); // don't forget base class implementation QWidget::showEvent(event); } void KForecastView::executeCustomAction(eView::Action action) { switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::SetDefaultFocus: { Q_D(KForecastView); QTimer::singleShot(0, d->ui->m_forecastButton, SLOT(setFocus())); } break; default: break; } } void KForecastView::refresh() { Q_D(KForecastView); d->m_needReload[SummaryView] = true; d->m_needReload[ListView] = true; d->m_needReload[AdvancedView] = true; d->m_needReload[BudgetView] = true; d->m_needReload[ChartView] = true; if (isVisible()) { //refresh settings d->loadForecastSettings(); slotTabChanged(d->ui->m_tab->currentIndex()); } } void KForecastView::itemExpanded(QTreeWidgetItem *item) { Q_D(KForecastView); if (!item->parent() || !item->parent()->parent()) return; for (int i = 1; i < item->columnCount(); ++i) { d->showAmount(item, i, item->data(i, AmountRole).value(), MyMoneyFile::instance()->security(item->data(0, AccountRole).value().currencyId())); } } void KForecastView::itemCollapsed(QTreeWidgetItem *item) { Q_D(KForecastView); for (int i = 1; i < item->columnCount(); ++i) { d->showAmount(item, i, item->data(i, ValueRole).value(), MyMoneyFile::instance()->baseCurrency()); } } diff --git a/kmymoney/plugins/views/reports/CMakeLists.txt b/kmymoney/plugins/views/reports/CMakeLists.txt index 8a06ecd35..1df6bf667 100644 --- a/kmymoney/plugins/views/reports/CMakeLists.txt +++ b/kmymoney/plugins/views/reports/CMakeLists.txt @@ -1,92 +1,93 @@ add_subdirectory(core) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/reportsview.json.cmake ${CMAKE_CURRENT_BINARY_DIR}/reportsview.json @ONLY) set(reportsview_SOURCES reportsview.cpp kreportsview.cpp tocitem.cpp tocitemgroup.cpp tocitemreport.cpp kreportconfigurationfilterdlg.cpp reporttabimpl.cpp reportcontrolimpl.cpp kbalancechartdlg.cpp ../../../views/kmymoneywebpage.cpp ) ki18n_wrap_ui(reportsview_SOURCES kreportconfigurationfilterdlg.ui reportcontrol.ui reporttabgeneral.ui reporttabrowcolquery.ui reporttabrowcolpivot.ui reporttabrange.ui reporttabchart.ui reporttabcapitalgain.ui reporttabperformance.ui ) # kconfig_add_kcfg_files(reportsview_SOURCES reportsviewsettings.kcfgc) kcoreaddons_add_plugin(reportsview SOURCES ${reportsview_SOURCES} JSON "${CMAKE_CURRENT_BINARY_DIR}/reportsview.json" INSTALL_NAMESPACE "kmymoney") #kcoreaddons_add_plugin sets LIBRARY_OUTPUT_DIRECTORY to ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${INSTALL_NAMESPACE} set_target_properties(reportsview PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") target_link_libraries(reportsview PUBLIC kmm_plugin KF5::TextWidgets KF5::KIOCore KF5::KIOFileWidgets KF5::KIOWidgets KF5::KIONTLM reports kmm_widgets kmm_menus + kmm_printer ) if(ENABLE_WEBENGINE) target_link_libraries(reportsview PRIVATE Qt5::WebEngineWidgets) else(ENABLE_WEBENGINE) target_link_libraries(reportsview PRIVATE KF5::WebKit) endif(ENABLE_WEBENGINE) # the KCM module set(kcm_reportsview_PART_SRCS kcm_reportsview.cpp ) kconfig_add_kcfg_files(kcm_reportsview_PART_SRCS reportsviewsettings.kcfgc) ki18n_wrap_ui(kcm_reportsview_PART_SRCS reportsviewsettings.ui) kcoreaddons_add_plugin(kcm_reportsview SOURCES ${kcm_reportsview_PART_SRCS} JSON "${CMAKE_CURRENT_BINARY_DIR}/kcm_reportsview.json" INSTALL_NAMESPACE "kmymoney") kcoreaddons_desktop_to_json(kcm_reportsview kcm_reportsview.desktop) #kcoreaddons_add_plugin sets LIBRARY_OUTPUT_DIRECTORY to ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${INSTALL_NAMESPACE} set_target_properties(kcm_reportsview PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") target_link_libraries(kcm_reportsview KF5::I18n KF5::ConfigWidgets KF5::Completion KF5::KIOWidgets KF5::CoreAddons kmm_settings ) install(FILES kcm_reportsview.desktop DESTINATION "${SERVICES_INSTALL_DIR}") diff --git a/kmymoney/plugins/views/reports/core/listtable.cpp b/kmymoney/plugins/views/reports/core/listtable.cpp index 0c0e00e61..6ce307ce3 100644 --- a/kmymoney/plugins/views/reports/core/listtable.cpp +++ b/kmymoney/plugins/views/reports/core/listtable.cpp @@ -1,744 +1,743 @@ /* * Copyright 2004-2005 Ace Jones * Copyright 2008-2011 Alvaro Soliverez * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "listtable.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // This is just needed for i18n(). Once I figure out how to handle i18n // without using this macro directly, I'll be freed of KDE dependency. // ---------------------------------------------------------------------------- // Project Includes #include "mymoneymoney.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyreport.h" #include "kmymoneysettings.h" #include "mymoneyenums.h" namespace reports { QVector ListTable::TableRow::m_sortCriteria; // **************************************************************************** // // ListTable implementation // // **************************************************************************** bool ListTable::TableRow::operator< (const TableRow& _compare) const { bool result = false; foreach (const auto criterion, m_sortCriteria) { if (this->operator[](criterion) < _compare[criterion]) { result = true; break; } else if (this->operator[](criterion) > _compare[criterion]) { break; } } return result; } // needed for KDE < 3.2 implementation of qHeapSort bool ListTable::TableRow::operator<= (const TableRow& _compare) const { return (!(_compare < *this)); } bool ListTable::TableRow::operator== (const TableRow& _compare) const { return (!(*this < _compare) && !(_compare < *this)); } bool ListTable::TableRow::operator> (const TableRow& _compare) const { return (_compare < *this); } /** * TODO * * - Collapse 2- & 3- groups when they are identical * - Way more test cases (especially splits & transfers) * - Option to collapse splits * - Option to exclude transfers * */ ListTable::ListTable(const MyMoneyReport& _report): ReportTable(_report) { } void ListTable::render(QString& result, QString& csv) const { MyMoneyFile* file = MyMoneyFile::instance(); result.clear(); csv.clear(); // retrieve the configuration parameters from the report definition. // the things that we care about for query reports are: // how to group the rows, what columns to display, and what field // to subtotal on QList columns = m_columns; if (!m_subtotal.isEmpty() && m_subtotal.count() == 1) // constructPerformanceRow has subtotal columns already in columns columns.append(m_subtotal); QList postcolumns = m_postcolumns; if (!m_postcolumns.isEmpty()) // prevent creation of empty column columns.append(postcolumns); result.append(QLatin1String("\n")); // // Table header // foreach (const auto cellType, columns) { result.append(QString::fromLatin1("").arg(tableHeader(cellType))); csv.append(tableHeader(cellType) + QLatin1Char(',')); } csv.chop(1); // remove last ',' character result.append(QLatin1String("\n")); csv.append(QLatin1Char('\n')); // initialize group names to empty, so any group will have to display its header QStringList prevGrpNames; for (int i = 0; i < m_group.count(); ++i) { prevGrpNames.append(QString()); } // // Rows // bool row_odd = true; bool isLowestGroupTotal = true; // hack to inform whether to put separator line or not // ***DV*** MyMoneyMoney startingBalance; MyMoneyMoney balanceChange = MyMoneyMoney(); for (QList::ConstIterator it_row = m_rows.constBegin(); it_row != m_rows.constEnd(); ++it_row) { /* rank can be: * 0 - opening balance * 1 - major split of transaction * 2 - minor split of transaction * 3 - closing balance * 4 - first totals row * 5 - middle totals row */ const int rowRank = (*it_row).value(ctRank).toInt(); // detect whether any of groups changed and display new group header in that case for (int i = 0; i < m_group.count(); ++i) { QString curGrpName = (*it_row).value(m_group.at(i)); if (curGrpName.isEmpty()) // it could be grand total continue; if (prevGrpNames.at(i) != curGrpName) { // group header of lowest group doesn't bring any useful information // if hide transaction is enabled, so don't display it int lowestGroup = m_group.count() - 1; if (!m_config.isHideTransactions() || i != lowestGroup) { row_odd = true; result.append(QString::fromLatin1("" "\n").arg(QString::number(i), QString::number(columns.count()), curGrpName)); csv.append(QString::fromLatin1("\"%1\"\n").arg(curGrpName)); } if (i == lowestGroup) // lowest group has been switched... isLowestGroupTotal = true; // ...so expect lowest group total prevGrpNames.replace(i, curGrpName); } } bool need_label = true; QString tlink; // link information to account and transaction if (!m_config.isHideTransactions() || rowRank == 4 || rowRank == 5) { // if hide transaction is enabled display only total rows i.e. rank = 4 || rank = 5 if (rowRank == 0 || rowRank == 3) { // skip the opening and closing balance row, // if the balance column is not shown // rank = 0 for opening balance, rank = 3 for closing balance if (!columns.contains(ctBalance)) continue; result.append(QString::fromLatin1("").arg((*it_row).value(ctID))); // ***DV*** } else if (rowRank == 1) { row_odd = ! row_odd; if (linkEntries()) { tlink = QString::fromLatin1("id=%1&tid=%2").arg((*it_row).value(ctAccountID), (*it_row).value(ctID)); } result.append(QString::fromLatin1("").arg(row_odd ? QLatin1String("odd") : QLatin1String("even"))); } else if (rowRank == 2) { result.append(QString::fromLatin1("").arg(row_odd ? QLatin1Char('1') : QLatin1Char('0'))); } else if (rowRank == 4 || rowRank == 5) { QList::const_iterator nextRow = std::next(it_row); if ((m_config.rowType() == eMyMoney::Report::RowType::Tag)) { //If we order by Tags don't show the Grand total as we can have multiple tags per transaction continue; } else if (rowRank == 4) { if (nextRow != m_rows.end()) { if (isLowestGroupTotal && m_config.isHideTransactions()) { result.append(QLatin1String("")); isLowestGroupTotal = false; } else if ((*nextRow).value(ctRank) == QLatin1String("5")) { result.append(QLatin1String("")); } else { result.append(QLatin1String("")); } } else { result.append(QLatin1String("")); } } else if (rowRank == 5) { if (nextRow != m_rows.end()) { if ((*nextRow).value(ctRank) == QLatin1String("5")) result.append(QLatin1String("")); else result.append(QLatin1String("")); } }/* else { dead code result.append(QLatin1String("")); }*/ } else { result.append(QString::fromLatin1("").arg(row_odd ? QLatin1String("odd") : QLatin1String("even"))); } } else { continue; } // // Columns // QList::ConstIterator it_column = columns.constBegin(); while (it_column != columns.constEnd()) { QString data = (*it_row).value(*it_column); // ***DV*** if (rowRank == 2) { if (*it_column == ctValue) data = (*it_row).value(ctSplit); else if (*it_column == ctPostDate || *it_column == ctNumber || *it_column == ctPayee - || *it_column == ctTag || *it_column == ctAction || *it_column == ctShares || *it_column == ctPrice || *it_column == ctNextDueDate || *it_column == ctBalance || *it_column == ctAccount || *it_column == ctName) data.clear(); } // ***DV*** else if (rowRank == 0 || rowRank == 3) { if (*it_column == ctBalance) { data = (*it_row).value(ctBalance); if ((*it_row).value(ctID) == QLatin1String("A")) { // opening balance? startingBalance = MyMoneyMoney(data); balanceChange = MyMoneyMoney(); } } if (need_label) { if ((*it_column == ctPayee) || (*it_column == ctCategory) || (*it_column == ctMemo)) { if (!(*it_row).value(ctShares).isEmpty()) { data = ((*it_row).value(ctID) == QLatin1String("A")) ? i18n("Initial Market Value") : i18n("Ending Market Value"); } else { data = ((*it_row).value(ctID) == QLatin1String("A")) ? i18n("Opening Balance") : i18n("Closing Balance"); } need_label = false; } } } // The 'balance' column is calculated at render-time // but not printed on split lines else if (*it_column == ctBalance && rowRank == 1) { // Take the balance off the deepest group iterator balanceChange += MyMoneyMoney((*it_row).value(ctValue, QLatin1String("0"))); data = (balanceChange + startingBalance).toString(); } else if ((rowRank == 4 || rowRank == 5)) { // display total title but only if first column doesn't contain any data if (it_column == columns.constBegin() && data.isEmpty()) { result.append(QString::fromLatin1("")); ++it_column; continue; } else if (!m_subtotal.contains(*it_column)) { // don't display e.g. account in totals row result.append(QLatin1String("")); ++it_column; continue; } } // Figure out how to render the value in this column, depending on // what its properties are. // // TODO: This and the i18n headings are handled // as a set of parallel vectors. Would be much better to make a single // vector of a properties class. QString tlinkBegin, tlinkEnd; if (!tlink.isEmpty()) { tlinkBegin = QString::fromLatin1("").arg(tlink); tlinkEnd = QLatin1String(""); } QString currencyID = (*it_row).value(ctCurrency); if (currencyID.isEmpty()) currencyID = file->baseCurrency().id(); int fraction = file->currency(currencyID).smallestAccountFraction(); if (m_config.isConvertCurrency()) // don't show currency id, if there is only single currency currencyID.clear(); switch (cellGroup(*it_column)) { case cgMoney: if (data.isEmpty()) { result.append(QString::fromLatin1("") .arg((*it_column == ctValue) ? QLatin1String(" class=\"value\"") : QString())); csv.append(QLatin1String("\"\",")); } else if (MyMoneyMoney(data) == MyMoneyMoney::autoCalc) { result.append(QString::fromLatin1("%3%2%4") .arg((*it_column == ctValue) ? QLatin1String(" class=\"value\"") : QString(), i18n("Calculated"), tlinkBegin, tlinkEnd)); csv.append(QString::fromLatin1("\"%1\",").arg(i18n("Calculated"))); } else { auto value = MyMoneyMoney(data); auto valueStr = value.formatMoney(fraction); csv.append(QString::fromLatin1("\"%1 %2\",") .arg(currencyID, valueStr)); QString colorBegin; QString colorEnd; if ((rowRank == 4 || rowRank == 5) && value.isNegative()) { colorBegin = QString::fromLatin1("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); colorEnd = QLatin1String(""); } result.append(QString::fromLatin1("%4%6%2 %3%7%5") .arg((*it_column == ctValue) ? QLatin1String(" class=\"value\"") : QString(), currencyID, valueStr, tlinkBegin, tlinkEnd, colorBegin, colorEnd)); } break; case cgPercent: if (data.isEmpty()) { result.append(QLatin1String("")); csv.append(QLatin1String("\"\",")); } else { auto value = MyMoneyMoney(data) * MyMoneyMoney(100, 1); auto valueStr = value.formatMoney(fraction); csv.append(QString::fromLatin1("%1%,").arg(valueStr)); QString colorBegin; QString colorEnd; if ((rowRank == 4 || rowRank == 5) && value.isNegative()) { colorBegin = QString::fromLatin1("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); colorEnd = QLatin1String(""); } if ((rowRank == 4 || rowRank == 5) && value.isNegative()) valueStr = QString::fromLatin1("%2") .arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name(), valueStr); result.append(QString::fromLatin1("").arg(valueStr, tlinkBegin, tlinkEnd, colorBegin, colorEnd)); } break; case cgPrice: { int pricePrecision = file->security(file->account((*it_row).value(ctAccountID)).currencyId()).pricePrecision(); result.append(QString::fromLatin1("") .arg(MyMoneyMoney(data).formatMoney(QString(), pricePrecision), currencyID, tlinkBegin, tlinkEnd)); csv.append(QString::fromLatin1("\"%1 %2\",").arg(currencyID, MyMoneyMoney(data).formatMoney(QString(), pricePrecision, false))); } break; case cgShares: if (data.isEmpty()) { result.append(QLatin1String("")); csv.append(QLatin1String("\"\",")); } else { int sharesPrecision = MyMoneyMoney::denomToPrec(file->security(file->account((*it_row).value(ctAccountID)).currencyId()).smallestAccountFraction()); result += QString::fromLatin1("").arg(MyMoneyMoney(data).formatMoney(QString(), sharesPrecision), tlinkBegin, tlinkEnd); csv.append(QString::fromLatin1("\"%1\",").arg(MyMoneyMoney(data).formatMoney(QString(), sharesPrecision, false))); } break; case cgDate: // do this before we possibly change data csv.append(QString::fromLatin1("\"%1\",").arg(data)); // if we have a locale() then use its date formatter if (!data.isEmpty()) { QDate qd = QDate::fromString(data, Qt::ISODate); data = QLocale().toString(qd, QLocale::ShortFormat); } result.append(QString::fromLatin1("").arg(data, tlinkBegin, tlinkEnd, QString::number(prevGrpNames.count() - 1))); break; default: result.append(QString::fromLatin1("").arg(data, tlinkBegin, tlinkEnd, QString::number(prevGrpNames.count() - 1))); csv.append(QString::fromLatin1("\"%1\",").arg(data)); break; } ++it_column; tlink.clear(); } result.append(QLatin1String("\n")); csv.chop(1); // remove final comma csv.append(QLatin1Char('\n')); } result.append(QLatin1String("
%1
%3
").arg((*it_row).value(ctDepth))); if (rowRank == 4) { if (!(*it_row).value(ctDepth).isEmpty()) result += i18nc("Total balance", "Total") + QLatin1Char(' ') + prevGrpNames.at((*it_row).value(ctDepth).toInt()); else result += i18n("Grand Total"); } result.append(QLatin1String("%2%4%1%%5%3%3%2 %1%4%2%1%3%2%1%3%2%1%3
\n")); } QString ListTable::renderHTML() const { QString html, csv; render(html, csv); return html; } QString ListTable::renderCSV() const { QString html, csv; render(html, csv); return csv; } void ListTable::dump(const QString& file, const QString& context) const { QFile g(file); g.open(QIODevice::WriteOnly | QIODevice::Text); if (! context.isEmpty()) QTextStream(&g) << context.arg(renderHTML()); else QTextStream(&g) << renderHTML(); g.close(); } void ListTable::includeInvestmentSubAccounts() { // if we're not in expert mode, we need to make sure // that all stock accounts for the selected investment // account are also selected. // In case we get called for a non investment only report we quit if (KMyMoneySettings::expertMode() || !m_config.isInvestmentsOnly()) { return; } // get all investment subAccountsList but do not include those with zero balance // or those which had no transactions during the timeframe of the report QStringList accountIdList; QStringList subAccountsList; MyMoneyFile* file = MyMoneyFile::instance(); // get the report account filter if (!m_config.accounts(accountIdList) && m_config.isInvestmentsOnly()) { // this will only execute if this is an investment-only report QList accountList; file->accountList(accountList); QList::const_iterator it_ma; for (it_ma = accountList.constBegin(); it_ma != accountList.constEnd(); ++it_ma) { if ((*it_ma).accountType() == eMyMoney::Account::Type::Investment) { accountIdList.append((*it_ma).id()); } } } foreach (const auto sAccount, accountIdList) { auto acc = file->account(sAccount); if (acc.accountType() == eMyMoney::Account::Type::Investment) { foreach (const auto sSubAccount, acc.accountList()) { if (!accountIdList.contains(sSubAccount)) { subAccountsList.append(sSubAccount); } } } } if (m_config.isInvestmentsOnly() && !m_config.isIncludingUnusedAccounts()) { // if the balance is not zero at the end, include the subaccount QStringList::iterator it_balance; for (it_balance = subAccountsList.begin(); it_balance != subAccountsList.end();) { if (!file->balance((*it_balance), m_config.toDate()).isZero()) { m_config.addAccount((*it_balance)); it_balance = subAccountsList.erase((it_balance)); } else { ++it_balance; } } // if there are transactions for that subaccount, include them MyMoneyTransactionFilter filter; filter.setDateFilter(m_config.fromDate(), m_config.toDate()); filter.addAccount(subAccountsList); filter.setReportAllSplits(false); QList transactions = file->transactionList(filter); QList::const_iterator it_t = transactions.constBegin(); //Check each split for a matching account for (; it_t != transactions.constEnd(); ++it_t) { const QList& splits = (*it_t).splits(); foreach (const auto split, splits) { const QString& accountId = split.accountId(); if (!split.shares().isZero() && subAccountsList.contains(accountId)) { subAccountsList.removeOne(accountId); m_config.addAccount(accountId); } } } } else { // if not an investment-only report or explicitly including unused accounts // add all investment subaccounts m_config.addAccount(subAccountsList); } } ListTable::cellGroupE ListTable::cellGroup(const cellTypeE cellType) { switch (cellType) { // the list of columns which represent money, so we can display them correctly case ctValue: case ctNetInvValue: case ctMarketValue: case ctBuys: case ctSells: case ctBuysST: case ctSellsST: case ctBuysLT: case ctSellsLT: case ctCapitalGain: case ctCapitalGainST: case ctCapitalGainLT: case ctCashIncome: case ctReinvestIncome: case ctFees: case ctInterest: case ctStartingBalance: case ctEndingBalance: case ctBalance: case ctCurrentBalance: case ctBalanceWarning: case ctMaxBalanceLimit: case ctCreditWarning: case ctMaxCreditLimit: case ctLoanAmount: case ctPeriodicPayment: case ctFinalPayment: case ctPayment: return cgMoney; case ctPrice: case ctLastPrice: case ctBuyPrice: return cgPrice; /* the list of columns which represent shares, which is like money except the transaction currency will not be displayed*/ case ctShares: return cgShares; // the list of columns which represent a percentage, so we can display them correctly case ctReturn: case ctReturnInvestment: case ctInterestRate: case ctPercentageGain: return cgPercent; // the list of columns which represent dates, so we can display them correctly case ctPostDate: case ctEntryDate: case ctNextDueDate: case ctOpeningDate: case ctNextInterestChange: return cgDate; default: break; } return cgMisc; } QString ListTable::tableHeader(const cellTypeE cellType) { switch (cellType) { case ctPostDate: return i18n("Date"); case ctValue: return i18n("Amount"); case ctNumber: return i18n("Num"); case ctPayee: return i18n("Payee"); case ctTag: return i18n("Tags"); case ctCategory: return i18n("Category"); case ctAccount: return i18n("Account"); case ctMemo: return i18n("Memo"); case ctTopCategory: return i18n("Top Category"); case ctCategoryType: return i18n("Category Type"); case ctMonth: return i18n("Month"); case ctWeek: return i18n("Week"); case ctReconcileFlag: return i18n("Reconciled"); case ctAction: return i18n("Action"); case ctShares: return i18n("Shares"); case ctPrice: return i18n("Price"); case ctLastPrice: return i18n("Last Price"); case ctBuyPrice: return i18n("Buy Price"); case ctNetInvValue: return i18n("Net Value"); case ctBuys: return i18n("Buy Value"); case ctSells: return i18n("Sell Value"); case ctBuysST: return i18n("Short-term Buy Value"); case ctSellsST: return i18n("Short-term Sell Value"); case ctBuysLT: return i18n("Long-term Buy Value"); case ctSellsLT: return i18n("Long-term Sell Value"); case ctReinvestIncome: return i18n("Dividends Reinvested"); case ctCashIncome: return i18n("Dividends Paid Out"); case ctStartingBalance: return i18n("Starting Balance"); case ctEndingBalance: return i18n("Ending Balance"); case ctMarketValue: return i18n("Market Value"); case ctReturn: return i18n("Annualized Return"); case ctReturnInvestment: return i18n("Return On Investment"); case ctFees: return i18n("Fees"); case ctInterest: return i18n("Interest"); case ctPayment: return i18n("Payment"); case ctBalance: return i18n("Balance"); case ctType: return i18n("Type"); case ctName: return i18nc("Account name", "Name"); case ctNextDueDate: return i18n("Next Due Date"); case ctOccurrence: return i18n("Occurrence"); case ctPaymentType: return i18n("Payment Method"); case ctInstitution: return i18n("Institution"); case ctDescription: return i18n("Description"); case ctOpeningDate: return i18n("Opening Date"); case ctCurrencyName: return i18n("Currency"); case ctBalanceWarning: return i18n("Balance Early Warning"); case ctMaxBalanceLimit: return i18n("Balance Max Limit"); case ctCreditWarning: return i18n("Credit Early Warning"); case ctMaxCreditLimit: return i18n("Credit Max Limit"); case ctTax: return i18n("Tax"); case ctFavorite: return i18n("Preferred"); case ctLoanAmount: return i18n("Loan Amount"); case ctInterestRate: return i18n("Interest Rate"); case ctNextInterestChange: return i18n("Next Interest Change"); case ctPeriodicPayment: return i18n("Periodic Payment"); case ctFinalPayment: return i18n("Final Payment"); case ctCurrentBalance: return i18n("Current Balance"); case ctCapitalGain: return i18n("Capital Gain"); case ctPercentageGain: return i18n("Percentage Gain"); case ctCapitalGainST: return i18n("Short-term Gain"); case ctCapitalGainLT: return i18n("Long-term Gain"); default: break; } return QLatin1String("None"); } } diff --git a/kmymoney/plugins/views/reports/core/querytable.cpp b/kmymoney/plugins/views/reports/core/querytable.cpp index 85f432c57..d6a20ac45 100644 --- a/kmymoney/plugins/views/reports/core/querytable.cpp +++ b/kmymoney/plugins/views/reports/core/querytable.cpp @@ -1,1988 +1,1994 @@ /* * Copyright 2005 Ace Jones * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "querytable.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "cashflowlist.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyinstitution.h" #include "mymoneyprice.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyreport.h" #include "mymoneyexception.h" #include "kmymoneyutils.h" #include "reportaccount.h" #include "mymoneyenums.h" namespace reports { // **************************************************************************** // // QueryTable implementation // // **************************************************************************** /** * TODO * * - Collapse 2- & 3- groups when they are identical * - Way more test cases (especially splits & transfers) * - Option to collapse splits * - Option to exclude transfers * */ QueryTable::QueryTable(const MyMoneyReport& _report): ListTable(_report) { // separated into its own method to allow debugging (setting breakpoints // directly in ctors somehow does not work for me (ipwizard)) // TODO: remove the init() method and move the code back to the ctor init(); } void QueryTable::init() { m_columns.clear(); m_group.clear(); m_subtotal.clear(); m_postcolumns.clear(); switch (m_config.rowType()) { case eMyMoney::Report::RowType::AccountByTopAccount: case eMyMoney::Report::RowType::EquityType: case eMyMoney::Report::RowType::AccountType: case eMyMoney::Report::RowType::Institution: constructAccountTable(); m_columns << ctAccount; break; case eMyMoney::Report::RowType::Account: constructTransactionTable(); m_columns << ctAccountID << ctPostDate; break; case eMyMoney::Report::RowType::Payee: case eMyMoney::Report::RowType::Tag: case eMyMoney::Report::RowType::Month: case eMyMoney::Report::RowType::Week: constructTransactionTable(); m_columns << ctPostDate << ctAccount; break; case eMyMoney::Report::RowType::CashFlow: constructSplitsTable(); m_columns << ctPostDate; break; default: constructTransactionTable(); m_columns << ctPostDate; } // Sort the data to match the report definition m_subtotal << ctValue; switch (m_config.rowType()) { case eMyMoney::Report::RowType::CashFlow: m_group << ctCategoryType << ctTopCategory << ctCategory; break; case eMyMoney::Report::RowType::Category: m_group << ctCategoryType << ctTopCategory << ctCategory; break; case eMyMoney::Report::RowType::TopCategory: m_group << ctCategoryType << ctTopCategory; break; case eMyMoney::Report::RowType::TopAccount: m_group << ctTopAccount << ctAccount; break; case eMyMoney::Report::RowType::Account: m_group << ctAccount; break; case eMyMoney::Report::RowType::AccountReconcile: m_group << ctAccount << ctReconcileFlag; break; case eMyMoney::Report::RowType::Payee: m_group << ctPayee; break; case eMyMoney::Report::RowType::Tag: m_group << ctTag; break; case eMyMoney::Report::RowType::Month: m_group << ctMonth; break; case eMyMoney::Report::RowType::Week: m_group << ctWeek; break; case eMyMoney::Report::RowType::AccountByTopAccount: m_group << ctTopAccount; break; case eMyMoney::Report::RowType::EquityType: m_group << ctEquityType; break; case eMyMoney::Report::RowType::AccountType: m_group << ctType; break; case eMyMoney::Report::RowType::Institution: m_group << ctInstitution << ctTopAccount; break; default: throw MYMONEYEXCEPTION_CSTRING("QueryTable::QueryTable(): unhandled row type"); } QVector sort = QVector::fromList(m_group) << QVector::fromList(m_columns) << ctID << ctRank; m_columns.clear(); switch (m_config.rowType()) { case eMyMoney::Report::RowType::AccountByTopAccount: case eMyMoney::Report::RowType::EquityType: case eMyMoney::Report::RowType::AccountType: case eMyMoney::Report::RowType::Institution: m_columns << ctAccount; break; default: m_columns << ctPostDate; } unsigned qc = m_config.queryColumns(); if (qc & eMyMoney::Report::QueryColumn::Number) m_columns << ctNumber; if (qc & eMyMoney::Report::QueryColumn::Payee) m_columns << ctPayee; if (qc & eMyMoney::Report::QueryColumn::Tag) m_columns << ctTag; if (qc & eMyMoney::Report::QueryColumn::Category) m_columns << ctCategory; if (qc & eMyMoney::Report::QueryColumn::Account) m_columns << ctAccount; if (qc & eMyMoney::Report::QueryColumn::Reconciled) m_columns << ctReconcileFlag; if (qc & eMyMoney::Report::QueryColumn::Memo) m_columns << ctMemo; if (qc & eMyMoney::Report::QueryColumn::Action) m_columns << ctAction; if (qc & eMyMoney::Report::QueryColumn::Shares) m_columns << ctShares; if (qc & eMyMoney::Report::QueryColumn::Price) m_columns << ctPrice; if (qc & eMyMoney::Report::QueryColumn::Performance) { m_subtotal.clear(); switch (m_config.investmentSum()) { case eMyMoney::Report::InvestmentSum::OwnedAndSold: m_columns << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; break; case eMyMoney::Report::InvestmentSum::Owned: m_columns << ctBuys << ctReinvestIncome << ctMarketValue << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctReinvestIncome << ctMarketValue << ctReturn << ctReturnInvestment; break; case eMyMoney::Report::InvestmentSum::Sold: m_columns << ctBuys << ctSells << ctCashIncome << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctSells << ctCashIncome << ctReturn << ctReturnInvestment; break; case eMyMoney::Report::InvestmentSum::Period: default: m_columns << ctStartingBalance << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; m_subtotal << ctStartingBalance << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; break; } } if (qc & eMyMoney::Report::QueryColumn::CapitalGain) { m_subtotal.clear(); switch (m_config.investmentSum()) { case eMyMoney::Report::InvestmentSum::Owned: m_columns << ctShares << ctBuyPrice << ctLastPrice << ctBuys << ctMarketValue << ctPercentageGain << ctCapitalGain; m_subtotal << ctShares << ctBuyPrice << ctLastPrice << ctBuys << ctMarketValue << ctPercentageGain << ctCapitalGain; break; case eMyMoney::Report::InvestmentSum::Sold: default: m_columns << ctBuys << ctSells << ctCapitalGain; m_subtotal << ctBuys << ctSells << ctCapitalGain; if (m_config.isShowingSTLTCapitalGains()) { m_columns << ctBuysST << ctSellsST << ctCapitalGainST << ctBuysLT << ctSellsLT << ctCapitalGainLT; m_subtotal << ctBuysST << ctSellsST << ctCapitalGainST << ctBuysLT << ctSellsLT << ctCapitalGainLT; } break; } } if (qc & eMyMoney::Report::QueryColumn::Loan) { m_columns << ctPayment << ctInterest << ctFees; m_postcolumns << ctBalance; } if (qc & eMyMoney::Report::QueryColumn::Balance) m_postcolumns << ctBalance; TableRow::setSortCriteria(sort); qSort(m_rows); if (m_config.isShowingColumnTotals()) constructTotalRows(); // adds total rows to m_rows } void QueryTable::constructTotalRows() { if (m_rows.isEmpty()) return; // qSort places grand total at last position, because it doesn't belong to any group for (int i = 0; i < m_rows.count(); ++i) { if (m_rows.at(0)[ctRank] == QLatin1String("4") || m_rows.at(0)[ctRank] == QLatin1String("5")) // it should be unlikely that total row is at the top of rows, so... m_rows.move(0, m_rows.count() - 1 - i); // ...move it at the bottom else break; } MyMoneyFile* file = MyMoneyFile::instance(); QList subtotals = m_subtotal; QList groups = m_group; QList columns = m_columns; if (!m_subtotal.isEmpty() && subtotals.count() == 1) columns.append(m_subtotal); QList postcolumns = m_postcolumns; if (!m_postcolumns.isEmpty()) columns.append(postcolumns); QMap>> totalCurrency; QList> totalGroups; QMap totalsValues; // initialize all total values under summed columns to be zero foreach (auto subtotal, subtotals) { totalsValues.insert(subtotal, MyMoneyMoney()); } totalsValues.insert(ctRowsCount, MyMoneyMoney()); // create total groups containing totals row for each group totalGroups.append(totalsValues); // prepend with extra group for grand total for (int j = 0; j < groups.count(); ++j) { totalGroups.append(totalsValues); } QList stashedTotalRows; int iCurrentRow, iNextRow; for (iCurrentRow = 0; iCurrentRow < m_rows.count();) { iNextRow = iCurrentRow + 1; // total rows are useless at summing so remove whole block of them at once while (iNextRow != m_rows.count() && (m_rows.at(iNextRow).value(ctRank) == QLatin1String("4") || m_rows.at(iNextRow).value(ctRank) == QLatin1String("5"))) { stashedTotalRows.append(m_rows.takeAt(iNextRow)); // ...but stash them just in case } bool lastRow = (iNextRow == m_rows.count()); // sum all subtotal values for lowest group QString currencyID = m_rows.at(iCurrentRow).value(ctCurrency); if (m_rows.at(iCurrentRow).value(ctRank) == QLatin1String("1")) { // don't sum up on balance (rank = 0 || rank = 3) and minor split (rank = 2) foreach (auto subtotal, subtotals) { if (!totalCurrency.contains(currencyID)) totalCurrency[currencyID].append(totalGroups); totalCurrency[currencyID].last()[subtotal] += MyMoneyMoney(m_rows.at(iCurrentRow)[subtotal]); } totalCurrency[currencyID].last()[ctRowsCount] += MyMoneyMoney::ONE; } // iterate over groups from the lowest to the highest to find group change for (int i = groups.count() - 1; i >= 0 ; --i) { // if any of groups from next row changes (or next row is the last row), then it's time to put totals row if (lastRow || m_rows.at(iCurrentRow)[groups.at(i)] != m_rows.at(iNextRow)[groups.at(i)]) { bool isMainCurrencyTotal = true; QMap>>::iterator currencyGrp = totalCurrency.begin(); while (currencyGrp != totalCurrency.end()) { if (!MyMoneyMoney((*currencyGrp).at(i + 1).value(ctRowsCount)).isZero()) { // if no rows summed up, then no totals row TableRow totalsRow; // sum all subtotal values for higher groups (excluding grand total) and reset lowest group values QMap::iterator upperGrp = (*currencyGrp)[i].begin(); QMap::iterator lowerGrp = (*currencyGrp)[i + 1].begin(); while(upperGrp != (*currencyGrp)[i].end()) { totalsRow[lowerGrp.key()] = lowerGrp.value().toString(); // fill totals row with subtotal values... (*upperGrp) += (*lowerGrp); // (*lowerGrp) = MyMoneyMoney(); ++upperGrp; ++lowerGrp; } // custom total values calculations foreach (auto subtotal, subtotals) { if (subtotal == ctReturnInvestment) totalsRow[subtotal] = helperROI((*currencyGrp).at(i + 1).value(ctBuys) - (*currencyGrp).at(i + 1).value(ctReinvestIncome), (*currencyGrp).at(i + 1).value(ctSells), (*currencyGrp).at(i + 1).value(ctStartingBalance), (*currencyGrp).at(i + 1).value(ctEndingBalance) + (*currencyGrp).at(i + 1).value(ctMarketValue), (*currencyGrp).at(i + 1).value(ctCashIncome)); else if (subtotal == ctPercentageGain) { const MyMoneyMoney denominator = (*currencyGrp).at(i + 1).value(ctBuys).abs(); totalsRow[subtotal] = denominator.isZero() ? QString(): (((*currencyGrp).at(i + 1).value(ctBuys) + (*currencyGrp).at(i + 1).value(ctMarketValue)) / denominator).toString(); } else if (subtotal == ctPrice) totalsRow[subtotal] = MyMoneyMoney((*currencyGrp).at(i + 1).value(ctPrice) / (*currencyGrp).at(i + 1).value(ctRowsCount)).toString(); } // total values that aren't calculated here, but are taken untouched from external source, e.g. constructPerformanceRow if (!stashedTotalRows.isEmpty()) { for (int j = 0; j < stashedTotalRows.count(); ++j) { if (stashedTotalRows.at(j).value(ctCurrency) != currencyID) continue; foreach (auto subtotal, subtotals) { if (subtotal == ctReturn) totalsRow[ctReturn] = stashedTotalRows.takeAt(j)[ctReturn]; } break; } } (*currencyGrp).replace(i + 1, totalsValues); for (int j = 0; j < groups.count(); ++j) { totalsRow[groups.at(j)] = m_rows.at(iCurrentRow)[groups.at(j)]; // ...and identification } currencyID = currencyGrp.key(); if (currencyID.isEmpty() && totalCurrency.count() > 1) currencyID = file->baseCurrency().id(); totalsRow[ctCurrency] = currencyID; if (isMainCurrencyTotal) { totalsRow[ctRank] = QLatin1Char('4'); isMainCurrencyTotal = false; } else totalsRow[ctRank] = QLatin1Char('5'); totalsRow[ctDepth] = QString::number(i); totalsRow.remove(ctRowsCount); m_rows.insert(iNextRow++, totalsRow); // iCurrentRow and iNextRow can diverge here by more than one } ++currencyGrp; } } } // code to put grand total row if (lastRow) { bool isMainCurrencyTotal = true; QMap>>::iterator currencyGrp = totalCurrency.begin(); while (currencyGrp != totalCurrency.end()) { TableRow totalsRow; QMap::const_iterator grandTotalGrp = (*currencyGrp)[0].constBegin(); while(grandTotalGrp != (*currencyGrp)[0].constEnd()) { totalsRow[grandTotalGrp.key()] = grandTotalGrp.value().toString(); ++grandTotalGrp; } foreach (auto subtotal, subtotals) { if (subtotal == ctReturnInvestment) totalsRow[subtotal] = helperROI((*currencyGrp).at(0).value(ctBuys) - (*currencyGrp).at(0).value(ctReinvestIncome), (*currencyGrp).at(0).value(ctSells), (*currencyGrp).at(0).value(ctStartingBalance), (*currencyGrp).at(0).value(ctEndingBalance) + (*currencyGrp).at(0).value(ctMarketValue), (*currencyGrp).at(0).value(ctCashIncome)); else if (subtotal == ctPercentageGain) totalsRow[subtotal] = (((*currencyGrp).at(0).value(ctBuys) + (*currencyGrp).at(0).value(ctMarketValue)) / (*currencyGrp).at(0).value(ctBuys).abs()).toString(); else if (subtotal == ctPrice) totalsRow[subtotal] = MyMoneyMoney((*currencyGrp).at(0).value(ctPrice) / (*currencyGrp).at(0).value(ctRowsCount)).toString(); } if (!stashedTotalRows.isEmpty()) { for (int j = 0; j < stashedTotalRows.count(); ++j) { foreach (auto subtotal, subtotals) { if (subtotal == ctReturn) totalsRow[ctReturn] = stashedTotalRows.takeAt(j)[ctReturn]; } } } for (int j = 0; j < groups.count(); ++j) { totalsRow[groups.at(j)] = QString(); // no identification } currencyID = currencyGrp.key(); if (currencyID.isEmpty() && totalCurrency.count() > 1) currencyID = file->baseCurrency().id(); totalsRow[ctCurrency] = currencyID; if (isMainCurrencyTotal) { totalsRow[ctRank] = QLatin1Char('4'); isMainCurrencyTotal = false; } else totalsRow[ctRank] = QLatin1Char('5'); totalsRow[ctDepth] = QString(); m_rows.append(totalsRow); ++currencyGrp; } break; // no use to loop further } iCurrentRow = iNextRow; // iCurrent makes here a leap forward by at least one } } void QueryTable::constructTransactionTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); MyMoneyReport report(m_config); report.setReportAllSplits(false); report.setConsiderCategory(true); bool use_transfers; bool use_summary; bool hide_details; bool tag_special_case = false; switch (m_config.rowType()) { case eMyMoney::Report::RowType::Category: case eMyMoney::Report::RowType::TopCategory: use_summary = false; use_transfers = false; hide_details = false; break; case eMyMoney::Report::RowType::Payee: use_summary = false; use_transfers = false; hide_details = (m_config.detailLevel() == eMyMoney::Report::DetailLevel::None); break; case eMyMoney::Report::RowType::Tag: use_summary = false; use_transfers = false; hide_details = (m_config.detailLevel() == eMyMoney::Report::DetailLevel::None); tag_special_case = true; break; default: use_summary = true; use_transfers = true; hide_details = (m_config.detailLevel() == eMyMoney::Report::DetailLevel::None); break; } // support for opening and closing balances QMap accts; //get all transactions for this report QList transactions = file->transactionList(report); for (QList::const_iterator it_transaction = transactions.constBegin(); it_transaction != transactions.constEnd(); ++it_transaction) { TableRow qA, qS; QDate pd; QList tagIdListCache; qA[ctID] = qS[ctID] = (* it_transaction).id(); qA[ctEntryDate] = qS[ctEntryDate] = (* it_transaction).entryDate().toString(Qt::ISODate); qA[ctPostDate] = qS[ctPostDate] = (* it_transaction).postDate().toString(Qt::ISODate); qA[ctCommodity] = qS[ctCommodity] = (* it_transaction).commodity(); pd = (* it_transaction).postDate(); qA[ctMonth] = qS[ctMonth] = i18n("Month of %1", QDate(pd.year(), pd.month(), 1).toString(Qt::ISODate)); qA[ctWeek] = qS[ctWeek] = i18n("Week of %1", pd.addDays(1 - pd.dayOfWeek()).toString(Qt::ISODate)); if (!m_containsNonBaseCurrency && (*it_transaction).commodity() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (report.isConvertCurrency()) qA[ctCurrency] = qS[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = qS[ctCurrency] = (*it_transaction).commodity(); // to handle splits, we decide on which account to base the split // (a reference point or point of view so to speak). here we take the // first account that is a stock account or loan account (or the first account // that is not an income or expense account if there is no stock or loan account) // to be the account (qA) that will have the sub-item "split" entries. we add // one transaction entry (qS) for each subsequent entry in the split. const QList& splits = (*it_transaction).splits(); QList::const_iterator myBegin, it_split; for (it_split = splits.constBegin(), myBegin = splits.constEnd(); it_split != splits.constEnd(); ++it_split) { ReportAccount splitAcc((* it_split).accountId()); // always put split with a "stock" account if it exists if (splitAcc.isInvest()) break; // prefer to put splits with a "loan" account if it exists if (splitAcc.isLoan()) myBegin = it_split; if ((myBegin == splits.end()) && ! splitAcc.isIncomeExpense()) { myBegin = it_split; } } // select our "reference" split if (it_split == splits.end()) { it_split = myBegin; } else { myBegin = it_split; } // skip this transaction if we didn't find a valid base account - see the above description // for the base account's description - if we don't find it avoid a crash by skipping the transaction if (myBegin == splits.end()) continue; // if the split is still unknown, use the first one. I have seen this // happen with a transaction that has only a single split referencing an income or expense // account and has an amount and value of 0. Such a transaction will fall through // the above logic and leave 'it_split' pointing to splits.end() which causes the remainder // of this to end in an infinite loop. if (it_split == splits.end()) { it_split = splits.begin(); } // for "loan" reports, the loan transaction gets special treatment. // the splits of a loan transaction are placed on one line in the // reference (loan) account (qA). however, we process the matching // split entries (qS) normally. bool loan_special_case = false; if (m_config.queryColumns() & eMyMoney::Report::QueryColumn::Loan) { ReportAccount splitAcc((*it_split).accountId()); loan_special_case = splitAcc.isLoan(); } bool include_me = true; bool transaction_text = false; //indicates whether a text should be considered as a match for the transaction or for a split only QString a_fullname; QString a_memo; int pass = 1; QString myBeginCurrency; QString baseCurrency = file->baseCurrency().id(); QMap xrMap; // container for conversion rates from given currency to myBeginCurrency do { MyMoneyMoney xr; ReportAccount splitAcc((* it_split).accountId()); QString splitCurrency; if (splitAcc.isInvest()) splitCurrency = file->account(file->account((*it_split).accountId()).parentAccountId()).currencyId(); else splitCurrency = file->account((*it_split).accountId()).currencyId(); if (it_split == myBegin) myBeginCurrency = splitCurrency; //get fraction for account int fraction = splitAcc.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = splitAcc.institutionId(); QString payee = (*it_split).payeeId(); const QList tagIdList = (*it_split).tagIdList(); //convert to base currency if (m_config.isConvertCurrency()) { xr = xrMap.value(splitCurrency, xr); // check if there is conversion rate to myBeginCurrency already stored... if (xr == MyMoneyMoney()) // ...if not... xr = (*it_split).price(); // ...take conversion rate to myBeginCurrency from split else if (splitAcc.isInvest()) // if it's stock split... xr *= (*it_split).price(); // ...multiply it by stock price stored in split if (!m_containsNonBaseCurrency && myBeginCurrency != baseCurrency) m_containsNonBaseCurrency = true; if (myBeginCurrency != baseCurrency) { // myBeginCurrency can differ from baseCurrency... MyMoneyPrice price = file->price(myBeginCurrency, baseCurrency, (*it_transaction).postDate()); // ...so check conversion rate... if (price.isValid()) { xr *= price.rate(baseCurrency); // ...and multiply it by current price... qA[ctCurrency] = qS[ctCurrency] = baseCurrency; } else qA[ctCurrency] = qS[ctCurrency] = myBeginCurrency; // ...and set information about non-baseCurrency } } else if (splitAcc.isInvest()) xr = (*it_split).price(); else xr = MyMoneyMoney::ONE; qA[ctTag].clear(); if (it_split == myBegin && splits.count() > 1) { include_me = m_config.includes(splitAcc); if (include_me) // track accts that will need opening and closing balances //FIXME in some cases it will show the opening and closing //balances but no transactions if the splits are all filtered out -- asoliverez accts.insert(splitAcc.id(), splitAcc); qA[ctAccount] = splitAcc.name(); qA[ctAccountID] = splitAcc.id(); qA[ctTopAccount] = splitAcc.topParentName(); if (splitAcc.isInvest()) { // use the institution of the parent for stock accounts institution = splitAcc.parent().institutionId(); MyMoneyMoney shares = (*it_split).shares(); int pricePrecision = file->security(splitAcc.currencyId()).pricePrecision(); qA[ctAction] = (*it_split).action(); qA[ctShares] = shares.isZero() ? QString() : shares.toString(); qA[ctPrice] = shares.isZero() ? QString() : xr.convertPrecision(pricePrecision).toString(); if (((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) && shares.isNegative()) qA[ctAction] = "Sell"; qA[ctInvestAccount] = splitAcc.parent().name(); MyMoneySplit stockSplit = (*it_split); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity currency; MyMoneySecurity security; eMyMoney::Split::InvestmentTransactionType transactionType; KMyMoneyUtils::dissectTransaction((*it_transaction), stockSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); if (!(assetAccountSplit == MyMoneySplit())) { for (it_split = splits.begin(); it_split != splits.end(); ++it_split) { if ((*it_split) == assetAccountSplit) { splitAcc = ReportAccount(assetAccountSplit.accountId()); // switch over from stock split to asset split because amount in stock split doesn't take fees/interests into account myBegin = it_split; // set myBegin to asset split, so stock split can be listed in details under splits myBeginCurrency = (file->account((*myBegin).accountId())).currencyId(); if (!m_containsNonBaseCurrency && myBeginCurrency != baseCurrency) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) { if (myBeginCurrency != baseCurrency) { MyMoneyPrice price = file->price(myBeginCurrency, baseCurrency, (*it_transaction).postDate()); if (price.isValid()) { xr = price.rate(baseCurrency); qA[ctCurrency] = qS[ctCurrency] = baseCurrency; } else qA[ctCurrency] = qS[ctCurrency] = myBeginCurrency; } else xr = MyMoneyMoney::ONE; qA[ctPrice] = shares.isZero() ? QString() : (stockSplit.price() * xr / (*it_split).price()).toString(); // put conversion rate for all splits with this currency, so... // every split of transaction have the same conversion rate xrMap.insert(splitCurrency, MyMoneyMoney::ONE / (*it_split).price()); } else xr = (*it_split).price(); break; } } } } else qA[ctPrice] = xr.toString(); a_fullname = splitAcc.fullName(); a_memo = (*it_split).memo(); transaction_text = m_config.match((*it_split)); qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA[ctPayee] = payee.isEmpty() ? i18n("[Empty Payee]") : file->payee(payee).name().simplified(); if (tag_special_case) { tagIdListCache = tagIdList; } else { QString delimiter; foreach(const auto tagId, tagIdList) { qA[ctTag] += delimiter + file->tag(tagId).name().simplified(); delimiter = QLatin1Char(','); } } qA[ctReconcileDate] = (*it_split).reconcileDate().toString(Qt::ISODate); qA[ctReconcileFlag] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true); qA[ctNumber] = (*it_split).number(); qA[ctMemo] = a_memo; qA[ctValue] = ((*it_split).shares() * xr).convert(fraction).toString(); qS[ctReconcileDate] = qA[ctReconcileDate]; qS[ctReconcileFlag] = qA[ctReconcileFlag]; qS[ctNumber] = qA[ctNumber]; qS[ctTopCategory] = splitAcc.topParentName(); qS[ctCategoryType] = i18n("Transfer"); // only include the configured accounts if (include_me) { if (loan_special_case) { // put the principal amount in the "value" column and convert to lowest fraction qA[ctValue] = (-(*it_split).shares() * xr).convert(fraction).toString(); qA[ctRank] = QLatin1Char('1'); qA[ctSplit].clear(); } else { if ((splits.count() > 2) && use_summary) { // add the "summarized" split transaction // this is the sub-total of the split detail // convert to lowest fraction qA[ctRank] = QLatin1Char('1'); qA[ctCategory] = i18n("[Split Transaction]"); qA[ctTopCategory] = i18nc("Split transaction", "Split"); qA[ctCategoryType] = i18nc("Split transaction", "Split"); m_rows += qA; } } } } else { if (include_me) { if (loan_special_case) { MyMoneyMoney value = (-(* it_split).shares() * xr).convert(fraction); if ((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization)) { // put the payment in the "payment" column and convert to lowest fraction qA[ctPayee] = value.toString(); } else if ((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { // put the interest in the "interest" column and convert to lowest fraction qA[ctInterest] = value.toString(); } else if (splits.count() > 2) { // [dv: This comment carried from the original code. I am // not exactly clear on what it means or why we do this.] // Put the initial pay-in nowhere (that is, ignore it). This // is dangerous, though. The only way I can tell the initial // pay-in apart from fees is if there are only 2 splits in // the transaction. I wish there was a better way. } else { // accumulate everything else in the "fees" column MyMoneyMoney n0 = MyMoneyMoney(qA[ctFees]); qA[ctFees] = (n0 + value).toString(); } // we don't add qA here for a loan transaction. we'll add one // qA after all of the split components have been processed. // (see below) } //--- special case to hide split transaction details else if (hide_details && (splits.count() > 2)) { // essentially, don't add any qA entries } //--- default case includes all transaction details else { //this is when the splits are going to be shown as children of the main split if ((splits.count() > 2) && use_summary) { qA[ctValue].clear(); //convert to lowest fraction qA[ctSplit] = (-(*it_split).shares() * xr).convert(fraction).toString(); qA[ctRank] = QLatin1Char('2'); + qA[ctTag] = ""; + QString delimiter = ""; + for (int i = 0; i < tagIdList.size(); i++) { + qA[ctTag] += delimiter + file->tag(tagIdList[i]).name().simplified(); + delimiter = ", "; + } } else { //this applies when the transaction has only 2 splits, or each split is going to be //shown separately, eg. transactions by category switch (m_config.rowType()) { case eMyMoney::Report::RowType::Category: case eMyMoney::Report::RowType::TopCategory: case eMyMoney::Report::RowType::Tag: case eMyMoney::Report::RowType::Payee: if (splitAcc.isIncomeExpense()) qA[ctValue] = (-(*it_split).shares() * xr).convert(fraction).toString(); // needed for category reports, in case of multicurrency transaction it breaks it break; default: break; } qA[ctSplit].clear(); qA[ctRank] = QLatin1Char('1'); } qA [ctMemo] = (*it_split).memo(); if (!m_containsNonBaseCurrency && splitAcc.currencyId() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (report.isConvertCurrency()) qS[ctCurrency] = file->baseCurrency().id(); else qS[ctCurrency] = splitAcc.currency().id(); if (! splitAcc.isIncomeExpense()) { qA[ctCategory] = ((*it_split).shares().isNegative()) ? i18n("Transfer from %1", splitAcc.fullName()) : i18n("Transfer to %1", splitAcc.fullName()); qA[ctTopCategory] = splitAcc.topParentName(); qA[ctCategoryType] = i18n("Transfer"); } else { qA [ctCategory] = splitAcc.fullName(); qA [ctTopCategory] = splitAcc.topParentName(); qA [ctCategoryType] = MyMoneyAccount::accountTypeToString(splitAcc.accountGroup()); } if (splits.count() > 1) { if (use_transfers || (splitAcc.isIncomeExpense() && m_config.includes(splitAcc))) { //if it matches the text of the main split of the transaction or //it matches this particular split, include it //otherwise, skip it //if the filter is "does not contain" exclude the split if it does not match //even it matches the whole split if ((m_config.isInvertingText() && m_config.match((*it_split))) || (!m_config.isInvertingText() && (transaction_text || m_config.match((*it_split))))) { if (tag_special_case) { if (tagIdListCache.isEmpty()) { qA[ctTag] = i18n("[No Tag]"); } else { QString delimiter; foreach(const auto tagId, tagIdListCache) { qA[ctTag] += delimiter + file->tag(tagId).name().simplified(); delimiter = QLatin1Char(','); } } } m_rows += qA; } } } } } if ((m_config.includes(splitAcc) && use_transfers && !(splitAcc.isInvest() && include_me)) || splits.count() == 1) { // otherwise stock split is displayed twice in report if (! splitAcc.isIncomeExpense()) { //multiply by currency and convert to lowest fraction qS[ctValue] = ((*it_split).shares() * xr).convert(fraction).toString(); qS[ctRank] = QLatin1Char('1'); qS[ctAccount] = splitAcc.name(); qS[ctAccountID] = splitAcc.id(); qS[ctTopAccount] = splitAcc.topParentName(); if (splits.count() > 1) { qS[ctCategory] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", a_fullname) : i18n("Transfer from %1", a_fullname); } else { qS[ctCategory] = i18n("*** UNASSIGNED ***"); } qS[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qS[ctMemo] = (*it_split).memo().isEmpty() ? a_memo : (*it_split).memo(); //FIXME-ALEX When is used this? I can't find in which condition we arrive here... maybe this code is useless? if (tagIdList.isEmpty()) { qS[ctTag] = i18n("[No Tag]"); } else { QString delimiter; foreach(const auto tagId, tagIdList) { qS[ctTag] += delimiter + file->tag(tagId).name().simplified(); delimiter = QLatin1Char(','); } } qS[ctPayee] = payee.isEmpty() ? qA[ctPayee] : file->payee(payee).name().simplified(); //check the specific split against the filter for text and amount //TODO this should be done at the engine, but I have no clear idea how -- asoliverez //if the filter is "does not contain" exclude the split if it does not match //even it matches the whole split if ((m_config.isInvertingText() && m_config.match((*it_split))) || (!m_config.isInvertingText() && (transaction_text || m_config.match((*it_split))))) { m_rows += qS; // track accts that will need opening and closing balances accts.insert(splitAcc.id(), splitAcc); } } } } ++it_split; // look for wrap-around if (it_split == splits.end()) it_split = splits.begin(); // but terminate if this transaction has only a single split if (splits.count() < 2) break; //check if there have been more passes than there are splits //this is to prevent infinite loops in cases of data inconsistency -- asoliverez ++pass; if (pass > splits.count()) break; } while (it_split != myBegin); if (loan_special_case) { m_rows += qA; } } // now run through our accts list and add opening and closing balances switch (m_config.rowType()) { case eMyMoney::Report::RowType::Account: case eMyMoney::Report::RowType::TopAccount: break; // case eMyMoney::Report::RowType::Category: // case MyMoneyReport::eTopCategory: // case MyMoneyReport::ePayee: // case MyMoneyReport::eMonth: // case MyMoneyReport::eWeek: default: return; } QDate startDate, endDate; report.validDateRange(startDate, endDate); QString strStartDate = startDate.toString(Qt::ISODate); QString strEndDate = endDate.toString(Qt::ISODate); startDate = startDate.addDays(-1); for (auto it_account = accts.constBegin(); it_account != accts.constEnd(); ++it_account) { TableRow qA; ReportAccount account(*it_account); //get fraction for account int fraction = account.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = account.institutionId(); // use the institution of the parent for stock accounts if (account.isInvest()) institution = account.parent().institutionId(); MyMoneyMoney startBalance, endBalance, startPrice, endPrice; MyMoneyMoney startShares, endShares; //get price and convert currency if necessary if (m_config.isConvertCurrency()) { startPrice = (account.deepCurrencyPrice(startDate) * account.baseCurrencyPrice(startDate)).reduce(); endPrice = (account.deepCurrencyPrice(endDate) * account.baseCurrencyPrice(endDate)).reduce(); } else { startPrice = account.deepCurrencyPrice(startDate).reduce(); endPrice = account.deepCurrencyPrice(endDate).reduce(); } startShares = file->balance(account.id(), startDate); endShares = file->balance(account.id(), endDate); //get starting and ending balances startBalance = startShares * startPrice; endBalance = endShares * endPrice; //starting balance // don't show currency if we're converting or if it's not foreign if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qA[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = account.currency().id(); qA[ctAccountID] = account.id(); qA[ctAccount] = account.name(); qA[ctTopAccount] = account.topParentName(); qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA[ctRank] = QLatin1Char('0'); qA[ctPrice] = startPrice.convertPrecision(account.currency().pricePrecision()).toString(); if (account.isInvest()) { qA[ctShares] = startShares.toString(); } qA[ctPostDate] = strStartDate; qA[ctBalance] = startBalance.convert(fraction).toString(); qA[ctValue].clear(); qA[ctID] = QLatin1Char('A'); m_rows += qA; //ending balance qA[ctPrice] = endPrice.convertPrecision(account.currency().pricePrecision()).toString(); if (account.isInvest()) { qA[ctShares] = endShares.toString(); } qA[ctPostDate] = strEndDate; qA[ctBalance] = endBalance.toString(); qA[ctRank] = QLatin1Char('3'); qA[ctID] = QLatin1Char('Z'); m_rows += qA; } } QString QueryTable::helperROI(const MyMoneyMoney &buys, const MyMoneyMoney &sells, const MyMoneyMoney &startingBal, const MyMoneyMoney &endingBal, const MyMoneyMoney &cashIncome) const { MyMoneyMoney returnInvestment; if (!(startingBal - buys).isZero()) { returnInvestment = (sells + buys + cashIncome + endingBal - startingBal) / (startingBal - buys); return returnInvestment.convert(10000).toString(); } else return QString(); } QString QueryTable::helperIRR(const CashFlowList &all) const { try { return MyMoneyMoney(all.XIRR(), 10000).toString(); } catch (MyMoneyException &e) { qDebug() << e.what(); all.dumpDebug(); return QString(); } } void QueryTable::sumInvestmentValues(const ReportAccount& account, QList& cfList, QList& shList) const { for (int i = InvestmentValue::Buys; i < InvestmentValue::End; ++i) cfList.append(CashFlowList()); for (int i = InvestmentValue::Buys; i <= InvestmentValue::BuysOfOwned; ++i) shList.append(MyMoneyMoney()); MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; QDate newStartingDate; QDate newEndingDate; const bool isSTLT = report.isShowingSTLTCapitalGains(); const int settlementPeriod = report.settlementPeriod(); QDate termSeparator = report.termSeparator().addDays(-settlementPeriod); report.validDateRange(startingDate, endingDate); newStartingDate = startingDate; newEndingDate = endingDate; if (report.queryColumns() & eMyMoney::Report::QueryColumn::CapitalGain) { // Saturday and Sunday aren't valid settlement dates if (endingDate.dayOfWeek() == Qt::Saturday) endingDate = endingDate.addDays(-1); else if (endingDate.dayOfWeek() == Qt::Sunday) endingDate = endingDate.addDays(-2); if (termSeparator.dayOfWeek() == Qt::Saturday) termSeparator = termSeparator.addDays(-1); else if (termSeparator.dayOfWeek() == Qt::Sunday) termSeparator = termSeparator.addDays(-2); if (startingDate.daysTo(endingDate) <= settlementPeriod) // no days to check for return; termSeparator = termSeparator.addDays(-settlementPeriod); newEndingDate = endingDate.addDays(-settlementPeriod); } shList[BuysOfOwned] = file->balance(account.id(), newEndingDate); // get how many shares there are at the end of period MyMoneyMoney stashedBuysOfOwned = shList.at(BuysOfOwned); bool reportedDateRange = true; // flag marking sell transactions between startingDate and endingDate report.setReportAllSplits(false); report.setConsiderCategory(true); report.clearAccountFilter(); report.addAccount(account.id()); report.setDateFilter(newStartingDate, newEndingDate); do { QList transactions = file->transactionList(report); for (QList::const_reverse_iterator it_t = transactions.crbegin(); it_t != transactions.crend(); ++it_t) { MyMoneySplit shareSplit = (*it_t).splitByAccount(account.id()); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity security; MyMoneySecurity currency; eMyMoney::Split::InvestmentTransactionType transactionType; KMyMoneyUtils::dissectTransaction((*it_t), shareSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); QDate postDate = (*it_t).postDate(); MyMoneyMoney price; //get price for the day of the transaction if we have to calculate base currency //we are using the value of the split which is in deep currency if (m_config.isConvertCurrency()) price = account.baseCurrencyPrice(postDate); //we only need base currency because the value is in deep currency else price = MyMoneyMoney::ONE; MyMoneyMoney value = assetAccountSplit.value() * price; MyMoneyMoney shares = shareSplit.shares(); if (transactionType == eMyMoney::Split::InvestmentTransactionType::BuyShares) { if (reportedDateRange) { cfList[Buys].append(CashFlowListItem(postDate, value)); shList[Buys] += shares; } if (shList.at(BuysOfOwned).isZero()) { // add sold shares if (shList.at(BuysOfSells) + shares > shList.at(Sells).abs()) { // add partially sold shares MyMoneyMoney tempVal = (((shList.at(Sells).abs() - shList.at(BuysOfSells))) / shares) * value; cfList[BuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[BuysOfSells] = shList.at(Sells).abs(); if (isSTLT && postDate < termSeparator) { cfList[LongTermBuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[LongTermBuysOfSells] = shList.at(BuysOfSells); } } else { // add wholly sold shares cfList[BuysOfSells].append(CashFlowListItem(postDate, value)); shList[BuysOfSells] += shares; if (isSTLT && postDate < termSeparator) { cfList[LongTermBuysOfSells].append(CashFlowListItem(postDate, value)); shList[LongTermBuysOfSells] += shares; } } } else if (shList.at(BuysOfOwned) >= shares) { // subtract not-sold shares shList[BuysOfOwned] -= shares; cfList[BuysOfOwned].append(CashFlowListItem(postDate, value)); } else { // subtract partially not-sold shares MyMoneyMoney tempVal = ((shares - shList.at(BuysOfOwned)) / shares) * value; MyMoneyMoney tempVal2 = (shares - shList.at(BuysOfOwned)); cfList[BuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[BuysOfSells] += tempVal2; if (isSTLT && postDate < termSeparator) { cfList[LongTermBuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[LongTermBuysOfSells] += tempVal2; } cfList[BuysOfOwned].append(CashFlowListItem(postDate, (shList.at(BuysOfOwned) / shares) * value)); shList[BuysOfOwned] = MyMoneyMoney(); } } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::SellShares && reportedDateRange) { cfList[Sells].append(CashFlowListItem(postDate, value)); shList[Sells] += shares; } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::SplitShares) { // shares variable is denominator of split ratio here for (int i = Buys; i <= InvestmentValue::BuysOfOwned; ++i) shList[i] /= shares; } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::AddShares || // added shares, when sold give 100% capital gain transactionType == eMyMoney::Split::InvestmentTransactionType::ReinvestDividend) { if (shList.at(BuysOfOwned).isZero()) { // add added/reinvested shares if (shList.at(BuysOfSells) + shares > shList.at(Sells).abs()) { // add partially added/reinvested shares shList[BuysOfSells] = shList.at(Sells).abs(); if (postDate < termSeparator) shList[LongTermBuysOfSells] = shList[BuysOfSells]; } else { // add wholly added/reinvested shares shList[BuysOfSells] += shares; if (postDate < termSeparator) shList[LongTermBuysOfSells] += shares; } } else if (shList.at(BuysOfOwned) >= shares) { // subtract not-added/not-reinvested shares shList[BuysOfOwned] -= shares; cfList[BuysOfOwned].append(CashFlowListItem(postDate, value)); } else { // subtract partially not-added/not-reinvested shares MyMoneyMoney tempVal = (shares - shList.at(BuysOfOwned)); shList[BuysOfSells] += tempVal; if (postDate < termSeparator) shList[LongTermBuysOfSells] += tempVal; cfList[BuysOfOwned].append(CashFlowListItem(postDate, (shList.at(BuysOfOwned) / shares) * value)); shList[BuysOfOwned] = MyMoneyMoney(); } if (transactionType == eMyMoney::Split::InvestmentTransactionType::ReinvestDividend) { value = MyMoneyMoney(); foreach (const auto split, interestSplits) value += split.value(); value *= price; cfList[ReinvestIncome].append(CashFlowListItem(postDate, -value)); } } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::RemoveShares && reportedDateRange) // removed shares give no value in return so no capital gain on them shList[Sells] += shares; else if (transactionType == eMyMoney::Split::InvestmentTransactionType::Dividend || transactionType == eMyMoney::Split::InvestmentTransactionType::Yield) cfList[CashIncome].append(CashFlowListItem(postDate, value)); } reportedDateRange = false; newEndingDate = newStartingDate; newStartingDate = newStartingDate.addYears(-1); report.setDateFilter(newStartingDate, newEndingDate); // search for matching buy transactions year earlier } while ( ( (report.investmentSum() == eMyMoney::Report::InvestmentSum::Owned && !shList[BuysOfOwned].isZero()) || (report.investmentSum() == eMyMoney::Report::InvestmentSum::Sold && !shList.at(Sells).isZero() && shList.at(Sells).abs() > shList.at(BuysOfSells).abs()) || (report.investmentSum() == eMyMoney::Report::InvestmentSum::OwnedAndSold && (!shList[BuysOfOwned].isZero() || (!shList.at(Sells).isZero() && shList.at(Sells).abs() > shList.at(BuysOfSells).abs()))) ) && account.openingDate() <= newEndingDate ); // we've got buy value and no sell value of long-term shares, so get them if (isSTLT && !shList[LongTermBuysOfSells].isZero()) { newStartingDate = startingDate; newEndingDate = endingDate.addDays(-settlementPeriod); report.setDateFilter(newStartingDate, newEndingDate); // search for matching buy transactions year earlier QList transactions = file->transactionList(report); shList[BuysOfOwned] = shList[LongTermBuysOfSells]; foreach (const auto transaction, transactions) { MyMoneySplit shareSplit = transaction.splitByAccount(account.id()); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity security; MyMoneySecurity currency; eMyMoney::Split::InvestmentTransactionType transactionType; KMyMoneyUtils::dissectTransaction(transaction, shareSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); QDate postDate = transaction.postDate(); MyMoneyMoney price; if (m_config.isConvertCurrency()) price = account.baseCurrencyPrice(postDate); //we only need base currency because the value is in deep currency else price = MyMoneyMoney::ONE; MyMoneyMoney value = assetAccountSplit.value() * price; MyMoneyMoney shares = shareSplit.shares(); if (transactionType == eMyMoney::Split::InvestmentTransactionType::SellShares) { if ((shList.at(LongTermSellsOfBuys) + shares).abs() >= shList.at(LongTermBuysOfSells)) { // add partially sold long-term shares cfList[LongTermSellsOfBuys].append(CashFlowListItem(postDate, (shList.at(LongTermSellsOfBuys).abs() - shList.at(LongTermBuysOfSells)) / shares * value)); shList[LongTermSellsOfBuys] = shList.at(LongTermBuysOfSells); break; } else { // add wholly sold long-term shares cfList[LongTermSellsOfBuys].append(CashFlowListItem(postDate, value)); shList[LongTermSellsOfBuys] += shares; } } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::RemoveShares) { if ((shList.at(LongTermSellsOfBuys) + shares).abs() >= shList.at(LongTermBuysOfSells)) { shList[LongTermSellsOfBuys] = shList.at(LongTermBuysOfSells); break; } else shList[LongTermSellsOfBuys] += shares; } } } shList[BuysOfOwned] = stashedBuysOfOwned; report.setDateFilter(startingDate, endingDate); // reset data filter for next security return; } void QueryTable::constructPerformanceRow(const ReportAccount& account, TableRow& result, CashFlowList &all) const { MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; report.validDateRange(startingDate, endingDate); startingDate = startingDate.addDays(-1); MyMoneyFile* file = MyMoneyFile::instance(); //get fraction depending on type of account int fraction = account.currency().smallestAccountFraction(); MyMoneyMoney price; if (m_config.isConvertCurrency()) price = account.deepCurrencyPrice(startingDate) * account.baseCurrencyPrice(startingDate); else price = account.deepCurrencyPrice(startingDate); MyMoneyMoney startingBal = file->balance(account.id(), startingDate) * price; //convert to lowest fraction startingBal = startingBal.convert(fraction); //calculate ending balance if (m_config.isConvertCurrency()) price = account.deepCurrencyPrice(endingDate) * account.baseCurrencyPrice(endingDate); else price = account.deepCurrencyPrice(endingDate); MyMoneyMoney endingBal = file->balance((account).id(), endingDate) * price; //convert to lowest fraction endingBal = endingBal.convert(fraction); QList cfList; QList shList; sumInvestmentValues(account, cfList, shList); MyMoneyMoney buysTotal; MyMoneyMoney sellsTotal; MyMoneyMoney cashIncomeTotal; MyMoneyMoney reinvestIncomeTotal; switch (m_config.investmentSum()) { case eMyMoney::Report::InvestmentSum::OwnedAndSold: buysTotal = cfList.at(BuysOfSells).total() + cfList.at(BuysOfOwned).total(); sellsTotal = cfList.at(Sells).total(); cashIncomeTotal = cfList.at(CashIncome).total(); reinvestIncomeTotal = cfList.at(ReinvestIncome).total(); startingBal = MyMoneyMoney(); if (buysTotal.isZero() && sellsTotal.isZero() && cashIncomeTotal.isZero() && reinvestIncomeTotal.isZero()) return; all.append(cfList.at(BuysOfSells)); all.append(cfList.at(BuysOfOwned)); all.append(cfList.at(Sells)); all.append(cfList.at(CashIncome)); result[ctSells] = sellsTotal.toString(); result[ctCashIncome] = cashIncomeTotal.toString(); result[ctReinvestIncome] = reinvestIncomeTotal.toString(); result[ctEndingBalance] = endingBal.toString(); break; case eMyMoney::Report::InvestmentSum::Owned: buysTotal = cfList.at(BuysOfOwned).total(); startingBal = MyMoneyMoney(); if (buysTotal.isZero() && endingBal.isZero()) return; all.append(cfList.at(BuysOfOwned)); all.append(CashFlowListItem(endingDate, endingBal)); result[ctReinvestIncome] = reinvestIncomeTotal.toString(); result[ctMarketValue] = endingBal.toString(); break; case eMyMoney::Report::InvestmentSum::Sold: buysTotal = cfList.at(BuysOfSells).total(); sellsTotal = cfList.at(Sells).total(); cashIncomeTotal = cfList.at(CashIncome).total(); startingBal = endingBal = MyMoneyMoney(); // check if there are any meaningfull values before adding them to results if (buysTotal.isZero() && sellsTotal.isZero() && cashIncomeTotal.isZero()) return; all.append(cfList.at(BuysOfSells)); all.append(cfList.at(Sells)); all.append(cfList.at(CashIncome)); result[ctSells] = sellsTotal.toString(); result[ctCashIncome] = cashIncomeTotal.toString(); break; case eMyMoney::Report::InvestmentSum::Period: default: buysTotal = cfList.at(Buys).total(); sellsTotal = cfList.at(Sells).total(); cashIncomeTotal = cfList.at(CashIncome).total(); reinvestIncomeTotal = cfList.at(ReinvestIncome).total(); if (buysTotal.isZero() && sellsTotal.isZero() && cashIncomeTotal.isZero() && reinvestIncomeTotal.isZero() && startingBal.isZero() && endingBal.isZero()) return; all.append(cfList.at(Buys)); all.append(cfList.at(Sells)); all.append(cfList.at(CashIncome)); all.append(CashFlowListItem(startingDate, -startingBal)); all.append(CashFlowListItem(endingDate, endingBal)); result[ctSells] = sellsTotal.toString(); result[ctCashIncome] = cashIncomeTotal.toString(); result[ctReinvestIncome] = reinvestIncomeTotal.toString(); result[ctStartingBalance] = startingBal.toString(); result[ctEndingBalance] = endingBal.toString(); break; } result[ctBuys] = buysTotal.toString(); result[ctReturn] = helperIRR(all); result[ctReturnInvestment] = helperROI(buysTotal - reinvestIncomeTotal, sellsTotal, startingBal, endingBal, cashIncomeTotal); result[ctEquityType] = MyMoneySecurity::securityTypeToString(file->security(account.currencyId()).securityType()); } void QueryTable::constructCapitalGainRow(const ReportAccount& account, TableRow& result) const { MyMoneyFile* file = MyMoneyFile::instance(); QList cfList; QList shList; sumInvestmentValues(account, cfList, shList); MyMoneyMoney buysTotal = cfList.at(BuysOfSells).total(); MyMoneyMoney sellsTotal = cfList.at(Sells).total(); MyMoneyMoney longTermBuysOfSellsTotal = cfList.at(LongTermBuysOfSells).total(); MyMoneyMoney longTermSellsOfBuys = cfList.at(LongTermSellsOfBuys).total(); switch (m_config.investmentSum()) { case eMyMoney::Report::InvestmentSum::Owned: { if (shList.at(BuysOfOwned).isZero()) return; MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; report.validDateRange(startingDate, endingDate); //get fraction depending on type of account int fraction = account.currency().smallestAccountFraction(); MyMoneyMoney price; //calculate ending balance if (m_config.isConvertCurrency()) price = account.deepCurrencyPrice(endingDate) * account.baseCurrencyPrice(endingDate); else price = account.deepCurrencyPrice(endingDate); MyMoneyMoney endingBal = shList.at(BuysOfOwned) * price; //convert to lowest fraction endingBal = endingBal.convert(fraction); buysTotal = cfList.at(BuysOfOwned).total() - cfList.at(ReinvestIncome).total(); int pricePrecision = file->security(account.currencyId()).pricePrecision(); result[ctBuys] = buysTotal.toString(); result[ctShares] = shList.at(BuysOfOwned).toString(); result[ctBuyPrice] = (buysTotal.abs() / shList.at(BuysOfOwned)).convertPrecision(pricePrecision).toString(); result[ctLastPrice] = price.toString(); result[ctMarketValue] = endingBal.toString(); result[ctCapitalGain] = (buysTotal + endingBal).toString(); result[ctPercentageGain] = buysTotal.isZero() ? QString() : ((buysTotal + endingBal)/buysTotal.abs()).toString(); break; } case eMyMoney::Report::InvestmentSum::Sold: default: buysTotal = cfList.at(BuysOfSells).total() - cfList.at(ReinvestIncome).total(); sellsTotal = cfList.at(Sells).total(); longTermBuysOfSellsTotal = cfList.at(LongTermBuysOfSells).total(); longTermSellsOfBuys = cfList.at(LongTermSellsOfBuys).total(); // check if there are any meaningfull values before adding them to results if (buysTotal.isZero() && sellsTotal.isZero() && longTermBuysOfSellsTotal.isZero() && longTermSellsOfBuys.isZero()) return; result[ctBuys] = buysTotal.toString(); result[ctSells] = sellsTotal.toString(); result[ctCapitalGain] = (buysTotal + sellsTotal).toString(); if (m_config.isShowingSTLTCapitalGains()) { result[ctBuysLT] = longTermBuysOfSellsTotal.toString(); result[ctSellsLT] = longTermSellsOfBuys.toString(); result[ctCapitalGainLT] = (longTermBuysOfSellsTotal + longTermSellsOfBuys).toString(); result[ctBuysST] = (buysTotal - longTermBuysOfSellsTotal).toString(); result[ctSellsST] = (sellsTotal - longTermSellsOfBuys).toString(); result[ctCapitalGainST] = ((buysTotal - longTermBuysOfSellsTotal) + (sellsTotal - longTermSellsOfBuys)).toString(); } break; } result[ctEquityType] = MyMoneySecurity::securityTypeToString(file->security(account.currencyId()).securityType()); } void QueryTable::constructAccountTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); QMap> currencyCashFlow; // for total calculation QList accounts; file->accountList(accounts); for (auto it_account = accounts.constBegin(); it_account != accounts.constEnd(); ++it_account) { // Note, "Investment" accounts are never included in account rows because // they don't contain anything by themselves. In reports, they are only // useful as a "topaccount" aggregator of stock accounts if ((*it_account).isAssetLiability() && m_config.includes((*it_account)) && (*it_account).accountType() != eMyMoney::Account::Type::Investment) { // don't add the account if it is closed. In fact, the business logic // should prevent that an account can be closed with a balance not equal // to zero, but we never know. MyMoneyMoney shares = file->balance((*it_account).id(), m_config.toDate()); if (shares.isZero() && (*it_account).isClosed()) continue; ReportAccount account(*it_account); TableRow qaccountrow; CashFlowList accountCashflow; // for total calculation switch(m_config.queryColumns()) { case eMyMoney::Report::QueryColumn::Performance: { constructPerformanceRow(account, qaccountrow, accountCashflow); if (!qaccountrow.isEmpty()) { // assuming that that report is grouped by topaccount qaccountrow[ctTopAccount] = account.topParentName(); if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qaccountrow[ctCurrency] = file->baseCurrency().id(); else qaccountrow[ctCurrency] = account.currency().id(); if (!currencyCashFlow.value(qaccountrow.value(ctCurrency)).contains(qaccountrow.value(ctTopAccount))) currencyCashFlow[qaccountrow.value(ctCurrency)].insert(qaccountrow.value(ctTopAccount), accountCashflow); // create cashflow for unknown account... else currencyCashFlow[qaccountrow.value(ctCurrency)][qaccountrow.value(ctTopAccount)] += accountCashflow; // ...or add cashflow for known account } break; } case eMyMoney::Report::QueryColumn::CapitalGain: constructCapitalGainRow(account, qaccountrow); break; default: { //get fraction for account int fraction = account.currency().smallestAccountFraction() != -1 ? account.currency().smallestAccountFraction() : file->baseCurrency().smallestAccountFraction(); MyMoneyMoney netprice = account.deepCurrencyPrice(m_config.toDate()); if (m_config.isConvertCurrency() && account.isForeignCurrency()) netprice *= account.baseCurrencyPrice(m_config.toDate()); // display currency is base currency, so set the price netprice = netprice.reduce(); shares = shares.reduce(); int pricePrecision = file->security(account.currencyId()).pricePrecision(); qaccountrow[ctPrice] = netprice.convertPrecision(pricePrecision).toString(); qaccountrow[ctValue] = (netprice * shares).convert(fraction).toString(); qaccountrow[ctShares] = shares.toString(); QString iid = account.institutionId(); // If an account does not have an institution, get it from the top-parent. if (iid.isEmpty() && !account.isTopLevel()) iid = account.topParent().institutionId(); if (iid.isEmpty()) qaccountrow[ctInstitution] = i18nc("No institution", "None"); else qaccountrow[ctInstitution] = file->institution(iid).name(); qaccountrow[ctType] = MyMoneyAccount::accountTypeToString(account.accountType()); } } if (qaccountrow.isEmpty()) // don't add the account if there are no calculated values continue; qaccountrow[ctRank] = QLatin1Char('1'); qaccountrow[ctAccount] = account.name(); qaccountrow[ctAccountID] = account.id(); qaccountrow[ctTopAccount] = account.topParentName(); if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qaccountrow[ctCurrency] = file->baseCurrency().id(); else qaccountrow[ctCurrency] = account.currency().id(); m_rows.append(qaccountrow); } } if (m_config.queryColumns() == eMyMoney::Report::QueryColumn::Performance && m_config.isShowingColumnTotals()) { TableRow qtotalsrow; qtotalsrow[ctRank] = QLatin1Char('4'); // add identification of row as total QMap currencyGrandCashFlow; QMap>::iterator currencyAccGrp = currencyCashFlow.begin(); while (currencyAccGrp != currencyCashFlow.end()) { // convert map of top accounts with cashflows to TableRow for (QMap::iterator topAccount = (*currencyAccGrp).begin(); topAccount != (*currencyAccGrp).end(); ++topAccount) { qtotalsrow[ctTopAccount] = topAccount.key(); qtotalsrow[ctReturn] = helperIRR(topAccount.value()); qtotalsrow[ctCurrency] = currencyAccGrp.key(); currencyGrandCashFlow[currencyAccGrp.key()] += topAccount.value(); // cumulative sum of cashflows of each topaccount m_rows.append(qtotalsrow); // rows aren't sorted yet, so no problem with adding them randomly at the end } ++currencyAccGrp; } QMap::iterator currencyGrp = currencyGrandCashFlow.begin(); qtotalsrow[ctTopAccount].clear(); // empty topaccount because it's grand cashflow while (currencyGrp != currencyGrandCashFlow.end()) { qtotalsrow[ctReturn] = helperIRR(currencyGrp.value()); qtotalsrow[ctCurrency] = currencyGrp.key(); m_rows.append(qtotalsrow); ++currencyGrp; } } } void QueryTable::constructSplitsTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); MyMoneyReport report(m_config); report.setReportAllSplits(false); report.setConsiderCategory(true); // support for opening and closing balances QMap accts; //get all transactions for this report QList transactions = file->transactionList(report); for (QList::const_iterator it_transaction = transactions.constBegin(); it_transaction != transactions.constEnd(); ++it_transaction) { TableRow qA, qS; QDate pd; qA[ctID] = qS[ctID] = (* it_transaction).id(); qA[ctEntryDate] = qS[ctEntryDate] = (* it_transaction).entryDate().toString(Qt::ISODate); qA[ctPostDate] = qS[ctPostDate] = (* it_transaction).postDate().toString(Qt::ISODate); qA[ctCommodity] = qS[ctCommodity] = (* it_transaction).commodity(); pd = (* it_transaction).postDate(); qA[ctMonth] = qS[ctMonth] = i18n("Month of %1", QDate(pd.year(), pd.month(), 1).toString(Qt::ISODate)); qA[ctWeek] = qS[ctWeek] = i18n("Week of %1", pd.addDays(1 - pd.dayOfWeek()).toString(Qt::ISODate)); if (!m_containsNonBaseCurrency && (*it_transaction).commodity() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (report.isConvertCurrency()) qA[ctCurrency] = qS[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = qS[ctCurrency] = (*it_transaction).commodity(); // to handle splits, we decide on which account to base the split // (a reference point or point of view so to speak). here we take the // first account that is a stock account or loan account (or the first account // that is not an income or expense account if there is no stock or loan account) // to be the account (qA) that will have the sub-item "split" entries. we add // one transaction entry (qS) for each subsequent entry in the split. const QList& splits = (*it_transaction).splits(); QList::const_iterator myBegin, it_split; //S_end = splits.end(); for (it_split = splits.constBegin(), myBegin = splits.constEnd(); it_split != splits.constEnd(); ++it_split) { ReportAccount splitAcc((* it_split).accountId()); // always put split with a "stock" account if it exists if (splitAcc.isInvest()) break; // prefer to put splits with a "loan" account if it exists if (splitAcc.isLoan()) myBegin = it_split; if ((myBegin == splits.end()) && ! splitAcc.isIncomeExpense()) { myBegin = it_split; } } // select our "reference" split if (it_split == splits.end()) { it_split = myBegin; } else { myBegin = it_split; } // if the split is still unknown, use the first one. I have seen this // happen with a transaction that has only a single split referencing an income or expense // account and has an amount and value of 0. Such a transaction will fall through // the above logic and leave 'it_split' pointing to splits.end() which causes the remainder // of this to end in an infinite loop. if (it_split == splits.end()) { it_split = splits.begin(); } // for "loan" reports, the loan transaction gets special treatment. // the splits of a loan transaction are placed on one line in the // reference (loan) account (qA). however, we process the matching // split entries (qS) normally. bool loan_special_case = false; if (m_config.queryColumns() & eMyMoney::Report::QueryColumn::Loan) { ReportAccount splitAcc((*it_split).accountId()); loan_special_case = splitAcc.isLoan(); } // There is a slight chance that at this point myBegin is still pointing to splits.end() if the // transaction only has income and expense splits (which should not happen). In that case, point // it to the first split if (myBegin == splits.end()) { myBegin = splits.begin(); } //the account of the beginning splits ReportAccount myBeginAcc((*myBegin).accountId()); bool include_me = true; QString a_fullname; QString a_memo; int pass = 1; do { MyMoneyMoney xr; ReportAccount splitAcc((* it_split).accountId()); //get fraction for account int fraction = splitAcc.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = splitAcc.institutionId(); QString payee = (*it_split).payeeId(); const QList tagIdList = (*it_split).tagIdList(); if (m_config.isConvertCurrency()) { xr = (splitAcc.deepCurrencyPrice((*it_transaction).postDate()) * splitAcc.baseCurrencyPrice((*it_transaction).postDate())).reduce(); } else { xr = splitAcc.deepCurrencyPrice((*it_transaction).postDate()).reduce(); } // reverse the sign of incomes and expenses to keep consistency in the way it is displayed in other reports if (splitAcc.isIncomeExpense()) { xr = -xr; } if (splitAcc.isInvest()) { // use the institution of the parent for stock accounts institution = splitAcc.parent().institutionId(); MyMoneyMoney shares = (*it_split).shares(); int pricePrecision = file->security(splitAcc.currencyId()).pricePrecision(); qA[ctAction] = (*it_split).action(); qA[ctShares] = shares.isZero() ? QString() : (*it_split).shares().toString(); qA[ctPrice] = shares.isZero() ? QString() : xr.convertPrecision(pricePrecision).toString(); if (((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) && (*it_split).shares().isNegative()) qA[ctAction] = "Sell"; qA[ctInvestAccount] = splitAcc.parent().name(); } include_me = m_config.includes(splitAcc); a_fullname = splitAcc.fullName(); a_memo = (*it_split).memo(); int pricePrecision = file->security(splitAcc.currencyId()).pricePrecision(); qA[ctPrice] = xr.convertPrecision(pricePrecision).toString(); qA[ctAccount] = splitAcc.name(); qA[ctAccountID] = splitAcc.id(); qA[ctTopAccount] = splitAcc.topParentName(); qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); //FIXME-ALEX Is this useless? Isn't constructSplitsTable called only for cashflow type report? QString delimiter; foreach(const auto tagId, tagIdList) { qA[ctTag] += delimiter + file->tag(tagId).name().simplified(); delimiter = QLatin1Char(','); } qA[ctPayee] = payee.isEmpty() ? i18n("[Empty Payee]") : file->payee(payee).name().simplified(); qA[ctReconcileDate] = (*it_split).reconcileDate().toString(Qt::ISODate); qA[ctReconcileFlag] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true); qA[ctNumber] = (*it_split).number(); qA[ctMemo] = a_memo; qS[ctReconcileDate] = qA[ctReconcileDate]; qS[ctReconcileFlag] = qA[ctReconcileFlag]; qS[ctNumber] = qA[ctNumber]; qS[ctTopCategory] = splitAcc.topParentName(); // only include the configured accounts if (include_me) { // add the "summarized" split transaction // this is the sub-total of the split detail // convert to lowest fraction qA[ctValue] = ((*it_split).shares() * xr).convert(fraction).toString(); qA[ctRank] = QLatin1Char('1'); //fill in account information if (! splitAcc.isIncomeExpense() && it_split != myBegin) { qA[ctAccount] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", myBeginAcc.fullName()) : i18n("Transfer from %1", myBeginAcc.fullName()); } else if (it_split == myBegin) { //handle the main split if ((splits.count() > 2)) { //if it is the main split and has multiple splits, note that qA[ctAccount] = i18n("[Split Transaction]"); } else { //fill the account name of the second split QList::const_iterator tempSplit = splits.constBegin(); //there are supposed to be only 2 splits if we ever get here if (tempSplit == myBegin && splits.count() > 1) ++tempSplit; //show the name of the category, or "transfer to/from" if it as an account ReportAccount tempSplitAcc((*tempSplit).accountId()); if (! tempSplitAcc.isIncomeExpense()) { qA[ctAccount] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", tempSplitAcc.fullName()) : i18n("Transfer from %1", tempSplitAcc.fullName()); } else { qA[ctAccount] = tempSplitAcc.fullName(); } } } else { //in any other case, fill in the account name of the main split qA[ctAccount] = myBeginAcc.fullName(); } //category data is always the one of the split qA [ctCategory] = splitAcc.fullName(); qA [ctTopCategory] = splitAcc.topParentName(); qA [ctCategoryType] = MyMoneyAccount::accountTypeToString(splitAcc.accountGroup()); m_rows += qA; // track accts that will need opening and closing balances accts.insert(splitAcc.id(), splitAcc); } ++it_split; // look for wrap-around if (it_split == splits.end()) it_split = splits.begin(); //check if there have been more passes than there are splits //this is to prevent infinite loops in cases of data inconsistency -- asoliverez ++pass; if (pass > splits.count()) break; } while (it_split != myBegin); if (loan_special_case) { m_rows += qA; } } // now run through our accts list and add opening and closing balances switch (m_config.rowType()) { case eMyMoney::Report::RowType::Account: case eMyMoney::Report::RowType::TopAccount: break; // case eMyMoney::Report::RowType::Category: // case MyMoneyReport::eTopCategory: // case MyMoneyReport::ePayee: // case MyMoneyReport::eMonth: // case MyMoneyReport::eWeek: default: return; } QDate startDate, endDate; report.validDateRange(startDate, endDate); QString strStartDate = startDate.toString(Qt::ISODate); QString strEndDate = endDate.toString(Qt::ISODate); startDate = startDate.addDays(-1); for (auto it_account = accts.constBegin(); it_account != accts.constEnd(); ++it_account) { TableRow qA; ReportAccount account((* it_account)); //get fraction for account int fraction = account.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = account.institutionId(); // use the institution of the parent for stock accounts if (account.isInvest()) institution = account.parent().institutionId(); MyMoneyMoney startBalance, endBalance, startPrice, endPrice; MyMoneyMoney startShares, endShares; //get price and convert currency if necessary if (m_config.isConvertCurrency()) { startPrice = (account.deepCurrencyPrice(startDate) * account.baseCurrencyPrice(startDate)).reduce(); endPrice = (account.deepCurrencyPrice(endDate) * account.baseCurrencyPrice(endDate)).reduce(); } else { startPrice = account.deepCurrencyPrice(startDate).reduce(); endPrice = account.deepCurrencyPrice(endDate).reduce(); } startShares = file->balance(account.id(), startDate); endShares = file->balance(account.id(), endDate); //get starting and ending balances startBalance = startShares * startPrice; endBalance = endShares * endPrice; //starting balance // don't show currency if we're converting or if it's not foreign if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qA[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = account.currency().id(); qA[ctAccountID] = account.id(); qA[ctAccount] = account.name(); qA[ctTopAccount] = account.topParentName(); qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA[ctRank] = QLatin1Char('0'); int pricePrecision = file->security(account.currencyId()).pricePrecision(); qA[ctPrice] = startPrice.convertPrecision(pricePrecision).toString(); if (account.isInvest()) { qA[ctShares] = startShares.toString(); } qA[ctPostDate] = strStartDate; qA[ctBalance] = startBalance.convert(fraction).toString(); qA[ctValue].clear(); qA[ctID] = QLatin1Char('A'); m_rows += qA; qA[ctRank] = QLatin1Char('3'); //ending balance qA[ctPrice] = endPrice.convertPrecision(pricePrecision).toString(); if (account.isInvest()) { qA[ctShares] = endShares.toString(); } qA[ctPostDate] = strEndDate; qA[ctBalance] = endBalance.toString(); qA[ctID] = QLatin1Char('Z'); m_rows += qA; } } } diff --git a/kmymoney/plugins/views/reports/kreportsview.cpp b/kmymoney/plugins/views/reports/kreportsview.cpp index 72a6294bf..21ae8e7a0 100644 --- a/kmymoney/plugins/views/reports/kreportsview.cpp +++ b/kmymoney/plugins/views/reports/kreportsview.cpp @@ -1,762 +1,761 @@ /*************************************************************************** kreportsview.cpp - description ------------------- begin : Sat Mar 27 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Ace Jones (C) 2017 Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kreportsview_p.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include #include #ifdef ENABLE_WEBENGINE #include #else #include #endif // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_reportcontrol.h" #include "mymoneyfile.h" #include "mymoneyreport.h" #include "mymoneyexception.h" #include "kmymoneysettings.h" #include "querytable.h" #include "objectinfotable.h" #include "kreportconfigurationfilterdlg.h" #include "icons/icons.h" #include "kbalancechartdlg.h" #include #include "tocitem.h" #include "tocitemgroup.h" #include "tocitemreport.h" #include "kreportchartview.h" #include "pivottable.h" #include "reporttable.h" #include "reportcontrolimpl.h" #include "mymoneyenums.h" #include "menuenums.h" using namespace reports; using namespace eMyMoney; using namespace Icons; #define VIEW_LEDGER "ledger" #define VIEW_SCHEDULE "schedule" #define VIEW_WELCOME "welcome" #define VIEW_HOME "home" #define VIEW_REPORTS "reports" /** * KReportsView Implementation */ KReportsView::KReportsView(QWidget *parent) : KMyMoneyViewBase(*new KReportsViewPrivate(this), parent) { connect(pActions[eMenu::Action::ReportAccountTransactions], &QAction::triggered, this, &KReportsView::slotReportAccountTransactions); } KReportsView::~KReportsView() { } void KReportsView::executeCustomAction(eView::Action action) { switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::SetDefaultFocus: { Q_D(KReportsView); QTimer::singleShot(0, d->m_tocTreeWidget, SLOT(setFocus())); } break; case eView::Action::Print: slotPrintView(); break; case eView::Action::CleanupBeforeFileClose: slotCloseAll(); break; case eView::Action::ShowBalanceChart: { Q_D(KReportsView); QPointer dlg = new KBalanceChartDlg(d->m_currentAccount, this); dlg->exec(); delete dlg; } break; default: break; } } void KReportsView::refresh() { Q_D(KReportsView); if (isVisible()) { d->loadView(); d->m_needsRefresh = false; } else { d->m_needsRefresh = true; } } void KReportsView::showEvent(QShowEvent * event) { if (MyMoneyFile::instance()->storageAttached()) { Q_D(KReportsView); if (d->m_needLoad) d->init(); emit customActionRequested(View::Reports, eView::Action::AboutToShow); if (d->m_needsRefresh) refresh(); if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) emit reportSelected(tab->report()); else emit reportSelected(MyMoneyReport()); } // don't forget base class implementation QWidget::showEvent(event); } void KReportsView::updateActions(const MyMoneyObject& obj) { Q_D(KReportsView); if (typeid(obj) != typeid(MyMoneyAccount) && (obj.id().isEmpty() && d->m_currentAccount.id().isEmpty())) // do not disable actions that were already disabled))) return; const auto& acc = static_cast(obj); bool b; if (MyMoneyFile::instance()->isStandardAccount(acc.id())) { b = false; } else { switch (acc.accountType()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Equity: case eMyMoney::Account::Type::Checkings: b = true; break; default: b = false; break; } } pActions[eMenu::Action::ReportAccountTransactions]->setEnabled(b); d->m_currentAccount = acc; } void KReportsView::slotOpenUrl(const QUrl &url) { QString view = url.fileName(); if (view.isEmpty()) return; QString command = QUrlQuery(url).queryItemValue("command"); QString id = QUrlQuery(url).queryItemValue("id"); QString tid = QUrlQuery(url).queryItemValue("tid"); if (view == VIEW_REPORTS) { if (command.isEmpty()) { // slotRefreshView(); } else if (command == QLatin1String("print")) slotPrintView(); else if (command == QLatin1String("copy")) slotCopyView(); else if (command == QLatin1String("save")) slotSaveView(); else if (command == QLatin1String("configure")) slotConfigure(); else if (command == QLatin1String("duplicate")) slotDuplicate(); else if (command == QLatin1String("close")) slotCloseCurrent(); else if (command == QLatin1String("delete")) slotDelete(); else qWarning() << i18n("Unknown command '%1' in KReportsView::slotOpenUrl()", qPrintable(command)); } else if (view == VIEW_LEDGER) { emit selectByVariant(QVariantList {QVariant(id), QVariant(tid)}, eView::Intent::ShowTransaction); } else { qWarning() << i18n("Unknown view '%1' in KReportsView::slotOpenUrl()", qPrintable(view)); } } void KReportsView::slotPrintView() { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) tab->print(); } void KReportsView::slotCopyView() { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) tab->copyToClipboard(); } void KReportsView::slotSaveView() { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) { QString filterList = i18nc("CSV (Filefilter)", "CSV files") + QLatin1String(" (*.csv);;") + i18nc("HTML (Filefilter)", "HTML files") + QLatin1String(" (*.html)"); QUrl newURL = QFileDialog::getSaveFileUrl(this, i18n("Export as"), QUrl::fromLocalFile(KRecentDirs::dir(":kmymoney-export")), filterList, &d->m_selectedExportFilter); if (!newURL.isEmpty()) { KRecentDirs::add(":kmymoney-export", newURL.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path()); QString newName = newURL.toDisplayString(QUrl::PreferLocalFile); try { tab->saveAs(newName, true); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Failed to save: %1", QString::fromLatin1(e.what()))); } } } } void KReportsView::slotConfigure() { Q_D(KReportsView); QString cm = "KReportsView::slotConfigure"; auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget()); if (!tab) // nothing to do return; int tabNr = d->m_reportTabWidget->currentIndex(); tab->updateDataRange(); // range will be needed during configuration, but cannot be obtained earlier MyMoneyReport report = tab->report(); if (report.comment() == i18n("Default Report") || report.comment() == i18n("Generated Report")) { report.setComment(i18n("Custom Report")); report.setName(i18n("%1 (Customized)", report.name())); } QPointer dlg = new KReportConfigurationFilterDlg(report); if (dlg->exec()) { MyMoneyReport newreport = dlg->getConfig(); // If this report has an ID, then MODIFY it, otherwise ADD it MyMoneyFileTransaction ft; try { if (! newreport.id().isEmpty()) { MyMoneyFile::instance()->modifyReport(newreport); ft.commit(); tab->modifyReport(newreport); d->m_reportTabWidget->setTabText(tabNr, newreport.name()); d->m_reportTabWidget->setCurrentIndex(tabNr) ; } else { MyMoneyFile::instance()->addReport(newreport); ft.commit(); QString reportGroupName = newreport.group(); // find report group TocItemGroup* tocItemGroup = d->m_allTocItemGroups[reportGroupName]; if (!tocItemGroup) { QString error = i18n("Could not find reportgroup \"%1\" for report \"%2\".\nPlease report this error to the developer's list: kmymoney-devel@kde.org", reportGroupName, newreport.name()); // write to messagehandler qWarning() << cm << error; // also inform user KMessageBox::error(d->m_reportTabWidget, error, i18n("Critical Error")); // cleanup delete dlg; return; } // do not add TocItemReport to TocItemGroup here, // this is done in loadView d->addReportTab(newreport); } } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Failed to configure report: %1", QString::fromLatin1(e.what()))); } } delete dlg; } void KReportsView::slotDuplicate() { Q_D(KReportsView); QString cm = "KReportsView::slotDuplicate"; auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget()); if (!tab) { // nothing to do return; } MyMoneyReport dupe = tab->report(); dupe.setName(i18n("Copy of %1", dupe.name())); if (dupe.comment() == i18n("Default Report")) dupe.setComment(i18n("Custom Report")); dupe.clearId(); QPointer dlg = new KReportConfigurationFilterDlg(dupe); if (dlg->exec()) { MyMoneyReport newReport = dlg->getConfig(); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->addReport(newReport); ft.commit(); QString reportGroupName = newReport.group(); // find report group TocItemGroup* tocItemGroup = d->m_allTocItemGroups[reportGroupName]; if (!tocItemGroup) { QString error = i18n("Could not find reportgroup \"%1\" for report \"%2\".\nPlease report this error to the developer's list: kmymoney-devel@kde.org", reportGroupName, newReport.name()); // write to messagehandler qWarning() << cm << error; // also inform user KMessageBox::error(d->m_reportTabWidget, error, i18n("Critical Error")); // cleanup delete dlg; return; } // do not add TocItemReport to TocItemGroup here, // this is done in loadView d->addReportTab(newReport); } catch (const MyMoneyException &e) { QString error = i18n("Cannot add report, reason: \"%1\"", e.what()); // write to messagehandler qWarning() << cm << error; // also inform user KMessageBox::error(d->m_reportTabWidget, error, i18n("Critical Error")); } } delete dlg; } void KReportsView::slotDelete() { Q_D(KReportsView); auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget()); if (!tab) { // nothing to do return; } MyMoneyReport report = tab->report(); if (! report.id().isEmpty()) { if (KMessageBox::Continue == d->deleteReportDialog(report.name())) { // close the tab and then remove the report so that it is not // generated again during the following loadView() call slotClose(d->m_reportTabWidget->currentIndex()); MyMoneyFileTransaction ft; MyMoneyFile::instance()->removeReport(report); ft.commit(); } } else { KMessageBox::information(this, QString("") + i18n("%1 is a default report, so it cannot be deleted.", report.name()) + QString(""), i18n("Delete Report?")); } } void KReportsView::slotOpenReport(const QString& id) { Q_D(KReportsView); if (id.isEmpty()) { // nothing to do return; } KReportTab* page = 0; // Find the tab which contains the report int index = 1; while (index < d->m_reportTabWidget->count()) { auto current = dynamic_cast(d->m_reportTabWidget->widget(index)); if (current && current->report().id() == id) { page = current; break; } ++index; } // Show the tab, or create a new one, as needed if (page) d->m_reportTabWidget->setCurrentIndex(index); else d->addReportTab(MyMoneyFile::instance()->report(id)); } void KReportsView::slotOpenReport(const MyMoneyReport& report) { Q_D(KReportsView); if (d->m_needLoad) d->init(); qDebug() << Q_FUNC_INFO << " " << report.name(); KReportTab* page = 0; // Find the tab which contains the report indicated by this list item int index = 1; while (index < d->m_reportTabWidget->count()) { auto current = dynamic_cast(d->m_reportTabWidget->widget(index)); if (current && current->report().name() == report.name()) { page = current; break; } ++index; } // Show the tab, or create a new one, as needed if (page) d->m_reportTabWidget->setCurrentIndex(index); else d->addReportTab(report); if (!isVisible()) emit switchViewRequested(View::Reports); } void KReportsView::slotItemDoubleClicked(QTreeWidgetItem* item, int) { Q_D(KReportsView); auto tocItem = dynamic_cast(item); if (tocItem && !tocItem->isReport()) { // toggle the expanded-state for reportgroup-items item->setExpanded(item->isExpanded() ? false : true); // nothing else to do for reportgroup-items return; } TocItemReport* reportTocItem = dynamic_cast(tocItem); MyMoneyReport& report = reportTocItem->getReport(); KReportTab* page = 0; // Find the tab which contains the report indicated by this list item int index = 1; while (index < d->m_reportTabWidget->count()) { auto current = dynamic_cast(d->m_reportTabWidget->widget(index)); if (current) { // If this report has an ID, we'll use the ID to match if (! report.id().isEmpty()) { if (current->report().id() == report.id()) { page = current; break; } } // Otherwise, use the name to match. THIS ASSUMES that no 2 default reports // have the same name...but that would be pretty a boneheaded thing to do. else { if (current->report().name() == report.name()) { page = current; break; } } } ++index; } // Show the tab, or create a new one, as needed if (page) d->m_reportTabWidget->setCurrentIndex(index); else d->addReportTab(report); } void KReportsView::slotToggleChart() { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) tab->toggleChart(); } void KReportsView::slotCloseCurrent() { Q_D(KReportsView); slotClose(d->m_reportTabWidget->currentIndex()); } void KReportsView::slotClose(int index) { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->widget(index))) { d->m_reportTabWidget->removeTab(index); tab->setReadyToDelete(true); } } void KReportsView::slotCloseAll() { Q_D(KReportsView); if(!d->m_needLoad) { while (true) { if (auto tab = dynamic_cast(d->m_reportTabWidget->widget(1))) { d->m_reportTabWidget->removeTab(1); tab->setReadyToDelete(true); } else break; } } } void KReportsView::slotListContextMenu(const QPoint & p) { Q_D(KReportsView); auto items = d->m_tocTreeWidget->selectedItems(); if (items.isEmpty()) { return; } QList tocItems; foreach(auto item, items) { auto tocItem = dynamic_cast(item); if (tocItem && tocItem->isReport()) { tocItems.append(tocItem); } } if (tocItems.isEmpty()) { return; } auto contextmenu = new QMenu(this); contextmenu->addAction(i18nc("To open a new report", "&Open"), this, SLOT(slotOpenFromList())); contextmenu->addAction(i18nc("To print a report", "&Print"), this, SLOT(slotPrintFromList())); if (tocItems.count() == 1) { contextmenu->addAction(i18nc("Configure a report", "&Configure"), this, SLOT(slotConfigureFromList())); contextmenu->addAction(i18n("&New report"), this, SLOT(slotNewFromList())); // Only add this option if it's a custom report. Default reports cannot be deleted auto reportTocItem = dynamic_cast(tocItems.at(0)); if (reportTocItem) { MyMoneyReport& report = reportTocItem->getReport(); if (! report.id().isEmpty()) { contextmenu->addAction(i18n("&Delete"), this, SLOT(slotDeleteFromList())); } } } contextmenu->popup(d->m_tocTreeWidget->mapToGlobal(p)); } void KReportsView::slotOpenFromList() { Q_D(KReportsView); auto items = d->m_tocTreeWidget->selectedItems(); if (items.isEmpty()) { return; } foreach(auto item, items) { auto tocItem = dynamic_cast(item); if (tocItem && tocItem->isReport()) { slotItemDoubleClicked(tocItem, 0); } } } void KReportsView::slotPrintFromList() { Q_D(KReportsView); auto items = d->m_tocTreeWidget->selectedItems(); if (items.isEmpty()) { return; } foreach(auto item, items) { auto tocItem = dynamic_cast(item); if (tocItem && tocItem->isReport()) { slotItemDoubleClicked(tocItem, 0); slotPrintView(); } } } void KReportsView::slotConfigureFromList() { Q_D(KReportsView); if (auto tocItem = dynamic_cast(d->m_tocTreeWidget->currentItem())) { slotItemDoubleClicked(tocItem, 0); slotConfigure(); } } void KReportsView::slotNewFromList() { Q_D(KReportsView); if (auto tocItem = dynamic_cast(d->m_tocTreeWidget->currentItem())) { slotItemDoubleClicked(tocItem, 0); slotDuplicate(); } } void KReportsView::slotDeleteFromList() { Q_D(KReportsView); if (auto tocItem = dynamic_cast(d->m_tocTreeWidget->currentItem())) { if (auto reportTocItem = dynamic_cast(tocItem)) { MyMoneyReport& report = reportTocItem->getReport(); // If this report does not have an ID, it's a default report and cannot be deleted if (! report.id().isEmpty() && KMessageBox::Continue == d->deleteReportDialog(report.name())) { // check if report's tab is open; start from 1 because 0 is toc tab for (int i = 1; i < d->m_reportTabWidget->count(); ++i) { auto tab = dynamic_cast(d->m_reportTabWidget->widget(i)); if (tab && tab->report().id() == report.id()) { slotClose(i); // if open, close it, so no crash when switching to it break; } } MyMoneyFileTransaction ft; MyMoneyFile::instance()->removeReport(report); ft.commit(); } } } } void KReportsView::slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent) { switch(intent) { case eView::Intent::UpdateActions: updateActions(obj); break; case eView::Intent::OpenObject: slotOpenReport(static_cast(obj)); default: break; } } void KReportsView::slotReportAccountTransactions() { Q_D(KReportsView); // Generate a transaction report that contains transactions for only the // currently selected account. if (!d->m_currentAccount.id().isEmpty()) { MyMoneyReport report( eMyMoney::Report::RowType::Account, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category, eMyMoney::TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("%1 YTD Account Transactions", d->m_currentAccount.name()), i18n("Generated Report") ); report.setGroup(i18n("Transactions")); report.addAccount(d->m_currentAccount.id()); emit customActionRequested(View::Reports, eView::Action::SwitchView); slotOpenReport(report); } } // Make sure, that these definitions are only used within this file // this does not seem to be necessary, but when building RPMs the // build option 'final' is used and all CPP files are concatenated. // So it could well be, that in another CPP file these definitions // are also used. #undef VIEW_LEDGER #undef VIEW_SCHEDULE #undef VIEW_WELCOME #undef VIEW_HOME #undef VIEW_REPORTS diff --git a/kmymoney/plugins/views/reports/kreportsview_p.h b/kmymoney/plugins/views/reports/kreportsview_p.h index e8282eaae..5ca6f47b9 100644 --- a/kmymoney/plugins/views/reports/kreportsview_p.h +++ b/kmymoney/plugins/views/reports/kreportsview_p.h @@ -1,1465 +1,1457 @@ /*************************************************************************** kreportsview_p.h - description ------------------- begin : Sat Mar 27 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Ace Jones (C) 2017 Łukasz Wojniłowicz 2018 Michael Kiefer ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KREPORTSVIEW_P_H #define KREPORTSVIEW_P_H #include "kreportsview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include #include #include -#include #ifdef ENABLE_WEBENGINE #include #else #include #endif // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_reportcontrol.h" #include "kmymoneyviewbase_p.h" #include "kreportconfigurationfilterdlg.h" #include "mymoneyfile.h" #include "mymoneyreport.h" #include "mymoneyexception.h" #include "kmymoneysettings.h" #include "querytable.h" #include "objectinfotable.h" #include "icons/icons.h" #include #include "tocitem.h" #include "tocitemgroup.h" #include "tocitemreport.h" #include "kreportchartview.h" #include "pivottable.h" #include "reporttable.h" #include "reportcontrolimpl.h" #include "mymoneyenums.h" +#include "kmm_printer.h" using namespace reports; using namespace eMyMoney; using namespace Icons; #define VIEW_LEDGER "ledger" #define VIEW_SCHEDULE "schedule" #define VIEW_WELCOME "welcome" #define VIEW_HOME "home" #define VIEW_REPORTS "reports" /** * Helper class for KReportView. * * This is the widget which displays a single report in the TabWidget that comprises this view. * * @author Ace Jones */ class KReportTab: public QWidget { private: #ifdef ENABLE_WEBENGINE QWebEngineView *m_tableView; #else KWebView *m_tableView; #endif reports::KReportChartView *m_chartView; ReportControl *m_control; QVBoxLayout *m_layout; - QPrinter *m_currentPrinter; MyMoneyReport m_report; bool m_deleteMe; bool m_chartEnabled; bool m_showingChart; bool m_needReload; bool m_isChartViewValid; bool m_isTableViewValid; QPointer m_table; /** * Users character set encoding. */ QByteArray m_encoding; public: KReportTab(QTabWidget* parent, const MyMoneyReport& report, const KReportsView *eventHandler); ~KReportTab(); const MyMoneyReport& report() const { return m_report; } void print(); void toggleChart(); /** * Updates information about plotted chart in report's data */ void updateDataRange(); void copyToClipboard(); void saveAs(const QString& filename, bool includeCSS = false); void updateReport(); QString createTable(const QString& links = QString()); const ReportControl* control() const { return m_control; } bool isReadyToDelete() const { return m_deleteMe; } void setReadyToDelete(bool f) { m_deleteMe = f; } void modifyReport(const MyMoneyReport& report) { m_report = report; } void showEvent(QShowEvent * event) final override; void loadTab(); }; /** * Helper class for KReportView. * * This is a named list of reports, which will be one section * in the list of default reports * * @author Ace Jones */ class ReportGroup: public QList { private: QString m_name; ///< the title of the group in non-translated form QString m_title; ///< the title of the group in i18n-ed form public: ReportGroup() {} ReportGroup(const QString& name, const QString& title): m_name(name), m_title(title) {} const QString& name() const { return m_name; } const QString& title() const { return m_title; } }; /** * KReportTab Implementation */ KReportTab::KReportTab(QTabWidget* parent, const MyMoneyReport& report, const KReportsView* eventHandler): QWidget(parent), #ifdef ENABLE_WEBENGINE m_tableView(new QWebEngineView(this)), #else m_tableView(new KWebView(this)), #endif m_chartView(new KReportChartView(this)), m_control(new ReportControl(this)), m_layout(new QVBoxLayout(this)), - m_currentPrinter(nullptr), m_report(report), m_deleteMe(false), m_chartEnabled(false), m_showingChart(report.isChartByDefault()), m_needReload(true), m_isChartViewValid(false), m_isTableViewValid(false), m_table(0) { m_layout->setSpacing(6); m_tableView->setPage(new MyQWebEnginePage(m_tableView)); m_tableView->setZoomFactor(KMyMoneySettings::zoomFactor()); //set button icons m_control->ui->buttonChart->setIcon(Icons::get(Icon::OfficeChartLine)); m_control->ui->buttonClose->setIcon(Icons::get(Icon::DocumentClose)); m_control->ui->buttonConfigure->setIcon(Icons::get(Icon::Configure)); m_control->ui->buttonCopy->setIcon(Icons::get(Icon::EditCopy)); m_control->ui->buttonDelete->setIcon(Icons::get(Icon::EditDelete)); m_control->ui->buttonExport->setIcon(Icons::get(Icon::DocumentExport)); m_control->ui->buttonNew->setIcon(Icons::get(Icon::DocumentNew)); m_chartView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_chartView->hide(); m_tableView->hide(); m_layout->addWidget(m_control); m_layout->addWidget(m_tableView); m_layout->addWidget(m_chartView); m_layout->setStretch(1, 10); m_layout->setStretch(2, 10); connect(m_control->ui->buttonChart, &QAbstractButton::clicked, eventHandler, &KReportsView::slotToggleChart); connect(m_control->ui->buttonConfigure, &QAbstractButton::clicked, eventHandler, &KReportsView::slotConfigure); connect(m_control->ui->buttonNew, &QAbstractButton::clicked, eventHandler, &KReportsView::slotDuplicate); connect(m_control->ui->buttonCopy, &QAbstractButton::clicked, eventHandler, &KReportsView::slotCopyView); connect(m_control->ui->buttonExport, &QAbstractButton::clicked, eventHandler, &KReportsView::slotSaveView); connect(m_control->ui->buttonDelete, &QAbstractButton::clicked, eventHandler, &KReportsView::slotDelete); connect(m_control->ui->buttonClose, &QAbstractButton::clicked, eventHandler, &KReportsView::slotCloseCurrent); #ifdef ENABLE_WEBENGINE connect(m_tableView->page(), &QWebEnginePage::urlChanged, eventHandler, &KReportsView::slotOpenUrl); #else m_tableView->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); connect(m_tableView->page(), &KWebPage::linkClicked, eventHandler, &KReportsView::slotOpenUrl); #endif // if this is a default report, then you can't delete it! if (report.id().isEmpty()) m_control->ui->buttonDelete->setEnabled(false); int tabNr = parent->addTab(this, Icons::get(Icon::Spreadsheet), report.name()); parent->setTabEnabled(tabNr, true); parent->setCurrentIndex(tabNr); // get users character set encoding m_encoding = QTextCodec::codecForLocale()->name(); } KReportTab::~KReportTab() { delete m_table; } void KReportTab::print() { if (m_tableView) { - m_currentPrinter = new QPrinter(); - QPointer dialog = new QPrintDialog(m_currentPrinter, this); - dialog->setWindowTitle(QString()); - if (dialog->exec() != QDialog::Accepted) { - delete m_currentPrinter; - m_currentPrinter = nullptr; - return; - } + auto printer = KMyMoneyPrinter::startPrint(); + if (printer != nullptr) { #ifdef ENABLE_WEBENGINE - m_tableView->page()->print(m_currentPrinter, [=] (bool) {delete m_currentPrinter; m_currentPrinter = nullptr;}); + m_tableView->page()->print(printer, [=] (bool) {}); #else - m_tableView->print(m_currentPrinter); + m_tableView->print(printer); #endif + } } } void KReportTab::copyToClipboard() { QMimeData* pMimeData = new QMimeData(); pMimeData->setHtml(m_table->renderReport(QLatin1String("html"), m_encoding, m_report.name(), true)); QApplication::clipboard()->setMimeData(pMimeData); } void KReportTab::saveAs(const QString& filename, bool includeCSS) { QFile file(filename); if (file.open(QIODevice::WriteOnly)) { if (QFileInfo(filename).suffix().toLower() == QLatin1String("csv")) { QTextStream(&file) << m_table->renderReport(QLatin1String("csv"), m_encoding, QString()); } else { QString table = m_table->renderReport(QLatin1String("html"), m_encoding, m_report.name(), includeCSS); QTextStream stream(&file); stream << table; } file.close(); } } void KReportTab::loadTab() { m_needReload = true; if (isVisible()) { m_needReload = false; updateReport(); } } void KReportTab::showEvent(QShowEvent * event) { if (m_needReload) { m_needReload = false; updateReport(); } QWidget::showEvent(event); } void KReportTab::updateReport() { m_isChartViewValid = false; m_isTableViewValid = false; // reload the report from the engine. It might have // been changed by the user try { // Don't try to reload default reports from the engine if (!m_report.id().isEmpty()) m_report = MyMoneyFile::instance()->report(m_report.id()); } catch (const MyMoneyException &) { } delete m_table; m_table = 0; if (m_report.reportType() == eMyMoney::Report::ReportType::PivotTable) { m_table = new PivotTable(m_report); m_chartEnabled = true; } else if (m_report.reportType() == eMyMoney::Report::ReportType::QueryTable) { m_table = new QueryTable(m_report); m_chartEnabled = false; } else if (m_report.reportType() == eMyMoney::Report::ReportType::InfoTable) { m_table = new ObjectInfoTable(m_report); m_chartEnabled = false; } m_control->ui->buttonChart->setEnabled(m_chartEnabled); m_showingChart = !m_showingChart; toggleChart(); } void KReportTab::toggleChart() { // for now it will just SHOW the chart. In the future it actually has to toggle it. if (m_showingChart) { if (!m_isTableViewValid) { m_tableView->setHtml(m_table->renderReport(QLatin1String("html"), m_encoding, m_report.name()), QUrl("file://")); // workaround for access permission to css file } m_isTableViewValid = true; m_tableView->show(); m_chartView->hide(); m_control->ui->buttonChart->setText(i18n("Chart")); m_control->ui->buttonChart->setToolTip(i18n("Show the chart version of this report")); m_control->ui->buttonChart->setIcon(Icons::get(Icon::OfficeChartLine)); } else { if (!m_isChartViewValid) m_table->drawChart(*m_chartView); m_isChartViewValid = true; m_tableView->hide(); m_chartView->show(); m_control->ui->buttonChart->setText(i18n("Report")); m_control->ui->buttonChart->setToolTip(i18n("Show the report version of this chart")); m_control->ui->buttonChart->setIcon(Icons::get(Icon::ViewFinancialList)); } m_showingChart = ! m_showingChart; } void KReportTab::updateDataRange() { QList grids = m_chartView->coordinatePlane()->gridDimensionsList(); // get dimensions of plotted graph if (grids.isEmpty()) return; QChar separator = locale().groupSeparator(); QChar decimalPoint = locale().decimalPoint(); int precision = m_report.yLabelsPrecision(); QList> dims; // create list of dimension values in string and qreal // get qreal values dims.append(qMakePair(QString(), grids.at(1).start)); dims.append(qMakePair(QString(), grids.at(1).end)); dims.append(qMakePair(QString(), grids.at(1).stepWidth)); dims.append(qMakePair(QString(), grids.at(1).subStepWidth)); // convert qreal values to string variables for (int i = 0; i < 4; ++i) { if (i > 2) ++precision; if (precision == 0) dims[i].first = locale().toString(qRound(dims.at(i).second)); else dims[i].first = locale().toString(dims.at(i).second, 'f', precision).remove(separator).remove(QRegularExpression("0+$")).remove(QRegularExpression("\\" + decimalPoint + "$")); } // save string variables in report's data m_report.setDataRangeStart(dims.at(0).first); m_report.setDataRangeEnd(dims.at(1).first); m_report.setDataMajorTick(dims.at(2).first); m_report.setDataMinorTick(dims.at(3).first); } class KReportsViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KReportsView) public: explicit KReportsViewPrivate(KReportsView *qq): q_ptr(qq), m_needLoad(true), m_reportListView(nullptr), m_reportTabWidget(nullptr), m_listTab(nullptr), m_listTabLayout(nullptr), m_tocTreeWidget(nullptr), m_columnsAlreadyAdjusted(false) { } ~KReportsViewPrivate() { } void init() { Q_Q(KReportsView); m_needLoad = false; auto vbox = new QVBoxLayout(q); q->setLayout(vbox); vbox->setSpacing(6); vbox->setMargin(0); // build reports toc setColumnsAlreadyAdjusted(false); m_reportTabWidget = new QTabWidget(q); vbox->addWidget(m_reportTabWidget); m_reportTabWidget->setTabsClosable(true); m_listTab = new QWidget(m_reportTabWidget); m_listTabLayout = new QVBoxLayout(m_listTab); m_listTabLayout->setSpacing(6); m_tocTreeWidget = new QTreeWidget(m_listTab); // report-group items have only 1 column (name of group), // report items have 2 columns (report name and comment) m_tocTreeWidget->setColumnCount(2); // headers QStringList headers; headers << i18n("Reports") << i18n("Comment"); m_tocTreeWidget->setHeaderLabels(headers); m_tocTreeWidget->setAlternatingRowColors(true); m_tocTreeWidget->setSortingEnabled(true); m_tocTreeWidget->sortByColumn(0, Qt::AscendingOrder); // for report group items: // doubleclick toggles the expand-state, m_tocTreeWidget->setExpandsOnDoubleClick(false); m_tocTreeWidget->setContextMenuPolicy(Qt::CustomContextMenu); m_tocTreeWidget->setSelectionMode(QAbstractItemView::ExtendedSelection); m_listTabLayout->addWidget(m_tocTreeWidget); m_reportTabWidget->addTab(m_listTab, i18n("Reports")); q->connect(m_reportTabWidget, &QTabWidget::tabCloseRequested, q, &KReportsView::slotClose); q->connect(m_tocTreeWidget, &QTreeWidget::itemDoubleClicked, q, &KReportsView::slotItemDoubleClicked); q->connect(m_tocTreeWidget, &QWidget::customContextMenuRequested, q, &KReportsView::slotListContextMenu); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KReportsView::refresh); } void restoreTocExpandState(QMap& expandStates) { for (auto i = 0; i < m_tocTreeWidget->topLevelItemCount(); ++i) { QTreeWidgetItem* item = m_tocTreeWidget->topLevelItem(i); if (item) { QString itemLabel = item->text(0); if (expandStates.contains(itemLabel)) { item->setExpanded(expandStates[itemLabel]); } else { item->setExpanded(false); } } } } /** * Display a dialog to confirm report deletion */ int deleteReportDialog(const QString &reportName) { Q_Q(KReportsView); return KMessageBox::warningContinueCancel(q, i18n("Are you sure you want to delete report %1? There is no way to recover it.", reportName), i18n("Delete Report?")); } void addReportTab(const MyMoneyReport& report) { Q_Q(KReportsView); new KReportTab(m_reportTabWidget, report, q); } void loadView() { // remember the id of the current selected item QTreeWidgetItem* item = m_tocTreeWidget->currentItem(); QString selectedItem = (item) ? item->text(0) : QString(); // save expand states of all top-level items QMap expandStates; for (int i = 0; i < m_tocTreeWidget->topLevelItemCount(); ++i) { item = m_tocTreeWidget->topLevelItem(i); if (item) { QString itemLabel = item->text(0); if (item->isExpanded()) { expandStates.insert(itemLabel, true); } else { expandStates.insert(itemLabel, false); } } } // find the item visible on top QTreeWidgetItem* visibleTopItem = m_tocTreeWidget->itemAt(0, 0); // text of column 0 identifies the item visible on top QString visibleTopItemText; bool visibleTopItemFound = true; if (visibleTopItem == NULL) { visibleTopItemFound = false; } else { // this assumes, that all item-texts in column 0 are unique, // no matter, whether the item is a report- or a group-item visibleTopItemText = visibleTopItem->text(0); } // turn off updates to avoid flickering during reload //m_reportListView->setUpdatesEnabled(false); // // Rebuild the list page // m_tocTreeWidget->clear(); // Default Reports QList defaultreports; defaultReports(defaultreports); QList::const_iterator it_group = defaultreports.constBegin(); // the item to be set as current item QTreeWidgetItem* currentItem = 0L; // group number, this will be used as sort key for reportgroup items // we have: // 1st some default groups // 2nd a chart group // 3rd maybe a favorite group // 4th maybe an orphan group (for old reports) int defaultGroupNo = 1; int chartGroupNo = defaultreports.size() + 1; // group for diagrams QString groupName = I18N_NOOP("Charts"); TocItemGroup* chartTocItemGroup = new TocItemGroup(m_tocTreeWidget, chartGroupNo, i18n(groupName.toLatin1().data())); m_allTocItemGroups.insert(groupName, chartTocItemGroup); while (it_group != defaultreports.constEnd()) { groupName = (*it_group).name(); TocItemGroup* defaultTocItemGroup = new TocItemGroup(m_tocTreeWidget, defaultGroupNo++, i18n(groupName.toLatin1().data())); m_allTocItemGroups.insert(groupName, defaultTocItemGroup); if (groupName == selectedItem) { currentItem = defaultTocItemGroup; } QList::const_iterator it_report = (*it_group).begin(); while (it_report != (*it_group).end()) { MyMoneyReport report = *it_report; report.setGroup(groupName); TocItemReport* reportTocItemReport = new TocItemReport(defaultTocItemGroup, report); if (report.name() == selectedItem) { currentItem = reportTocItemReport; } // ALSO place it into the Charts list if it's displayed as a chart by default if (report.isChartByDefault()) { new TocItemReport(chartTocItemGroup, report); } ++it_report; } ++it_group; } // group for custom (favorite) reports int favoriteGroupNo = chartGroupNo + 1; groupName = I18N_NOOP("Favorite Reports"); TocItemGroup* favoriteTocItemGroup = new TocItemGroup(m_tocTreeWidget, favoriteGroupNo, i18n(groupName.toLatin1().data())); m_allTocItemGroups.insert(groupName, favoriteTocItemGroup); TocItemGroup* orphanTocItemGroup = 0; QList customreports = MyMoneyFile::instance()->reportList(); QList::const_iterator it_report = customreports.constBegin(); while (it_report != customreports.constEnd()) { MyMoneyReport report = *it_report; groupName = (*it_report).group(); // If this report is in a known group, place it there // KReportGroupListItem* groupnode = groupitems[(*it_report).group()]; TocItemGroup* groupNode = m_allTocItemGroups[groupName]; if (groupNode) { new TocItemReport(groupNode, report); } else { // otherwise, place it in the orphanage if (!orphanTocItemGroup) { // group for orphaned reports int orphanGroupNo = favoriteGroupNo + 1; groupName = I18N_NOOP("Old Customized Reports"); orphanTocItemGroup = new TocItemGroup(m_tocTreeWidget, orphanGroupNo, i18n(groupName.toLatin1().data())); m_allTocItemGroups.insert(groupName, orphanTocItemGroup); } new TocItemReport(orphanTocItemGroup, report); } // ALSO place it into the Favorites list if it's a favorite if ((*it_report).isFavorite()) { new TocItemReport(favoriteTocItemGroup, report); } // ALSO place it into the Charts list if it's displayed as a chart by default if ((*it_report).isChartByDefault()) { new TocItemReport(chartTocItemGroup, report); } ++it_report; } // // Go through the tabs to set their update flag or delete them if needed // int index = 1; while (index < m_reportTabWidget->count()) { // TODO: Find some way of detecting the file is closed and kill these tabs!! if (auto tab = dynamic_cast(m_reportTabWidget->widget(index))) { if (tab->isReadyToDelete() /* || ! reports.count() */) { delete tab; --index; } else { tab->loadTab(); } } ++index; } if (visibleTopItemFound) { // try to find the visibleTopItem that we had at the start of this method // intentionally not using 'Qt::MatchCaseSensitive' here // to avoid 'item not found' if someone corrected a typo only QList visibleTopItemList = m_tocTreeWidget->findItems(visibleTopItemText, Qt::MatchFixedString | Qt::MatchRecursive); if (visibleTopItemList.isEmpty()) { // the item could not be found, it was deleted or renamed visibleTopItemFound = false; } else { visibleTopItem = visibleTopItemList.at(0); if (visibleTopItem == NULL) { visibleTopItemFound = false; } } } // adjust column widths, // but only the first time when the view is loaded, // maybe the user sets other column widths later, // so don't disturb him if (columnsAlreadyAdjusted()) { // restore expand states of all top-level items restoreTocExpandState(expandStates); // restore current item m_tocTreeWidget->setCurrentItem(currentItem); // try to scroll to the item visible on top // when this method started if (visibleTopItemFound) { m_tocTreeWidget->scrollToItem(visibleTopItem); } else { m_tocTreeWidget->scrollToTop(); } return; } // avoid flickering m_tocTreeWidget->setUpdatesEnabled(false); // expand all top-level items m_tocTreeWidget->expandAll(); // resize columns m_tocTreeWidget->resizeColumnToContents(0); m_tocTreeWidget->resizeColumnToContents(1); // restore expand states of all top-level items restoreTocExpandState(expandStates); // restore current item m_tocTreeWidget->setCurrentItem(currentItem); // try to scroll to the item visible on top // when this method started if (visibleTopItemFound) { m_tocTreeWidget->scrollToItem(visibleTopItem); } else { m_tocTreeWidget->scrollToTop(); } setColumnsAlreadyAdjusted(true); m_tocTreeWidget->setUpdatesEnabled(true); } void defaultReports(QList& groups) { { ReportGroup list("Income and Expenses", i18n("Income and Expenses")); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::ExpenseIncome, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentMonth, eMyMoney::Report::DetailLevel::All, i18n("Income and Expenses This Month"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::ExpenseIncome, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Income and Expenses This Year"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::ExpenseIncome, static_cast(eMyMoney::Report::ColumnType::Years), TransactionFilter::Date::All, eMyMoney::Report::DetailLevel::All, i18n("Income and Expenses By Year"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::ExpenseIncome, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last12Months, eMyMoney::Report::DetailLevel::Top, i18n("Income and Expenses Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartType(eMyMoney::Report::ChartType::Line); list.back().setChartDataLabels(false); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::ExpenseIncome, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last12Months, eMyMoney::Report::DetailLevel::Top, i18n("Income and Expenses Bar Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartType(eMyMoney::Report::ChartType::StackedBar); list.back().setChartDataLabels(false); list.back().setNegExpenses(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::ExpenseIncome, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::Group, i18n("Income and Expenses Pie Chart"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartType(eMyMoney::Report::ChartType::Pie); list.back().setShowingRowTotals(false); groups.push_back(list); } { ReportGroup list("Net Worth", i18n("Net Worth")); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::Top, i18n("Net Worth By Month"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Today, eMyMoney::Report::DetailLevel::Top, i18n("Net Worth Today"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Years), TransactionFilter::Date::All, eMyMoney::Report::DetailLevel::Top, i18n("Net Worth By Year"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Next7Days, eMyMoney::Report::DetailLevel::Top, i18n("7-day Cash Flow Forecast"), i18n("Default Report") )); list.back().setIncludingSchedules(true); list.back().setColumnsAreDays(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last12Months, eMyMoney::Report::DetailLevel::Total, i18n("Net Worth Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(eMyMoney::Report::ChartType::Line); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Institution, eMyMoney::Report::QueryColumn::None, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::Top, i18n("Account Balances by Institution"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AccountType, eMyMoney::Report::QueryColumn::None, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::Top, i18n("Account Balances by Type"), i18n("Default Report") )); groups.push_back(list); } { ReportGroup list("Transactions", i18n("Transactions")); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Account, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Tag | eMyMoney::Report::QueryColumn::Balance, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Transactions by Account"), i18n("Default Report") )); //list.back().setConvertCurrency(false); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Category, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Account | eMyMoney::Report::QueryColumn::Tag, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Transactions by Category"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Payee, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Tag, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Transactions by Payee"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Tag, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Category, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Transactions by Tag"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Month, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Tag, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Transactions by Month"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Week, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Tag, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Transactions by Week"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Account, eMyMoney::Report::QueryColumn::Loan, TransactionFilter::Date::All, eMyMoney::Report::DetailLevel::All, i18n("Loan Transactions"), i18n("Default Report") )); list.back().setLoansOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AccountReconcile, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Balance, TransactionFilter::Date::Last3Months, eMyMoney::Report::DetailLevel::All, i18n("Transactions by Reconciliation Status"), i18n("Default Report") )); groups.push_back(list); } { ReportGroup list("CashFlow", i18n("Cash Flow")); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::CashFlow, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Account, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Cash Flow Transactions This Month"), i18n("Default Report") )); groups.push_back(list); } { ReportGroup list("Investments", i18n("Investments")); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::TopAccount, eMyMoney::Report::QueryColumn::Action | eMyMoney::Report::QueryColumn::Shares | eMyMoney::Report::QueryColumn::Price, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Investment Transactions"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AccountByTopAccount, eMyMoney::Report::QueryColumn::Shares | eMyMoney::Report::QueryColumn::Price, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Investment Holdings by Account"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::EquityType, eMyMoney::Report::QueryColumn::Shares | eMyMoney::Report::QueryColumn::Price, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Investment Holdings by Type"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AccountByTopAccount, eMyMoney::Report::QueryColumn::Performance, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Investment Performance by Account"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::EquityType, eMyMoney::Report::QueryColumn::Performance, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Investment Performance by Type"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AccountByTopAccount, eMyMoney::Report::QueryColumn::CapitalGain, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Investment Capital Gains by Account"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::EquityType, eMyMoney::Report::QueryColumn::CapitalGain, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Investment Capital Gains by Type"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Today, eMyMoney::Report::DetailLevel::All, i18n("Investment Holdings Pie"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(eMyMoney::Report::ChartType::Pie); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last12Months, eMyMoney::Report::DetailLevel::All, i18n("Investment Worth Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(eMyMoney::Report::ChartType::Line); list.back().setColumnsAreDays(true); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last12Months, eMyMoney::Report::DetailLevel::All, i18n("Investment Price Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(eMyMoney::Report::ChartType::Line); list.back().setColumnsAreDays(true); list.back().setInvestmentsOnly(true); list.back().setIncludingBudgetActuals(false); list.back().setIncludingPrice(true); list.back().setConvertCurrency(true); list.back().setChartDataLabels(false); list.back().setSkipZero(true); list.back().setShowingColumnTotals(false); list.back().setShowingRowTotals(false); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last12Months, eMyMoney::Report::DetailLevel::All, i18n("Investment Moving Average Price Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(eMyMoney::Report::ChartType::Line); list.back().setColumnsAreDays(true); list.back().setInvestmentsOnly(true); list.back().setIncludingBudgetActuals(false); list.back().setIncludingAveragePrice(true); list.back().setMovingAverageDays(10); list.back().setConvertCurrency(true); list.back().setChartDataLabels(false); list.back().setShowingColumnTotals(false); list.back().setShowingRowTotals(false); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last30Days, eMyMoney::Report::DetailLevel::All, i18n("Investment Moving Average"), i18n("Default Report") )); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(eMyMoney::Report::ChartType::Line); list.back().setColumnsAreDays(true); list.back().setInvestmentsOnly(true); list.back().setIncludingBudgetActuals(false); list.back().setIncludingMovingAverage(true); list.back().setMovingAverageDays(10); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last30Days, eMyMoney::Report::DetailLevel::All, i18n("Investment Moving Average vs Actual"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(eMyMoney::Report::ChartType::Line); list.back().setColumnsAreDays(true); list.back().setInvestmentsOnly(true); list.back().setIncludingBudgetActuals(true); list.back().setIncludingMovingAverage(true); list.back().setMovingAverageDays(10); groups.push_back(list); } { ReportGroup list("Taxes", i18n("Taxes")); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Category, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Account, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Tax Transactions by Category"), i18n("Default Report") )); list.back().setTax(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Payee, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Account, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Tax Transactions by Payee"), i18n("Default Report") )); list.back().setTax(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Category, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Account, TransactionFilter::Date::LastFiscalYear, eMyMoney::Report::DetailLevel::All, i18n("Tax Transactions by Category Last Fiscal Year"), i18n("Default Report") )); list.back().setTax(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Payee, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Account, TransactionFilter::Date::LastFiscalYear, eMyMoney::Report::DetailLevel::All, i18n("Tax Transactions by Payee Last Fiscal Year"), i18n("Default Report") )); list.back().setTax(true); groups.push_back(list); } { ReportGroup list("Budgeting", i18n("Budgeting")); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::BudgetActual, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Budgeted vs. Actual This Year"), i18n("Default Report") )); list.back().setShowingRowTotals(true); list.back().setBudget("Any", true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::BudgetActual, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::YearToMonth, eMyMoney::Report::DetailLevel::All, i18n("Budgeted vs. Actual This Year (YTM)"), i18n("Default Report") )); list.back().setShowingRowTotals(true); list.back().setBudget("Any", true); // in case we're in January, we show the last year if (QDate::currentDate().month() == 1) { list.back().setDateFilter(TransactionFilter::Date::LastYear); } list.push_back(MyMoneyReport( eMyMoney::Report::RowType::BudgetActual, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentMonth, eMyMoney::Report::DetailLevel::All, i18n("Monthly Budgeted vs. Actual"), i18n("Default Report") )); list.back().setBudget("Any", true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::BudgetActual, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentYear, eMyMoney::Report::DetailLevel::All, i18n("Yearly Budgeted vs. Actual"), i18n("Default Report") )); list.back().setBudget("Any", true); list.back().setShowingRowTotals(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Budget, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentMonth, eMyMoney::Report::DetailLevel::All, i18n("Monthly Budget"), i18n("Default Report") )); list.back().setBudget("Any", false); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Budget, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentYear, eMyMoney::Report::DetailLevel::All, i18n("Yearly Budget"), i18n("Default Report") )); list.back().setBudget("Any", false); list.back().setShowingRowTotals(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::BudgetActual, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentYear, eMyMoney::Report::DetailLevel::Group, i18n("Yearly Budgeted vs Actual Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setBudget("Any", true); list.back().setChartType(eMyMoney::Report::ChartType::Line); groups.push_back(list); } { ReportGroup list("Forecast", i18n("Forecast")); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Next12Months, eMyMoney::Report::DetailLevel::Top, i18n("Forecast By Month"), i18n("Default Report") )); list.back().setIncludingForecast(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::NextQuarter, eMyMoney::Report::DetailLevel::Top, i18n("Forecast Next Quarter"), i18n("Default Report") )); list.back().setColumnsAreDays(true); list.back().setIncludingForecast(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::ExpenseIncome, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentYear, eMyMoney::Report::DetailLevel::Top, i18n("Income and Expenses Forecast This Year"), i18n("Default Report") )); list.back().setIncludingForecast(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Next3Months, eMyMoney::Report::DetailLevel::Total, i18n("Net Worth Forecast Graph"), i18n("Default Report") )); list.back().setColumnsAreDays(true); list.back().setIncludingForecast(true); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(eMyMoney::Report::ChartType::Line); groups.push_back(list); } { ReportGroup list("Information", i18n("General Information")); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Schedule, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Next12Months, eMyMoney::Report::DetailLevel::All, i18n("Schedule Information"), i18n("Default Report") )); list.back().setDetailLevel(eMyMoney::Report::DetailLevel::All); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Schedule, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Next12Months, eMyMoney::Report::DetailLevel::All, i18n("Schedule Summary Information"), i18n("Default Report") )); list.back().setDetailLevel(eMyMoney::Report::DetailLevel::Top); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AccountInfo, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Today, eMyMoney::Report::DetailLevel::All, i18n("Account Information"), i18n("Default Report") )); list.back().setConvertCurrency(false); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AccountLoanInfo, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Today, eMyMoney::Report::DetailLevel::All, i18n("Loan Information"), i18n("Default Report") )); list.back().setConvertCurrency(false); groups.push_back(list); } } bool columnsAlreadyAdjusted() const { return m_columnsAlreadyAdjusted; } void setColumnsAlreadyAdjusted(bool adjusted) { m_columnsAlreadyAdjusted = adjusted; } KReportsView *q_ptr; /** * This member holds the load state of page */ bool m_needLoad; QListWidget* m_reportListView; QTabWidget* m_reportTabWidget; QWidget* m_listTab; QVBoxLayout* m_listTabLayout; QTreeWidget* m_tocTreeWidget; QMap m_allTocItemGroups; QString m_selectedExportFilter; bool m_columnsAlreadyAdjusted; MyMoneyAccount m_currentAccount; }; #endif diff --git a/kmymoney/views/CMakeLists.txt b/kmymoney/views/CMakeLists.txt index 4cc82b7b3..415991cff 100644 --- a/kmymoney/views/CMakeLists.txt +++ b/kmymoney/views/CMakeLists.txt @@ -1,81 +1,82 @@ ############# next target (views) STATIC ################### set(libviews_a_SOURCES kaccountsview.cpp kcategoriesview.cpp kgloballedgerview.cpp kwelcomepage.cpp khomeview.cpp kinstitutionsview.cpp kinvestmentview.cpp kmymoneyfile.cpp kmymoneyview.cpp kpayeesview.cpp kscheduledview.cpp kscheduletreeitem.cpp ktagsview.cpp kpayeeidentifierview.cpp payeeidentifierselectiondelegate.cpp kmymoneywebpage.cpp ) if(ENABLE_UNFINISHEDFEATURES) list(APPEND libviews_a_SOURCES simpleledgerview.cpp ledgerviewpage.cpp ledgerview.cpp ledgerdelegate.cpp newspliteditor.cpp newtransactioneditor.cpp newtransactionform.cpp splitdialog.cpp splitdelegate.cpp widgethintframe.cpp ) endif() set(libviews_a_UI kaccountsview.ui kcategoriesview.ui kinstitutionsview.ui kinvestmentview.ui kpayeesview.ui kscheduledview.ui ktagsview.ui kpayeeidentifierview.ui ) if(ENABLE_UNFINISHEDFEATURES) list(APPEND libviews_a_UI simpleledgerview.ui ledgerview.ui ledgerviewpage.ui splitdialog.ui newspliteditor.ui newtransactioneditor.ui newtransactionform.ui ) endif() # The handling of these ui files depends # on libkmymoney.so (the widgets library) ki18n_wrap_ui(libviews_a_SOURCES ${libviews_a_UI}) add_library(views STATIC ${libviews_a_SOURCES}) target_link_libraries(views PUBLIC newaccountwizard KF5::TextWidgets Qt5::PrintSupport) if(ENABLE_WEBENGINE) target_link_libraries(views PUBLIC Qt5::WebEngineWidgets) else(ENABLE_WEBENGINE) target_link_libraries(views PUBLIC KF5::WebKit) endif(ENABLE_WEBENGINE) # TODO: Remove this dependency. But it is needed as long as the payee editor uses these objects directly # This should be replaced by virtual methods in a pure abstract object. target_link_libraries( views PUBLIC kmm_mymoney # needed to load payeeIdentifier kmm_widgets + kmm_printer ) # we rely on some of the dialogs to be generated add_dependencies(views dialogs newinvestmentwizard newaccountwizard newloanwizard endingbalancedlg) diff --git a/kmymoney/views/kgloballedgerview_p.h b/kmymoney/views/kgloballedgerview_p.h index e15d1e741..4513010cc 100644 --- a/kmymoney/views/kgloballedgerview_p.h +++ b/kmymoney/views/kgloballedgerview_p.h @@ -1,1665 +1,1669 @@ /*************************************************************************** kgloballedgerview_p.h - description ------------------- begin : Wed Jul 26 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KGLOBALLEDGERVIEW_P_H #define KGLOBALLEDGERVIEW_P_H #include "kgloballedgerview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyviewbase_p.h" #include "kendingbalancedlg.h" #include "kfindtransactiondlg.h" #include "kmymoneyaccountselector.h" #include "kmymoneyutils.h" #include "mymoneyexception.h" #include "mymoneymoney.h" #include "mymoneyaccount.h" #include "mymoneyfile.h" #include "kmymoneyaccountcombo.h" #include "kbalancewarning.h" #include "transactionmatcher.h" #include "tabbar.h" #include "register.h" #include "transactioneditor.h" #include "selectedtransactions.h" #include "kmymoneysettings.h" #include "registersearchline.h" #include "scheduledtransaction.h" #include "accountsmodel.h" #include "models.h" #include "mymoneyprice.h" #include "mymoneyschedule.h" #include "mymoneysecurity.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneysplit.h" #include "mymoneypayee.h" #include "mymoneytracer.h" #include "transaction.h" #include "transactionform.h" #include "fancydategroupmarkers.h" #include "widgetenums.h" #include "mymoneyenums.h" #include "modelenums.h" #include "menuenums.h" #include #ifdef KMM_DEBUG #include "mymoneyutils.h" #endif using namespace eMenu; using namespace eMyMoney; /** * helper class implementing an event filter to detect mouse button press * events on widgets outside a given set of widgets. This is used internally * to detect when to leave the edit mode. */ class MousePressFilter : public QObject { Q_OBJECT public: explicit MousePressFilter(QWidget* parent = nullptr) : QObject(parent), m_lastMousePressEvent(0), m_filterActive(true) { } /** * Add widget @p w to the list of possible parent objects. See eventFilter() how * they will be used. */ void addWidget(QWidget* w) { m_parents.append(w); } public Q_SLOTS: /** * This slot allows to activate/deactivate the filter. By default the * filter is active. * * @param state Allows to activate (@a true) or deactivate (@a false) the filter */ void setFilterActive(bool state = true) { m_filterActive = state; } /** * This slot allows to activate/deactivate the filter. By default the * filter is active. * * @param state Allows to deactivate (@a true) or activate (@a false) the filter */ void setFilterDeactive(bool state = false) { setFilterActive(!state); } protected: /** * This method checks if the widget @p child is a child of * the widget @p parent and returns either @a true or @a false. * * @param child pointer to child widget * @param parent pointer to parent widget * @retval true @p child points to widget which has @p parent as parent or grand-parent * @retval false @p child points to a widget which is not related to @p parent */ bool isChildOf(QWidget* child, QWidget* parent) { // QDialogs cannot be detected directly, but it can be assumed, // that events on a widget that do not have a parent widget within // our application are dialogs. if (!child->parentWidget()) return true; while (child) { // if we are a child of the given parent, we have a match if (child == parent) return true; // if we are at the application level, we don't have a match if (child->inherits("KMyMoneyApp")) return false; // If one of the ancestors is a KPassivePopup or a KDialog or a popup widget then // it's as if it is a child of our own because these widgets could // appear during transaction entry (message boxes, completer widgets) if (dynamic_cast(child) || ((child->windowFlags() & Qt::Popup) && /*child != kmymoney*/ !child->parentWidget())) // has no parent, then it must be top-level window return true; child = child->parentWidget(); } return false; } /** * Reimplemented from base class. Sends out the mousePressedOnExternalWidget() signal * if object @p o points to an object which is not a child widget of any added previously * using the addWidget() method. The signal is sent out only once for each event @p e. * * @param o pointer to QObject * @param e pointer to QEvent * @return always returns @a false */ bool eventFilter(QObject* o, QEvent* e) final override { if (m_filterActive) { if (e->type() == QEvent::MouseButtonPress && !m_lastMousePressEvent) { QWidget* w = qobject_cast(o); if (!w) { return QObject::eventFilter(o, e); } QList::const_iterator it_w; for (it_w = m_parents.constBegin(); it_w != m_parents.constEnd(); ++it_w) { if (isChildOf(w, (*it_w))) { m_lastMousePressEvent = e; break; } } if (it_w == m_parents.constEnd()) { m_lastMousePressEvent = e; bool rc = false; emit mousePressedOnExternalWidget(rc); } } if (e->type() != QEvent::MouseButtonPress) { m_lastMousePressEvent = 0; } } return false; } Q_SIGNALS: void mousePressedOnExternalWidget(bool&); private: QList m_parents; QEvent* m_lastMousePressEvent; bool m_filterActive; }; class KGlobalLedgerViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KGlobalLedgerView) public: explicit KGlobalLedgerViewPrivate(KGlobalLedgerView *qq) : q_ptr(qq), m_mousePressFilter(0), m_registerSearchLine(0), m_precision(2), m_recursion(false), m_showDetails(false), m_action(eWidgets::eRegister::Action::None), m_filterProxyModel(0), m_accountComboBox(0), m_balanceIsApproximated(false), m_toolbarFrame(nullptr), m_registerFrame(nullptr), m_buttonFrame(nullptr), m_formFrame(nullptr), m_summaryFrame(nullptr), m_register(nullptr), m_buttonbar(nullptr), m_leftSummaryLabel(nullptr), m_centerSummaryLabel(nullptr), m_rightSummaryLabel(nullptr), m_form(nullptr), m_needLoad(true), m_newAccountLoaded(true), m_inEditMode(false), m_transactionEditor(nullptr), m_balanceWarning(nullptr), m_moveToAccountSelector(nullptr), m_endingBalanceDlg(nullptr), m_searchDlg(nullptr) { } ~KGlobalLedgerViewPrivate() { delete m_moveToAccountSelector; delete m_endingBalanceDlg; delete m_searchDlg; } void init() { Q_Q(KGlobalLedgerView); m_needLoad = false; auto vbox = new QVBoxLayout(q); q->setLayout(vbox); vbox->setSpacing(6); vbox->setMargin(0); m_mousePressFilter = new MousePressFilter((QWidget*)q); m_action = eWidgets::eRegister::Action::None; // the proxy filter model m_filterProxyModel = new AccountNamesFilterProxyModel(q); m_filterProxyModel->addAccountGroup(QVector {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, eMyMoney::Account::Type::Equity}); auto const model = Models::instance()->accountsModel(); m_filterProxyModel->setSourceModel(model); m_filterProxyModel->setSourceColumns(model->getColumns()); m_filterProxyModel->sort((int)eAccountsModel::Column::Account); // create the toolbar frame at the top of the view m_toolbarFrame = new QFrame(); QHBoxLayout* toolbarLayout = new QHBoxLayout(m_toolbarFrame); toolbarLayout->setContentsMargins(0, 0, 0, 0); toolbarLayout->setSpacing(6); // the account selector widget m_accountComboBox = new KMyMoneyAccountCombo(); m_accountComboBox->setModel(m_filterProxyModel); toolbarLayout->addWidget(m_accountComboBox); vbox->addWidget(m_toolbarFrame); toolbarLayout->setStretchFactor(m_accountComboBox, 60); // create the register frame m_registerFrame = new QFrame(); QVBoxLayout* registerFrameLayout = new QVBoxLayout(m_registerFrame); registerFrameLayout->setContentsMargins(0, 0, 0, 0); registerFrameLayout->setSpacing(0); vbox->addWidget(m_registerFrame); vbox->setStretchFactor(m_registerFrame, 2); m_register = new KMyMoneyRegister::Register(m_registerFrame); m_register->setUsedWithEditor(true); registerFrameLayout->addWidget(m_register); m_register->installEventFilter(q); q->connect(m_register, &KMyMoneyRegister::Register::openContextMenu, q, &KGlobalLedgerView::slotTransactionsContextMenuRequested); q->connect(m_register, &KMyMoneyRegister::Register::transactionsSelected, q, &KGlobalLedgerView::slotUpdateSummaryLine); q->connect(m_register->horizontalHeader(), &QWidget::customContextMenuRequested, q, &KGlobalLedgerView::slotSortOptions); q->connect(m_register, &KMyMoneyRegister::Register::reconcileStateColumnClicked, q, &KGlobalLedgerView::slotToggleTransactionMark); // insert search line widget m_registerSearchLine = new KMyMoneyRegister::RegisterSearchLineWidget(m_register, m_toolbarFrame); toolbarLayout->addWidget(m_registerSearchLine); toolbarLayout->setStretchFactor(m_registerSearchLine, 100); // create the summary frame m_summaryFrame = new QFrame(); QHBoxLayout* summaryFrameLayout = new QHBoxLayout(m_summaryFrame); summaryFrameLayout->setContentsMargins(0, 0, 0, 0); summaryFrameLayout->setSpacing(0); m_leftSummaryLabel = new QLabel(m_summaryFrame); m_centerSummaryLabel = new QLabel(m_summaryFrame); m_rightSummaryLabel = new QLabel(m_summaryFrame); summaryFrameLayout->addWidget(m_leftSummaryLabel); QSpacerItem* spacer = new QSpacerItem(20, 1, QSizePolicy::Expanding, QSizePolicy::Minimum); summaryFrameLayout->addItem(spacer); summaryFrameLayout->addWidget(m_centerSummaryLabel); spacer = new QSpacerItem(20, 1, QSizePolicy::Expanding, QSizePolicy::Minimum); summaryFrameLayout->addItem(spacer); summaryFrameLayout->addWidget(m_rightSummaryLabel); vbox->addWidget(m_summaryFrame); // create the button frame m_buttonFrame = new QFrame(q); QVBoxLayout* buttonLayout = new QVBoxLayout(m_buttonFrame); buttonLayout->setContentsMargins(0, 0, 0, 0); buttonLayout->setSpacing(0); vbox->addWidget(m_buttonFrame); m_buttonbar = new KToolBar(m_buttonFrame, 0, true); m_buttonbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); buttonLayout->addWidget(m_buttonbar); m_buttonbar->addAction(pActions[eMenu::Action::NewTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::DeleteTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::EditTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::EnterTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::CancelTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::AcceptTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::MatchTransaction]); // create the transaction form frame m_formFrame = new QFrame(q); QVBoxLayout* frameLayout = new QVBoxLayout(m_formFrame); frameLayout->setContentsMargins(5, 5, 5, 5); frameLayout->setSpacing(0); m_form = new KMyMoneyTransactionForm::TransactionForm(m_formFrame); frameLayout->addWidget(m_form->getTabBar(m_formFrame)); frameLayout->addWidget(m_form); m_formFrame->setFrameShape(QFrame::Panel); m_formFrame->setFrameShadow(QFrame::Raised); vbox->addWidget(m_formFrame); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KGlobalLedgerView::refresh); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KGlobalLedgerView::slotUpdateMoveToAccountMenu); q->connect(m_register, static_cast(&KMyMoneyRegister::Register::focusChanged), m_form, &KMyMoneyTransactionForm::TransactionForm::slotSetTransaction); q->connect(m_register, static_cast(&KMyMoneyRegister::Register::focusChanged), q, &KGlobalLedgerView::updateLedgerActionsInternal); // q->connect(m_accountComboBox, &KMyMoneyAccountCombo::accountSelected, q, &KGlobalLedgerView::slotAccountSelected); q->connect(m_accountComboBox, &KMyMoneyAccountCombo::accountSelected, q, static_cast(&KGlobalLedgerView::slotSelectAccount)); q->connect(m_accountComboBox, &KMyMoneyAccountCombo::accountSelected, q, &KGlobalLedgerView::slotUpdateMoveToAccountMenu); q->connect(m_register, &KMyMoneyRegister::Register::transactionsSelected, q, &KGlobalLedgerView::slotTransactionsSelected); q->connect(m_register, &KMyMoneyRegister::Register::transactionsSelected, q, &KGlobalLedgerView::slotUpdateMoveToAccountMenu); q->connect(m_register, &KMyMoneyRegister::Register::editTransaction, q, &KGlobalLedgerView::slotEditTransaction); q->connect(m_register, &KMyMoneyRegister::Register::emptyItemSelected, q, &KGlobalLedgerView::slotNewTransaction); q->connect(m_register, &KMyMoneyRegister::Register::aboutToSelectItem, q, &KGlobalLedgerView::slotAboutToSelectItem); q->connect(m_mousePressFilter, &MousePressFilter::mousePressedOnExternalWidget, q, &KGlobalLedgerView::slotCancelOrEnterTransactions); q->connect(m_form, &KMyMoneyTransactionForm::TransactionForm::newTransaction, q, static_cast(&KGlobalLedgerView::slotNewTransactionForm)); // setup mouse press filter m_mousePressFilter->addWidget(m_formFrame); m_mousePressFilter->addWidget(m_buttonFrame); m_mousePressFilter->addWidget(m_summaryFrame); m_mousePressFilter->addWidget(m_registerFrame); m_tooltipPosn = QPoint(); } /** * This method reloads the account selection combo box of the * view with all asset and liability accounts from the engine. * If the account id of the current account held in @p m_accountId is * empty or if the referenced account does not exist in the engine, * the first account found in the list will be made the current account. */ void loadAccounts() { const auto file = MyMoneyFile::instance(); // check if the current account still exists and make it the // current account if (!m_lastSelectedAccountID.isEmpty()) { try { m_currentAccount = file->account(m_lastSelectedAccountID); } catch (const MyMoneyException &) { m_lastSelectedAccountID.clear(); m_currentAccount = MyMoneyAccount(); m_accountComboBox->setSelected(QString()); } } // TODO: check why the invalidate is needed here m_filterProxyModel->invalidate(); m_filterProxyModel->sort((int)eAccountsModel::Column::Account); m_filterProxyModel->setHideClosedAccounts(KMyMoneySettings::hideClosedAccounts() && !KMyMoneySettings::showAllAccounts()); m_filterProxyModel->setHideEquityAccounts(!KMyMoneySettings::expertMode()); m_accountComboBox->expandAll(); if (m_currentAccount.id().isEmpty()) { // find the first favorite account QModelIndexList list = m_filterProxyModel->match(m_filterProxyModel->index(0, 0), (int)eAccountsModel::Role::Favorite, QVariant(true), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); if (list.count() > 0) { QVariant accountId = list.front().data((int)eAccountsModel::Role::ID); if (accountId.isValid()) { m_currentAccount = file->account(accountId.toString()); } } if (m_currentAccount.id().isEmpty()) { // there are no favorite accounts find any account list = m_filterProxyModel->match(m_filterProxyModel->index(0, 0), Qt::DisplayRole, QVariant(QString("*")), -1, Qt::MatchFlags(Qt::MatchWildcard | Qt::MatchRecursive)); for (QModelIndexList::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it) { if (!it->parent().isValid()) continue; // skip the top level accounts QVariant accountId = (*it).data((int)eAccountsModel::Role::ID); if (accountId.isValid()) { MyMoneyAccount a = file->account(accountId.toString()); if (!a.isInvest() && !a.isClosed()) { m_currentAccount = a; break; } } } } } if (!m_currentAccount.id().isEmpty()) { m_accountComboBox->setSelected(m_currentAccount.id()); try { m_precision = MyMoneyMoney::denomToPrec(m_currentAccount.fraction()); } catch (const MyMoneyException &) { qDebug("Security %s for account %s not found", qPrintable(m_currentAccount.currencyId()), qPrintable(m_currentAccount.name())); m_precision = 2; } } } /** * This method clears the register, form, transaction list. See @sa m_register, * @sa m_transactionList */ void clear() { // clear current register contents m_register->clear(); // setup header font QFont font = KMyMoneySettings::listHeaderFontEx(); QFontMetrics fm(font); int height = fm.lineSpacing() + 6; m_register->horizontalHeader()->setMinimumHeight(height); m_register->horizontalHeader()->setMaximumHeight(height); m_register->horizontalHeader()->setFont(font); // setup cell font font = KMyMoneySettings::listCellFontEx(); m_register->setFont(font); // clear the form m_form->clear(); // the selected transactions list m_transactionList.clear(); // and the selected account in the combo box m_accountComboBox->setSelected(QString()); // fraction defaults to two digits m_precision = 2; } void loadView() { MYMONEYTRACER(tracer); Q_Q(KGlobalLedgerView); // setup form visibility m_formFrame->setVisible(KMyMoneySettings::transactionForm()); // no account selected // emit q->objectSelected(MyMoneyAccount()); // no transaction selected KMyMoneyRegister::SelectedTransactions list; emit q->selectByVariant(QVariantList {QVariant::fromValue(list)}, eView::Intent::SelectRegisterTransactions); QMap isSelected; QString focusItemId; QString backUpFocusItemId; // in case the focus item is removed QString anchorItemId; QString backUpAnchorItemId; // in case the anchor item is removed if (!m_newAccountLoaded) { // remember the current selected transactions KMyMoneyRegister::RegisterItem* item = m_register->firstItem(); for (; item; item = item->nextItem()) { if (item->isSelected()) { isSelected[item->id()] = true; } } // remember the item that has the focus storeId(m_register->focusItem(), focusItemId, backUpFocusItemId); // and the one that has the selection anchor storeId(m_register->anchorItem(), anchorItemId, backUpAnchorItemId); } else { m_registerSearchLine->searchLine()->clear(); } // clear the current contents ... clear(); // ... load the combobox widget and select current account ... loadAccounts(); // ... setup the register columns ... m_register->setupRegister(m_currentAccount); // ... setup the form ... m_form->setupForm(m_currentAccount); if (m_currentAccount.id().isEmpty()) { // if we don't have an account we bail out q->setEnabled(false); return; } q->setEnabled(true); m_register->setUpdatesEnabled(false); // ... and recreate it KMyMoneyRegister::RegisterItem* focusItem = 0; KMyMoneyRegister::RegisterItem* anchorItem = 0; QMap actBalance, clearedBalance, futureBalance; QMap::iterator it_b; try { // setup the filter to select the transactions we want to display // and update the sort order QString sortOrder; QString key; QDate reconciliationDate = m_reconciliationDate; MyMoneyTransactionFilter filter(m_currentAccount.id()); // if it's an investment account, we also take care of // the sub-accounts (stock accounts) if (m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) filter.addAccount(m_currentAccount.accountList()); if (isReconciliationAccount()) { key = "kmm-sort-reconcile"; sortOrder = KMyMoneySettings::sortReconcileView(); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); } else { filter.setDateFilter(KMyMoneySettings::startDate().date(), QDate()); key = "kmm-sort-std"; sortOrder = KMyMoneySettings::sortNormalView(); if (KMyMoneySettings::hideReconciledTransactions() && !m_currentAccount.isIncomeExpense()) { filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); } } filter.setReportAllSplits(true); // check if we have an account override of the sort order if (!m_currentAccount.value(key).isEmpty()) sortOrder = m_currentAccount.value(key); // setup sort order m_register->setSortOrder(sortOrder); // retrieve the list from the engine MyMoneyFile::instance()->transactionList(m_transactionList, filter); emit q->slotStatusProgress(0, m_transactionList.count()); // create the elements for the register QList >::const_iterator it; QMapuniqueMap; int i = 0; for (it = m_transactionList.constBegin(); it != m_transactionList.constEnd(); ++it) { uniqueMap[(*it).first.id()]++; KMyMoneyRegister::Transaction* t = KMyMoneyRegister::Register::transactionFactory(m_register, (*it).first, (*it).second, uniqueMap[(*it).first.id()]); actBalance[t->split().accountId()] = MyMoneyMoney(); emit q->slotStatusProgress(++i, 0); // if we're in reconciliation and the state is cleared, we // force the item to show in dimmed intensity to get a visual focus // on those items, that we need to work on if (isReconciliationAccount() && (*it).second.reconcileFlag() == eMyMoney::Split::State::Cleared) { t->setReducedIntensity(true); } } // create dummy entries for the scheduled transactions if sorted by postdate int period = KMyMoneySettings::schedulePreview(); if (m_register->primarySortKey() == eWidgets::SortField::PostDate) { // show scheduled transactions which have a scheduled postdate // within the next 'period' days. In reconciliation mode, the // period starts on the statement date. QDate endDate = QDate::currentDate().addDays(period); if (isReconciliationAccount()) endDate = reconciliationDate.addDays(period); QList scheduleList = MyMoneyFile::instance()->scheduleList(m_currentAccount.id()); while (!scheduleList.isEmpty()) { MyMoneySchedule& s = scheduleList.first(); for (;;) { if (s.isFinished() || s.adjustedNextDueDate() > endDate) { break; } MyMoneyTransaction t(s.id(), KMyMoneyUtils::scheduledTransaction(s)); if (s.isOverdue() && !KMyMoneySettings::showPlannedScheduleDates()) { // if the transaction is scheduled and overdue, it can't // certainly be posted in the past. So we take today's date // as the alternative t.setPostDate(s.adjustedDate(QDate::currentDate(), s.weekendOption())); } else { t.setPostDate(s.adjustedNextDueDate()); } foreach (const auto split, t.splits()) { if (split.accountId() == m_currentAccount.id()) { new KMyMoneyRegister::StdTransactionScheduled(m_register, t, split, uniqueMap[t.id()]); } } // keep track of this payment locally (not in the engine) if (s.isOverdue() && !KMyMoneySettings::showPlannedScheduleDates()) { s.setLastPayment(QDate::currentDate()); } else { s.setLastPayment(s.nextDueDate()); } // if this is a one time schedule, we can bail out here as we're done if (s.occurrence() == eMyMoney::Schedule::Occurrence::Once) break; // for all others, we check if the next payment date is still 'in range' QDate nextDueDate = s.nextPayment(s.nextDueDate()); if (nextDueDate.isValid()) { s.setNextDueDate(nextDueDate); } else { break; } } scheduleList.pop_front(); } } // add the group markers m_register->addGroupMarkers(); // sort the transactions according to the sort setting m_register->sortItems(); // remove trailing and adjacent markers m_register->removeUnwantedGroupMarkers(); // add special markers for reconciliation now so that they do not get // removed by m_register->removeUnwantedGroupMarkers(). Needs resorting // of items but that's ok. KMyMoneyRegister::StatementGroupMarker* statement = 0; KMyMoneyRegister::StatementGroupMarker* dStatement = 0; KMyMoneyRegister::StatementGroupMarker* pStatement = 0; if (isReconciliationAccount()) { switch (m_register->primarySortKey()) { case eWidgets::SortField::PostDate: statement = new KMyMoneyRegister::StatementGroupMarker(m_register, eWidgets::eRegister::CashFlowDirection::Deposit, reconciliationDate, i18n("Statement Details")); m_register->sortItems(); break; case eWidgets::SortField::Type: dStatement = new KMyMoneyRegister::StatementGroupMarker(m_register, eWidgets::eRegister::CashFlowDirection::Deposit, reconciliationDate, i18n("Statement Deposit Details")); pStatement = new KMyMoneyRegister::StatementGroupMarker(m_register, eWidgets::eRegister::CashFlowDirection::Payment, reconciliationDate, i18n("Statement Payment Details")); m_register->sortItems(); break; default: break; } } // we need at least the balance for the account we currently show actBalance[m_currentAccount.id()] = MyMoneyMoney(); if (m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) foreach (const auto accountID, m_currentAccount.accountList()) actBalance[accountID] = MyMoneyMoney(); // determine balances (actual, cleared). We do this by getting the actual // balance of all entered transactions from the engine and walk the list // of transactions backward. Also re-select a transaction if it was // selected before and setup the focus item. MyMoneyMoney factor(1, 1); if (m_currentAccount.accountGroup() == eMyMoney::Account::Type::Liability || m_currentAccount.accountGroup() == eMyMoney::Account::Type::Equity) factor = -factor; QMap deposits; QMap payments; QMap depositAmount; QMap paymentAmount; for (it_b = actBalance.begin(); it_b != actBalance.end(); ++it_b) { MyMoneyMoney balance = MyMoneyFile::instance()->balance(it_b.key()); balance = balance * factor; clearedBalance[it_b.key()] = futureBalance[it_b.key()] = (*it_b) = balance; deposits[it_b.key()] = payments[it_b.key()] = 0; depositAmount[it_b.key()] = MyMoneyMoney(); paymentAmount[it_b.key()] = MyMoneyMoney(); } tracer.printf("total balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(actBalance[m_currentAccount.id()].formatMoney("", 2))); tracer.printf("future balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(futureBalance[m_currentAccount.id()].formatMoney("", 2))); tracer.printf("cleared balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(clearedBalance[m_currentAccount.id()].formatMoney("", 2))); KMyMoneyRegister::RegisterItem* p = m_register->lastItem(); focusItem = 0; // take care of possibly trailing scheduled transactions (bump up the future balance) while (p) { if (p->isSelectable()) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t && t->isScheduled()) { MyMoneyMoney balance = futureBalance[t->split().accountId()]; const MyMoneySplit& split = t->split(); // if this split is a stock split, we can't just add the amount of shares if (t->transaction().isStockSplit()) { balance = balance * split.shares(); } else { balance += split.shares() * factor; } futureBalance[split.accountId()] = balance; } else if (t && !focusItem) focusItem = p; } p = p->prevItem(); } p = m_register->lastItem(); while (p) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) { if (isSelected.contains(t->id())) t->setSelected(true); matchItemById(&focusItem, t, focusItemId, backUpFocusItemId); matchItemById(&anchorItem, t, anchorItemId, backUpAnchorItemId); const MyMoneySplit& split = t->split(); MyMoneyMoney balance = futureBalance[split.accountId()]; t->setBalance(balance); // if this split is a stock split, we can't just add the amount of shares if (t->transaction().isStockSplit()) { balance /= split.shares(); } else { balance -= split.shares() * factor; } if (!t->isScheduled()) { if (isReconciliationAccount() && t->transaction().postDate() <= reconciliationDate && split.reconcileFlag() == eMyMoney::Split::State::Cleared) { if (split.shares().isNegative()) { payments[split.accountId()]++; paymentAmount[split.accountId()] += split.shares(); } else { deposits[split.accountId()]++; depositAmount[split.accountId()] += split.shares(); } } if (t->transaction().postDate() > QDate::currentDate()) { tracer.printf("Reducing actual balance by %s because %s/%s(%s) is in the future", qPrintable((split.shares() * factor).formatMoney("", 2)), qPrintable(t->transaction().id()), qPrintable(split.id()), qPrintable(t->transaction().postDate().toString(Qt::ISODate))); actBalance[split.accountId()] -= split.shares() * factor; } } futureBalance[split.accountId()] = balance; } p = p->prevItem(); } clearedBalance[m_currentAccount.id()] = MyMoneyFile::instance()->clearedBalance(m_currentAccount.id(), reconciliationDate); tracer.printf("total balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(actBalance[m_currentAccount.id()].formatMoney("", 2))); tracer.printf("future balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(futureBalance[m_currentAccount.id()].formatMoney("", 2))); tracer.printf("cleared balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(clearedBalance[m_currentAccount.id()].formatMoney("", 2))); // update statement information if (statement) { const QString aboutDeposits = i18np("%1 deposit (%2)", "%1 deposits (%2)", deposits[m_currentAccount.id()], depositAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction())); const QString aboutPayments = i18np("%1 payment (%2)", "%1 payments (%2)", payments[m_currentAccount.id()], paymentAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction())); statement->setText(i18nc("%1 is a string, e.g. 7 deposits; %2 is a string, e.g. 4 payments", "%1, %2", aboutDeposits, aboutPayments)); } if (pStatement) { pStatement->setText(i18np("%1 payment (%2)", "%1 payments (%2)", payments[m_currentAccount.id()] , paymentAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction()))); } if (dStatement) { dStatement->setText(i18np("%1 deposit (%2)", "%1 deposits (%2)", deposits[m_currentAccount.id()] , depositAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction()))); } // add a last empty entry for new transactions // leave some information about the current account MyMoneySplit split; split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); // make sure to use the value specified in the option during reconciliation if (isReconciliationAccount()) split.setReconcileFlag(static_cast(KMyMoneySettings::defaultReconciliationState())); MyMoneyTransaction emptyTransaction; emptyTransaction.setCommodity(m_currentAccount.currencyId()); KMyMoneyRegister::Register::transactionFactory(m_register, emptyTransaction, split, 0); m_register->updateRegister(true); if (focusItem) { // in case we have some selected items we just set the focus item // in other cases, we make the focusitem also the selected item if (anchorItem && (anchorItem != focusItem)) { m_register->setFocusItem(focusItem); m_register->setAnchorItem(anchorItem); } else m_register->selectItem(focusItem, true); } else { // just use the empty line at the end if nothing else exists in the ledger p = m_register->lastItem(); m_register->setFocusItem(p); m_register->selectItem(p); focusItem = p; } updateSummaryLine(actBalance, clearedBalance); emit q->slotStatusProgress(-1, -1); } catch (const MyMoneyException &) { m_currentAccount = MyMoneyAccount(); clear(); } m_showDetails = KMyMoneySettings::showRegisterDetailed(); // and tell everyone what's selected emit q->selectByObject(m_currentAccount, eView::Intent::None); KMyMoneyRegister::SelectedTransactions actualSelection(m_register); emit q->selectByVariant(QVariantList {QVariant::fromValue(actualSelection)}, eView::Intent::SelectRegisterTransactions); } void selectTransaction(const QString& id) { if (!id.isEmpty()) { KMyMoneyRegister::RegisterItem* p = m_register->lastItem(); while (p) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) { if (t->transaction().id() == id) { m_register->selectItem(t); m_register->ensureItemVisible(t); break; } } p = p->prevItem(); } } } /** * @brief selects transactions for processing with slots * @param list of transactions * @return false if only schedule is to be selected */ bool selectTransactions(const KMyMoneyRegister::SelectedTransactions list) { Q_Q(KGlobalLedgerView); // list can either contain a list of transactions or a single selected scheduled transaction // in the latter case, the transaction id is actually the one of the schedule. In order // to differentiate between the two, we just ask for the schedule. If we don't find one - because // we passed the id of a real transaction - then we know that fact. We use the schedule here, // because the list of schedules is kept in a cache by MyMoneyFile. This way, we save some trips // to the backend which we would have to do if we check for the transaction. m_selectedTransactions.clear(); auto sch = MyMoneySchedule(); auto ret = true; m_accountGoto.clear(); m_payeeGoto.clear(); if (!list.isEmpty() && !list.first().isScheduled()) { m_selectedTransactions = list; if (list.count() == 1) { const MyMoneySplit& sp = m_selectedTransactions[0].split(); if (!sp.payeeId().isEmpty()) { try { auto payee = MyMoneyFile::instance()->payee(sp.payeeId()); if (!payee.name().isEmpty()) { m_payeeGoto = payee.id(); auto name = payee.name(); name.replace(QRegExp("&(?!&)"), "&&"); pActions[Action::GoToPayee]->setText(i18n("Go to '%1'", name)); } } catch (const MyMoneyException &) { } } try { const auto& t = m_selectedTransactions[0].transaction(); // search the first non-income/non-expense account and use it for the 'goto account' const auto& selectedTransactionSplit = m_selectedTransactions[0].split(); foreach (const auto split, t.splits()) { if (split.id() != selectedTransactionSplit.id()) { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (!acc.isIncomeExpense()) { // for stock accounts we show the portfolio account if (acc.isInvest()) { acc = MyMoneyFile::instance()->account(acc.parentAccountId()); } m_accountGoto = acc.id(); auto name = acc.name(); name.replace(QRegExp("&(?!&)"), "&&"); pActions[Action::GoToAccount]->setText(i18n("Go to '%1'", name)); break; } } } } catch (const MyMoneyException &) { } } } else if (!list.isEmpty()) { sch = MyMoneyFile::instance()->schedule(list.first().scheduleId()); m_selectedTransactions.append(list.first()); ret = false; } emit q->selectByObject(sch, eView::Intent::None); // make sure, we show some neutral menu entry if we don't have an object if (m_payeeGoto.isEmpty()) pActions[Action::GoToPayee]->setText(i18n("Go to payee")); if (m_accountGoto.isEmpty()) pActions[Action::GoToAccount]->setText(i18n("Go to account")); return ret; } /** * Returns @a true if setReconciliationAccount() has been called for * the current loaded account. * * @retval true current account is in reconciliation mode * @retval false current account is not in reconciliation mode */ bool isReconciliationAccount() const { return m_currentAccount.id() == m_reconciliationAccount.id(); } /** * Updates the values on the summary line beneath the register with * the given values. The contents shown differs between reconciliation * mode and normal mode. * * @param actBalance map of account indexed values to be used as actual balance * @param clearedBalance map of account indexed values to be used as cleared balance */ void updateSummaryLine(const QMap& actBalance, const QMap& clearedBalance) { Q_Q(KGlobalLedgerView); const auto file = MyMoneyFile::instance(); m_leftSummaryLabel->show(); m_centerSummaryLabel->show(); m_rightSummaryLabel->show(); if (isReconciliationAccount()) { if (m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { m_leftSummaryLabel->setText(i18n("Statement: %1", m_endingBalance.formatMoney("", m_precision))); m_centerSummaryLabel->setText(i18nc("Cleared balance", "Cleared: %1", clearedBalance[m_currentAccount.id()].formatMoney("", m_precision))); m_totalBalance = clearedBalance[m_currentAccount.id()] - m_endingBalance; } } else { // update summary line in normal mode QDate reconcileDate = m_currentAccount.lastReconciliationDate(); if (reconcileDate.isValid()) { m_leftSummaryLabel->setText(i18n("Last reconciled: %1", QLocale().toString(reconcileDate, QLocale::ShortFormat))); } else { m_leftSummaryLabel->setText(i18n("Never reconciled")); } QPalette palette = m_rightSummaryLabel->palette(); palette.setColor(m_rightSummaryLabel->foregroundRole(), m_leftSummaryLabel->palette().color(q->foregroundRole())); if (m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { m_centerSummaryLabel->setText(i18nc("Cleared balance", "Cleared: %1", clearedBalance[m_currentAccount.id()].formatMoney("", m_precision))); m_totalBalance = actBalance[m_currentAccount.id()]; } else { m_centerSummaryLabel->hide(); MyMoneyMoney balance; MyMoneySecurity base = file->baseCurrency(); QMap::const_iterator it_b; // reset the approximated flag m_balanceIsApproximated = false; for (it_b = actBalance.begin(); it_b != actBalance.end(); ++it_b) { MyMoneyAccount stock = file->account(it_b.key()); QString currencyId = stock.currencyId(); MyMoneySecurity sec = file->security(currencyId); MyMoneyMoney rate(1, 1); if (stock.isInvest()) { currencyId = sec.tradingCurrency(); const MyMoneyPrice &priceInfo = file->price(sec.id(), currencyId); m_balanceIsApproximated |= !priceInfo.isValid(); rate = priceInfo.rate(sec.tradingCurrency()); } if (currencyId != base.id()) { const MyMoneyPrice &priceInfo = file->price(sec.tradingCurrency(), base.id()); m_balanceIsApproximated |= !priceInfo.isValid(); rate = (rate * priceInfo.rate(base.id())).convertPrecision(sec.pricePrecision()); } balance += ((*it_b) * rate).convert(base.smallestAccountFraction()); } m_totalBalance = balance; } m_rightSummaryLabel->setPalette(palette); } // determine the number of selected transactions KMyMoneyRegister::SelectedTransactions selection; m_register->selectedTransactions(selection); q->slotUpdateSummaryLine(selection); } /** * setup the default action according to the current account type */ void setupDefaultAction() { switch (m_currentAccount.accountType()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::AssetLoan: case eMyMoney::Account::Type::Savings: m_action = eWidgets::eRegister::Action::Deposit; break; default: m_action = eWidgets::eRegister::Action::Withdrawal; break; } } // used to store the id of an item and the id of an immediate unselected sibling void storeId(KMyMoneyRegister::RegisterItem *item, QString &id, QString &backupId) { if (item) { // the id of the item id = item->id(); // the id of the item's previous/next unselected item for (KMyMoneyRegister::RegisterItem *it = item->prevItem(); it != 0 && backupId.isEmpty(); it = it->prevItem()) { if (!it->isSelected()) { backupId = it->id(); } } // if we didn't found previous unselected items search trough the next items for (KMyMoneyRegister::RegisterItem *it = item->nextItem(); it != 0 && backupId.isEmpty(); it = it->nextItem()) { if (!it->isSelected()) { backupId = it->id(); } } } } // use to match an item by it's id or a backup id which has a lower precedence void matchItemById(KMyMoneyRegister::RegisterItem **item, KMyMoneyRegister::Transaction* t, QString &id, QString &backupId) { if (!backupId.isEmpty() && t->id() == backupId) *item = t; if (!id.isEmpty() && t->id() == id) { // we found the real thing there's no need for the backup anymore backupId.clear(); *item = t; } } bool canProcessTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { if (m_register->focusItem() == 0) return false; bool rc = true; if (list.warnLevel() == KMyMoneyRegister::SelectedTransaction::OneAccountClosed) { // scan all splits for the first closed account QString closedAccount; foreach(const auto selectedTransaction, list) { foreach(const auto split, selectedTransaction.transaction().splits()) { const auto id = split.accountId(); const auto acc = MyMoneyFile::instance()->account(id); if (acc.isClosed()) { closedAccount = acc.name(); // we're done rc = false; break; } } if(!rc) break; } tooltip = i18n("Cannot process transactions in account %1, which is closed.", closedAccount); showTooltip(tooltip); return false; } if (!m_register->focusItem()->isSelected()) { tooltip = i18n("Cannot process transaction with focus if it is not selected."); showTooltip(tooltip); return false; } tooltip.clear(); return !list.isEmpty(); } void showTooltip(const QString msg) const { QToolTip::showText(m_tooltipPosn, msg); } bool createNewTransaction() { Q_Q(KGlobalLedgerView); auto rc = false; QString txt; if (q->canCreateTransactions(txt)) { rc = q->selectEmptyTransaction(); } return rc; } TransactionEditor* startEdit(const KMyMoneyRegister::SelectedTransactions& list) { Q_Q(KGlobalLedgerView); TransactionEditor* editor = 0; QString txt; if (q->canEditTransactions(list, txt) || q->canCreateTransactions(txt)) { editor = q->startEdit(list); } return editor; } void doDeleteTransactions() { Q_Q(KGlobalLedgerView); KMyMoneyRegister::SelectedTransactions list = m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::iterator it_t; int cnt = list.count(); int i = 0; emit q->slotStatusProgress(0, cnt); MyMoneyFileTransaction ft; const auto file = MyMoneyFile::instance(); try { it_t = list.begin(); while (it_t != list.end()) { + const auto accountId = (*it_t).split().accountId(); + const auto deletedNum = (*it_t).split().number(); + const auto transactionId = (*it_t).transaction().id(); // only remove those transactions that do not reference a closed account if (!file->referencesClosedAccount((*it_t).transaction())) { file->removeTransaction((*it_t).transaction()); // remove all those references in the list of selected transactions // that refer to the same transaction we just removed so that we // will not be caught by an exception later on (see bko #285310) - KMyMoneyRegister::SelectedTransactions::iterator it_td = it_t; - ++it_td; - while (it_td != list.end()) { - if ((*it_t).transaction().id() == (*it_td).transaction().id()) { - it_td = list.erase(it_td); + while (it_t != list.end()) { + if (transactionId == (*it_t).transaction().id()) { + it_t = list.erase(it_t); i++; // bump count of deleted transactions } else { - ++it_td; + ++it_t; } } + + } else { + list.erase(it_t); } + it_t = list.begin(); + // need to ensure "nextCheckNumber" is still correct - auto acc = file->account((*it_t).split().accountId()); + auto acc = file->account(accountId); // the "lastNumberUsed" might have been the txn number deleted // so adjust it - QString deletedNum = (*it_t).split().number(); if (deletedNum == acc.value("lastNumberUsed")) { // decrement deletedNum and set new "lastNumberUsed" QString num = KMyMoneyUtils::getAdjacentNumber(deletedNum, -1); acc.setValue("lastNumberUsed", num); file->modifyAccount(acc); } - list.erase(it_t); - it_t = list.begin(); emit q->slotStatusProgress(i++, 0); } ft.commit(); + } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to delete transaction(s)"), e.what()); } emit q->slotStatusProgress(-1, -1); } void deleteTransactionEditor() { // make sure, we don't use the transaction editor pointer // anymore from now on auto p = m_transactionEditor; m_transactionEditor = nullptr; delete p; } void transactionUnmatch() { Q_Q(KGlobalLedgerView); KMyMoneyRegister::SelectedTransactions::const_iterator it; MyMoneyFileTransaction ft; try { for (it = m_selectedTransactions.constBegin(); it != m_selectedTransactions.constEnd(); ++it) { if ((*it).split().isMatched()) { TransactionMatcher matcher(m_currentAccount); matcher.unmatch((*it).transaction(), (*it).split()); } } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to unmatch the selected transactions"), e.what()); } } void transactionMatch() { Q_Q(KGlobalLedgerView); if (m_selectedTransactions.count() != 2) return; MyMoneyTransaction startMatchTransaction; MyMoneyTransaction endMatchTransaction; MyMoneySplit startSplit; MyMoneySplit endSplit; KMyMoneyRegister::SelectedTransactions::const_iterator it; KMyMoneyRegister::SelectedTransactions toBeDeleted; for (it = m_selectedTransactions.constBegin(); it != m_selectedTransactions.constEnd(); ++it) { if ((*it).transaction().isImported()) { if (endMatchTransaction.id().isEmpty()) { endMatchTransaction = (*it).transaction(); endSplit = (*it).split(); toBeDeleted << *it; } else { //This is a second imported transaction, we still want to merge startMatchTransaction = (*it).transaction(); startSplit = (*it).split(); } } else if (!(*it).split().isMatched()) { if (startMatchTransaction.id().isEmpty()) { startMatchTransaction = (*it).transaction(); startSplit = (*it).split(); } else { endMatchTransaction = (*it).transaction(); endSplit = (*it).split(); toBeDeleted << *it; } } } #if 0 KMergeTransactionsDlg dlg(m_selectedAccount); dlg.addTransaction(startMatchTransaction); dlg.addTransaction(endMatchTransaction); if (dlg.exec() == QDialog::Accepted) #endif { MyMoneyFileTransaction ft; try { if (startMatchTransaction.id().isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("No manually entered transaction selected for matching")); if (endMatchTransaction.id().isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("No imported transaction selected for matching")); TransactionMatcher matcher(m_currentAccount); matcher.match(startMatchTransaction, startSplit, endMatchTransaction, endSplit, true); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to match the selected transactions"), e.what()); } } } /** * Mark the selected transactions as provided by @a flag. If * flag is @a MyMoneySplit::Unknown, the future state depends * on the current stat of the split's flag according to the * following table: * * - NotReconciled --> Cleared * - Cleared --> Reconciled * - Reconciled --> NotReconciled */ void markTransaction(eMyMoney::Split::State flag) { Q_Q(KGlobalLedgerView); auto list = m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; auto cnt = list.count(); auto i = 0; emit q->slotStatusProgress(0, cnt); MyMoneyFileTransaction ft; try { for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { // turn on signals before we modify the last entry in the list cnt--; MyMoneyFile::instance()->blockSignals(cnt != 0); // get a fresh copy auto t = MyMoneyFile::instance()->transaction((*it_t).transaction().id()); auto sp = t.splitById((*it_t).split().id()); if (sp.reconcileFlag() != flag) { if (flag == eMyMoney::Split::State::Unknown) { if (m_reconciliationAccount.id().isEmpty()) { // in normal mode we cycle through all states switch (sp.reconcileFlag()) { case eMyMoney::Split::State::NotReconciled: sp.setReconcileFlag(eMyMoney::Split::State::Cleared); break; case eMyMoney::Split::State::Cleared: sp.setReconcileFlag(eMyMoney::Split::State::Reconciled); break; case eMyMoney::Split::State::Reconciled: sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled); break; default: break; } } else { // in reconciliation mode we skip the reconciled state switch (sp.reconcileFlag()) { case eMyMoney::Split::State::NotReconciled: sp.setReconcileFlag(eMyMoney::Split::State::Cleared); break; case eMyMoney::Split::State::Cleared: sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled); break; default: break; } } } else { sp.setReconcileFlag(flag); } t.modifySplit(sp); MyMoneyFile::instance()->modifyTransaction(t); } emit q->slotStatusProgress(i++, 0); } emit q->slotStatusProgress(-1, -1); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to modify transaction"), e.what()); } } // move a stock transaction from one investment account to another void moveInvestmentTransaction(const QString& /*fromId*/, const QString& toId, const MyMoneyTransaction& tx) { MyMoneyAccount toInvAcc = MyMoneyFile::instance()->account(toId); MyMoneyTransaction t(tx); // first determine which stock we are dealing with. // fortunately, investment transactions have only one stock involved QString stockAccountId; QString stockSecurityId; MyMoneySplit s; foreach (const auto split, t.splits()) { stockAccountId = split.accountId(); stockSecurityId = MyMoneyFile::instance()->account(stockAccountId).currencyId(); if (!MyMoneyFile::instance()->security(stockSecurityId).isCurrency()) { s = split; break; } } // Now check the target investment account to see if it // contains a stock with this id QString newStockAccountId; foreach (const auto sAccount, toInvAcc.accountList()) { if (MyMoneyFile::instance()->account(sAccount).currencyId() == stockSecurityId) { newStockAccountId = sAccount; break; } } // if it doesn't exist, we need to add it as a copy of the old one // no 'copyAccount()' function?? if (newStockAccountId.isEmpty()) { MyMoneyAccount stockAccount = MyMoneyFile::instance()->account(stockAccountId); MyMoneyAccount newStock; newStock.setName(stockAccount.name()); newStock.setNumber(stockAccount.number()); newStock.setDescription(stockAccount.description()); newStock.setInstitutionId(stockAccount.institutionId()); newStock.setOpeningDate(stockAccount.openingDate()); newStock.setAccountType(stockAccount.accountType()); newStock.setCurrencyId(stockAccount.currencyId()); newStock.setClosed(stockAccount.isClosed()); MyMoneyFile::instance()->addAccount(newStock, toInvAcc); newStockAccountId = newStock.id(); } // now update the split and the transaction s.setAccountId(newStockAccountId); t.modifySplit(s); MyMoneyFile::instance()->modifyTransaction(t); } void createTransactionMoveMenu() { Q_Q(KGlobalLedgerView); if (!m_moveToAccountSelector) { auto menu = pMenus[eMenu::Menu::MoveTransaction]; if (menu ) { auto accountSelectorAction = new QWidgetAction(menu); m_moveToAccountSelector = new KMyMoneyAccountSelector(menu, 0, false); m_moveToAccountSelector->setObjectName("transaction_move_menu_selector"); accountSelectorAction->setDefaultWidget(m_moveToAccountSelector); menu->addAction(accountSelectorAction); q->connect(m_moveToAccountSelector, &QObject::destroyed, q, &KGlobalLedgerView::slotObjectDestroyed); q->connect(m_moveToAccountSelector, &KMyMoneySelector::itemSelected, q, &KGlobalLedgerView::slotMoveToAccount); } } } QList > automaticReconciliation(const MyMoneyAccount &account, const QList > &transactions, const MyMoneyMoney &amount) { Q_Q(KGlobalLedgerView); static const int NR_OF_STEPS_LIMIT = 60000; static const int PROGRESSBAR_STEPS = 1000; QList > result = transactions; // optimize the most common case - all transactions should be cleared QListIterator > itTransactionSplitResult(result); MyMoneyMoney transactionsBalance; while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); transactionsBalance += transactionSplit.second.shares(); } if (amount == transactionsBalance) { result = transactions; return result; } // only one transaction is uncleared itTransactionSplitResult.toFront(); int index = 0; while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); if (transactionsBalance - transactionSplit.second.shares() == amount) { result.removeAt(index); return result; } index++; } // more than one transaction is uncleared - apply the algorithm result.clear(); const auto& security = MyMoneyFile::instance()->security(account.currencyId()); double precision = 0.1 / account.fraction(security); QList sumList; sumList << MyMoneyMoney(); QMap > > sumToComponentsMap; struct restoreStatusMsgHelper { restoreStatusMsgHelper(KGlobalLedgerView* qq) : q(qq) {} ~restoreStatusMsgHelper() { q->slotStatusMsg(QString()); q->slotStatusProgress(-1, -1); } KGlobalLedgerView* q; } restoreHelper(q); q->slotStatusMsg(i18n("Running automatic reconciliation")); q->slotStatusProgress(0, NR_OF_STEPS_LIMIT); // compute the possible matches QListIterator > it_ts(transactions); while (it_ts.hasNext()) { const QPair &transactionSplit = it_ts.next(); QListIterator itSum(sumList); QList tempList; while (itSum.hasNext()) { const MyMoneyMoney &sum = itSum.next(); QList > splitIds; splitIds << qMakePair(transactionSplit.first.id(), transactionSplit.second.id()); if (sumToComponentsMap.contains(sum)) { if (sumToComponentsMap.value(sum).contains(qMakePair(transactionSplit.first.id(), transactionSplit.second.id()))) { continue; } splitIds.append(sumToComponentsMap.value(sum)); } tempList << transactionSplit.second.shares() + sum; sumToComponentsMap[transactionSplit.second.shares() + sum] = splitIds; int size = sumToComponentsMap.size(); if (size % PROGRESSBAR_STEPS == 0) { q->slotStatusProgress(size, 0); } if (size > NR_OF_STEPS_LIMIT) { return result; // it's taking too much resources abort the algorithm } } QList unionList; unionList.append(tempList); unionList.append(sumList); qSort(unionList); sumList.clear(); MyMoneyMoney smallestSumFromUnion = unionList.first(); sumList.append(smallestSumFromUnion); QListIterator itUnion(unionList); while (itUnion.hasNext()) { MyMoneyMoney sumFromUnion = itUnion.next(); if (smallestSumFromUnion < MyMoneyMoney(1 - precision / transactions.size())*sumFromUnion) { smallestSumFromUnion = sumFromUnion; sumList.append(sumFromUnion); } } } q->slotStatusProgress(NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS, 0); if (sumToComponentsMap.contains(amount)) { QListIterator > itTransactionSplit(transactions); while (itTransactionSplit.hasNext()) { const QPair &transactionSplit = itTransactionSplit.next(); const QList > &splitIds = sumToComponentsMap.value(amount); if (splitIds.contains(qMakePair(transactionSplit.first.id(), transactionSplit.second.id()))) { result.append(transactionSplit); } } } #ifdef KMM_DEBUG qDebug("For the amount %s a number of %d possible sums where computed from the set of %d transactions: ", qPrintable(MyMoneyUtils::formatMoney(amount, security)), sumToComponentsMap.size(), transactions.size()); #endif return result; } KGlobalLedgerView *q_ptr; MousePressFilter *m_mousePressFilter; KMyMoneyRegister::RegisterSearchLineWidget* m_registerSearchLine; // QString m_reconciliationAccount; QDate m_reconciliationDate; MyMoneyMoney m_endingBalance; int m_precision; bool m_recursion; bool m_showDetails; eWidgets::eRegister::Action m_action; // models AccountNamesFilterProxyModel *m_filterProxyModel; // widgets KMyMoneyAccountCombo* m_accountComboBox; MyMoneyMoney m_totalBalance; bool m_balanceIsApproximated; // frames QFrame* m_toolbarFrame; QFrame* m_registerFrame; QFrame* m_buttonFrame; QFrame* m_formFrame; QFrame* m_summaryFrame; // widgets KMyMoneyRegister::Register* m_register; KToolBar* m_buttonbar; /** * This member holds the currently selected account */ MyMoneyAccount m_currentAccount; QString m_lastSelectedAccountID; MyMoneyAccount m_reconciliationAccount; /** * This member holds the transaction list */ QList > m_transactionList; QLabel* m_leftSummaryLabel; QLabel* m_centerSummaryLabel; QLabel* m_rightSummaryLabel; KMyMoneyTransactionForm::TransactionForm* m_form; /** * This member holds the load state of page */ bool m_needLoad; bool m_newAccountLoaded; bool m_inEditMode; QWidgetList m_tabOrderWidgets; QPoint m_tooltipPosn; KMyMoneyRegister::SelectedTransactions m_selectedTransactions; /** * This member keeps the date that was used as the last posting date. * It will be updated whenever the user modifies the post date * and is used to preset the posting date when new transactions are created. * This member is initialised to the current date when the program is started. */ static QDate m_lastPostDate; // pointer to the current transaction editor QPointer m_transactionEditor; // id's that need to be remembered QString m_accountGoto, m_payeeGoto; QString m_lastPayeeEnteredId; QScopedPointer m_balanceWarning; KMyMoneyAccountSelector* m_moveToAccountSelector; // Reconciliation dialog KEndingBalanceDlg* m_endingBalanceDlg; KFindTransactionDlg* m_searchDlg; }; #endif diff --git a/kmymoney/views/khomeview.cpp b/kmymoney/views/khomeview.cpp index d6ad25753..3b6a44c8d 100644 --- a/kmymoney/views/khomeview.cpp +++ b/kmymoney/views/khomeview.cpp @@ -1,221 +1,218 @@ /*************************************************************************** khomeview.cpp - description ------------------- begin : Tue Jan 22 2002 copyright : (C) 2000-2002 by Michael Edwardes Javier Campos Morales Felix Rodriguez John C - Thomas Baumgart Kevin Tambascio (C) 2017 by Łukasz Wojniłowicz + (C) 2002-2019 by Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "khomeview_p.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes +#include "kmm_printer.h" + KHomeView::KHomeView(QWidget *parent) : KMyMoneyViewBase(*new KHomeViewPrivate(this), parent) { } KHomeView::~KHomeView() { } void KHomeView::slotAdjustScrollPos() { #ifndef ENABLE_WEBENGINE Q_D(KHomeView); if (d && d->m_view && d->m_view->page() && d->m_view->page()->mainFrame()) { d->m_view->page()->mainFrame()->setScrollBarValue(Qt::Vertical, d->m_scrollBarPos); } #endif } void KHomeView::wheelEvent(QWheelEvent* event) { Q_D(KHomeView); // Zoom text on Ctrl + Scroll if (event->modifiers() & Qt::CTRL) { qreal factor = d->m_view->zoomFactor(); if (event->delta() > 0) factor += 0.1; else if (event->delta() < 0) factor -= 0.1; d->m_view->setZoomFactor(factor); event->accept(); return; } } void KHomeView::executeCustomAction(eView::Action action) { Q_D(KHomeView); switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::Print: slotPrintView(); break; case eView::Action::CleanupBeforeFileClose: d->m_view->setHtml(KWelcomePage::welcomePage(), QUrl("file://")); break; default: break; } } void KHomeView::refresh() { Q_D(KHomeView); if (isVisible()) { d->loadView(); d->m_needsRefresh = false; } else { d->m_needsRefresh = true; } } void KHomeView::showEvent(QShowEvent* event) { Q_D(KHomeView); if (d->m_needLoad) d->init(); emit customActionRequested(View::Home, eView::Action::AboutToShow); if (d->m_needsRefresh) refresh(); QWidget::showEvent(event); } void KHomeView::slotPrintView() { Q_D(KHomeView); if (d->m_view) { - d->m_currentPrinter = new QPrinter(); - QPointer dialog = new QPrintDialog(d->m_currentPrinter, this); - dialog->setWindowTitle(QString()); - if (dialog->exec() != QDialog::Accepted) { - delete d->m_currentPrinter; - d->m_currentPrinter = nullptr; - return; + auto printer = KMyMoneyPrinter::startPrint(); + if (printer != nullptr) { + #ifdef ENABLE_WEBENGINE + d->m_view->page()->print(printer, [=] (bool) {}); + #else + d->m_view->print(printer); + #endif } - #ifdef ENABLE_WEBENGINE - d->m_view->page()->print(d->m_currentPrinter, [=] (bool) {delete d->m_currentPrinter; d->m_currentPrinter = nullptr;}); - #else - d->m_view->print(d->m_currentPrinter); - #endif } } void KHomeView::slotOpenUrl(const QUrl &url) { Q_D(KHomeView); QString protocol = url.scheme(); QString view = url.fileName(); // empty view -> bail out if (view.isEmpty()) return; QUrlQuery query(url); QString id = query.queryItemValue("id"); QString mode = query.queryItemValue("mode"); const auto file = MyMoneyFile::instance(); if (protocol == QLatin1String("https")) { QDesktopServices::openUrl(url); } else if (protocol == QLatin1String("mailto")) { QDesktopServices::openUrl(url); } else { KXmlGuiWindow* mw = KMyMoneyUtils::mainWindow(); Q_CHECK_PTR(mw); if (view == VIEW_LEDGER) { emit selectByVariant(QVariantList {QVariant(id), QVariant(QString())}, eView::Intent::ShowTransaction); } else if (view == VIEW_SCHEDULE) { if (mode == QLatin1String("enter")) { emit selectByObject(file->schedule(id), eView::Intent::None); QTimer::singleShot(0, pActions[eMenu::Action::EnterSchedule], SLOT(trigger())); } else if (mode == QLatin1String("edit")) { emit selectByObject(file->schedule(id), eView::Intent::None); QTimer::singleShot(0, pActions[eMenu::Action::EditSchedule], SLOT(trigger())); } else if (mode == QLatin1String("skip")) { emit selectByObject(file->schedule(id), eView::Intent::None); QTimer::singleShot(0, pActions[eMenu::Action::SkipSchedule], SLOT(trigger())); } else if (mode == QLatin1String("full")) { d->m_showAllSchedules = true; d->loadView(); } else if (mode == QLatin1String("reduced")) { d->m_showAllSchedules = false; d->loadView(); } } else if (view == VIEW_REPORTS) { emit selectByObject(file->report(id), eView::Intent::OpenObject); // emit openObjectRequested(file->report(id)); } else if (view == VIEW_WELCOME) { if (mode == QLatin1String("whatsnew")) d->m_view->setHtml(KWelcomePage::whatsNewPage(), QUrl("file://")); else d->m_view->setHtml(KWelcomePage::welcomePage(), QUrl("file://")); } else if (view == QLatin1String("action")) { QTimer::singleShot(0, mw->actionCollection()->action(id), SLOT(trigger())); } else if (view == VIEW_HOME) { QList list; // it could be, that we don't even have a storage object attached. // in this case the call to accountList() will throw an MyMoneyException // which we catch here and treat it as 'no accounts found'. try { MyMoneyFile::instance()->accountList(list); } catch(const MyMoneyException& e) { } if (list.count() == 0) { KMessageBox::information(this, i18n("Before KMyMoney can give you detailed information about your financial status, you need to create at least one account. Until then, KMyMoney shows the welcome page instead.")); } d->loadView(); } else { qDebug("Unknown view '%s' in slotOpenURL()", qPrintable(view)); } } } // Make sure, that these definitions are only used within this file // this does not seem to be necessary, but when building RPMs the // build option 'final' is used and all CPP files are concatenated. // So it could well be, that in another CPP file these definitions // are also used. #undef VIEW_LEDGER #undef VIEW_SCHEDULE #undef VIEW_WELCOME #undef VIEW_HOME #undef VIEW_REPORTS diff --git a/kmymoney/views/khomeview_p.h b/kmymoney/views/khomeview_p.h index afa268a58..c58f25871 100644 --- a/kmymoney/views/khomeview_p.h +++ b/kmymoney/views/khomeview_p.h @@ -1,1855 +1,1852 @@ /*************************************************************************** khomeview_p.h - description ------------------- begin : Tue Jan 22 2002 copyright : (C) 2000-2002 by Michael Edwardes Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KHOMEVIEW_P_H #define KHOMEVIEW_P_H #include "khomeview.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include -#include #ifdef ENABLE_WEBENGINE #include #else #include #include #endif // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyviewbase_p.h" #include "mymoneyutils.h" #include "kmymoneyutils.h" #include "kwelcomepage.h" #include "kmymoneysettings.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyprice.h" #include "mymoneyreport.h" #include "mymoneymoney.h" #include "mymoneyforecast.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "icons.h" #include "kmymoneywebpage.h" #include "mymoneyschedule.h" #include "mymoneysecurity.h" #include "mymoneyexception.h" #include "kmymoneyplugin.h" #include "mymoneyenums.h" #include "menuenums.h" #include "plugins/views/reports/reportsviewenums.h" #define VIEW_LEDGER "ledger" #define VIEW_SCHEDULE "schedule" #define VIEW_WELCOME "welcome" #define VIEW_HOME "home" #define VIEW_REPORTS "reports" using namespace Icons; using namespace eMyMoney; /** * @brief Converts a QPixmap to an data URI scheme * * According to RFC 2397 * * @param pixmap Source to convert * @return full data URI */ QString QPixmapToDataUri(const QPixmap& pixmap) { QImage image(pixmap.toImage()); QByteArray byteArray; QBuffer buffer(&byteArray); buffer.open(QIODevice::WriteOnly); image.save(&buffer, "PNG"); // writes the image in PNG format inside the buffer return QLatin1String("data:image/png;base64,") + QString(byteArray.toBase64()); } bool accountNameLess(const MyMoneyAccount &acc1, const MyMoneyAccount &acc2) { return acc1.name().localeAwareCompare(acc2.name()) < 0; } class KHomeViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KHomeView) public: explicit KHomeViewPrivate(KHomeView *qq) : KMyMoneyViewBasePrivate(), q_ptr(qq), m_view(nullptr), m_showAllSchedules(false), m_needLoad(true), m_netWorthGraphLastValidSize(400, 300), - m_currentPrinter(nullptr), m_scrollBarPos(0) { } ~KHomeViewPrivate() { // if user wants to remember the font size, store it here if (KMyMoneySettings::rememberZoomFactor() && m_view) { KMyMoneySettings::setZoomFactor(m_view->zoomFactor()); KMyMoneySettings::self()->save(); } } /** * Definition of bitmap used as argument for showAccounts(). */ enum paymentTypeE { Preferred = 1, ///< show preferred accounts Payment = 2 ///< show payment accounts }; void init() { Q_Q(KHomeView); m_needLoad = false; auto vbox = new QVBoxLayout(q); q->setLayout(vbox); vbox->setSpacing(6); vbox->setMargin(0); #ifdef ENABLE_WEBENGINE m_view = new QWebEngineView(q); #else m_view = new KWebView(q); #endif m_view->setPage(new MyQWebEnginePage(m_view)); vbox->addWidget(m_view); #ifdef ENABLE_WEBENGINE q->connect(m_view->page(), &QWebEnginePage::urlChanged, q, &KHomeView::slotOpenUrl); #else m_view->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); q->connect(m_view->page(), &KWebPage::linkClicked, q, &KHomeView::slotOpenUrl); #endif q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KHomeView::refresh); } /** * Print an account and its balance and limit */ void showAccountEntry(const MyMoneyAccount& acc, const MyMoneyMoney& value, const MyMoneyMoney& valueToMinBal, const bool showMinBal) { MyMoneyFile* file = MyMoneyFile::instance(); QString tmp; MyMoneySecurity currency = file->currency(acc.currencyId()); QString amount; QString amountToMinBal; //format amounts amount = MyMoneyUtils::formatMoney(value, acc, currency); amount.replace(QChar(' '), " "); if (showMinBal) { amountToMinBal = MyMoneyUtils::formatMoney(valueToMinBal, acc, currency); amountToMinBal.replace(QChar(' '), " "); } QString cellStatus, pathOK, pathTODO, pathNotOK; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { //show account's online-status pathOK = QPixmapToDataUri(Icons::get(Icon::DialogOKApply).pixmap(QSize(16,16))); pathTODO = QPixmapToDataUri(Icons::get(Icon::MailReceive).pixmap(QSize(16,16))); pathNotOK = QPixmapToDataUri(Icons::get(Icon::DialogCancel).pixmap(QSize(16,16))); if (acc.value("lastImportedTransactionDate").isEmpty() || acc.value("lastStatementBalance").isEmpty()) cellStatus = '-'; else if (file->hasMatchingOnlineBalance(acc)) { if (file->hasNewerTransaction(acc.id(), QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate))) cellStatus = QString("").arg(pathTODO); else cellStatus = QString("").arg(pathOK); } else cellStatus = QString("").arg(pathNotOK); tmp = QString("%1").arg(cellStatus); } tmp += QString("") + link(VIEW_LEDGER, QString("?id=%1").arg(acc.id())) + acc.name() + linkend() + ""; int countNotMarked = 0, countCleared = 0, countNotReconciled = 0; QString countStr; if (KMyMoneySettings::showCountOfUnmarkedTransactions() || KMyMoneySettings::showCountOfNotReconciledTransactions()) countNotMarked = m_transactionStats[acc.id()][(int)Split::State::NotReconciled]; if (KMyMoneySettings::showCountOfClearedTransactions() || KMyMoneySettings::showCountOfNotReconciledTransactions()) countCleared = m_transactionStats[acc.id()][(int)Split::State::Cleared]; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) countNotReconciled = countNotMarked + countCleared; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) { if (countNotMarked) countStr = QString("%1").arg(countNotMarked); else countStr = '-'; tmp += QString("%1").arg(countStr); } if (KMyMoneySettings::showCountOfClearedTransactions()) { if (countCleared) countStr = QString("%1").arg(countCleared); else countStr = '-'; tmp += QString("%1").arg(countStr); } if (KMyMoneySettings::showCountOfNotReconciledTransactions()) { if (countNotReconciled) countStr = QString("%1").arg(countNotReconciled); else countStr = '-'; tmp += QString("%1").arg(countStr); } if (KMyMoneySettings::showDateOfLastReconciliation()) { const auto lastReconciliationDate = acc.lastReconciliationDate().toString(Qt::SystemLocaleShortDate).replace(QChar(' '), " "); tmp += QString("%1").arg(lastReconciliationDate); } //show account balance tmp += QString("%1").arg(showColoredAmount(amount, value.isNegative())); //show minimum balance column if requested if (showMinBal) { //if it is an investment, show minimum balance empty if (acc.accountType() == Account::Type::Investment) { tmp += QString(" "); } else { //show minimum balance entry tmp += QString("%1").arg(showColoredAmount(amountToMinBal, valueToMinBal.isNegative())); } } // qDebug("accountEntry = '%s'", tmp.toLatin1()); m_html += tmp; } void showAccountEntry(const MyMoneyAccount& acc) { const auto file = MyMoneyFile::instance(); MyMoneyMoney value; bool showLimit = KMyMoneySettings::showLimitInfo(); if (acc.accountType() == Account::Type::Investment) { //investment accounts show the balances of all its subaccounts value = investmentBalance(acc); //investment accounts have no minimum balance showAccountEntry(acc, value, MyMoneyMoney(), showLimit); } else { //get balance for normal accounts value = file->balance(acc.id(), QDate::currentDate()); if (acc.currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price(acc.tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; baseValue = baseValue.convert(file->baseCurrency().smallestAccountFraction()); m_total += baseValue; } else { m_total += value; } //if credit card or checkings account, show maximum credit if (acc.accountType() == Account::Type::CreditCard || acc.accountType() == Account::Type::Checkings) { QString maximumCredit = acc.value("maxCreditAbsolute"); if (maximumCredit.isEmpty()) { maximumCredit = acc.value("minBalanceAbsolute"); } MyMoneyMoney maxCredit = MyMoneyMoney(maximumCredit); showAccountEntry(acc, value, value - maxCredit, showLimit); } else { //otherwise use minimum balance QString minimumBalance = acc.value("minBalanceAbsolute"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); showAccountEntry(acc, value, value - minBalance, showLimit); } } } /** * @param acc the investment account * @return the balance in the currency of the investment account */ MyMoneyMoney investmentBalance(const MyMoneyAccount& acc) { auto file = MyMoneyFile::instance(); auto value = file->balance(acc.id(), QDate::currentDate()); foreach (const auto accountID, acc.accountList()) { auto stock = file->account(accountID); if (!stock.isClosed()) { try { MyMoneyMoney val; MyMoneyMoney balance = file->balance(stock.id(), QDate::currentDate()); MyMoneySecurity security = file->security(stock.currencyId()); const MyMoneyPrice &price = file->price(stock.currencyId(), security.tradingCurrency()); val = (balance * price.rate(security.tradingCurrency())).convertPrecision(security.pricePrecision()); // adjust value of security to the currency of the account MyMoneySecurity accountCurrency = file->currency(acc.currencyId()); val = val * file->price(security.tradingCurrency(), accountCurrency.id()).rate(accountCurrency.id()); val = val.convert(acc.fraction()); value += val; } catch (const MyMoneyException &e) { qWarning("%s", qPrintable(QString("cannot convert stock balance of %1 to base currency: %2").arg(stock.name(), e.what()))); } } } return value; } /** * Print text in the color set for negative numbers, if @p amount is negative * abd @p isNegative is true */ QString showColoredAmount(const QString& amount, bool isNegative) { if (isNegative) { //if negative, get the settings for negative numbers return QString("%2").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name(), amount); } //if positive, return the same string return amount; } /** * Run the forecast */ void doForecast() { //clear m_accountList because forecast is about to changed m_accountList.clear(); //reinitialize the object m_forecast = KMyMoneyUtils::forecast(); //If forecastDays lower than accountsCycle, adjust to the first cycle if (m_forecast.accountsCycle() > m_forecast.forecastDays()) m_forecast.setForecastDays(m_forecast.accountsCycle()); //Get all accounts of the right type to calculate forecast m_forecast.doForecast(); } /** * Calculate the forecast balance after a payment has been made */ MyMoneyMoney forecastPaymentBalance(const MyMoneyAccount& acc, const MyMoneyMoney& payment, QDate& paymentDate) { //if paymentDate before or equal to currentDate set it to current date plus 1 //so we get to accumulate forecast balance correctly if (paymentDate <= QDate::currentDate()) paymentDate = QDate::currentDate().addDays(1); //check if the account is already there if (m_accountList.find(acc.id()) == m_accountList.end() || m_accountList[acc.id()].find(paymentDate) == m_accountList[acc.id()].end()) { if (paymentDate == QDate::currentDate()) { m_accountList[acc.id()][paymentDate] = m_forecast.forecastBalance(acc, paymentDate); } else { m_accountList[acc.id()][paymentDate] = m_forecast.forecastBalance(acc, paymentDate.addDays(-1)); } } m_accountList[acc.id()][paymentDate] = m_accountList[acc.id()][paymentDate] + payment; return m_accountList[acc.id()][paymentDate]; } void loadView() { Q_Q(KHomeView); m_view->setZoomFactor(KMyMoneySettings::zoomFactor()); QList list; if (MyMoneyFile::instance()->storage()) { MyMoneyFile::instance()->accountList(list); } if (list.isEmpty()) { m_view->setHtml(KWelcomePage::welcomePage(), QUrl("file://")); } else { // preload transaction statistics m_transactionStats = MyMoneyFile::instance()->countTransactionsWithSpecificReconciliationState(); // keep current location on page m_scrollBarPos = 0; #ifndef ENABLE_WEBENGINE m_scrollBarPos = m_view->page()->mainFrame()->scrollBarValue(Qt::Vertical); #endif //clear the forecast flag so it will be reloaded m_forecast.setForecastDone(false); const QString filename = QStandardPaths::locate(QStandardPaths::AppConfigLocation, "html/kmymoney.css"); QString header = QString("\n\n").arg(QUrl::fromLocalFile(filename).url()); header += KMyMoneyUtils::variableCSS(); header += "\n"; QString footer = "\n"; m_html.clear(); m_html += header; m_html += QString("
%1
").arg(i18n("Your Financial Summary")); QStringList settings = KMyMoneySettings::listOfItems(); QStringList::ConstIterator it; for (it = settings.constBegin(); it != settings.constEnd(); ++it) { int option = (*it).toInt(); if (option > 0) { switch (option) { case 1: // payments showPayments(); break; case 2: // preferred accounts showAccounts(Preferred, i18n("Preferred Accounts")); break; case 3: // payment accounts // Check if preferred accounts are shown separately if (settings.contains("2")) { showAccounts(static_cast(Payment | Preferred), i18n("Payment Accounts")); } else { showAccounts(Payment, i18n("Payment Accounts")); } break; case 4: // favorite reports showFavoriteReports(); break; case 5: // forecast showForecast(); break; case 6: // net worth graph over all accounts showNetWorthGraph(); break; + case 7: // forecast (history) - currently unused + break; case 8: // assets and liabilities showAssetsLiabilities(); break; case 9: // budget showBudget(); break; case 10: // cash flow summary showCashFlowSummary(); break; - - } m_html += "
 
\n"; } } m_html += "
"; m_html += link(VIEW_WELCOME, QString()) + i18n("Show KMyMoney welcome page") + linkend(); m_html += "
"; m_html += "
"; m_html += footer; m_view->setHtml(m_html, QUrl("file://")); #ifndef ENABLE_WEBENGINE if (m_scrollBarPos) { QMetaObject::invokeMethod(q, "slotAdjustScrollPos", Qt::QueuedConnection); } #endif } } void showNetWorthGraph() { Q_Q(KHomeView); // Adjust the size QSize netWorthGraphSize = q->size(); netWorthGraphSize -= QSize(80, 30); m_netWorthGraphLastValidSize = netWorthGraphSize; m_html += QString("
%1
\n
 
\n").arg(i18n("Net Worth Forecast")); m_html += QString(""); m_html += QString(""); if (const auto reportsPlugin = pPlugins.data.value(QStringLiteral("reportsview"), nullptr)) { const auto variantReport = reportsPlugin->requestData(QString(), eWidgetPlugin::WidgetType::NetWorthForecast); if (!variantReport.isNull()) { auto report = variantReport.value(); report->resize(m_netWorthGraphLastValidSize); m_html += QString("").arg(QPixmapToDataUri(report->grab())); delete report; } } else { m_html += QString("").arg(i18n("Enable reports plugin to see this chart.")); } m_html += QString(""); m_html += QString("
\"Networth\"
%1
"); } void showPayments() { MyMoneyFile* file = MyMoneyFile::instance(); QList overdues; QList schedule; int i = 0; //if forecast has not been executed yet, do it. if (!m_forecast.isForecastDone()) doForecast(); schedule = file->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate::currentDate(), QDate::currentDate().addMonths(1), false); overdues = file->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), true); if (schedule.empty() && overdues.empty()) return; // HACK // Remove the finished schedules QList::Iterator d_it; //regular schedules d_it = schedule.begin(); while (d_it != schedule.end()) { if ((*d_it).isFinished()) { d_it = schedule.erase(d_it); continue; } ++d_it; } //overdue schedules d_it = overdues.begin(); while (d_it != overdues.end()) { if ((*d_it).isFinished()) { d_it = overdues.erase(d_it); continue; } ++d_it; } m_html += "
"; m_html += QString("
%1
\n").arg(i18n("Payments")); if (!overdues.isEmpty()) { m_html += "
 
\n"; qSort(overdues); QList::Iterator it; QList::Iterator it_f; m_html += ""; m_html += QString("\n").arg(showColoredAmount(i18n("Overdue payments"), true)); m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; for (it = overdues.begin(); it != overdues.end(); ++it) { // determine number of overdue payments int cnt = (*it).transactionsRemainingUntil(QDate::currentDate().addDays(-1)); m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showPaymentEntry(*it, cnt); m_html += ""; } m_html += "
%1
"; m_html += i18n("Date"); m_html += ""; m_html += i18n("Schedule"); m_html += ""; m_html += i18n("Account"); m_html += ""; m_html += i18n("Amount"); m_html += ""; m_html += i18n("Balance after"); m_html += "
"; } if (!schedule.isEmpty()) { qSort(schedule); // Extract todays payments if any QList todays; QList::Iterator t_it; for (t_it = schedule.begin(); t_it != schedule.end();) { if ((*t_it).adjustedNextDueDate() == QDate::currentDate()) { todays.append(*t_it); (*t_it).setNextDueDate((*t_it).nextPayment(QDate::currentDate())); // if adjustedNextDueDate is still currentDate then remove it from // scheduled payments if ((*t_it).adjustedNextDueDate() == QDate::currentDate()) { t_it = schedule.erase(t_it); continue; } } ++t_it; } if (todays.count() > 0) { m_html += "
 
\n"; m_html += ""; m_html += QString("\n").arg(i18n("Today's due payments")); m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; for (t_it = todays.begin(); t_it != todays.end(); ++t_it) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showPaymentEntry(*t_it); m_html += ""; } m_html += "
%1
"; m_html += i18n("Date"); m_html += ""; m_html += i18n("Schedule"); m_html += ""; m_html += i18n("Account"); m_html += ""; m_html += i18n("Amount"); m_html += ""; m_html += i18n("Balance after"); m_html += "
"; } if (!schedule.isEmpty()) { m_html += "
 
\n"; QList::Iterator it; m_html += ""; m_html += QString("\n").arg(i18n("Future payments")); m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; // show all or the first 6 entries int cnt; cnt = (m_showAllSchedules) ? -1 : 6; bool needMoreLess = m_showAllSchedules; QDate lastDate = QDate::currentDate().addMonths(1); qSort(schedule); do { it = schedule.begin(); if (it == schedule.end()) break; // if the next due date is invalid (schedule is finished) // we remove it from the list QDate nextDate = (*it).nextDueDate(); if (!nextDate.isValid()) { schedule.erase(it); continue; } if (nextDate > lastDate) break; if (cnt == 0) { needMoreLess = true; break; } // in case we've shown the current recurrence as overdue, // we don't show it here again, but keep the schedule // as it might show up later in the list again if (!(*it).isOverdue()) { if (cnt > 0) --cnt; m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showPaymentEntry(*it); m_html += ""; // for single occurrence we have reported everything so we // better get out of here. if ((*it).occurrence() == Schedule::Occurrence::Once) { schedule.erase(it); continue; } } // if nextPayment returns an invalid date, setNextDueDate will // just skip it, resulting in a loop // we check the resulting date and erase the schedule if invalid if (!((*it).nextPayment((*it).nextDueDate())).isValid()) { schedule.erase(it); continue; } (*it).setNextDueDate((*it).nextPayment((*it).nextDueDate())); qSort(schedule); } while (1); if (needMoreLess) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); m_html += ""; m_html += ""; } m_html += "
%1
"; m_html += i18n("Date"); m_html += ""; m_html += i18n("Schedule"); m_html += ""; m_html += i18n("Account"); m_html += ""; m_html += i18n("Amount"); m_html += ""; m_html += i18n("Balance after"); m_html += "
"; if (m_showAllSchedules) { m_html += link(VIEW_SCHEDULE, QString("?mode=%1").arg("reduced")) + i18nc("Less...", "Show fewer schedules on the list") + linkend(); } else { m_html += link(VIEW_SCHEDULE, QString("?mode=%1").arg("full")) + i18nc("More...", "Show more schedules on the list") + linkend(); } m_html += "
"; } } m_html += "
"; } void showPaymentEntry(const MyMoneySchedule& sched, int cnt = 1) { QString tmp; MyMoneyFile* file = MyMoneyFile::instance(); try { MyMoneyAccount acc = sched.account(); if (!acc.id().isEmpty()) { MyMoneyTransaction t = sched.transaction(); // only show the entry, if it is still active if (!sched.isFinished()) { MyMoneySplit sp = t.splitByAccount(acc.id(), true); QString pathEnter = QPixmapToDataUri(Icons::get(Icon::KeyEnter).pixmap(QSize(16,16))); QString pathSkip = QPixmapToDataUri(Icons::get(Icon::MediaSkipForward).pixmap(QSize(16,16))); //show payment date tmp = QString("") + QLocale().toString(sched.adjustedNextDueDate(), QLocale::ShortFormat) + ""; if (!pathEnter.isEmpty()) tmp += link(VIEW_SCHEDULE, QString("?id=%1&mode=enter").arg(sched.id()), i18n("Enter schedule")) + QString("").arg(pathEnter) + linkend(); if (!pathSkip.isEmpty()) tmp += " " + link(VIEW_SCHEDULE, QString("?id=%1&mode=skip").arg(sched.id()), i18n("Skip schedule")) + QString("").arg(pathSkip) + linkend(); tmp += QString(" "); tmp += link(VIEW_SCHEDULE, QString("?id=%1&mode=edit").arg(sched.id()), i18n("Edit schedule")) + sched.name() + linkend(); //show quantity of payments overdue if any if (cnt > 1) tmp += i18np(" (%1 payment)", " (%1 payments)", cnt); //show account of the main split tmp += ""; tmp += QString(file->account(acc.id()).name()); //show amount of the schedule tmp += ""; const MyMoneySecurity& currency = MyMoneyFile::instance()->currency(acc.currencyId()); MyMoneyMoney payment = MyMoneyMoney(sp.value(t.commodity(), acc.currencyId()) * cnt); QString amount = MyMoneyUtils::formatMoney(payment, acc, currency); amount.replace(QChar(' '), " "); tmp += showColoredAmount(amount, payment.isNegative()); tmp += ""; //show balance after payments tmp += ""; QDate paymentDate = QDate(sched.adjustedNextDueDate()); MyMoneyMoney balanceAfter = forecastPaymentBalance(acc, payment, paymentDate); QString balance = MyMoneyUtils::formatMoney(balanceAfter, acc, currency); balance.replace(QChar(' '), " "); tmp += showColoredAmount(balance, balanceAfter.isNegative()); tmp += ""; // qDebug("paymentEntry = '%s'", tmp.toLatin1()); m_html += tmp; } } } catch (const MyMoneyException &e) { qDebug("Unable to display schedule entry: %s", e.what()); } } void showAccounts(paymentTypeE type, const QString& header) { MyMoneyFile* file = MyMoneyFile::instance(); int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); QList accounts; auto showClosedAccounts = KMyMoneySettings::showAllAccounts(); // get list of all accounts file->accountList(accounts); for (QList::Iterator it = accounts.begin(); it != accounts.end();) { bool removeAccount = false; if (!(*it).isClosed() || showClosedAccounts) { switch ((*it).accountType()) { case Account::Type::Expense: case Account::Type::Income: // never show a category account // Note: This might be different in a future version when // the homepage also shows category based information removeAccount = true; break; // Asset and Liability accounts are only shown if they // have the preferred flag set case Account::Type::Asset: case Account::Type::Liability: case Account::Type::Investment: // if preferred accounts are requested, then keep in list if ((*it).value("PreferredAccount") != "Yes" || (type & Preferred) == 0) { removeAccount = true; } break; // Check payment accounts. If payment and preferred is selected, // then always show them. If only payment is selected, then // show only if preferred flag is not set. case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::CreditCard: switch (type & (Payment | Preferred)) { case Payment: if ((*it).value("PreferredAccount") == "Yes") removeAccount = true; break; case Preferred: if ((*it).value("PreferredAccount") != "Yes") removeAccount = true; break; case Payment | Preferred: break; default: removeAccount = true; break; } break; // filter all accounts that are not used on homepage views default: removeAccount = true; break; } } else if ((*it).isClosed() || (*it).isInvest()) { // don't show if closed or a stock account removeAccount = true; } if (removeAccount) it = accounts.erase(it); else ++it; } if (!accounts.isEmpty()) { // sort the accounts by name qStableSort(accounts.begin(), accounts.end(), accountNameLess); QString tmp; int i = 0; tmp = "
" + header + "
\n
 
\n"; m_html += tmp; m_html += ""; m_html += ""; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { QString pathStatusHeader = QPixmapToDataUri(Icons::get(Icon::Download).pixmap(QSize(16,16))); m_html += QString("").arg(pathStatusHeader); } m_html += ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += QString(""); if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += QString(""); if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += QString(""); if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += QString("").arg(i18n("Last Reconciled")); m_html += ""; //only show limit info if user chose to do so if (KMyMoneySettings::showLimitInfo()) { m_html += ""; } m_html += ""; m_total = 0; QList::const_iterator it_m; for (it_m = accounts.constBegin(); it_m != accounts.constEnd(); ++it_m) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showAccountEntry(*it_m); m_html += ""; } m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); QString amount = m_total.formatMoney(file->baseCurrency().tradingSymbol(), prec); if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) m_html += ""; m_html += QString("").arg(i18n("Total")); if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += ""; m_html += QString("").arg(showColoredAmount(amount, m_total.isNegative())); m_html += "
"; m_html += i18n("Account"); m_html += "!MC!R%1"; m_html += i18n("Current Balance"); m_html += ""; m_html += i18n("To Minimum Balance / Maximum Credit"); m_html += "
%1%1
"; } } void showFavoriteReports() { QList reports = MyMoneyFile::instance()->reportList(); if (!reports.isEmpty()) { bool firstTime = 1; int row = 0; QList::const_iterator it_report = reports.constBegin(); while (it_report != reports.constEnd()) { if ((*it_report).isFavorite()) { if (firstTime) { m_html += QString("
%1
\n
 
\n").arg(i18n("Favorite Reports")); m_html += ""; m_html += ""; firstTime = false; } m_html += QString("") .arg(row++ & 0x01 ? "even" : "odd") .arg(link(VIEW_REPORTS, QString("?id=%1").arg((*it_report).id()))) .arg((*it_report).name()) .arg(linkend()) .arg((*it_report).comment()); } ++it_report; } if (!firstTime) m_html += "
"; m_html += i18n("Report"); m_html += ""; m_html += i18n("Comment"); m_html += "
%2%3%4%5
"; } } void showForecast() { MyMoneyFile* file = MyMoneyFile::instance(); QList accList; //if forecast has not been executed yet, do it. if (!m_forecast.isForecastDone()) doForecast(); accList = m_forecast.accountList(); if (accList.count() > 0) { // sort the accounts by name qStableSort(accList.begin(), accList.end(), accountNameLess); auto i = 0; auto colspan = 1; //get begin day auto beginDay = QDate::currentDate().daysTo(m_forecast.beginForecastDate()); //if begin day is today skip to next cycle if (beginDay == 0) beginDay = m_forecast.accountsCycle(); // Now output header m_html += QString("
%1
\n
 
\n").arg(i18n("%1 Day Forecast", m_forecast.forecastDays())); m_html += ""; m_html += ""; auto colWidth = 55 / (m_forecast.forecastDays() / m_forecast.accountsCycle()); for (i = 0; (i*m_forecast.accountsCycle() + beginDay) <= m_forecast.forecastDays(); ++i) { m_html += QString(""; colspan++; } m_html += ""; // Now output entries i = 0; QList::ConstIterator it_account; for (it_account = accList.constBegin(); it_account != accList.constEnd(); ++it_account) { //MyMoneyAccount acc = (*it_n); m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); m_html += QString(""; qint64 dropZero = -1; //account dropped below zero qint64 dropMinimum = -1; //account dropped below minimum balance QString minimumBalance = (*it_account).value("minimumBalance"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); MyMoneySecurity currency; MyMoneyMoney forecastBalance; //change account to deep currency if account is an investment if ((*it_account).isInvest()) { MyMoneySecurity underSecurity = file->security((*it_account).currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security((*it_account).currencyId()); } for (auto f = beginDay; f <= m_forecast.forecastDays(); f += m_forecast.accountsCycle()) { forecastBalance = m_forecast.forecastBalance(*it_account, QDate::currentDate().addDays(f)); QString amount; amount = MyMoneyUtils::formatMoney(forecastBalance, *it_account, currency); amount.replace(QChar(' '), " "); m_html += QString("").arg(showColoredAmount(amount, forecastBalance.isNegative())); } m_html += ""; //Check if the account is going to be below zero or below the minimal balance in the forecast period //Check if the account is going to be below minimal balance dropMinimum = m_forecast.daysToMinimumBalance(*it_account); //Check if the account is going to be below zero in the future dropZero = m_forecast.daysToZeroBalance(*it_account); // spit out possible warnings QString msg; // if a minimum balance has been specified, an appropriate warning will // only be shown, if the drop below 0 is on a different day or not present if (dropMinimum != -1 && !minBalance.isZero() && (dropMinimum < dropZero || dropZero == -1)) { switch (dropMinimum) { case 0: msg = i18n("The balance of %1 is below the minimum balance %2 today.", (*it_account).name(), MyMoneyUtils::formatMoney(minBalance, *it_account, currency)); msg = showColoredAmount(msg, true); break; default: msg = i18np("The balance of %2 will drop below the minimum balance %3 in %1 day.", "The balance of %2 will drop below the minimum balance %3 in %1 days.", dropMinimum - 1, (*it_account).name(), MyMoneyUtils::formatMoney(minBalance, *it_account, currency)); msg = showColoredAmount(msg, true); break; } if (!msg.isEmpty()) { m_html += QString("").arg(msg).arg(colspan); } } // a drop below zero is always shown msg.clear(); switch (dropZero) { case -1: break; case 0: if ((*it_account).accountGroup() == Account::Type::Asset) { msg = i18n("The balance of %1 is below %2 today.", (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); msg = showColoredAmount(msg, true); break; } if ((*it_account).accountGroup() == Account::Type::Liability) { msg = i18n("The balance of %1 is above %2 today.", (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); break; } break; default: if ((*it_account).accountGroup() == Account::Type::Asset) { msg = i18np("The balance of %2 will drop below %3 in %1 day.", "The balance of %2 will drop below %3 in %1 days.", dropZero, (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); msg = showColoredAmount(msg, true); break; } if ((*it_account).accountGroup() == Account::Type::Liability) { msg = i18np("The balance of %2 will raise above %3 in %1 day.", "The balance of %2 will raise above %3 in %1 days.", dropZero, (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); break; } } if (!msg.isEmpty()) { m_html += QString("").arg(msg).arg(colspan); } } m_html += "
"; m_html += i18n("Account"); m_html += "").arg(colWidth); m_html += i18ncp("Forecast days", "%1 day", "%1 days", i * m_forecast.accountsCycle() + beginDay); m_html += "
") + link(VIEW_LEDGER, QString("?id=%1").arg((*it_account).id())) + (*it_account).name() + linkend() + "").arg(colWidth); m_html += QString("%1
%1
%1
"; } } QString link(const QString& view, const QString& query, const QString& _title = QString()) const { QString titlePart; QString title(_title); if (!title.isEmpty()) titlePart = QString(" title=\"%1\"").arg(title.replace(QLatin1Char(' '), " ")); return QString("").arg(view, query, titlePart); } QString linkend() const { return QStringLiteral(""); } void showAssetsLiabilities() { QList accounts; QList::ConstIterator it; QList assets; QList liabilities; MyMoneyMoney netAssets; MyMoneyMoney netLiabilities; MyMoneyFile* file = MyMoneyFile::instance(); int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); int i = 0; // get list of all accounts file->accountList(accounts); for (it = accounts.constBegin(); it != accounts.constEnd();) { if (!(*it).isClosed()) { switch ((*it).accountType()) { // group all assets into one list but make sure that investment accounts always show up case Account::Type::Investment: assets << *it; break; case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::Asset: case Account::Type::AssetLoan: // list account if it's the last in the hierarchy or has transactions in it if ((*it).accountList().isEmpty() || (file->transactionCount((*it).id()) > 0)) { assets << *it; } break; // group the liabilities into the other case Account::Type::CreditCard: case Account::Type::Liability: case Account::Type::Loan: // list account if it's the last in the hierarchy or has transactions in it if ((*it).accountList().isEmpty() || (file->transactionCount((*it).id()) > 0)) { liabilities << *it; } break; default: break; } } ++it; } //only do it if we have assets or liabilities account if (assets.count() > 0 || liabilities.count() > 0) { // sort the accounts by name qStableSort(assets.begin(), assets.end(), accountNameLess); qStableSort(liabilities.begin(), liabilities.end(), accountNameLess); QString statusHeader; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { QString pathStatusHeader; pathStatusHeader = QPixmapToDataUri(Icons::get(Icon::ViewOutbox).pixmap(QSize(16,16))); statusHeader = QString("").arg(pathStatusHeader); } //print header m_html += "
" + i18n("Assets and Liabilities Summary") + "
\n
 
\n"; m_html += ""; //column titles m_html += ""; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { m_html += ""; } m_html += ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += ""; m_html += ""; //intermediate row to separate both columns m_html += ""; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { m_html += ""; } m_html += ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += ""; m_html += ""; QString placeHolder_Status, placeHolder_Counts; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) placeHolder_Status = ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) placeHolder_Counts = ""; if (KMyMoneySettings::showCountOfClearedTransactions()) placeHolder_Counts += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) placeHolder_Counts += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) placeHolder_Counts += ""; //get asset and liability accounts QList::const_iterator asset_it = assets.constBegin(); QList::const_iterator liabilities_it = liabilities.constBegin(); for (; asset_it != assets.constEnd() || liabilities_it != liabilities.constEnd();) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); //write an asset account if we still have any if (asset_it != assets.constEnd()) { MyMoneyMoney value; //investment accounts consolidate the balance of its subaccounts if ((*asset_it).accountType() == Account::Type::Investment) { value = investmentBalance(*asset_it); } else { value = MyMoneyFile::instance()->balance((*asset_it).id(), QDate::currentDate()); } //calculate balance for foreign currency accounts if ((*asset_it).currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price((*asset_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; baseValue = baseValue.convert(10000); netAssets += baseValue; } else { netAssets += value; } //show the account without minimum balance showAccountEntry(*asset_it, value, MyMoneyMoney(), false); ++asset_it; } else { //write a white space if we don't m_html += QString("%1%2").arg(placeHolder_Status).arg(placeHolder_Counts); } //leave the intermediate column empty m_html += ""; //write a liability account if (liabilities_it != liabilities.constEnd()) { MyMoneyMoney value; value = MyMoneyFile::instance()->balance((*liabilities_it).id(), QDate::currentDate()); //calculate balance if foreign currency if ((*liabilities_it).currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price((*liabilities_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; baseValue = baseValue.convert(10000); netLiabilities += baseValue; } else { netLiabilities += value; } //show the account without minimum balance showAccountEntry(*liabilities_it, value, MyMoneyMoney(), false); ++liabilities_it; } else { //leave the space empty if we run out of liabilities m_html += QString("%1%2").arg(placeHolder_Status).arg(placeHolder_Counts); } m_html += ""; } //calculate net worth MyMoneyMoney netWorth = netAssets + netLiabilities; //format assets, liabilities and net worth QString amountAssets = netAssets.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountLiabilities = netLiabilities.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountNetWorth = netWorth.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountAssets.replace(QChar(' '), " "); amountLiabilities.replace(QChar(' '), " "); amountNetWorth.replace(QChar(' '), " "); m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); //print total for assets m_html += QString("%1%3").arg(placeHolder_Status).arg(i18n("Total Assets")).arg(placeHolder_Counts).arg(showColoredAmount(amountAssets, netAssets.isNegative())); //leave the intermediate column empty m_html += ""; //print total liabilities m_html += QString("%1%3").arg(placeHolder_Status).arg(i18n("Total Liabilities")).arg(placeHolder_Counts).arg(showColoredAmount(amountLiabilities, netLiabilities.isNegative())); m_html += ""; //print net worth m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); m_html += QString("%1%2").arg(placeHolder_Status).arg(placeHolder_Counts); m_html += QString("%1%3").arg(placeHolder_Status).arg(i18n("Net Worth")).arg(placeHolder_Counts).arg(showColoredAmount(amountNetWorth, netWorth.isNegative())); m_html += ""; m_html += "
"; m_html += statusHeader; m_html += ""; m_html += i18n("Asset Accounts"); m_html += "!MC!R" + i18n("Last Reconciled") + ""; m_html += i18n("Current Balance"); m_html += ""; m_html += statusHeader; m_html += ""; m_html += i18n("Liability Accounts"); m_html += "!MC!R" + i18n("Last Reconciled") + ""; m_html += i18n("Current Balance"); m_html += "
%2%4%2%4
%2%4
"; m_html += "
"; } } void showBudget() { m_html += "
" + i18n("Budget") + "
\n
 
\n"; m_html += ""; if (const auto reportsPlugin = pPlugins.data.value(QStringLiteral("reportsview"), nullptr)) { const auto variantReport = reportsPlugin->requestData(QString(), eWidgetPlugin::WidgetType::Budget); if (!variantReport.isNull()) m_html.append(variantReport.toString()); } else { m_html += QString(""); m_html += QString("").arg(i18n("Enable reports plugin to see this chart.")); m_html += QString(""); } m_html += QString("
%1
"); } void showCashFlowSummary() { MyMoneyTransactionFilter filter; MyMoneyMoney incomeValue; MyMoneyMoney expenseValue; MyMoneyFile* file = MyMoneyFile::instance(); int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); //set start and end of month dates QDate startOfMonth = QDate(QDate::currentDate().year(), QDate::currentDate().month(), 1); QDate endOfMonth = QDate(QDate::currentDate().year(), QDate::currentDate().month(), QDate::currentDate().daysInMonth()); //Add total income and expenses for this month //get transactions for current month filter.setDateFilter(startOfMonth, endOfMonth); filter.setReportAllSplits(false); QList transactions = file->transactionList(filter); //if no transaction then skip and print total in zero if (transactions.size() > 0) { //get all transactions for this month foreach (const auto transaction, transactions) { //get the splits for each transaction foreach (const auto split, transaction.splits()) { if (!split.shares().isZero()) { auto repSplitAcc = file->account(split.accountId()); //only add if it is an income or expense if (repSplitAcc.isIncomeExpense()) { MyMoneyMoney value; //convert to base currency if necessary if (repSplitAcc.currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price(repSplitAcc.tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); value = (split.shares() * MyMoneyMoney::MINUS_ONE) * curRate; value = value.convert(10000); } else { value = (split.shares() * MyMoneyMoney::MINUS_ONE); } //store depending on account type if (repSplitAcc.accountType() == Account::Type::Income) { incomeValue += value; } else { expenseValue += value; } } } } } } //format income and expenses QString amountIncome = incomeValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountExpense = expenseValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountIncome.replace(QChar(' '), " "); amountExpense.replace(QChar(' '), " "); //calculate schedules //Add all schedules for this month MyMoneyMoney scheduledIncome; MyMoneyMoney scheduledExpense; MyMoneyMoney scheduledLiquidTransfer; MyMoneyMoney scheduledOtherTransfer; //get overdues and schedules until the end of this month QList schedule = file->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), endOfMonth, false); //Remove the finished schedules QList::Iterator finished_it; for (finished_it = schedule.begin(); finished_it != schedule.end();) { if ((*finished_it).isFinished()) { finished_it = schedule.erase(finished_it); continue; } ++finished_it; } //add income and expenses QList::Iterator sched_it; for (sched_it = schedule.begin(); sched_it != schedule.end();) { QDate nextDate = (*sched_it).nextDueDate(); int cnt = 0; while (nextDate.isValid() && nextDate <= endOfMonth) { ++cnt; nextDate = (*sched_it).nextPayment(nextDate); // for single occurrence nextDate will not change, so we // better get out of here. if ((*sched_it).occurrence() == Schedule::Occurrence::Once) break; } MyMoneyAccount acc = (*sched_it).account(); if (!acc.id().isEmpty()) { MyMoneyTransaction transaction = (*sched_it).transaction(); // only show the entry, if it is still active MyMoneySplit sp = transaction.splitByAccount(acc.id(), true); // take care of the autoCalc stuff if ((*sched_it).type() == Schedule::Type::LoanPayment) { nextDate = (*sched_it).nextPayment((*sched_it).lastPayment()); //make sure we have all 'starting balances' so that the autocalc works QMap balanceMap; foreach (const auto split, transaction.splits()) { acc = file->account(split.accountId()); // collect all overdues on the first day QDate schedDate = nextDate; if (QDate::currentDate() >= nextDate) schedDate = QDate::currentDate().addDays(1); balanceMap[acc.id()] += file->balance(acc.id(), QDate::currentDate()); } KMyMoneyUtils::calculateAutoLoan(*sched_it, transaction, balanceMap); } //go through the splits and assign to liquid or other transfers const QList splits = transaction.splits(); QList::const_iterator split_it; for (split_it = splits.constBegin(); split_it != splits.constEnd(); ++split_it) { if ((*split_it).accountId() != acc.id()) { auto repSplitAcc = file->account((*split_it).accountId()); //get the shares and multiply by the quantity of occurrences in the period MyMoneyMoney value = (*split_it).shares() * cnt; //convert to foreign currency if needed if (repSplitAcc.currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price(repSplitAcc.tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); value = value * curRate; value = value.convert(10000); } if ((repSplitAcc.isLiquidLiability() || repSplitAcc.isLiquidAsset()) && acc.accountGroup() != repSplitAcc.accountGroup()) { scheduledLiquidTransfer += value; } else if (repSplitAcc.isAssetLiability() && !repSplitAcc.isLiquidLiability() && !repSplitAcc.isLiquidAsset()) { scheduledOtherTransfer += value; } else if (repSplitAcc.isIncomeExpense()) { //income and expenses are stored as negative values if (repSplitAcc.accountType() == Account::Type::Income) scheduledIncome -= value; if (repSplitAcc.accountType() == Account::Type::Expense) scheduledExpense -= value; } } } } ++sched_it; } //format the currency strings QString amountScheduledIncome = scheduledIncome.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountScheduledExpense = scheduledExpense.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountScheduledLiquidTransfer = scheduledLiquidTransfer.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountScheduledOtherTransfer = scheduledOtherTransfer.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountScheduledIncome.replace(QChar(' '), " "); amountScheduledExpense.replace(QChar(' '), " "); amountScheduledLiquidTransfer.replace(QChar(' '), " "); amountScheduledOtherTransfer.replace(QChar(' '), " "); //get liquid assets and liabilities QList accounts; QList::const_iterator account_it; MyMoneyMoney liquidAssets; MyMoneyMoney liquidLiabilities; // get list of all accounts file->accountList(accounts); for (account_it = accounts.constBegin(); account_it != accounts.constEnd();) { if (!(*account_it).isClosed()) { switch ((*account_it).accountType()) { //group all assets into one list case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: { MyMoneyMoney value = MyMoneyFile::instance()->balance((*account_it).id(), QDate::currentDate()); //calculate balance for foreign currency accounts if ((*account_it).currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price((*account_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; liquidAssets += baseValue; liquidAssets = liquidAssets.convert(10000); } else { liquidAssets += value; } break; } //group the liabilities into the other case Account::Type::CreditCard: { MyMoneyMoney value; value = MyMoneyFile::instance()->balance((*account_it).id(), QDate::currentDate()); //calculate balance if foreign currency if ((*account_it).currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price((*account_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; liquidLiabilities += baseValue; liquidLiabilities = liquidLiabilities.convert(10000); } else { liquidLiabilities += value; } break; } default: break; } } ++account_it; } //calculate net worth MyMoneyMoney liquidWorth = liquidAssets + liquidLiabilities; //format assets, liabilities and net worth QString amountLiquidAssets = liquidAssets.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountLiquidLiabilities = liquidLiabilities.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountLiquidWorth = liquidWorth.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountLiquidAssets.replace(QChar(' '), " "); amountLiquidLiabilities.replace(QChar(' '), " "); amountLiquidWorth.replace(QChar(' '), " "); //show the summary m_html += "
" + i18n("Cash Flow Summary") + "
\n
 
\n"; //print header m_html += ""; //income and expense title m_html += ""; m_html += ""; //column titles m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; //add row with banding m_html += QString(""); //print current income m_html += QString("").arg(showColoredAmount(amountIncome, incomeValue.isNegative())); //print the scheduled income m_html += QString("").arg(showColoredAmount(amountScheduledIncome, scheduledIncome.isNegative())); //print current expenses m_html += QString("").arg(showColoredAmount(amountExpense, expenseValue.isNegative())); //print the scheduled expenses m_html += QString("").arg(showColoredAmount(amountScheduledExpense, scheduledExpense.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Income and Expenses of Current Month"); m_html += "
"; m_html += i18n("Income"); m_html += ""; m_html += i18n("Scheduled Income"); m_html += ""; m_html += i18n("Expenses"); m_html += ""; m_html += i18n("Scheduled Expenses"); m_html += "
%2%2%2%2
"; //print header of assets and liabilities m_html += "
 
\n"; m_html += ""; //assets and liabilities title m_html += ""; m_html += ""; //column titles m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; //add row with banding m_html += QString(""); //print current liquid assets m_html += QString("").arg(showColoredAmount(amountLiquidAssets, liquidAssets.isNegative())); //print the scheduled transfers m_html += QString("").arg(showColoredAmount(amountScheduledLiquidTransfer, scheduledLiquidTransfer.isNegative())); //print current liabilities m_html += QString("").arg(showColoredAmount(amountLiquidLiabilities, liquidLiabilities.isNegative())); //print the scheduled transfers m_html += QString("").arg(showColoredAmount(amountScheduledOtherTransfer, scheduledOtherTransfer.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Liquid Assets and Liabilities"); m_html += "
"; m_html += i18n("Liquid Assets"); m_html += ""; m_html += i18n("Transfers to Liquid Liabilities"); m_html += ""; m_html += i18n("Liquid Liabilities"); m_html += ""; m_html += i18n("Other Transfers"); m_html += "
%2%2%2%2
"; //final conclusion MyMoneyMoney profitValue = incomeValue + expenseValue + scheduledIncome + scheduledExpense; MyMoneyMoney expectedAsset = liquidAssets + scheduledIncome + scheduledExpense + scheduledLiquidTransfer + scheduledOtherTransfer; MyMoneyMoney expectedLiabilities = liquidLiabilities + scheduledLiquidTransfer; QString amountExpectedAsset = expectedAsset.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountExpectedLiabilities = expectedLiabilities.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountProfit = profitValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountProfit.replace(QChar(' '), " "); amountExpectedAsset.replace(QChar(' '), " "); amountExpectedLiabilities.replace(QChar(' '), " "); //print header of cash flow status m_html += "
 
\n"; m_html += ""; //income and expense title m_html += ""; m_html += ""; //column titles m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; //add row with banding m_html += QString(""); m_html += ""; //print expected assets m_html += QString("").arg(showColoredAmount(amountExpectedAsset, expectedAsset.isNegative())); //print expected liabilities m_html += QString("").arg(showColoredAmount(amountExpectedLiabilities, expectedLiabilities.isNegative())); //print expected profit m_html += QString("").arg(showColoredAmount(amountProfit, profitValue.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Cash Flow Status"); m_html += "
 "; m_html += i18n("Expected Liquid Assets"); m_html += ""; m_html += i18n("Expected Liquid Liabilities"); m_html += ""; m_html += i18n("Expected Profit/Loss"); m_html += "
 %2%2%2
"; m_html += "
"; } KHomeView *q_ptr; /** * daily balances of an account */ typedef QMap dailyBalances; #ifdef ENABLE_WEBENGINE QWebEngineView *m_view; #else KWebView *m_view; #endif QString m_html; bool m_showAllSchedules; bool m_needLoad; MyMoneyForecast m_forecast; MyMoneyMoney m_total; /** * Hold the last valid size of the net worth graph * for the times when the needed size can't be computed. */ QSize m_netWorthGraphLastValidSize; QMap< QString, QVector > m_transactionStats; /** * daily forecast balance of accounts */ QMap m_accountList; - QPrinter *m_currentPrinter; int m_scrollBarPos; }; #endif