rvgp 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +23 -0
- data/LICENSE +504 -0
- data/README.md +223 -0
- data/Rakefile +32 -0
- data/bin/rvgp +8 -0
- data/lib/rvgp/application/config.rb +159 -0
- data/lib/rvgp/application/descendant_registry.rb +122 -0
- data/lib/rvgp/application/status_output.rb +139 -0
- data/lib/rvgp/application.rb +170 -0
- data/lib/rvgp/base/command.rb +457 -0
- data/lib/rvgp/base/grid.rb +531 -0
- data/lib/rvgp/base/reader.rb +29 -0
- data/lib/rvgp/base/reconciler.rb +434 -0
- data/lib/rvgp/base/validation.rb +261 -0
- data/lib/rvgp/commands/cashflow.rb +160 -0
- data/lib/rvgp/commands/grid.rb +70 -0
- data/lib/rvgp/commands/ireconcile.rb +95 -0
- data/lib/rvgp/commands/new_project.rb +296 -0
- data/lib/rvgp/commands/plot.rb +41 -0
- data/lib/rvgp/commands/publish_gsheets.rb +83 -0
- data/lib/rvgp/commands/reconcile.rb +58 -0
- data/lib/rvgp/commands/rotate_year.rb +202 -0
- data/lib/rvgp/commands/validate_journal.rb +59 -0
- data/lib/rvgp/commands/validate_system.rb +44 -0
- data/lib/rvgp/commands.rb +160 -0
- data/lib/rvgp/dashboard.rb +252 -0
- data/lib/rvgp/fakers/fake_feed.rb +245 -0
- data/lib/rvgp/fakers/fake_journal.rb +57 -0
- data/lib/rvgp/fakers/fake_reconciler.rb +88 -0
- data/lib/rvgp/fakers/faker_helpers.rb +25 -0
- data/lib/rvgp/gem.rb +80 -0
- data/lib/rvgp/journal/commodity.rb +453 -0
- data/lib/rvgp/journal/complex_commodity.rb +214 -0
- data/lib/rvgp/journal/currency.rb +101 -0
- data/lib/rvgp/journal/journal.rb +141 -0
- data/lib/rvgp/journal/posting.rb +156 -0
- data/lib/rvgp/journal/pricer.rb +267 -0
- data/lib/rvgp/journal.rb +24 -0
- data/lib/rvgp/plot/gnuplot.rb +478 -0
- data/lib/rvgp/plot/google-drive/output_csv.rb +44 -0
- data/lib/rvgp/plot/google-drive/output_google_sheets.rb +434 -0
- data/lib/rvgp/plot/google-drive/sheet.rb +67 -0
- data/lib/rvgp/plot.rb +293 -0
- data/lib/rvgp/pta/hledger.rb +237 -0
- data/lib/rvgp/pta/ledger.rb +308 -0
- data/lib/rvgp/pta.rb +311 -0
- data/lib/rvgp/reconcilers/csv_reconciler.rb +424 -0
- data/lib/rvgp/reconcilers/journal_reconciler.rb +41 -0
- data/lib/rvgp/reconcilers/shorthand/finance_gem_hacks.rb +48 -0
- data/lib/rvgp/reconcilers/shorthand/international_atm.rb +152 -0
- data/lib/rvgp/reconcilers/shorthand/investment.rb +144 -0
- data/lib/rvgp/reconcilers/shorthand/mortgage.rb +195 -0
- data/lib/rvgp/utilities/grid_query.rb +190 -0
- data/lib/rvgp/utilities/yaml.rb +131 -0
- data/lib/rvgp/utilities.rb +44 -0
- data/lib/rvgp/validations/balance_validation.rb +68 -0
- data/lib/rvgp/validations/duplicate_tags_validation.rb +48 -0
- data/lib/rvgp/validations/uncategorized_validation.rb +15 -0
- data/lib/rvgp.rb +66 -0
- data/resources/README.MD/2022-cashflow-google.png +0 -0
- data/resources/README.MD/2022-cashflow.png +0 -0
- data/resources/README.MD/all-wealth-growth-google.png +0 -0
- data/resources/README.MD/all-wealth-growth.png +0 -0
- data/resources/gnuplot/default.yml +80 -0
- data/resources/i18n/en.yml +192 -0
- data/resources/iso-4217-currencies.json +171 -0
- data/resources/skel/Rakefile +5 -0
- data/resources/skel/app/grids/cashflow_grid.rb +27 -0
- data/resources/skel/app/grids/monthly_income_and_expenses_grid.rb +25 -0
- data/resources/skel/app/grids/wealth_growth_grid.rb +35 -0
- data/resources/skel/app/plots/cashflow.yml +33 -0
- data/resources/skel/app/plots/monthly-income-and-expenses.yml +17 -0
- data/resources/skel/app/plots/wealth-growth.yml +20 -0
- data/resources/skel/config/csv-format-acme-checking.yml +9 -0
- data/resources/skel/config/google-secrets.yml +5 -0
- data/resources/skel/config/rvgp.yml +0 -0
- data/resources/skel/journals/prices.db +0 -0
- data/rvgp.gemspec +6 -0
- data/test/assets/ledger_total_monthly_liabilities_with_empty.xml +383 -0
- data/test/assets/ledger_total_monthly_liabilities_with_empty2.xml +428 -0
- data/test/test_command_base.rb +61 -0
- data/test/test_commodity.rb +270 -0
- data/test/test_csv_reconciler.rb +60 -0
- data/test/test_currency.rb +24 -0
- data/test/test_fake_feed.rb +228 -0
- data/test/test_fake_journal.rb +98 -0
- data/test/test_fake_reconciler.rb +60 -0
- data/test/test_journal_parse.rb +545 -0
- data/test/test_ledger.rb +102 -0
- data/test/test_plot.rb +133 -0
- data/test/test_posting.rb +50 -0
- data/test/test_pricer.rb +139 -0
- data/test/test_pta_adapter.rb +575 -0
- data/test/test_utilities.rb +45 -0
- metadata +268 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'csv'
|
|
4
|
+
require_relative '../journal'
|
|
5
|
+
|
|
6
|
+
module RVGP
|
|
7
|
+
# Reconcilers are a cornerstone of the RVGP build, and an integral part of your project. Reconcilers, take an input
|
|
8
|
+
# file (Either a csv file or a journal file), and reconcile them into a reconciled pta journal. This class
|
|
9
|
+
# implements most of the functionality needed to make that happen.
|
|
10
|
+
#
|
|
11
|
+
# Reconcilers take two files as input. Firstly, it takes an aforementioned input file. But, secondly, it takes a
|
|
12
|
+
# yaml file with reconciliation directives. What follows is a guide on those directives.
|
|
13
|
+
#
|
|
14
|
+
# Most of your time spent in these files, will be spent adding rules to the income and expense sections (see
|
|
15
|
+
# the 'Defining income and expense sections'). However, in order to get the reconciler far enough into the parsing
|
|
16
|
+
# logic to get to that section, you'll need to understand the general structure of these files.
|
|
17
|
+
#
|
|
18
|
+
# ## The General Structure of Reconciler Yaml's
|
|
19
|
+
# Reconciler yaml files are expected to be found in the app/reconciler directory. Typically with a four-digit year
|
|
20
|
+
# as the start of its filename, and a yml extension. Here's a simple example reconciler directory:
|
|
21
|
+
# ```
|
|
22
|
+
# ~/ledger> lsd -1 app/reconcilers/
|
|
23
|
+
# 2022-business-checking.yml
|
|
24
|
+
# 2023-business-checking.yml
|
|
25
|
+
# 2022-personal-amex.yml
|
|
26
|
+
# 2023-personal-amex.yml
|
|
27
|
+
# 2022-personal-checking.yml
|
|
28
|
+
# 2023-personal-checking.yml
|
|
29
|
+
# 2022-personal-saving.yml
|
|
30
|
+
# 2023-personal-saving.yml
|
|
31
|
+
# ```
|
|
32
|
+
# In this example directory, we can see eight reconcilers defined, on each of the years 2022 and 2023, for each of
|
|
33
|
+
# the accounts: business-checking, personal-checking, personal-saving, and personal-amex. Each of these files will
|
|
34
|
+
# reference a separate input. Each of this files will produce a journal, with a corresponding name in
|
|
35
|
+
# build/journals.
|
|
36
|
+
#
|
|
37
|
+
# All reconciler files, are required to have a :label, :output, :input, :from, :income, and :expense key defined.
|
|
38
|
+
# Here's an example of a reconciler, with all of these sections present. Let's take a look at the
|
|
39
|
+
# '2023-personal-checking.yml' reconciler, from above:
|
|
40
|
+
# ```
|
|
41
|
+
# from: "Personal:Assets:AcmeBank:Checking"
|
|
42
|
+
# label: "Personal AcmeBank:Checking (2023)"
|
|
43
|
+
# input: 2023-personal-basic-checking.csv
|
|
44
|
+
# output: 2023-personal-basic-checking.journal
|
|
45
|
+
# format:
|
|
46
|
+
# csv_headers: true
|
|
47
|
+
# fields:
|
|
48
|
+
# date: !!proc Date.strptime(row['Date'], '%m/%d/%Y')
|
|
49
|
+
# amount: !!proc row['Amount']
|
|
50
|
+
# description: !!proc row['Description']
|
|
51
|
+
# income:
|
|
52
|
+
# - match: /.*/
|
|
53
|
+
# to: Personal:Income:Unknown
|
|
54
|
+
# expense:
|
|
55
|
+
# - match: /.*/
|
|
56
|
+
# to: Personal:Expenses:Unknown
|
|
57
|
+
# ```
|
|
58
|
+
#
|
|
59
|
+
# This file has a number of fairly obvious fields, and some not-so obvious fields. Let's take a look at these fields
|
|
60
|
+
# one by one:
|
|
61
|
+
#
|
|
62
|
+
# - **from** [String] - This is the pta account, that the reconciler will ascribe as it's default source of funds.
|
|
63
|
+
# - **label** [String] - This a label for the reconciler, that is mostly just used for status output on the cli.
|
|
64
|
+
# - **input** [String] - The filename/path to the input to this file. Typically this is a csv file, located in the
|
|
65
|
+
# project's 'feeds' directory.
|
|
66
|
+
# - **output** [String] - The filename to output in the project's 'build/journals' directory.
|
|
67
|
+
# - **starts_on** [String] - A cut-off date, before which, transactions in the input file are ignored. Date is
|
|
68
|
+
# expected to be provided in YYYY-MM-DD format.
|
|
69
|
+
# - **format** [Hash] - This section defines the logic used to decode a csv into fields. Typically, this section is
|
|
70
|
+
# shared between multiple reconcilers by way of an 'include' directive, to a file in your
|
|
71
|
+
# config/ directory. More on this below.<br><br>
|
|
72
|
+
# Note the use of the !!proc directive. These values are explained in the 'Special yaml features'
|
|
73
|
+
# section.
|
|
74
|
+
# - **income** [Array<Hash>] - This collection matches one or more income entries in the input file, and reconciles
|
|
75
|
+
# them to an output entry.
|
|
76
|
+
# - **expense** [Array<Hash>] - This collection matches one or more expense entries in the input file, and reconciles
|
|
77
|
+
# them to an output entry.
|
|
78
|
+
#
|
|
79
|
+
# Income and expenses are nearly identical in their rules and features, and are further explained in the 'Defining
|
|
80
|
+
# income and expense sections' below.
|
|
81
|
+
#
|
|
82
|
+
# In addition to these basic parameters, the following parameters are also supported in the root of your reconciler
|
|
83
|
+
# file:
|
|
84
|
+
# - **transform_commodities** [Hash] - This directive can be used to convert commodities in the format specified by
|
|
85
|
+
# its keys, to the commodity specified in its values. For example, the following will ensure that all USD values
|
|
86
|
+
# encountered in the input file, are transcribed as '$' in the output files:
|
|
87
|
+
#
|
|
88
|
+
# ...
|
|
89
|
+
# transform_commodities:
|
|
90
|
+
# USD: '$'
|
|
91
|
+
# ...
|
|
92
|
+
#
|
|
93
|
+
# - **balances** [Hash] - This feature raises an error, if the balance of the :from account on a given date(key)
|
|
94
|
+
# doesn't match the provided value. Here's an example of what this looks like in a reconciler:
|
|
95
|
+
#
|
|
96
|
+
# ...
|
|
97
|
+
# balances:
|
|
98
|
+
# '2023-01-15': $ 2345.67
|
|
99
|
+
# '2023-06-15': $ 3456,78
|
|
100
|
+
# ...
|
|
101
|
+
# This feature is mostly implemented by the {RVGP::Validations::BalanceValidation}, and is provided as a
|
|
102
|
+
# fail-safe, in which you can input the balance reported by the statements from your financial institution,
|
|
103
|
+
# and ensure your build is consistent with the expectation of that institution.
|
|
104
|
+
# - **disable_checks** [Array<String>] - This declaration can be used to disable one or more of your journal
|
|
105
|
+
# validations. This is described in greater depth in the {RVGP::Base::Validation} documentation. Here's a sample
|
|
106
|
+
# of this feature, which can be used to disable the balances section that was explained above:
|
|
107
|
+
#
|
|
108
|
+
# ...
|
|
109
|
+
# disable_checks:
|
|
110
|
+
# - balance
|
|
111
|
+
# ...
|
|
112
|
+
# - **tag_accounts** [Array<Hash>] - This feature is preliminary, and subject to change. The gist of this feature, is
|
|
113
|
+
# that it offers a second pass, after the income/expense rules have applied. This pass enables additional tags to
|
|
114
|
+
# be applied to a posting, based on how that posting was reconciled in the first pass. I'm not sure I like how
|
|
115
|
+
# this feature came out, so, I'm disinclined to document it for now. If there's an interest in this feature, I can
|
|
116
|
+
# stabilize it's support, and better document it.
|
|
117
|
+
#
|
|
118
|
+
# ## Understanding 'format' parameters
|
|
119
|
+
# The format section applies rules to the parsing of the input file. Some of these parameters are
|
|
120
|
+
# specific to the format of the input file. These rules are typically specific to a financial instution's specific
|
|
121
|
+
# output formatting. And are typically shared between multiple reconciler files in the form of an
|
|
122
|
+
# !!include directive (see below).
|
|
123
|
+
#
|
|
124
|
+
# ### CSV specific format parameters
|
|
125
|
+
# The parameters are specific to .csv input files.
|
|
126
|
+
# - **fields** [Hash<String, Proc>] - This field is required for csv's. This hash contains a map of field names, to
|
|
127
|
+
# !!proc's. The supported (required) field keys are: date, amount, and description. The values for each of these
|
|
128
|
+
# keys is evaluated (in ruby), and provided a single parameter, 'row' which contains a row as returned from ruby's
|
|
129
|
+
# CSV.parse method. The example project, supplied by the new_project command, contains an easy implementation
|
|
130
|
+
# of this feature in action.
|
|
131
|
+
# - **invert_amount** [bool] (default: false) - Whether to call the {RVGP::Journal::Commodity#invert!} on every
|
|
132
|
+
# amount that's encountered in the input file
|
|
133
|
+
# - **encoding** [String] - This parameter is passed to the :encoding parameter of File.read, during the parsing of
|
|
134
|
+
# the supplied input file. This can be used to prevent CSV::MalformedCSVError in cases such as a bom encoded
|
|
135
|
+
# input file.
|
|
136
|
+
# - **csv_headers** [bool] (default: false) - Whether or not the first row of the input file, contains column headers
|
|
137
|
+
# for the rows that follow.
|
|
138
|
+
# - **skip_lines** [Integer, String] - This option will direct the reconciler to skip over lines at the beginning of
|
|
139
|
+
# the input file. This can be specified either as a number, which indicates the number of lines to ignore. Or,
|
|
140
|
+
# alternatively, this can be specified as a RegExp (provided in the form of a yaml string). In which case, the
|
|
141
|
+
# reconciler will begin to parse one character after the end of the regular expression match.
|
|
142
|
+
# - **trim_lines** [Integer, String] - This option will direct the reconciler to skip over lines at the end of
|
|
143
|
+
# the input file. This can be specified either as a number, which indicates the number of lines to trim. Or,
|
|
144
|
+
# alternatively, this can be specified as a RegExp (provided in the form of a yaml string). In which case, the
|
|
145
|
+
# reconciler will trim the file up to one character to the left of the regular expression match.
|
|
146
|
+
#
|
|
147
|
+
# ### CSV and Journal file format parameters
|
|
148
|
+
# These parameters are available to both .journal as well as .csv files.
|
|
149
|
+
# - **default_currency** [String] (default: '$') - A currency to default amount's to, if a currency isn't specified
|
|
150
|
+
# - **reverse_order** [bool] (default: false) - Whether to output transactions in the opposite order of how they were
|
|
151
|
+
# encoded in the input file.
|
|
152
|
+
# - **cash_back** [Hash] - This feature enables you to match transaction descriptions for a cash back indication and
|
|
153
|
+
# amount, and to break that component of the charge into a separate account. The valid keys in this hash are
|
|
154
|
+
# :match and :to . The matched captures of the regex are assumed to be symbol (1) and amount (2), which are used
|
|
155
|
+
# to construct a commodity that's assigned to the :to value. Here's an easy exmple
|
|
156
|
+
#
|
|
157
|
+
# ...
|
|
158
|
+
# cash_back:
|
|
159
|
+
# match: '/\(CASH BACK: ([^ ]) ([\d]+\.[\d]{2})\)\Z/'
|
|
160
|
+
# to: Personal:Assets:Cash
|
|
161
|
+
# ...
|
|
162
|
+
#
|
|
163
|
+
# ## Defining income and expense sections
|
|
164
|
+
# This is where you'll spend most of your time reconciling. Once the basic csv structure is parsing, these sections
|
|
165
|
+
# are how you'll match entries in your input file, and turn them into reconciled output entries. The income_rules
|
|
166
|
+
# and expense_rules are governed by the same logic. Let's breakout some of their rules, that you should understand:
|
|
167
|
+
# - The *_rules section of the yaml is a array of hashes
|
|
168
|
+
# - These hashes contain 'match' directives, and 'assignment' directives
|
|
169
|
+
# - All transactions in the input file, are sent to either income_rules, or expense_rules, depending on whether
|
|
170
|
+
# their amount is a credit(income), or a debit(expense).
|
|
171
|
+
# - Each transaction in the input file is sent down the chain of rules (either income or expense) from the top of
|
|
172
|
+
# the list, towards the bottom - until a matching rule is found. At that time, traversal will stop. And, all
|
|
173
|
+
# directives in this rule will apply to the input transaction.
|
|
174
|
+
# - If you've ever managed a firewall, this matching and directive process works very similarly to how packets are
|
|
175
|
+
# managed by a firewall ruleset.
|
|
176
|
+
# - If no matches were found, an error is raised. Typically, you'll want a catch-all at the end of the chain, like
|
|
177
|
+
# so:
|
|
178
|
+
#
|
|
179
|
+
# ...
|
|
180
|
+
# - match: /.*/
|
|
181
|
+
# to: Personal:Expenses:Unknown
|
|
182
|
+
#
|
|
183
|
+
# For every hash in an array of income and expense rules, you can specify one or more of the following yaml
|
|
184
|
+
# directives. Note that these directives all serve to provide two function: matching input transactions, indicating
|
|
185
|
+
# how to reconcile any matches it captures.
|
|
186
|
+
#
|
|
187
|
+
# ### Income & Expense Rules: Matching
|
|
188
|
+
# The following directives are matching rules. If more than one of these directives are encountered in a rule,
|
|
189
|
+
# they're and'd together. Meaning: all of the conditions that are listed, need to apply to subject, in order for a
|
|
190
|
+
# match to execute.
|
|
191
|
+
# - **match** [Regexp,String] - If a string is provided, compares the :description of the feed transaction against the
|
|
192
|
+
# value provided, and matches if they're equal. If a regex is provided, matches the
|
|
193
|
+
# :description of the feed transaction against the regex provided.
|
|
194
|
+
# If a regex is provided, captures are supported. (see the note below)
|
|
195
|
+
# - **account** [Regexp,String] - This matcher is useful for reconcilers that support the :to field.
|
|
196
|
+
# (see {RVGP::Reconcilers::JournalReconciler}). If a string is provided, compares the
|
|
197
|
+
# account :to which a transaction was assigned, to the value provided. And matches if
|
|
198
|
+
# they're equal. If a regex is provided, matches the account :to which a transaction
|
|
199
|
+
# was assigned, against the regex provided.
|
|
200
|
+
# If a regex is provided, captures are supported. (see the note below)
|
|
201
|
+
# - **account_is_not** [String] - This matcher is useful for reconcilers that support the :to field.
|
|
202
|
+
# (see {RVGP::Reconcilers::JournalReconciler}). This field matches any transaction
|
|
203
|
+
# whose account :to, does not equal the provided string.
|
|
204
|
+
# - **amount_less_than** [Commodity] - This field compares it's value to the transaction :amount , and matches if
|
|
205
|
+
# that amount is less than the provided amount.
|
|
206
|
+
# - **amount_greater_than** [Commodity] - This field compares it's value to the transaction :amount , and matches
|
|
207
|
+
# if that amount is greater than the provided amount.
|
|
208
|
+
# - **amount_equals** [Commodity] - This field compares it's value to the transaction :amount , and matches if
|
|
209
|
+
# that amount is equal to the provided amount.
|
|
210
|
+
# - **on_date** [Regexp,Date] - If a date is provided, compares the :date of the feed transaction against the value
|
|
211
|
+
# provided, and matches if they're equal. If a regex is provided, matches the
|
|
212
|
+
# :date of the feed, converted to a string in the format 'YYYY-MM-DD', against the regex
|
|
213
|
+
# provided.
|
|
214
|
+
# If a regex is provided, captures are supported. (see the note below)
|
|
215
|
+
# - **before_date** [Date] - This field compares it's value to the feed transaction :date, and matches if the feed's
|
|
216
|
+
# :date occurred before the provided :date.
|
|
217
|
+
# - **after_date** [Date] - This field compares it's value to the feed transaction :date, and matches if the feed's
|
|
218
|
+
# :date occurred after the provided :date.
|
|
219
|
+
# - **from_is_not** [String] - This field matches any transaction whose account :from, does not equal the provided
|
|
220
|
+
# string.
|
|
221
|
+
#
|
|
222
|
+
# **NOTE** Some matchers which support captures: This is a powerful feature that allows regexp captured values, to
|
|
223
|
+
# substitute in the :to field of the reconciled transaction. Here's an example of how this feature works:
|
|
224
|
+
#
|
|
225
|
+
# - match: '/^Reservation\: (?<unit>[^ ]+)/'
|
|
226
|
+
# to: AirBNB:Income:$unit
|
|
227
|
+
# In this example, the text that existed in the "(?<unit>[^ ]+)" section of the 'match' field, is substituted in
|
|
228
|
+
# place of "$unit" in the output journal.
|
|
229
|
+
#
|
|
230
|
+
# ### Income & Expense Rules: Reconciliation
|
|
231
|
+
# The following directives are reconciliation rules. These rules have nothing to do with matching, and instead
|
|
232
|
+
# apply to the outputted transaction for the rule in which they're declared. If more than one of these rules are
|
|
233
|
+
# present - they all apply.
|
|
234
|
+
# - **to** [String] - This field will provide the :to account to reconcile an input transaction against. Be aware
|
|
235
|
+
# aware of the above note on captures, as this field supports capture variable substitution.
|
|
236
|
+
# - **from** [String] - This field can be used to change the reconciled :from account, to a different account than
|
|
237
|
+
# the default :from, that was specified in the root of the reconciler yaml.
|
|
238
|
+
# - **tag** [String] - Tag(s) to apply to the reconciled posting.
|
|
239
|
+
# - **to_tag** [String] - Tag(s) to apply to the :to transfer, the first transfer, in the posting
|
|
240
|
+
# - **targets** [Array<Hash>] - For some transactions, multiple transfers need to expand from a single input
|
|
241
|
+
# transaction. In those cases, :targets is the reconciliation rule you'll want to use.
|
|
242
|
+
# This field is expected to be an array of Hashes. With, each hash supporting the
|
|
243
|
+
# following fields:
|
|
244
|
+
# - **to** [String] - As with the above :to, this field will provide the account to reconcile the transfer to.
|
|
245
|
+
# - **amount** [Commodity] - The amount to ascribe to this transfer. While the sum of the targets 'should' equal the
|
|
246
|
+
# input transaction amount, there is no validation performed by RVGP to do so. So, excercize discretion when
|
|
247
|
+
# manually breaking out input transactions into multiple transfers.
|
|
248
|
+
# - **complex_commodity** [ComplexCommodity] - A complex commodity to ascribe to this transfer, instead of an
|
|
249
|
+
# :amount. See {RVGP::Journal::ComplexCommodity.from_s} for more details on this feature.
|
|
250
|
+
# - **tags** [Array<String>] - An array of tags to assign to this transfer. See {RVGP::Journal::Posting::Tag.from_s}
|
|
251
|
+
# for more details on tag formatting.
|
|
252
|
+
# - **to_shorthand** [String] - Reconciler Shorthand is a powerful feature that can reduce duplication, and manual
|
|
253
|
+
# calculations from your reconciler yaml. The value provided here, must correlate with an available reconciler
|
|
254
|
+
# shorthand, and if so, sends this rule to that shorthand for reconciliation. See the below section for further
|
|
255
|
+
# details on this feature.
|
|
256
|
+
# - **shorthand_params** [Hash] - This section is specific to the reconciler shorthand that was specified in the
|
|
257
|
+
# :to_shorthand field. Any of the key/value pairs specified here, are sent to the reconciler shorthand, along
|
|
258
|
+
# with the rest of the input transaction. And, presumably, these fields will futher direct the reconciliation
|
|
259
|
+
# of the input transaction.
|
|
260
|
+
#
|
|
261
|
+
# ## Shorthand
|
|
262
|
+
# Additional time-saving syntax is available in the form of 'Shorthand'. This feature is reliable, though,
|
|
263
|
+
# experimental. The point of 'Shorthand' is to provide ruby modules that takes a matched transaction, and
|
|
264
|
+
# automatically expands this transaction in the form of a ruby-defined macro. Currently, there are a handful
|
|
265
|
+
# of such shorthand macros shipped with RVGP. If there's an interest, user-defined shorthand can be supported
|
|
266
|
+
# in the future. The following shorthand classes, are currently provided in RVGP:
|
|
267
|
+
# - {RVGP::Reconcilers::Shorthand::InternationalAtm} - This Shorthand is useful for unrolling a complex International
|
|
268
|
+
# ATM withdrawal. This shorthand will automatically calculate and allocate fees around the amounnt withdrawn.
|
|
269
|
+
# - {RVGP::Reconcilers::Shorthand::Investment} - Allocate capital gains or losses, given a symbol, amount, and price.
|
|
270
|
+
# - {RVGP::Reconcilers::Shorthand::Mortgage} - This shorthand will automatically allocate the the escrow, principal,
|
|
271
|
+
# and interest components of a mortage payment, into constituent accounts.
|
|
272
|
+
# See the documentation in each of these classes, for details on what **:shorthand_params** each of these modules
|
|
273
|
+
# supports.
|
|
274
|
+
#
|
|
275
|
+
# ## Special yaml features
|
|
276
|
+
# All of these pysch extensions, are prefixed with two exclamation points, and can be placed in lieu of a value, for
|
|
277
|
+
# some of the fields outlined above.
|
|
278
|
+
# - <b>!!include</b> [String] - Include another yaml file, in place of this directive. The file is expected to be
|
|
279
|
+
# provided, immediately followed by this declaration (separated by a space). It's common to see this directive
|
|
280
|
+
# used as a shortcut to shared :format sections. But, these can be used almost anywhere. Here's an example:
|
|
281
|
+
#
|
|
282
|
+
# ...
|
|
283
|
+
# from: "Personal:Assets:AcmeBank:Checking"
|
|
284
|
+
# label: "Personal AcmeBank:Checking (2023)"
|
|
285
|
+
# format: !!include config/csv-format-acmebank.yml
|
|
286
|
+
# ...
|
|
287
|
+
# - <b>!!proc</b> [String] - Convert the contents of the text following this directive, into a Proc object. It'
|
|
288
|
+
# common to see this directive used in the format section of a reconciler yaml. Here's an example:
|
|
289
|
+
#
|
|
290
|
+
# ...
|
|
291
|
+
# fields:
|
|
292
|
+
# date: !!proc >
|
|
293
|
+
# date = Date.strptime(row[0], '%m/%d/%Y');
|
|
294
|
+
# date - 1
|
|
295
|
+
# amount: !!proc row[1]
|
|
296
|
+
# description: !!proc row[2]
|
|
297
|
+
# ...
|
|
298
|
+
# Note that the use of '>' is a yaml feature, that allows multiline strings to compose by way of an indent in
|
|
299
|
+
# the lines that follow. For one-line '!!proc' declarations, this character is not needed. Additionally, this
|
|
300
|
+
# means that in most cases, carriage returns are not parsed. As such, you'll want to terminate lines in these
|
|
301
|
+
# segments, with a semicolon, to achieve the same end.
|
|
302
|
+
#
|
|
303
|
+
# ## Available Implementations
|
|
304
|
+
# Currently, the following reconciller implementations are available. These implementations support all of the
|
|
305
|
+
# above features, and, may implement additional features.
|
|
306
|
+
# - {RVGP::Reconcilers::CsvReconciler} - this reconciler handles input files of type csv
|
|
307
|
+
# - {RVGP::Reconcilers::JournalReconciler} - this reconciler handles input files of type .journal (Pta accounting
|
|
308
|
+
# files)
|
|
309
|
+
module Reconcilers
|
|
310
|
+
# This reconciler is instantiated for input files of type csv. Additional parameters are supported in the
|
|
311
|
+
# :format section of this reconciler, which are documented in {RVGP::Reconcilers} under the
|
|
312
|
+
# 'CSV specific format parameters' section.
|
|
313
|
+
# @attr_reader [Hash<String, <Proc,String,Integer>>] fields_format A hash of field names, to their location in
|
|
314
|
+
# the input file. Supported key names include: date, amount, description. These keys can map to either a
|
|
315
|
+
# 'string' type (indicating which column of the input file contains the key's value). An Integer (indicating
|
|
316
|
+
# which column offset contains the key's value). Or, a Proc (which executes for every row in the input file,
|
|
317
|
+
# and whose return value will be used)
|
|
318
|
+
# @attr_reader [Hash] csv_format This hash is sent to the options parameter of CSV.parse
|
|
319
|
+
# @attr_reader [Boolean] invert_amount Whether or not to multiple the :amount field by negative one.
|
|
320
|
+
# @attr_reader [<Regexp, Integer>] skip_lines Given a regex, the input file will discard the match for the
|
|
321
|
+
# provided regex from the start of the input file. Given an integer, the provided number of lines will be
|
|
322
|
+
# removed from the start of the input file.
|
|
323
|
+
# @attr_reader [<Regexp, Integer>] trim_lines Given a regex, the input file will discard the match for the
|
|
324
|
+
# provided regex from the end of the input file. Given an integer, the provided number of lines will be
|
|
325
|
+
# removed from the end of the input file.
|
|
326
|
+
class CsvReconciler < RVGP::Base::Reconciler
|
|
327
|
+
attr_reader :fields_format, :csv_format, :invert_amount, :skip_lines, :trim_lines
|
|
328
|
+
|
|
329
|
+
def initialize(yaml)
|
|
330
|
+
super yaml
|
|
331
|
+
|
|
332
|
+
missing_fields = if yaml.key? :format
|
|
333
|
+
if yaml[:format].key?(:fields)
|
|
334
|
+
%i[date amount description].map do |attr|
|
|
335
|
+
format('format/fields/%s', attr) unless yaml[:format][:fields].key?(attr)
|
|
336
|
+
end.compact
|
|
337
|
+
else
|
|
338
|
+
['format/fields']
|
|
339
|
+
end
|
|
340
|
+
else
|
|
341
|
+
['format']
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
raise MissingFields.new(*missing_fields) unless missing_fields.empty?
|
|
345
|
+
|
|
346
|
+
@fields_format = yaml[:format][:fields] if yaml[:format].key? :fields
|
|
347
|
+
@encoding_format = yaml[:format][:encoding] if yaml[:format].key? :encoding
|
|
348
|
+
@invert_amount = yaml[:format][:invert_amount] || false if yaml[:format].key? :invert_amount
|
|
349
|
+
@skip_lines = yaml[:format][:skip_lines]
|
|
350
|
+
@trim_lines = yaml[:format][:trim_lines]
|
|
351
|
+
@csv_format = { headers: yaml[:format][:csv_headers] } if yaml[:format].key? :csv_headers
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
class << self
|
|
355
|
+
include RVGP::Utilities
|
|
356
|
+
|
|
357
|
+
# Mostly this is a class mathed, to make testing easier
|
|
358
|
+
def input_file_contents(contents, skip_lines = nil, trim_lines = nil)
|
|
359
|
+
start_offset = 0
|
|
360
|
+
end_offset = contents.length
|
|
361
|
+
|
|
362
|
+
if trim_lines
|
|
363
|
+
trim_lines_regex = string_to_regex trim_lines.to_s
|
|
364
|
+
trim_lines_regex ||= /(?:[^\n]*\n?){0,#{trim_lines}}\Z/m
|
|
365
|
+
match = trim_lines_regex.match contents
|
|
366
|
+
end_offset = match.begin 0 if match
|
|
367
|
+
return String.new if end_offset.zero?
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
if skip_lines
|
|
371
|
+
skip_lines_regex = string_to_regex skip_lines.to_s
|
|
372
|
+
skip_lines_regex ||= /(?:[^\n]*\n){0,#{skip_lines}}/m
|
|
373
|
+
match = skip_lines_regex.match contents
|
|
374
|
+
start_offset = match.end 0 if match
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# If our cursors overlapped, that means we're just returning an empty string
|
|
378
|
+
return String.new if end_offset < start_offset
|
|
379
|
+
|
|
380
|
+
contents[start_offset..(end_offset - 1)]
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
private
|
|
385
|
+
|
|
386
|
+
def input_file_contents
|
|
387
|
+
open_args = {}
|
|
388
|
+
open_args[:encoding] = @encoding_format if @encoding_format
|
|
389
|
+
self.class.input_file_contents File.read(input_file, **open_args), skip_lines, trim_lines
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# We actually returned semi-reconciled transactions here. That lets us do
|
|
393
|
+
# some remedial parsing before rule application, as well as reversing the order
|
|
394
|
+
# which, is needed for the to_shorthand to run in sequence.
|
|
395
|
+
def source_postings
|
|
396
|
+
@source_postings ||= begin
|
|
397
|
+
rows = CSV.parse input_file_contents, **csv_format
|
|
398
|
+
rows.collect.with_index do |csv_row, i|
|
|
399
|
+
# Set the object values, return the reconciled row:
|
|
400
|
+
tx = fields_format.collect do |field, formatter|
|
|
401
|
+
# TODO: I think we can stick formatter as a key, if it's a string, or int
|
|
402
|
+
[field.to_sym, formatter.respond_to?(:call) ? formatter.call(row: csv_row) : csv_row[field]]
|
|
403
|
+
end.compact.to_h
|
|
404
|
+
|
|
405
|
+
# Amount is a special case, which, we have now converted into
|
|
406
|
+
# commodity
|
|
407
|
+
if [RVGP::Journal::ComplexCommodity, RVGP::Journal::Commodity].any? { |klass| tx[:amount].is_a? klass }
|
|
408
|
+
commodity = tx[:amount]
|
|
409
|
+
end
|
|
410
|
+
commodity ||= RVGP::Journal::Commodity.from_symbol_and_amount(default_currency, tx[:amount])
|
|
411
|
+
|
|
412
|
+
commodity.invert! if invert_amount
|
|
413
|
+
|
|
414
|
+
RVGP::Base::Reconciler::Posting.new i + 1,
|
|
415
|
+
date: tx[:date],
|
|
416
|
+
description: tx[:description],
|
|
417
|
+
commodity: transform_commodity(commodity),
|
|
418
|
+
from: from
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../journal'
|
|
4
|
+
|
|
5
|
+
module RVGP
|
|
6
|
+
module Reconcilers
|
|
7
|
+
# This reconciler is instantiated for input files of type journal (Plain Text Accounting text files).
|
|
8
|
+
# There are no parameters to this Reconciler, that aren't already documented in {RVGP::Reconcilers}
|
|
9
|
+
class JournalReconciler < RVGP::Base::Reconciler
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def journal
|
|
13
|
+
RVGP::Journal.parse File.read(input_file)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def source_postings
|
|
17
|
+
@source_postings ||= journal.postings.map do |posting|
|
|
18
|
+
unless posting.transfers.first.commodity && posting.transfers.last.commodity.nil?
|
|
19
|
+
raise StandardError, format('Unimplemented posting on: %<file>s:%<line_no>d',
|
|
20
|
+
file: input_file, line_no: posting.line_number)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# For Journal:Posting's with multiple account transfer lines, we break it into
|
|
24
|
+
# multiple RVGP::Base::Reconciler::Posting postings.
|
|
25
|
+
posting.transfers[0...-1].map do |transfer|
|
|
26
|
+
# NOTE: The tags.dup appears to be needed, because otherwise the
|
|
27
|
+
# tags array ends up shared between the two entries, and
|
|
28
|
+
# operations on one, appear in the other's contents
|
|
29
|
+
RVGP::Base::Reconciler::Posting.new posting.line_number,
|
|
30
|
+
date: posting.date,
|
|
31
|
+
tags: posting.tags.dup,
|
|
32
|
+
from: from,
|
|
33
|
+
description: posting.description,
|
|
34
|
+
commodity: transform_commodity(transfer.commodity),
|
|
35
|
+
to: transfer.account
|
|
36
|
+
end
|
|
37
|
+
end.flatten
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Finance
|
|
4
|
+
# The default functionality in this class, specified in the finance gem, is
|
|
5
|
+
# overwritten, to support the additional_payments feature of RVGP::Reconcilers::Shorthand::Mortgage
|
|
6
|
+
class Amortization
|
|
7
|
+
def balance=(val)
|
|
8
|
+
@balance = DecNum.new val
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# This was copied out of :
|
|
12
|
+
# https://github.com/marksweston/finance/blob/master/lib/finance/amortization.rb
|
|
13
|
+
# Because bank of america doesn't round the same way...
|
|
14
|
+
def amortize(rate)
|
|
15
|
+
# For the purposes of calculating a payment, the relevant time
|
|
16
|
+
# period is the remaining number of periods in the loan, not
|
|
17
|
+
# necessarily the duration of the rate itself.
|
|
18
|
+
periods = @periods - @period
|
|
19
|
+
amount = Finance::Amortization.payment @balance, rate.monthly, periods
|
|
20
|
+
|
|
21
|
+
pmt = Finance::Payment.new amount, period: @period
|
|
22
|
+
|
|
23
|
+
rate.duration.to_i.times do
|
|
24
|
+
# NOTE: This is the only change I made:
|
|
25
|
+
# (well, I also removed the pmt based block.call above)
|
|
26
|
+
@block&.call(@period, self)
|
|
27
|
+
|
|
28
|
+
# Do this first in case the balance is zero already.
|
|
29
|
+
break if @balance.zero?
|
|
30
|
+
|
|
31
|
+
# Compute and record interest on the outstanding balance.
|
|
32
|
+
int = (@balance * rate.monthly).round(2)
|
|
33
|
+
|
|
34
|
+
interest = Finance::Interest.new int, period: @period
|
|
35
|
+
|
|
36
|
+
@balance += interest.amount
|
|
37
|
+
@transactions << interest.dup
|
|
38
|
+
|
|
39
|
+
# Record payment. Don't pay more than the outstanding balance.
|
|
40
|
+
pmt.amount = -@balance if pmt.amount.abs > @balance
|
|
41
|
+
@transactions << pmt.dup
|
|
42
|
+
@balance += pmt.amount
|
|
43
|
+
|
|
44
|
+
@period += 1
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RVGP
|
|
4
|
+
module Reconcilers
|
|
5
|
+
module Shorthand
|
|
6
|
+
# This reconciler module will automatically allocate ATM components of a transaction, to constituent
|
|
7
|
+
# accounts. This module is useful for tracking the myriad expenses that banks impose on your atm
|
|
8
|
+
# withdrawals internationally. This module takes the total withdrawal, as reported in the input file
|
|
9
|
+
# and deducts conversion_markup and operation_costs from that total. It then takes the remainder balance
|
|
10
|
+
# and constructs a {RVGP::Journal::ComplexCommodity} with the provided :amount as the :left side of that
|
|
11
|
+
# balance, and the remainder after fees on the right side. This seems to be how all ATM's (that I've
|
|
12
|
+
# encountered) work. Note that not all atm, use all of the fees listed below. Some will use them all,
|
|
13
|
+
# some will use a subset.
|
|
14
|
+
#
|
|
15
|
+
# The module parameters we support are:
|
|
16
|
+
# - **amount** [Commodity] - The amount you withdrew on the ATM screen. This is paper amount, that you received.
|
|
17
|
+
# This amount should be denoted in the commodity you received.
|
|
18
|
+
# - **operation_cost** [Commodity] - This amount is denominated in the same currency you received in paper, and
|
|
19
|
+
# is typically listed in a summary screen, and on your printed receipt.
|
|
20
|
+
# - **conversion_markup** [String] - This is a percentage, expressed as a string. So, "7.5%" would be expected
|
|
21
|
+
# to be written as "7.5", here. This amount is typically listed on a summary screen, and in your printed
|
|
22
|
+
# receipt.
|
|
23
|
+
# - **conversion_markup_to** [String] - The account that :conversion_markup fees should be transferred to
|
|
24
|
+
# - **operation_cost_to** [String] - The account that :operation_cost fees should be transferred to
|
|
25
|
+
#
|
|
26
|
+
# # Example
|
|
27
|
+
# Here's how this module might be used in your reconciler:
|
|
28
|
+
# ```
|
|
29
|
+
# ...
|
|
30
|
+
# - match: /BANCOLOMBIA/
|
|
31
|
+
# to: Personal:Assets:Cash
|
|
32
|
+
# to_shorthand: InternationalAtm
|
|
33
|
+
# shorthand_params:
|
|
34
|
+
# amount: "600000 COP"
|
|
35
|
+
# operation_cost: "24290.00 COP"
|
|
36
|
+
# operation_cost_to: Personal:Expenses:Banking:Fees:RandomAtmOperator
|
|
37
|
+
# conversion_markup: "7.5"
|
|
38
|
+
# conversion_markup_to: Personal:Expenses:Banking:Fees:RandomAtmOperator
|
|
39
|
+
# ...
|
|
40
|
+
# ```
|
|
41
|
+
# And how one of these above uses will reconcile, in your build:
|
|
42
|
+
# ```
|
|
43
|
+
# ...
|
|
44
|
+
# 2023-02-18 BANCOLOMBIA AERO_JMC4 antioquia
|
|
45
|
+
# Personal:Assets:Cash 600000.00 COP @@ $ 123.26
|
|
46
|
+
# Personal:Expenses:Banking:Fees:RandomAtmOperator 24290.00 COP @@ $ 4.99
|
|
47
|
+
# Personal:Expenses:Banking:Fees:RandomAtmOperator $ 9.62
|
|
48
|
+
# Personal:Assets:AcmeBank:Checking
|
|
49
|
+
# ...
|
|
50
|
+
# ```
|
|
51
|
+
# Note that the reconciler line above, could match more than one transaction in the input file, and if it
|
|
52
|
+
# does, each of them will be expanded similarly to the expansion below. Though, with international exchange
|
|
53
|
+
# rates changing on a daily basis, the numbers may be different, depending on the debit amount encountered
|
|
54
|
+
# in the input file.
|
|
55
|
+
class InternationalAtm
|
|
56
|
+
# @!visibility private
|
|
57
|
+
MSG_MISSING_REQUIRED_FIELDS = "'International Atm' module at line:%s missing required field %s"
|
|
58
|
+
# @!visibility private
|
|
59
|
+
MSG_OPERATION_COST_AND_AMOUNT_MUST_HAVE_SAME_COMMODITY = "'International Atm' module at line:%s requires " \
|
|
60
|
+
'that the operation cost currency matches the ' \
|
|
61
|
+
'amount withdrawn'
|
|
62
|
+
# @!visibility private
|
|
63
|
+
MSG_FIELD_REQUIRED_IF_FIELD_EXISTS = "'International Atm' module at line:%s. Field %s is required if field " \
|
|
64
|
+
'%s is provided.'
|
|
65
|
+
|
|
66
|
+
# @!visibility private
|
|
67
|
+
attr_reader :tag, :targets, :to, :amount, :operation_cost, :conversion_markup,
|
|
68
|
+
:conversion_markup_to, :operation_cost_to
|
|
69
|
+
|
|
70
|
+
# @!visibility private
|
|
71
|
+
def initialize(rule)
|
|
72
|
+
@tag = rule[:tag]
|
|
73
|
+
@targets = rule[:targets]
|
|
74
|
+
@to = rule[:to] || 'Personal:Assets'
|
|
75
|
+
|
|
76
|
+
if rule.key? :shorthand_params
|
|
77
|
+
shorthand_params = rule[:shorthand_params]
|
|
78
|
+
@amount = shorthand_params[:amount].to_commodity if shorthand_params.key? :amount
|
|
79
|
+
@operation_cost = shorthand_params[:operation_cost].to_commodity if shorthand_params.key? :operation_cost
|
|
80
|
+
if shorthand_params.key? :conversion_markup
|
|
81
|
+
@conversion_markup = (BigDecimal(shorthand_params[:conversion_markup]) / 100) + 1
|
|
82
|
+
end
|
|
83
|
+
if shorthand_params.key? :conversion_markup_to
|
|
84
|
+
@conversion_markup_to = shorthand_params[:conversion_markup_to]
|
|
85
|
+
end
|
|
86
|
+
@operation_cost_to = shorthand_params[:operation_cost_to] if shorthand_params.key? :operation_cost_to
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
raise StandardError, format(MSG_MISSING_REQUIRED_FIELDS, rule[:line].inspect, 'amount') unless amount
|
|
90
|
+
|
|
91
|
+
if conversion_markup && conversion_markup_to.nil?
|
|
92
|
+
raise StandardError, format(MSG_MISSING_REQUIRED_FIELDS, rule[:line].inspect, 'conversion_markup_to',
|
|
93
|
+
'conversion_markup')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if operation_cost && operation_cost_to.nil?
|
|
97
|
+
raise StandardError, format(MSG_MISSING_REQUIRED_FIELDS, rule[:line].inspect, 'operation_cost_to',
|
|
98
|
+
'operation_cost')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
if operation_cost && operation_cost.alphabetic_code != amount.alphabetic_code
|
|
102
|
+
raise StandardError, format(MSG_OPERATION_COST_AND_AMOUNT_MUST_HAVE_SAME_COMMODITY, rule[:line].inspect)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @!visibility private
|
|
107
|
+
def to_tx(from_posting)
|
|
108
|
+
reported_amount = from_posting.commodity
|
|
109
|
+
targets = []
|
|
110
|
+
|
|
111
|
+
if conversion_markup
|
|
112
|
+
conversion_markup_fees = (reported_amount - (reported_amount / conversion_markup)).round(
|
|
113
|
+
RVGP::Journal::Currency.from_code_or_symbol(reported_amount.code).minor_unit
|
|
114
|
+
)
|
|
115
|
+
targets << { to: conversion_markup_to, commodity: conversion_markup_fees }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if operation_cost
|
|
119
|
+
amount_with_operation_cost = amount + operation_cost
|
|
120
|
+
operation_cost_fraction = (
|
|
121
|
+
operation_cost.quantity_as_bigdecimal / amount_with_operation_cost.quantity_as_bigdecimal
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
amount_after_conversion_fees = [reported_amount, conversion_markup_fees].compact.reduce(:-)
|
|
125
|
+
|
|
126
|
+
operation_cost_fees = (amount_after_conversion_fees * operation_cost_fraction).round(
|
|
127
|
+
RVGP::Journal::Currency.from_code_or_symbol(amount_after_conversion_fees.code).minor_unit
|
|
128
|
+
)
|
|
129
|
+
targets << { to: operation_cost_to,
|
|
130
|
+
complex_commodity: RVGP::Journal::ComplexCommodity.new(left: operation_cost,
|
|
131
|
+
operation: :per_lot,
|
|
132
|
+
right: operation_cost_fees) }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
remitted = [reported_amount, conversion_markup_fees, operation_cost_fees].compact.reduce(:-)
|
|
136
|
+
|
|
137
|
+
targets << { to: to,
|
|
138
|
+
complex_commodity: RVGP::Journal::ComplexCommodity.new(left: amount,
|
|
139
|
+
operation: :per_lot,
|
|
140
|
+
right: remitted) }
|
|
141
|
+
|
|
142
|
+
RVGP::Base::Reconciler::Posting.new from_posting.line_number,
|
|
143
|
+
date: from_posting.date,
|
|
144
|
+
description: from_posting.description,
|
|
145
|
+
from: from_posting.from,
|
|
146
|
+
tags: from_posting.tags,
|
|
147
|
+
targets: targets.reverse
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|