rock_books 0.6.0 → 0.9.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 +49 -10
- 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 -2
- data/lib/rock_books/documents/chart_of_accounts.rb +29 -12
- data/lib/rock_books/documents/journal.rb +3 -8
- 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/reports/balance_sheet.rb +9 -43
- data/lib/rock_books/reports/book_set_reporter.rb +115 -98
- data/lib/rock_books/reports/data/bs_is_data.rb +61 -0
- data/lib/rock_books/reports/data/bs_is_section_data.rb +30 -0
- data/lib/rock_books/reports/data/journal_data.rb +38 -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 +21 -0
- data/lib/rock_books/reports/helpers/receipts_hyperlink_converter.rb +47 -0
- data/lib/rock_books/reports/helpers/text_report_helper.rb +144 -0
- data/lib/rock_books/reports/income_statement.rb +9 -47
- data/lib/rock_books/reports/index_html_page.rb +27 -0
- data/lib/rock_books/reports/journal_report.rb +82 -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 +6 -55
- data/lib/rock_books/reports/templates/html/index.html.erb +158 -0
- data/lib/rock_books/reports/templates/html/report_page.html.erb +13 -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 +23 -0
- data/lib/rock_books/reports/templates/text/income_statement.txt.erb +23 -0
- data/lib/rock_books/reports/templates/text/journal.txt.erb +23 -0
- data/lib/rock_books/reports/templates/text/multidoc_txn_by_account_report.txt.erb +31 -0
- data/lib/rock_books/reports/templates/text/multidoc_txn_report.txt.erb +25 -0
- data/lib/rock_books/reports/templates/text/receipts_report.txt.erb +16 -0
- data/lib/rock_books/reports/templates/text/tx_one_account.txt.erb +21 -0
- data/lib/rock_books/reports/tx_one_account.rb +10 -45
- 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 +2 -1
- data/rock_books.gemspec +1 -0
- metadata +43 -10
- data/lib/rock_books/helpers/html_helper.rb +0 -29
- 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
@@ -1,26 +1,36 @@
|
|
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 'index_html_page'
|
8
|
+
require_relative 'multidoc_txn_report'
|
6
9
|
require_relative 'receipts_report'
|
7
10
|
require_relative 'report_context'
|
8
|
-
require_relative '
|
9
|
-
require_relative '
|
11
|
+
require_relative 'journal_report'
|
12
|
+
require_relative 'multidoc_txn_by_account_report'
|
10
13
|
require_relative 'tx_one_account'
|
14
|
+
require_relative 'helpers/erb_helper'
|
15
|
+
require_relative 'helpers/text_report_helper'
|
16
|
+
require_relative 'helpers/receipts_hyperlink_converter'
|
17
|
+
|
18
|
+
require 'prawn'
|
11
19
|
|
12
20
|
module RockBooks
|
13
21
|
class BookSetReporter
|
14
22
|
|
15
23
|
extend Forwardable
|
16
24
|
|
17
|
-
attr_reader :book_set, :
|
25
|
+
attr_reader :book_set, :context, :filter, :output_dir
|
18
26
|
|
19
27
|
def_delegator :book_set, :all_entries
|
20
28
|
def_delegator :book_set, :journals
|
21
29
|
def_delegator :book_set, :chart_of_accounts
|
22
30
|
def_delegator :book_set, :run_options
|
23
31
|
|
32
|
+
FONT_FILESPEC = File.absolute_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'assets', 'fonts', 'JetBrainsMono-Medium.ttf'))
|
33
|
+
|
24
34
|
|
25
35
|
def initialize(book_set, output_dir, filter = nil)
|
26
36
|
@book_set = book_set
|
@@ -30,72 +40,74 @@ class BookSetReporter
|
|
30
40
|
end
|
31
41
|
|
32
42
|
|
33
|
-
def
|
34
|
-
check_prequisite_executables
|
35
|
-
reports = all_reports(filter)
|
43
|
+
def generate
|
36
44
|
create_directories
|
37
45
|
create_index_html
|
38
|
-
|
46
|
+
|
47
|
+
do_statements
|
48
|
+
do_journals
|
49
|
+
do_transaction_reports
|
50
|
+
do_single_account_reports
|
51
|
+
do_receipts_report
|
39
52
|
end
|
40
53
|
|
41
54
|
|
42
55
|
# All methods after this point are private.
|
43
56
|
|
44
|
-
private def
|
57
|
+
private def do_statements
|
58
|
+
bs_is_data = BsIsData.new(context)
|
45
59
|
|
46
|
-
|
47
|
-
|
48
|
-
report_hash[key] = TransactionReport.new(journal, context).call(filter)
|
49
|
-
end
|
60
|
+
bal_sheet_text_report = BalanceSheet.new(context, bs_is_data.bal_sheet_data).generate
|
61
|
+
write_report(:balance_sheet, bal_sheet_text_report)
|
50
62
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
report_hash[:balance_sheet] = BalanceSheet.new(context).call
|
55
|
-
report_hash[:income_statement] = IncomeStatement.new(context).call
|
63
|
+
inc_stat_text_report = IncomeStatement.new(context, bs_is_data.inc_stat_data).generate
|
64
|
+
write_report(:income_statement, inc_stat_text_report)
|
65
|
+
end
|
56
66
|
|
57
|
-
if run_options.do_receipts
|
58
|
-
report_hash[:receipts] = ReceiptsReport.new(context, *missing_existing_unused_receipts).call
|
59
|
-
end
|
60
67
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
68
|
+
private def do_journals
|
69
|
+
journals.each do |journal|
|
70
|
+
report_data = JournalData.new(journal, context, filter).fetch
|
71
|
+
text_report = JournalReport.new(report_data, context, filter).generate
|
72
|
+
write_report(journal.short_name, text_report)
|
65
73
|
end
|
66
|
-
|
67
|
-
report_hash
|
68
74
|
end
|
69
75
|
|
70
76
|
|
71
|
-
private def
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
+
private def do_transaction_reports
|
78
|
+
|
79
|
+
do_date_or_amount_report = ->(sort_field, short_name) do
|
80
|
+
data = MultidocTxnReportData.new(context, sort_field, filter).fetch
|
81
|
+
text_report = MultidocTransactionReport.new(data, context).generate
|
82
|
+
write_report(short_name, text_report)
|
77
83
|
end
|
78
|
-
|
79
|
-
|
84
|
+
|
85
|
+
do_acct_report = -> do
|
86
|
+
data = MultidocTxnByAccountData.new(context).fetch
|
87
|
+
text_report = MultidocTransactionByAccountReport.new(data, context).generate
|
88
|
+
write_report(:all_txns_by_acct, text_report)
|
80
89
|
end
|
81
|
-
|
82
|
-
|
90
|
+
|
91
|
+
do_date_or_amount_report.(:date, :all_txns_by_date)
|
92
|
+
do_date_or_amount_report.(:amount, :all_txns_by_amount)
|
93
|
+
do_acct_report.()
|
83
94
|
end
|
84
95
|
|
85
96
|
|
86
|
-
private def
|
87
|
-
|
88
|
-
|
97
|
+
private def do_single_account_reports
|
98
|
+
chart_of_accounts.accounts.each do |account|
|
99
|
+
short_name = ('acct_' + account.code).to_sym
|
100
|
+
data = TxOneAccountData.new(context, account.code).fetch
|
101
|
+
text_report = TxOneAccount.new(data, context).generate
|
102
|
+
write_report(short_name, text_report)
|
103
|
+
end
|
89
104
|
end
|
90
105
|
|
91
106
|
|
92
|
-
private def
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
if missing_exes.any?
|
97
|
-
raise "Missing required report generation executable(s): #{missing_exes.join(', ')}"
|
98
|
-
end
|
107
|
+
private def do_receipts_report
|
108
|
+
data = ReceiptsReportData.new(book_set.all_entries, run_options.receipt_dir).fetch
|
109
|
+
text_report = ReceiptsReport.new(context, data).generate
|
110
|
+
write_report(:receipts, text_report)
|
99
111
|
end
|
100
112
|
|
101
113
|
|
@@ -118,84 +130,89 @@ class BookSetReporter
|
|
118
130
|
end
|
119
131
|
|
120
132
|
|
121
|
-
private def
|
122
|
-
|
123
|
-
|
124
|
-
|
133
|
+
private def report_metadata(doc_short_name)
|
134
|
+
{
|
135
|
+
RBCreator: "RockBooks v#{VERSION} (#{PROJECT_URL})",
|
136
|
+
RBEntity: context.entity,
|
137
|
+
RBCreated: Time.now.to_s,
|
138
|
+
RBDocumentCode: doc_short_name.to_s,
|
139
|
+
}
|
125
140
|
end
|
126
141
|
|
127
142
|
|
128
|
-
private def
|
143
|
+
private def prawn_create_document(pdf_filespec, report_text, doc_short_name)
|
144
|
+
Prawn::Document.generate(pdf_filespec, info: report_metadata(doc_short_name)) do
|
145
|
+
font(FONT_FILESPEC, size: 10)
|
129
146
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
147
|
+
utf8_nonbreaking_space = "\uC2A0"
|
148
|
+
unicode_nonbreaking_space = "\u00A0"
|
149
|
+
text(report_text.gsub(' ', unicode_nonbreaking_space))
|
150
|
+
end
|
151
|
+
end
|
134
152
|
|
135
|
-
File.write(txt_filespec, report_text)
|
136
153
|
|
137
|
-
|
138
|
-
|
154
|
+
private def html_metadata_comment(doc_short_name)
|
155
|
+
"\n" + report_metadata(doc_short_name).ai(plain: true) + "\n"
|
156
|
+
end
|
139
157
|
|
140
|
-
# Mac OS
|
141
|
-
textutil = ->(font_size) do
|
142
|
-
run_command("textutil -convert html -font 'Courier New Bold' -fontsize #{font_size} #{txt_filespec} -output #{html_filespec}")
|
143
|
-
end
|
144
158
|
|
145
|
-
|
146
|
-
txt2html = -> { run_command("txt2html --preformat_trigger_lines 0 #{txt_filespec} > #{html_filespec}") }
|
147
|
-
|
148
|
-
# Use smaller size for the PDF but larger size for the web pages:
|
149
|
-
if OS.mac?
|
150
|
-
textutil.(11)
|
151
|
-
cupsfilter.()
|
152
|
-
textutil.(14)
|
153
|
-
else
|
154
|
-
txt2html.()
|
155
|
-
cupsfilter.()
|
156
|
-
end
|
159
|
+
private def write_report(short_name, text_report)
|
157
160
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
end
|
161
|
+
txt_filespec = build_filespec(output_dir, short_name, 'txt')
|
162
|
+
html_filespec = build_filespec(output_dir, short_name, 'html')
|
163
|
+
pdf_filespec = build_filespec(output_dir, short_name, 'pdf')
|
162
164
|
|
163
|
-
|
165
|
+
create_text_report = -> { File.write(txt_filespec, text_report) }
|
166
|
+
|
167
|
+
create_pdf_report = -> { prawn_create_document(pdf_filespec, text_report, short_name) }
|
168
|
+
|
169
|
+
create_html_report = -> do
|
170
|
+
data = {
|
171
|
+
report_body: text_report,
|
172
|
+
title: "#{short_name} Report -- RockBooks",
|
173
|
+
metadata_comment: html_metadata_comment(short_name)
|
174
|
+
}
|
175
|
+
html_raw_report = ErbHelper.render_hashes("html/report_page.html.erb", data, {})
|
176
|
+
html_report = ReceiptsHyperlinkConverter.convert(html_raw_report, html_filespec)
|
177
|
+
File.write(html_filespec, html_report)
|
164
178
|
end
|
165
|
-
end
|
166
179
|
|
180
|
+
create_text_report.()
|
181
|
+
create_pdf_report.()
|
182
|
+
create_html_report.()
|
167
183
|
|
168
|
-
|
169
|
-
File.join(run_options.receipt_dir, receipt_filespec)
|
184
|
+
puts "Created text, PDF, and HTML reports for #{short_name}."
|
170
185
|
end
|
171
186
|
|
172
187
|
|
173
188
|
private def missing_existing_unused_receipts
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
189
|
+
missing_receipts = []
|
190
|
+
existing_receipts = []
|
191
|
+
receipt_full_filespec = ->(receipt_filespec) { File.join(run_options.receipt_dir, receipt_filespec) }
|
192
|
+
|
193
|
+
# We will start out putting all filespecs in the unused array, and delete them as they are found in the transactions.
|
194
|
+
unused_receipt_filespecs = Dir['receipts/**/*'].select { |s| File.file?(s) } \
|
195
|
+
.sort \
|
196
|
+
.map { |s| "./" + s } # Prepend './' to match the data
|
178
197
|
|
179
198
|
all_entries.each do |entry|
|
180
199
|
entry.receipts.each do |receipt|
|
181
|
-
filespec = receipt_full_filespec(receipt)
|
182
|
-
|
200
|
+
filespec = receipt_full_filespec.(receipt)
|
201
|
+
unused_receipt_filespecs.delete(filespec)
|
183
202
|
file_exists = File.file?(filespec)
|
184
|
-
list = (file_exists ?
|
203
|
+
list = (file_exists ? existing_receipts : missing_receipts)
|
185
204
|
list << { receipt: receipt, journal: entry.doc_short_name }
|
186
205
|
end
|
187
206
|
end
|
188
|
-
[
|
207
|
+
[missing_receipts, existing_receipts, unused_receipt_filespecs]
|
189
208
|
end
|
190
209
|
|
191
210
|
|
192
|
-
private def
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
chart_of_accounts: chart_of_accounts,
|
198
|
-
run_options: run_options)
|
211
|
+
private def create_index_html
|
212
|
+
filespec = build_filespec(output_dir, 'index', 'html')
|
213
|
+
content = IndexHtmlPage.new(context, html_metadata_comment('index.html'), run_options).generate
|
214
|
+
File.write(filespec, content)
|
215
|
+
puts "Created index.html"
|
199
216
|
end
|
200
217
|
end
|
201
218
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require_relative 'bs_is_section_data'
|
2
|
+
require_relative '../../filters/journal_entry_filters'
|
3
|
+
require_relative '../../documents/journal'
|
4
|
+
require_relative '../report_context'
|
5
|
+
|
6
|
+
module RockBooks
|
7
|
+
|
8
|
+
class BsIsData
|
9
|
+
|
10
|
+
attr_reader :journals_acct_totals, :context, :start_date, :end_date, :totals
|
11
|
+
|
12
|
+
def initialize(context)
|
13
|
+
@context = context
|
14
|
+
@start_date = context.chart_of_accounts.start_date
|
15
|
+
@end_date = context.chart_of_accounts.end_date
|
16
|
+
filter = JournalEntryFilters.date_on_or_before(end_date)
|
17
|
+
acct_amounts = Journal.acct_amounts_in_documents(context.journals, filter)
|
18
|
+
@journals_acct_totals = AcctAmount.aggregate_amounts_by_account(acct_amounts)
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def section_data(type)
|
23
|
+
BsIsSectionData.new(type, context, journals_acct_totals).fetch
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def bal_sheet_data
|
28
|
+
{
|
29
|
+
end_date: end_date,
|
30
|
+
entity: context.entity,
|
31
|
+
sections: {
|
32
|
+
asset: section_data(:asset),
|
33
|
+
liability: section_data(:liability),
|
34
|
+
equity: section_data(:equity),
|
35
|
+
},
|
36
|
+
grand_total: journals_acct_totals.values.sum.round(2)
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
def inc_stat_data
|
42
|
+
income_section_data = section_data(:income)
|
43
|
+
expense_section_data = section_data(:expense)
|
44
|
+
net_income = (income_section_data[:acct_totals].values.sum.round(2) -
|
45
|
+
expense_section_data[:acct_totals].values.sum.round(2)
|
46
|
+
).round(2)
|
47
|
+
|
48
|
+
{
|
49
|
+
start_date: start_date,
|
50
|
+
end_date: end_date,
|
51
|
+
entity: context.entity,
|
52
|
+
sections: {
|
53
|
+
income: income_section_data,
|
54
|
+
expense: expense_section_data,
|
55
|
+
},
|
56
|
+
net_income: net_income
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module RockBooks
|
2
|
+
|
3
|
+
BsIsSectionData = Struct.new(:type, :context, :journals_acct_totals)
|
4
|
+
class BsIsSectionData
|
5
|
+
|
6
|
+
def fetch
|
7
|
+
{
|
8
|
+
acct_totals: totals,
|
9
|
+
total: totals.map(&:last).sum.round(2)
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
private def totals
|
14
|
+
@totals ||= calc_section_acct_totals
|
15
|
+
end
|
16
|
+
|
17
|
+
private def calc_section_acct_totals
|
18
|
+
codes = context.chart_of_accounts.account_codes_of_type(type)
|
19
|
+
totals = journals_acct_totals.select { |code, _amount| codes.include?(code) }
|
20
|
+
need_to_reverse_sign = %i{liability equity income}.include?(type)
|
21
|
+
if need_to_reverse_sign
|
22
|
+
totals.keys.each do |code|
|
23
|
+
totals[code] = -totals[code] unless totals[code] == 0.0
|
24
|
+
end
|
25
|
+
end
|
26
|
+
totals
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative '../../types/acct_amount'
|
2
|
+
|
3
|
+
module RockBooks
|
4
|
+
class JournalData
|
5
|
+
|
6
|
+
attr_reader :journal, :context, :filter
|
7
|
+
|
8
|
+
def initialize(journal, report_context, filter = nil)
|
9
|
+
@journal = journal
|
10
|
+
@context = report_context
|
11
|
+
@filter = filter
|
12
|
+
end
|
13
|
+
|
14
|
+
def entries
|
15
|
+
return @entries if @entries
|
16
|
+
@entries = journal.entries
|
17
|
+
@entries = @entries.select { |entry| filter.(entry) } if filter
|
18
|
+
@entries
|
19
|
+
end
|
20
|
+
|
21
|
+
def fetch
|
22
|
+
totals = AcctAmount.aggregate_amounts_by_account(JournalEntry.entries_acct_amounts(entries))
|
23
|
+
{
|
24
|
+
code: journal.account_code,
|
25
|
+
name: journal.chart_of_accounts.name_for_code(journal.account_code),
|
26
|
+
title: journal.title,
|
27
|
+
short_name: journal.short_name,
|
28
|
+
debit_or_credit: journal.debit_or_credit,
|
29
|
+
start_date: context.chart_of_accounts.start_date,
|
30
|
+
end_date: context.chart_of_accounts.end_date,
|
31
|
+
entries: entries,
|
32
|
+
totals: totals,
|
33
|
+
grand_total: totals.values.sum.round(2),
|
34
|
+
max_acct_code_len: context.chart_of_accounts.max_account_code_length
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module RockBooks
|
2
|
+
class MultidocTxnByAccountData
|
3
|
+
|
4
|
+
include TextReportHelper
|
5
|
+
|
6
|
+
attr_reader :context, :account_code
|
7
|
+
|
8
|
+
|
9
|
+
def initialize(report_context)
|
10
|
+
@context = report_context
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
def fetch
|
15
|
+
all_journal_entries = Journal.entries_in_documents(context.journals)
|
16
|
+
totals = AcctAmount.aggregate_amounts_by_account(JournalEntry.entries_acct_amounts(all_journal_entries))
|
17
|
+
{
|
18
|
+
journals: context.journals,
|
19
|
+
entries: all_journal_entries,
|
20
|
+
totals: totals,
|
21
|
+
grand_total: totals.values.sum.round(2),
|
22
|
+
acct_sections: fetch_acct_sections(all_journal_entries),
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
private def fetch_acct_sections(all_journal_entries)
|
28
|
+
context.chart_of_accounts.accounts.map do |account|
|
29
|
+
code = account.code
|
30
|
+
acct_entries = JournalEntry.entries_containing_account_code(all_journal_entries, code)
|
31
|
+
total = JournalEntry.total_for_code(acct_entries, code)
|
32
|
+
{
|
33
|
+
code: code,
|
34
|
+
entries: acct_entries,
|
35
|
+
total: total
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|