rock_books 0.6.1 → 0.7.0
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 +4 -4
- data/README.md +1 -3
- data/RELEASE_NOTES.md +12 -0
- data/assets/fonts/JetBrainsMono-Medium.ttf +0 -0
- data/lib/rock_books/cmd_line/command_line_interface.rb +6 -6
- data/lib/rock_books/cmd_line/main.rb +1 -9
- data/lib/rock_books/documents/book_set.rb +1 -1
- data/lib/rock_books/documents/chart_of_accounts.rb +29 -12
- data/lib/rock_books/documents/journal.rb +2 -6
- data/lib/rock_books/documents/journal_entry.rb +7 -2
- data/lib/rock_books/documents/journal_entry_builder.rb +4 -0
- data/lib/rock_books/helpers/book_set_loader.rb +3 -3
- data/lib/rock_books/helpers/html_helper.rb +22 -16
- data/lib/rock_books/reports/balance_sheet.rb +8 -42
- data/lib/rock_books/reports/book_set_reporter.rb +104 -86
- data/lib/rock_books/reports/data/bs_is_data.rb +61 -0
- data/lib/rock_books/reports/data/bs_is_section_data.rb +28 -0
- data/lib/rock_books/reports/data/journal_data.rb +37 -0
- data/lib/rock_books/reports/data/multidoc_txn_by_account_data.rb +40 -0
- data/lib/rock_books/reports/data/multidoc_txn_report_data.rb +39 -0
- data/lib/rock_books/reports/data/receipts_report_data.rb +47 -0
- data/lib/rock_books/reports/data/tx_one_account_data.rb +37 -0
- data/lib/rock_books/reports/helpers/erb_helper.rb +26 -0
- data/lib/rock_books/reports/helpers/reporter.rb +134 -0
- data/lib/rock_books/reports/income_statement.rb +8 -46
- data/lib/rock_books/reports/journal_report.rb +72 -0
- data/lib/rock_books/reports/multidoc_txn_by_account_report.rb +32 -0
- data/lib/rock_books/reports/multidoc_txn_report.rb +25 -0
- data/lib/rock_books/reports/receipts_report.rb +5 -54
- data/lib/rock_books/reports/templates/html/index.html.erb +141 -0
- data/lib/rock_books/reports/templates/html/report_page.html.erb +12 -0
- data/lib/rock_books/reports/templates/text/_receipt_section.txt.erb +17 -0
- data/lib/rock_books/reports/templates/text/_totals.txt.erb +8 -0
- data/lib/rock_books/reports/templates/text/balance_sheet.txt.erb +21 -0
- data/lib/rock_books/reports/templates/text/income_statement.txt.erb +21 -0
- data/lib/rock_books/reports/templates/text/journal.txt.erb +20 -0
- data/lib/rock_books/reports/templates/text/multidoc_txn_by_account_report.txt.erb +28 -0
- data/lib/rock_books/reports/templates/text/multidoc_txn_report.txt.erb +22 -0
- data/lib/rock_books/reports/templates/text/receipts_report.txt.erb +13 -0
- data/lib/rock_books/reports/templates/text/tx_one_account.txt.erb +18 -0
- data/lib/rock_books/reports/tx_one_account.rb +9 -44
- data/lib/rock_books/types/account.rb +13 -1
- data/lib/rock_books/types/account_type.rb +18 -7
- data/lib/rock_books/version.rb +1 -1
- data/rock_books.gemspec +1 -0
- metadata +41 -9
- data/lib/rock_books/reports/index.html.erb +0 -156
- data/lib/rock_books/reports/multidoc_transaction_report.rb +0 -66
- data/lib/rock_books/reports/receipts.html.erb +0 -54
- data/lib/rock_books/reports/reporter.rb +0 -118
- data/lib/rock_books/reports/transaction_report.rb +0 -105
- data/lib/rock_books/reports/tx_by_account.rb +0 -82
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c30673728b99315fc841b99b96081751f91459663c736758b7340a317407cc56
|
4
|
+
data.tar.gz: 751adad9c6038a85a696deb4076bbc93bbfb8a8650d53159aca691b281388d9f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 84ff76b2001eed18b4f4ec69f9949720a4dbca892f41a4fa6b96ddda9e19dde5660b34c514568f5410a117df8afddf84c33bf23ba31256efc2bceccbdb06bd01
|
7
|
+
data.tar.gz: 2214c3b52b24bf4b9b822059c4f41c67d7d3fc3a6fc666b8a073be79a794c769d1403d8f164085e382e1714320d71cbacd0673f591b26dd958e43f7b4cb1ab50
|
data/README.md
CHANGED
@@ -33,10 +33,8 @@ To simplify its implementation, RockBooks assumes some conventions:
|
|
33
33
|
|
34
34
|
#### Supported Operating Systems
|
35
35
|
|
36
|
-
At this time, RockBooks is
|
36
|
+
At this time, RockBooks is tested only on Mac OS and Linux. However, it will probably work fine on Windows.
|
37
37
|
|
38
|
-
If you get an error message saying that an external command is missing, install that command using your system's package manager (e.g. `sudo apt install txt2html`). For `wkhtmltopdf` on Linux, see [https://wkhtmltopdf.org/](https://wkhtmltopdf.org/).
|
39
|
-
|
40
38
|
#### Text Files as Input
|
41
39
|
|
42
40
|
Instead of a web interface, data input is done in plain text files. This isn't as fancy but has the following advantages:
|
data/RELEASE_NOTES.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
### v0.7.0
|
2
|
+
|
3
|
+
* Dependencies on external commands in Linux and Mac OS for generating PDF and HTML files has been eliminated,
|
4
|
+
using the prawn gem for PDF and simple ERB templating for HTML.
|
5
|
+
* Massive refactoring of reports to separate data generation from presentation.
|
6
|
+
* Reports are now computed and then written one at a time. Previously they were all computed, then all written.
|
7
|
+
* ERB is now used for generating text reports.
|
8
|
+
* Fix receipt hyperlinks in HTML output.
|
9
|
+
* Improve some error output.
|
10
|
+
* Various minor improvements and bug fixes.
|
11
|
+
|
12
|
+
|
1
13
|
### v0.6.1
|
2
14
|
|
3
15
|
* Linux PDF generation fixed by using wkhtmltopdf instead of cupsfilter.
|
Binary file
|
@@ -4,7 +4,7 @@ require 'ostruct'
|
|
4
4
|
|
5
5
|
require_relative '../../rock_books'
|
6
6
|
require_relative '../version'
|
7
|
-
require_relative '../reports/reporter'
|
7
|
+
require_relative '../reports/helpers/reporter'
|
8
8
|
require_relative '../helpers/book_set_loader'
|
9
9
|
|
10
10
|
module RockBooks
|
@@ -105,7 +105,7 @@ When in interactive shell mode:
|
|
105
105
|
end
|
106
106
|
end
|
107
107
|
|
108
|
-
|
108
|
+
validate_receipt_dir = -> do
|
109
109
|
File.directory?(options.receipt_dir) ? nil : \
|
110
110
|
"Receipts directory '#{options.receipt_dir}' does not exist. "
|
111
111
|
end
|
@@ -114,7 +114,7 @@ When in interactive shell mode:
|
|
114
114
|
output << validate_input_dir.()
|
115
115
|
output << validate_output_dir.()
|
116
116
|
if run_options.do_receipts
|
117
|
-
output <<
|
117
|
+
output << validate_receipt_dir.()
|
118
118
|
end
|
119
119
|
|
120
120
|
output.compact!
|
@@ -298,9 +298,9 @@ When in interactive shell mode:
|
|
298
298
|
|
299
299
|
|
300
300
|
def cmd_d
|
301
|
-
book_set.all_reports($filter).each do |short_name,
|
301
|
+
book_set.all_reports($filter).each do |short_name, text_report|
|
302
302
|
puts "#{short_name}:\n\n"
|
303
|
-
puts
|
303
|
+
puts text_report
|
304
304
|
puts "\n\n\n"
|
305
305
|
end
|
306
306
|
nil
|
@@ -358,7 +358,7 @@ When in interactive shell mode:
|
|
358
358
|
end
|
359
359
|
|
360
360
|
def cmd_w
|
361
|
-
BookSetReporter.new(book_set, run_options.output_dir, $filter).
|
361
|
+
BookSetReporter.new(book_set, run_options.output_dir, $filter).generate
|
362
362
|
nil
|
363
363
|
end
|
364
364
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require '
|
1
|
+
require 'amazing_print'
|
2
2
|
require 'optparse'
|
3
3
|
require 'pry'
|
4
4
|
require 'shellwords'
|
@@ -67,10 +67,6 @@ class Main
|
|
67
67
|
options.verbose_mode = v
|
68
68
|
end
|
69
69
|
|
70
|
-
parser.on('-y', '--[no-]say', 'Say error messages.') do |v|
|
71
|
-
options.say = v
|
72
|
-
end
|
73
|
-
|
74
70
|
parser.on('', '--[no-]receipts', 'Include report on existing and missing receipts.') do |v|
|
75
71
|
options.do_receipts = v
|
76
72
|
end
|
@@ -101,10 +97,6 @@ class Main
|
|
101
97
|
|
102
98
|
HEREDOC
|
103
99
|
|
104
|
-
if run_options.say
|
105
|
-
`say #{error}`
|
106
|
-
end
|
107
|
-
|
108
100
|
exit(-1)
|
109
101
|
binding.pry
|
110
102
|
raise error
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'stringio'
|
1
2
|
require_relative '../types/account'
|
2
3
|
require_relative '../types/account_type'
|
3
4
|
require_relative '../errors/error'
|
@@ -10,8 +11,8 @@ class ChartOfAccounts
|
|
10
11
|
REQUIRED_FIELDS.each { |field| attr_reader(field) }
|
11
12
|
|
12
13
|
|
13
|
-
def self.from_file(
|
14
|
-
self.new(File.readlines(
|
14
|
+
def self.from_file(filespec)
|
15
|
+
self.new(File.readlines(filespec).map(&:chomp), filespec)
|
15
16
|
end
|
16
17
|
|
17
18
|
|
@@ -20,11 +21,16 @@ class ChartOfAccounts
|
|
20
21
|
end
|
21
22
|
|
22
23
|
|
23
|
-
def initialize(input_lines)
|
24
|
+
def initialize(input_lines, filespec = nil)
|
25
|
+
@filespec = filespec
|
24
26
|
@accounts = []
|
25
|
-
input_lines
|
27
|
+
parse_lines(input_lines)
|
26
28
|
# TODO: Add validation for required fields.
|
29
|
+
check_for_missing_fields
|
30
|
+
end
|
31
|
+
|
27
32
|
|
33
|
+
def check_for_missing_fields
|
28
34
|
missing_fields = REQUIRED_FIELDS.select do |field|
|
29
35
|
instance_variable_get("@#{field}").nil?
|
30
36
|
end
|
@@ -35,6 +41,19 @@ class ChartOfAccounts
|
|
35
41
|
end
|
36
42
|
|
37
43
|
|
44
|
+
def parse_lines(input_lines)
|
45
|
+
input_lines.each_with_index do |line, line_num|
|
46
|
+
begin
|
47
|
+
parse_line(line)
|
48
|
+
rescue => e
|
49
|
+
file_message_fragment = (@filespec ? " in file '#{@filespec}'" : '')
|
50
|
+
puts "Error parsing chart of accounts#{file_message_fragment}. Bad line is line ##{line_num}, text is:\n#{line}\n\n"
|
51
|
+
raise
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
38
57
|
def parse_date(date_string)
|
39
58
|
# TODO: Add better handling for this error.
|
40
59
|
# begin
|
@@ -69,16 +88,14 @@ class ChartOfAccounts
|
|
69
88
|
rest = matcher[2]
|
70
89
|
|
71
90
|
matcher = rest.match(/^(\S+)\s+(.*)$/)
|
91
|
+
|
72
92
|
account_type_token = matcher[1]
|
73
|
-
account_type = AccountType.
|
93
|
+
account_type = AccountType.letter_to_type(account_type_token)
|
74
94
|
|
75
95
|
name = matcher[2]
|
76
96
|
|
77
|
-
accounts << Account.new(code, account_type, name)
|
97
|
+
accounts << Account.new(code, account_type.symbol, name)
|
78
98
|
end
|
79
|
-
rescue => e
|
80
|
-
puts "Error parsing chart of accounts. Line text is:\n#{line}\n\n"
|
81
|
-
raise
|
82
99
|
end
|
83
100
|
|
84
101
|
end
|
@@ -105,7 +122,7 @@ class ChartOfAccounts
|
|
105
122
|
|
106
123
|
|
107
124
|
def report_string
|
108
|
-
result =
|
125
|
+
result = StringIO.new
|
109
126
|
|
110
127
|
if title
|
111
128
|
result << title << "\n\n"
|
@@ -113,9 +130,9 @@ class ChartOfAccounts
|
|
113
130
|
|
114
131
|
code_width = @accounts.inject(0) { |width, a| width = [width, a.code.length].max }
|
115
132
|
format_string = "%-#{code_width}s %-10.10s %s\n"
|
116
|
-
accounts.each { |a| result << (format_string
|
133
|
+
accounts.each { |a| result << sprintf(format_string, a.code, a.type.to_s, a.name) }
|
117
134
|
|
118
|
-
result
|
135
|
+
result.string
|
119
136
|
end
|
120
137
|
|
121
138
|
|
@@ -7,7 +7,7 @@ require_relative '../types/acct_amount'
|
|
7
7
|
require_relative '../types/journal_entry_context'
|
8
8
|
require_relative 'journal_entry'
|
9
9
|
require_relative 'journal_entry_builder'
|
10
|
-
require_relative '../reports/reporter'
|
10
|
+
require_relative '../reports/helpers/reporter'
|
11
11
|
|
12
12
|
module RockBooks
|
13
13
|
|
@@ -45,9 +45,7 @@ class Journal
|
|
45
45
|
end
|
46
46
|
|
47
47
|
|
48
|
-
|
49
|
-
|
50
|
-
def self.acct_amounts_in_documents(documents, entries_filter = nil, acct_amounts_filter = nil)
|
48
|
+
def self.acct_amounts_in_documents(documents, entries_filter = nil, acct_amounts_filter = nil)
|
51
49
|
entries = entries_in_documents(documents, entries_filter)
|
52
50
|
|
53
51
|
acct_amounts = entries.each_with_object([]) do |entry, acct_amounts|
|
@@ -72,7 +70,6 @@ class Journal
|
|
72
70
|
@short_name = short_name
|
73
71
|
@entries = []
|
74
72
|
@date_prefix = ''
|
75
|
-
@title = ''
|
76
73
|
input_lines.each_with_index do |line, linenum|
|
77
74
|
context = JournalEntryContext.new(self, linenum + 1, line)
|
78
75
|
parse_line(context)
|
@@ -131,7 +128,6 @@ class Journal
|
|
131
128
|
end
|
132
129
|
|
133
130
|
|
134
|
-
|
135
131
|
def acct_amounts
|
136
132
|
entries.each_with_object([]) { |entry, acct_amounts| acct_amounts << entry.acct_amounts }.flatten
|
137
133
|
end
|
@@ -32,12 +32,17 @@ class JournalEntry < Struct.new(:date, :acct_amounts, :doc_short_name, :descript
|
|
32
32
|
|
33
33
|
def self.sort_entries_by_amount_descending!(entries)
|
34
34
|
entries.sort_by! do |entry|
|
35
|
-
[entry.total_absolute_value, entry.doc_short_name]
|
35
|
+
[-entry.total_absolute_value, entry.doc_short_name]
|
36
36
|
end
|
37
|
-
entries.reverse!
|
38
37
|
end
|
39
38
|
|
40
39
|
|
40
|
+
def self.sort_entries_by_date!(entries)
|
41
|
+
entries.sort_by! { |entry| [entry.date, entry.doc_short_name] }
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
|
41
46
|
def total_for_code(account_code)
|
42
47
|
acct_amounts_with_code(account_code).map(&:amount).sum
|
43
48
|
end
|
@@ -14,6 +14,10 @@ class JournalEntryBuilder < Struct.new(:journal_entry_context)
|
|
14
14
|
def chart_of_accounts; journal_entry_context.chart_of_accounts; end
|
15
15
|
|
16
16
|
|
17
|
+
# A "token" in this context means an account name and account amount pair, e.g. "pnc.checking 1234.56".
|
18
|
+
# @param tokens the account name/amount pairs found in the source text
|
19
|
+
# @date transaction date
|
20
|
+
# @return array of AcctAmount instances
|
17
21
|
def acct_amounts_from_tokens(tokens, date)
|
18
22
|
acct_amounts = []
|
19
23
|
|
@@ -7,6 +7,7 @@ module RockBooks
|
|
7
7
|
|
8
8
|
module_function
|
9
9
|
|
10
|
+
# @return a hash whose keys are the filespecs and values are the document types
|
10
11
|
def get_files_with_types(directory)
|
11
12
|
files = Dir[File.join(directory, '*.txt')]
|
12
13
|
files.each_with_object({}) do |filespec, files_with_types|
|
@@ -42,7 +43,7 @@ module RockBooks
|
|
42
43
|
|
43
44
|
# Uses all *.txt files in the specified directory; uses @doc_type to determine which
|
44
45
|
# is the chart of accounts and which are journals.
|
45
|
-
# To exclude a file, make the extension other than .
|
46
|
+
# To exclude a file, make the extension something other than .txt.
|
46
47
|
def load(run_options)
|
47
48
|
|
48
49
|
files_with_types = get_files_with_types(run_options.input_dir)
|
@@ -54,9 +55,8 @@ module RockBooks
|
|
54
55
|
validate_journal_file_count(journal_files)
|
55
56
|
|
56
57
|
chart_of_accounts = ChartOfAccounts.from_file(chart_of_account_files.first)
|
57
|
-
journals = journal_files.map { |
|
58
|
+
journals = journal_files.map { |filespec| Journal.from_file(chart_of_accounts, filespec) }
|
58
59
|
BookSet.new(run_options, chart_of_accounts, journals)
|
59
60
|
end
|
60
|
-
|
61
61
|
end
|
62
62
|
end
|
@@ -1,29 +1,35 @@
|
|
1
1
|
module RockBooks
|
2
2
|
module HtmlHelper
|
3
3
|
|
4
|
-
|
4
|
+
def self.convert_receipts_to_hyperlinks(original_html_string, html_filespec)
|
5
|
+
html_lines = original_html_string.split("\n")
|
6
|
+
content_changed = false
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
|
8
|
+
# If the HTML file being created is in DATA_DIR/rockbooks-reports/html/single-account, then
|
9
|
+
# the processed link should be '../../../receipts/[receipt_filespec]'
|
10
|
+
# else it's in DATA_DIR/rockbooks-reports/html, and
|
11
|
+
# the processed link should be '../../receipts/[receipt_filespec]'
|
12
|
+
processed_receipt_filespec = ->(listed_receipt_filespec) do
|
13
|
+
num_dirs_up = html_filespec.include?('/single-account/') ? 3 : 2
|
14
|
+
File.join(('../' * num_dirs_up), 'receipts', listed_receipt_filespec)
|
15
|
+
end
|
16
|
+
|
17
|
+
receipt_anchor_line = ->(line, listed_receipt_filespec) do
|
18
|
+
line.gsub( \
|
19
|
+
/Receipt:\s*#{listed_receipt_filespec}/, \
|
20
|
+
%Q{Receipt: <a href="#{processed_receipt_filespec.(listed_receipt_filespec)}">#{listed_receipt_filespec}</a>})
|
21
|
+
end
|
9
22
|
|
10
23
|
html_lines.each_with_index do |line, index|
|
11
|
-
matches = /Receipt:\s*(
|
24
|
+
matches = /Receipt:\s*(\S*)/.match(line)
|
12
25
|
if matches
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
%Q{Receipt: <a href="../../../receipts/#{receipt_filespec}">#{receipt_filespec}</a>})
|
17
|
-
html_lines[index] = line_with_hyperlink
|
18
|
-
replacements_made = true
|
26
|
+
listed_receipt_filespec = matches[1]
|
27
|
+
html_lines[index] = receipt_anchor_line.(line, listed_receipt_filespec)
|
28
|
+
content_changed = true
|
19
29
|
end
|
20
30
|
end
|
21
31
|
|
22
|
-
|
23
|
-
html_text = html_lines.join("\n")
|
24
|
-
end
|
25
|
-
|
26
|
-
[html_text, replacements_made]
|
32
|
+
content_changed ? html_lines.join("\n") : original_html_string
|
27
33
|
end
|
28
34
|
end
|
29
35
|
end
|
@@ -1,6 +1,5 @@
|
|
1
|
-
require_relative '
|
2
|
-
require_relative '
|
3
|
-
require_relative 'report_context'
|
1
|
+
require_relative 'helpers/erb_helper'
|
2
|
+
require_relative 'helpers/reporter'
|
4
3
|
|
5
4
|
module RockBooks
|
6
5
|
|
@@ -10,51 +9,18 @@ module RockBooks
|
|
10
9
|
class BalanceSheet
|
11
10
|
|
12
11
|
include Reporter
|
12
|
+
include ErbHelper
|
13
13
|
|
14
|
-
attr_accessor :context
|
14
|
+
attr_accessor :context, :data
|
15
15
|
|
16
|
-
def initialize(report_context)
|
16
|
+
def initialize(report_context, data)
|
17
17
|
@context = report_context
|
18
|
+
@data = data
|
18
19
|
end
|
19
20
|
|
20
21
|
|
21
|
-
def
|
22
|
-
|
22
|
+
def generate
|
23
|
+
ErbHelper.render_hashes('text/balance_sheet.txt.erb', data, template_presentation_context)
|
23
24
|
end
|
24
|
-
|
25
|
-
|
26
|
-
def generate_header
|
27
|
-
lines = [banner_line]
|
28
|
-
lines << center(context.entity || 'Unspecified Entity')
|
29
|
-
lines << center("Balance Sheet for Period Ending #{end_date}")
|
30
|
-
lines << banner_line
|
31
|
-
lines << ''
|
32
|
-
lines << ''
|
33
|
-
lines << ''
|
34
|
-
lines.join("\n")
|
35
|
-
end
|
36
|
-
|
37
|
-
|
38
|
-
def generate_report
|
39
|
-
filter = RockBooks::JournalEntryFilters.date_on_or_before(end_date)
|
40
|
-
acct_amounts = Journal.acct_amounts_in_documents(context.journals, filter)
|
41
|
-
totals = AcctAmount.aggregate_amounts_by_account(acct_amounts)
|
42
|
-
output = generate_header
|
43
|
-
|
44
|
-
asset_output, asset_total = generate_account_type_section('Assets', totals, :asset, false)
|
45
|
-
liab_output, liab_total = generate_account_type_section('Liabilities', totals, :liability, true)
|
46
|
-
equity_output, equity_total = generate_account_type_section('Equity', totals, :equity, true)
|
47
|
-
|
48
|
-
output << [asset_output, liab_output, equity_output].join("\n\n")
|
49
|
-
|
50
|
-
grand_total = asset_total - (liab_total + equity_total)
|
51
|
-
|
52
|
-
output << "\n#{"%12.2f Assets - (Liabilities + Equity)" % grand_total}\n============\n"
|
53
|
-
output
|
54
|
-
end
|
55
|
-
|
56
|
-
alias_method :to_s, :generate_report
|
57
|
-
alias_method :call, :generate_report
|
58
|
-
|
59
25
|
end
|
60
26
|
end
|
@@ -1,13 +1,19 @@
|
|
1
1
|
require_relative '../documents/book_set'
|
2
2
|
|
3
3
|
require_relative 'balance_sheet'
|
4
|
+
require_relative 'data/bs_is_data'
|
5
|
+
require_relative 'data/receipts_report_data'
|
4
6
|
require_relative 'income_statement'
|
5
|
-
require_relative '
|
7
|
+
require_relative 'multidoc_txn_report'
|
6
8
|
require_relative 'receipts_report'
|
7
9
|
require_relative 'report_context'
|
8
|
-
require_relative '
|
9
|
-
require_relative '
|
10
|
+
require_relative 'journal_report'
|
11
|
+
require_relative 'multidoc_txn_by_account_report'
|
10
12
|
require_relative 'tx_one_account'
|
13
|
+
require_relative 'helpers/erb_helper'
|
14
|
+
require_relative 'helpers/reporter'
|
15
|
+
|
16
|
+
require 'prawn'
|
11
17
|
|
12
18
|
module RockBooks
|
13
19
|
class BookSetReporter
|
@@ -21,6 +27,8 @@ class BookSetReporter
|
|
21
27
|
def_delegator :book_set, :chart_of_accounts
|
22
28
|
def_delegator :book_set, :run_options
|
23
29
|
|
30
|
+
FONT_FILESPEC = File.absolute_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'assets', 'fonts', 'JetBrainsMono-Medium.ttf'))
|
31
|
+
|
24
32
|
|
25
33
|
def initialize(book_set, output_dir, filter = nil)
|
26
34
|
@book_set = book_set
|
@@ -30,72 +38,84 @@ class BookSetReporter
|
|
30
38
|
end
|
31
39
|
|
32
40
|
|
33
|
-
def
|
34
|
-
check_prequisite_executables
|
35
|
-
reports = all_reports(filter)
|
41
|
+
def generate
|
36
42
|
create_directories
|
37
43
|
create_index_html
|
38
|
-
|
44
|
+
|
45
|
+
do_statements
|
46
|
+
do_journals
|
47
|
+
do_transaction_reports
|
48
|
+
do_single_account_reports
|
49
|
+
do_receipts_report
|
39
50
|
end
|
40
51
|
|
41
52
|
|
42
53
|
# All methods after this point are private.
|
43
54
|
|
44
|
-
private def
|
55
|
+
private def do_statements
|
56
|
+
bs_is_data = BsIsData.new(context)
|
57
|
+
|
58
|
+
bal_sheet_text_report = BalanceSheet.new(context, bs_is_data.bal_sheet_data).generate
|
59
|
+
write_report(:balance_sheet, bal_sheet_text_report)
|
45
60
|
|
46
|
-
|
47
|
-
|
48
|
-
|
61
|
+
inc_stat_text_report = IncomeStatement.new(context, bs_is_data.inc_stat_data).generate
|
62
|
+
write_report(:income_statement, inc_stat_text_report)
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
private def do_journals
|
67
|
+
journals.each do |journal|
|
68
|
+
report_data = JournalData.new(journal, context, filter).fetch
|
69
|
+
text_report = JournalReport.new(report_data, context, filter).generate
|
70
|
+
write_report(journal.short_name, text_report)
|
49
71
|
end
|
72
|
+
end
|
50
73
|
|
51
|
-
report_hash[:all_txns_by_date] = MultidocTransactionReport.new(context).call(filter)
|
52
|
-
report_hash[:all_txns_by_amount] = MultidocTransactionReport.new(context).call(filter, :amount)
|
53
|
-
report_hash[:all_txns_by_acct] = TxByAccount.new(context).call
|
54
|
-
report_hash[:balance_sheet] = BalanceSheet.new(context).call
|
55
|
-
report_hash[:income_statement] = IncomeStatement.new(context).call
|
56
74
|
|
57
|
-
|
58
|
-
|
75
|
+
private def do_transaction_reports
|
76
|
+
|
77
|
+
do_date_or_amount_report = ->(sort_field, short_name) do
|
78
|
+
data = MultidocTxnReportData.new(context, sort_field, filter).fetch
|
79
|
+
text_report = MultidocTransactionReport.new(data, context).generate
|
80
|
+
write_report(short_name, text_report)
|
59
81
|
end
|
60
82
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
83
|
+
do_acct_report = -> do
|
84
|
+
data = MultidocTxnByAccountData.new(context).fetch
|
85
|
+
text_report = MultidocTransactionByAccountReport.new(data, context).generate
|
86
|
+
write_report(:all_txns_by_acct, text_report)
|
65
87
|
end
|
66
88
|
|
67
|
-
|
89
|
+
do_date_or_amount_report.(:date, :all_txns_by_date)
|
90
|
+
do_date_or_amount_report.(:amount, :all_txns_by_amount)
|
91
|
+
do_acct_report.()
|
68
92
|
end
|
69
93
|
|
70
94
|
|
71
|
-
private def
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
end
|
78
|
-
unless stderr.size == 0
|
79
|
-
puts "\nStderr was:\n\n#{stderr}"
|
95
|
+
private def do_single_account_reports
|
96
|
+
chart_of_accounts.accounts.each do |account|
|
97
|
+
short_name = ('acct_' + account.code).to_sym
|
98
|
+
data = TxOneAccountData.new(context, account.code).fetch
|
99
|
+
text_report = TxOneAccount.new(data, context).generate
|
100
|
+
write_report(short_name, text_report)
|
80
101
|
end
|
81
|
-
puts
|
82
|
-
stdout
|
83
102
|
end
|
84
103
|
|
85
104
|
|
86
|
-
private def
|
87
|
-
|
88
|
-
|
105
|
+
private def do_receipts_report
|
106
|
+
data = ReceiptsReportData.new(book_set.all_entries, run_options.receipt_dir).fetch
|
107
|
+
text_report = ReceiptsReport.new(context, data).generate
|
108
|
+
write_report(:receipts, text_report)
|
89
109
|
end
|
90
110
|
|
91
111
|
|
92
|
-
private def
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
112
|
+
private def run_command(command)
|
113
|
+
puts "\n----\nRunning command: #{command}"
|
114
|
+
stdout, stderr, status = Open3.capture3(command)
|
115
|
+
puts "Exit code was #{status.exitstatus}."
|
116
|
+
puts "\nStdout was:\n\n#{stdout}" unless stdout.size == 0
|
117
|
+
puts "\nStderr was:\n\n#{stderr}" unless stderr.size == 0
|
118
|
+
puts
|
99
119
|
end
|
100
120
|
|
101
121
|
|
@@ -125,71 +145,69 @@ class BookSetReporter
|
|
125
145
|
end
|
126
146
|
|
127
147
|
|
128
|
-
private def
|
148
|
+
private def prawn_create_document(pdf_filespec, text)
|
149
|
+
Prawn::Document.generate(pdf_filespec) do
|
150
|
+
font(FONT_FILESPEC, size: 10)
|
129
151
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
152
|
+
utf8_nonbreaking_space = "\uC2A0"
|
153
|
+
unicode_nonbreaking_space = "\u00A0"
|
154
|
+
text(text.gsub(' ', unicode_nonbreaking_space))
|
155
|
+
end
|
156
|
+
puts "Finished generating #{pdf_filespec} with prawn."
|
157
|
+
end
|
134
158
|
|
135
|
-
File.write(txt_filespec, report_text)
|
136
159
|
|
137
|
-
|
138
|
-
textutil = ->(font_size) do
|
139
|
-
run_command("textutil -convert html -font 'Courier New Bold' -fontsize #{font_size} #{txt_filespec} -output #{html_filespec}")
|
140
|
-
end
|
141
|
-
cupsfilter = -> { run_command("cupsfilter #{txt_filespec} > #{pdf_filespec}") }
|
142
|
-
|
143
|
-
# Linux
|
144
|
-
txt2html = -> { run_command("txt2html --preformat_trigger_lines 0 #{txt_filespec} > #{html_filespec}") }
|
145
|
-
html2pdf = -> { run_command("wkhtmltopdf #{html_filespec} #{pdf_filespec}") }
|
146
|
-
|
147
|
-
# Use smaller size for the PDF but larger size for the web pages:
|
148
|
-
if OS.mac?
|
149
|
-
textutil.(11)
|
150
|
-
cupsfilter.()
|
151
|
-
textutil.(14)
|
152
|
-
else
|
153
|
-
txt2html.()
|
154
|
-
html2pdf.()
|
155
|
-
end
|
160
|
+
private def write_report(short_name, text_report)
|
156
161
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
162
|
+
txt_filespec = build_filespec(output_dir, short_name, 'txt')
|
163
|
+
html_filespec = build_filespec(output_dir, short_name, 'html')
|
164
|
+
pdf_filespec = build_filespec(output_dir, short_name, 'pdf')
|
165
|
+
|
166
|
+
create_text_report = -> { File.write(txt_filespec, text_report) }
|
167
|
+
|
168
|
+
create_pdf_report = -> { prawn_create_document(pdf_filespec, text_report) }
|
161
169
|
|
162
|
-
|
170
|
+
create_html_report = -> do
|
171
|
+
data = { report_body: text_report }
|
172
|
+
html_raw_report = ErbHelper.render_hashes("html/report_page.html.erb", data, {})
|
173
|
+
html_report = HtmlHelper.convert_receipts_to_hyperlinks(html_raw_report, html_filespec)
|
174
|
+
File.write(html_filespec, html_report)
|
163
175
|
end
|
164
|
-
|
176
|
+
|
177
|
+
create_text_report.()
|
178
|
+
create_pdf_report.()
|
179
|
+
create_html_report.()
|
165
180
|
|
166
181
|
|
167
|
-
|
168
|
-
File.join(run_options.receipt_dir, receipt_filespec)
|
182
|
+
puts "Created reports in txt, html, and pdf for #{"%-20s" % short_name} at #{File.dirname(txt_filespec)}.\n\n\n"
|
169
183
|
end
|
170
184
|
|
171
185
|
|
172
186
|
private def missing_existing_unused_receipts
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
187
|
+
missing_receipts = []
|
188
|
+
existing_receipts = []
|
189
|
+
receipt_full_filespec = ->(receipt_filespec) { File.join(run_options.receipt_dir, receipt_filespec) }
|
190
|
+
|
191
|
+
# We will start out putting all filespecs in the unused array, and delete them as they are found in the transactions.
|
192
|
+
unused_receipt_filespecs = Dir['receipts/**/*'].select { |s| File.file?(s) } \
|
193
|
+
.sort \
|
194
|
+
.map { |s| "./" + s } # Prepend './' to match the data
|
177
195
|
|
178
196
|
all_entries.each do |entry|
|
179
197
|
entry.receipts.each do |receipt|
|
180
|
-
filespec = receipt_full_filespec(receipt)
|
181
|
-
|
198
|
+
filespec = receipt_full_filespec.(receipt)
|
199
|
+
unused_receipt_filespecs.delete(filespec)
|
182
200
|
file_exists = File.file?(filespec)
|
183
|
-
list = (file_exists ?
|
201
|
+
list = (file_exists ? existing_receipts : missing_receipts)
|
184
202
|
list << { receipt: receipt, journal: entry.doc_short_name }
|
185
203
|
end
|
186
204
|
end
|
187
|
-
[
|
205
|
+
[missing_receipts, existing_receipts, unused_receipt_filespecs]
|
188
206
|
end
|
189
207
|
|
190
208
|
|
191
209
|
private def index_html_content
|
192
|
-
erb_filespec = File.join(File.dirname(__FILE__), 'index.html.erb')
|
210
|
+
erb_filespec = File.join(File.dirname(__FILE__), 'templates', 'html', 'index.html.erb')
|
193
211
|
erb = ERB.new(File.read(erb_filespec))
|
194
212
|
erb.result_with_hash(
|
195
213
|
journals: journals,
|