statements 0.1.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 +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +22 -0
- data/bin/statements +8 -0
- data/lib/html/css/main.css +115 -0
- data/lib/html/css/main.css.map +7 -0
- data/lib/html/css/main.scss +173 -0
- data/lib/html/index.html +94 -0
- data/lib/html/js/main.coffee +146 -0
- data/lib/html/js/main.js +174 -0
- data/lib/html/js/main.js.map +10 -0
- data/lib/html/vendor/bootstrap/css/bootstrap-theme.css +469 -0
- data/lib/html/vendor/bootstrap/css/bootstrap.css +6331 -0
- data/lib/html/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot +0 -0
- data/lib/html/vendor/bootstrap/fonts/glyphicons-halflings-regular.svg +229 -0
- data/lib/html/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/lib/html/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff +0 -0
- data/lib/html/vendor/bootstrap/js/bootstrap.js +2320 -0
- data/lib/html/vendor/bootstrap-datepicker.js +1681 -0
- data/lib/html/vendor/datepicker3.css +786 -0
- data/lib/html/vendor/jquery-2.1.3.js +9205 -0
- data/lib/html/vendor/underscore.js +1416 -0
- data/lib/statements/cli.rb +17 -0
- data/lib/statements/database.rb +22 -0
- data/lib/statements/migrations/00_alpha.rb +43 -0
- data/lib/statements/models/account.rb +9 -0
- data/lib/statements/models/document.rb +29 -0
- data/lib/statements/models/transaction.rb +43 -0
- data/lib/statements/pdf_reader.rb +34 -0
- data/lib/statements/reader/common/st_george.rb +31 -0
- data/lib/statements/reader/st_george_credit_card.rb +53 -0
- data/lib/statements/reader/st_george_savings.rb +95 -0
- data/lib/statements/reader.rb +76 -0
- data/lib/statements/search.rb +48 -0
- data/lib/statements/server.rb +69 -0
- data/lib/statements/version.rb +3 -0
- data/lib/statements/views/footer.erb +6 -0
- data/lib/statements/views/row.erb +16 -0
- data/lib/statements/views/search.erb +25 -0
- data/lib/statements.rb +17 -0
- metadata +141 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'thin'
|
2
|
+
|
3
|
+
module Statements
|
4
|
+
class Cli
|
5
|
+
|
6
|
+
def self.main(pwd, argv)
|
7
|
+
Statements::Database.new "#{pwd}/statements.sqlite3"
|
8
|
+
Statements::Reader.read_dir pwd
|
9
|
+
Thin::Server.start '0.0.0.0', 57473 do
|
10
|
+
map('/q') { run Server.new }
|
11
|
+
use Rack::Static, urls: [''], root: "#{ROOT}/lib/html", index: 'index.html'
|
12
|
+
run Server
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module Statements
|
4
|
+
class Database
|
5
|
+
|
6
|
+
def initialize(path = nil)
|
7
|
+
@path = ENV['DB_PATH'] || path
|
8
|
+
ActiveRecord::Base.establish_connection(
|
9
|
+
adapter: 'sqlite3',
|
10
|
+
database: @path
|
11
|
+
)
|
12
|
+
ActiveRecord::Base.logger = Logger.new(ENV['DB_LOG']) if ENV['DB_LOG']
|
13
|
+
ActiveRecord::Migrator.migrate migrations_dir
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def migrations_dir
|
19
|
+
@migrations_dir ||= Statements::ROOT.join('lib/statements/migrations').to_s
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class Alpha < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
|
4
|
+
create_table :documents do |t|
|
5
|
+
t.string :path
|
6
|
+
t.string :md5, limit: 32
|
7
|
+
|
8
|
+
t.timestamps null: true
|
9
|
+
|
10
|
+
t.index :md5, unique: true
|
11
|
+
end
|
12
|
+
|
13
|
+
create_table :accounts do |t|
|
14
|
+
t.string :name
|
15
|
+
t.string :number
|
16
|
+
|
17
|
+
t.timestamps null: true
|
18
|
+
|
19
|
+
t.index [:name, :number], unique: true
|
20
|
+
end
|
21
|
+
|
22
|
+
create_table :transactions do |t|
|
23
|
+
t.references :document
|
24
|
+
t.references :account
|
25
|
+
t.integer :document_line
|
26
|
+
t.datetime :transacted_at, null: true
|
27
|
+
t.datetime :posted_at, null: true
|
28
|
+
t.string :description
|
29
|
+
t.decimal :amount, precision: 2, scale: 13
|
30
|
+
t.decimal :balance, precision: 2, scale: 13
|
31
|
+
t.decimal :foreign_amount, precision: 2, scale: 13
|
32
|
+
t.string :foreign_currency, limit: 3
|
33
|
+
t.string :colour, limit: 20, default: 'white'
|
34
|
+
|
35
|
+
t.string :checksum, limit: 40
|
36
|
+
|
37
|
+
t.timestamps null: true
|
38
|
+
|
39
|
+
t.index [:document_id, :document_line], unique: true
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
class Document < ActiveRecord::Base
|
4
|
+
has_many :transactions
|
5
|
+
|
6
|
+
def scan(base: nil)
|
7
|
+
path = base + self.path
|
8
|
+
md5 = Digest::MD5.file(path).hexdigest.downcase
|
9
|
+
print "Scanning #{self.path} ... "
|
10
|
+
if md5 == self.md5
|
11
|
+
puts 'skipping (unchanged)'
|
12
|
+
else
|
13
|
+
reader = Statements::Reader.for_file(path)
|
14
|
+
if reader
|
15
|
+
Transaction.delete_all document: self if persisted?
|
16
|
+
reader.transactions.each do |t|
|
17
|
+
t.document = self
|
18
|
+
t.save! unless Transaction.find_by('checksum = ? AND document_id != ?', t.checksum!, id || 0)
|
19
|
+
end
|
20
|
+
puts "added #{transactions.count} transactions(s)"
|
21
|
+
else
|
22
|
+
puts 'skipping (unknown format)'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
self.md5 = md5
|
26
|
+
save!
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'digest'
|
3
|
+
|
4
|
+
class Transaction < ActiveRecord::Base
|
5
|
+
belongs_to :account
|
6
|
+
belongs_to :document
|
7
|
+
|
8
|
+
before_save :checksum!
|
9
|
+
|
10
|
+
def set_account(name, number)
|
11
|
+
self.account = Account.find_or_create_by(
|
12
|
+
name: name,
|
13
|
+
number: number
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def checksum!
|
18
|
+
self.checksum = calculate_checksum
|
19
|
+
end
|
20
|
+
|
21
|
+
def credit?
|
22
|
+
amount > 0
|
23
|
+
end
|
24
|
+
|
25
|
+
def debit?
|
26
|
+
amount < 0
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def calculate_checksum
|
32
|
+
parts = [
|
33
|
+
account_id.to_s,
|
34
|
+
transacted_at.strftime('%F'),
|
35
|
+
posted_at.strftime('%F'),
|
36
|
+
description,
|
37
|
+
amount.to_s,
|
38
|
+
balance.to_s
|
39
|
+
]
|
40
|
+
Digest::SHA1.hexdigest parts.join "\0"
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
|
3
|
+
module Statements
|
4
|
+
module PdfReader
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def read(path)
|
8
|
+
x = 0
|
9
|
+
result = []
|
10
|
+
loop do
|
11
|
+
page = read_page(path, x += 1)
|
12
|
+
break if page.chomp.empty?
|
13
|
+
result << page
|
14
|
+
end
|
15
|
+
result
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def read_page(path, page)
|
21
|
+
`#{pdftotext_path} -enc UTF-8 -table -q -f #{page} -l #{page} #{Shellwords.escape path} /dev/stdout`
|
22
|
+
end
|
23
|
+
|
24
|
+
def pdftotext_path
|
25
|
+
unless @pdftotext_path
|
26
|
+
@pdftotext_path = `which pdftotext`.chomp
|
27
|
+
raise 'Could not find `pdftotext`. Please install Xpdf from http://www.foolabs.com/xpdf/download.html' if @pdftotext_path.empty?
|
28
|
+
end
|
29
|
+
@pdftotext_path
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module StGeorge
|
2
|
+
ABNS = %w[33007457141 92055513070]
|
3
|
+
|
4
|
+
def st_george?
|
5
|
+
document =~ /\bSt\. *George\b/ && document =~ /\bABN *((?:\d *){11})/ && ABNS.include?($1.delete ' ')
|
6
|
+
end
|
7
|
+
|
8
|
+
def years
|
9
|
+
@years ||= period.map(&:year)
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse_date(str)
|
13
|
+
date = change_year(Time.parse(str.to_s.strip), years.first)
|
14
|
+
date = change_year(date, years.last) if date < period.first
|
15
|
+
date
|
16
|
+
end
|
17
|
+
|
18
|
+
def period
|
19
|
+
@period ||= (pages.first =~ %r`Statement Period\s+(\d\d/\d\d/\d{4})\s+to\s+(\d\d/\d\d/\d{4})` && [Time.parse($1), Time.parse($2)])
|
20
|
+
end
|
21
|
+
|
22
|
+
def account_number
|
23
|
+
@account_number ||= document[/Account Number ([\d ]+)/, 1].strip.gsub(/\s+/, ' ')
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def change_year(time, year)
|
29
|
+
Time.new year, time.month, time.day
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require_relative 'common/st_george'
|
2
|
+
require 'time'
|
3
|
+
require 'bigdecimal'
|
4
|
+
|
5
|
+
module Statements
|
6
|
+
class Reader
|
7
|
+
class StGeorgeCreditCard < self
|
8
|
+
include StGeorge
|
9
|
+
|
10
|
+
def valid?
|
11
|
+
st_george? && pages.first.include?('Credit Limit')
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.cell_pattern
|
15
|
+
@cell_pattern ||= %r`^
|
16
|
+
\s* (\d{1,2}\s+[A-Z][a-z]{2})
|
17
|
+
\s* (\d{1,2}\s+[A-Z][a-z]{2})
|
18
|
+
\s* (.+?)
|
19
|
+
\s* (\$[\d,]+\.\d\d(?:\s+CR)?)
|
20
|
+
\s* (\$[\d,]+\.\d\d(?:\s+CR)?)
|
21
|
+
\s* (\n\s+\d+\.\d\d\s+[A-Z]{3})?
|
22
|
+
\s*$
|
23
|
+
`x
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_cells(cells, tx)
|
27
|
+
[:posted_at, :transacted_at].each.with_index do |attr, index|
|
28
|
+
date = Time.parse(cells[index])
|
29
|
+
tx[attr] = Time.new((date.month == 12 ? years.first : years.last), date.month, date.day)
|
30
|
+
end
|
31
|
+
tx.description = cells[2]
|
32
|
+
{amount: 3, balance: 4}.each do |attr, index|
|
33
|
+
number = BigDecimal cells[index][/[\d,]+\.\d+/].delete(',')
|
34
|
+
credit = cells[index].end_with? 'CR'
|
35
|
+
number *= -1 unless credit
|
36
|
+
tx[attr] = number
|
37
|
+
end
|
38
|
+
foreign = cells[5]
|
39
|
+
if foreign
|
40
|
+
tx.foreign_amount = BigDecimal foreign[0..-5]
|
41
|
+
tx.foreign_amount *= -1 if tx.amount < 0
|
42
|
+
tx.foreign_currency = foreign[-3..-1]
|
43
|
+
end
|
44
|
+
tx.set_account account_name, account_number
|
45
|
+
end
|
46
|
+
|
47
|
+
def account_name
|
48
|
+
@account_name ||= document[/^\s*(.+)\s*Statement\s*$/, 1].strip
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require_relative 'common/st_george'
|
2
|
+
require 'time'
|
3
|
+
require 'bigdecimal'
|
4
|
+
|
5
|
+
module Statements
|
6
|
+
class Reader
|
7
|
+
class StGeorgeSavings < self
|
8
|
+
include StGeorge
|
9
|
+
|
10
|
+
DATED_DESCRIPTIONS = [
|
11
|
+
'INTERNET WITHDRAWAL',
|
12
|
+
'INTERNET DEPOSIT',
|
13
|
+
'ATM DEPOSIT',
|
14
|
+
'ATM WITHDRAWAL',
|
15
|
+
'VISA PURCHASE',
|
16
|
+
'VISA PURCHASE O/SEAS',
|
17
|
+
'EFTPOS PURCHASE'
|
18
|
+
]
|
19
|
+
|
20
|
+
def valid?
|
21
|
+
st_george? && pages.first =~ /BSB\s+Number\s+112-879/
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.cell_pattern
|
25
|
+
@cell_pattern ||= %r`^
|
26
|
+
(\d\d\s+)
|
27
|
+
([A-Z]{3}\s+)
|
28
|
+
(.+?)
|
29
|
+
([\d,]+\.\d\d) \s+
|
30
|
+
([\d,]+\.\d\d) \s*
|
31
|
+
$
|
32
|
+
((?:
|
33
|
+
\n+\040[^\r\n]+
|
34
|
+
)*)
|
35
|
+
`x
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_cells(cells, tx, page_index)
|
39
|
+
|
40
|
+
# Examples cells:
|
41
|
+
# ['17 ', 'NOV ', 'LINE 1 ', '12,345.67', '50.00', "\n LINE 2\n Line 3"]
|
42
|
+
|
43
|
+
tx.posted_at = parse_date(cells[0..1].join)
|
44
|
+
|
45
|
+
is_debit = is_debit?(cells, page_index)
|
46
|
+
debit_factor = is_debit ? -1 : 1
|
47
|
+
|
48
|
+
tx.amount = BigDecimal(cells[3].delete ',') * debit_factor
|
49
|
+
tx.balance = BigDecimal(cells[4].delete ',')
|
50
|
+
|
51
|
+
# TODO: negative balances
|
52
|
+
|
53
|
+
lines = [cells[2].strip]
|
54
|
+
lines += cells[5].strip.split(/\s*\n\s*/) if cells[5]
|
55
|
+
lines.each { |line| line.gsub! /\s+/, ' ' }
|
56
|
+
lines.reject! { |line| line.delete(' ').start_with? 'SUBTOTALCARRIEDFORWARDTONEXTPAGE' }
|
57
|
+
|
58
|
+
tx.description = lines.join("\n")
|
59
|
+
|
60
|
+
if lines.first =~ %r`^(.+?) (?:(\d\d)/(\d\d)/\d\d|(\d\d\w{3}) \d\d:\d\d)$` && DATED_DESCRIPTIONS.include?($1)
|
61
|
+
description = $1
|
62
|
+
tx.transacted_at = $2 ? parse_date("2000-#{$3}-#{$2}") : parse_date($4)
|
63
|
+
if description.end_with?('O/SEAS') && lines.last =~ /\b([A-Z]{3}) ([\d,]+\.\d\d)$/
|
64
|
+
tx.foreign_currency = $1
|
65
|
+
tx.foreign_amount = BigDecimal($2.delete ',') * debit_factor
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
tx.transacted_at ||= tx.posted_at
|
70
|
+
|
71
|
+
tx.set_account account_name, account_number
|
72
|
+
end
|
73
|
+
|
74
|
+
def is_debit?(cells, page_index)
|
75
|
+
amount_offset = cells[0..2].map(&:length).inject(:+)
|
76
|
+
amount_center = amount_offset + cells[3].length / 2
|
77
|
+
amount_center < credit_threshold(page_index)
|
78
|
+
end
|
79
|
+
|
80
|
+
def credit_threshold(page)
|
81
|
+
(@credit_thresholds ||= {})[page] ||= find_credit_threshold(page)
|
82
|
+
end
|
83
|
+
|
84
|
+
def find_credit_threshold(page)
|
85
|
+
lengths = pages[page].scan(/^(Date\s+Transaction\s+De\w+\s+)(Debit\s+Credit)/).first.map(&:length)
|
86
|
+
lengths.first + lengths.last / 2
|
87
|
+
end
|
88
|
+
|
89
|
+
def account_name
|
90
|
+
@account_name ||= document[/^\s*Statement\s+of\s+Account\s*((?:\S\s{0,4})+)/, 1].strip
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Statements
|
4
|
+
class Reader
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_reader :classes
|
9
|
+
end
|
10
|
+
|
11
|
+
unless classes
|
12
|
+
Dir[Statements::ROOT.join('lib/statements/reader/*.rb')].each { |p| require p }
|
13
|
+
@classes = constants.map { |n| const_get n }.select { |c| Class === c && c < Reader }
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.for_file(file)
|
17
|
+
file = file.to_s
|
18
|
+
pages = (file =~ /\.pdf$/i) ? PdfReader.read(file) : File.read(file).split(/-{5,}/)
|
19
|
+
classes.each do |klass|
|
20
|
+
reader = klass.new(pages)
|
21
|
+
return reader if reader.valid?
|
22
|
+
end
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.read_dir(dir)
|
27
|
+
base = Pathname(dir).realpath
|
28
|
+
Dir[base.join('**/*.{pdf,txt}')].each do |path|
|
29
|
+
rel_path = Pathname(path).relative_path_from(base)
|
30
|
+
begin
|
31
|
+
doc = Document.find_or_initialize_by(path: rel_path.to_s)
|
32
|
+
doc.scan base: base
|
33
|
+
rescue => e
|
34
|
+
puts "error: #{e.class.name} #{e.message}\n #{e.backtrace.join "\n "}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :pages, :document
|
40
|
+
|
41
|
+
delegate [:include?, :scan] => :document
|
42
|
+
|
43
|
+
def initialize(pages)
|
44
|
+
@pages = pages
|
45
|
+
@document = pages.join("\n").freeze
|
46
|
+
end
|
47
|
+
|
48
|
+
def cell_pattern
|
49
|
+
self.class.cell_pattern
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.cell_pattern
|
53
|
+
raise NotImplementedError
|
54
|
+
end
|
55
|
+
|
56
|
+
def transactions
|
57
|
+
@transactions ||= search_for_transactions
|
58
|
+
end
|
59
|
+
|
60
|
+
def search_for_transactions
|
61
|
+
index = 0
|
62
|
+
result = []
|
63
|
+
pages.each.with_index do |page, page_index|
|
64
|
+
page.scan(cell_pattern).each do |cells|
|
65
|
+
result << Transaction.new(document_line: index += 1).tap do |transaction|
|
66
|
+
args = [cells, transaction, page_index]
|
67
|
+
arity = method(:parse_cells).arity
|
68
|
+
parse_cells *args[0..(arity - 1)]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
result
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Statements
|
4
|
+
class Search
|
5
|
+
|
6
|
+
attr_reader :input
|
7
|
+
|
8
|
+
def initialize(input)
|
9
|
+
@input = input
|
10
|
+
end
|
11
|
+
|
12
|
+
def transactions
|
13
|
+
@transactions ||= query.all
|
14
|
+
end
|
15
|
+
|
16
|
+
def debits
|
17
|
+
transactions.select { |t| t.amount < 0 }
|
18
|
+
end
|
19
|
+
|
20
|
+
def credits
|
21
|
+
transactions.select { |t| t.amount > 0 }
|
22
|
+
end
|
23
|
+
|
24
|
+
def total(collection = :transactions)
|
25
|
+
__send__(collection).inject(0) { |sum, t| sum + t.amount }
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def query
|
31
|
+
query = Transaction.order(input['order'])
|
32
|
+
query = query.where(account_id: input['accounts'])
|
33
|
+
query = query.where('posted_at > ? and posted_at < ?',
|
34
|
+
Time.parse(input['date_start']),
|
35
|
+
Time.parse(input['date_end']))
|
36
|
+
query = query.where('amount < 0') if input['type'] == 'debits'
|
37
|
+
query = query.where('amount > 0') if input['type'] == 'credits'
|
38
|
+
query = query.where(colour: input['colours'])
|
39
|
+
text = input['search'].strip.downcase
|
40
|
+
unless text.empty?
|
41
|
+
words = text.split(/\s+/)
|
42
|
+
query = query.where('lower(description) like ?', "%#{words.join '%'}%")
|
43
|
+
end
|
44
|
+
query
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'erb'
|
3
|
+
|
4
|
+
module Statements
|
5
|
+
class Server
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
request = Rack::Request.new(env)
|
13
|
+
verb = request.request_method.downcase
|
14
|
+
path = request.path_info[1..-1].split('/')
|
15
|
+
handler_name = "#{verb}_#{path.first || 'index'}".gsub('.', '_')
|
16
|
+
args = [request] + path[1..-1]
|
17
|
+
method = respond_to?(handler_name) && method(handler_name)
|
18
|
+
if method && method.arity == args.length
|
19
|
+
__send__ handler_name, *args
|
20
|
+
else
|
21
|
+
[404, {}, ['Not found']]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# noinspection RubyStringKeysInHashInspection
|
26
|
+
def serve(type, str)
|
27
|
+
[200, {'Content-Type' => type, 'Content-Length' => str.length.to_s}, [str]]
|
28
|
+
end
|
29
|
+
|
30
|
+
def json(data)
|
31
|
+
serve 'application/json', JSON.generate(data, quirks_mode: true)
|
32
|
+
end
|
33
|
+
|
34
|
+
def js(script)
|
35
|
+
serve 'application/x-javascript', script
|
36
|
+
end
|
37
|
+
|
38
|
+
def html(html)
|
39
|
+
serve 'text/html; charset=UTF-8', html
|
40
|
+
end
|
41
|
+
|
42
|
+
def get_accounts_js(request)
|
43
|
+
js "window.accounts = #{Account.to_json}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.render(template, obj = nil)
|
47
|
+
@templates ||= {}
|
48
|
+
@templates[template] ||= ERB.new(File.read File.expand_path("../views/#{template}.erb", __FILE__))
|
49
|
+
@templates[template].result (obj || self).instance_eval { binding }
|
50
|
+
end
|
51
|
+
|
52
|
+
def post_search_html(request)
|
53
|
+
html self.class.render 'search', Search.new(JSON.parse request.body.read)
|
54
|
+
end
|
55
|
+
|
56
|
+
def post_colour_json(request)
|
57
|
+
input = JSON.parse(request.body.read)
|
58
|
+
transaction = Transaction.find(input['id']) rescue false
|
59
|
+
if transaction
|
60
|
+
transaction.colour = input['colour']
|
61
|
+
transaction.save
|
62
|
+
json success: true
|
63
|
+
else
|
64
|
+
400
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<tr class="<%= credit? ? 'credit' : 'debit' %> colour-<%= colour %>" data-id="<%= id %>">
|
2
|
+
<td class="date">
|
3
|
+
<%= posted_at.strftime '%-d %b %Y' %>
|
4
|
+
</td>
|
5
|
+
<td class="description">
|
6
|
+
<% desc = description.lines %>
|
7
|
+
<p><%= desc[0] %></p>
|
8
|
+
<% if desc.length > 1 %>
|
9
|
+
<p class="small"><%= desc[1..-1].join '<br/>' %></p>
|
10
|
+
<% end %>
|
11
|
+
</td>
|
12
|
+
<td class="amount"><%= ActiveSupport::NumberHelper.number_to_currency amount.abs, unit: '' %></td>
|
13
|
+
<td class="colour">
|
14
|
+
<a href="javascript:" class="picker"></a>
|
15
|
+
</td>
|
16
|
+
</tr>
|
@@ -0,0 +1,25 @@
|
|
1
|
+
<table class="table table-condensed">
|
2
|
+
<thead>
|
3
|
+
<tr>
|
4
|
+
<th>Posted</th>
|
5
|
+
<th>Description</th>
|
6
|
+
<th class="amount">Amount</th>
|
7
|
+
<th><!-- Tags --></th>
|
8
|
+
</tr>
|
9
|
+
</thead>
|
10
|
+
<tbody>
|
11
|
+
<%= transactions.map { |x| Statements::Server.render 'row', x }.join %>
|
12
|
+
</tbody>
|
13
|
+
<tfoot>
|
14
|
+
<%= %w[debits credits transactions].map do |n|
|
15
|
+
list = __send__(n)
|
16
|
+
data = {
|
17
|
+
name: n,
|
18
|
+
count: list.count,
|
19
|
+
total: total(n)
|
20
|
+
}
|
21
|
+
Statements::Server.render 'footer', OpenStruct.new(data)
|
22
|
+
end.join %>
|
23
|
+
</tfoot>
|
24
|
+
</table>
|
25
|
+
<!-- fix for long response bug -->
|
data/lib/statements.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Statements
|
4
|
+
ROOT = Pathname File.expand_path('../..', __FILE__)
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'statements/version'
|
8
|
+
require 'statements/cli'
|
9
|
+
require 'statements/reader'
|
10
|
+
require 'statements/database'
|
11
|
+
require 'statements/pdf_reader'
|
12
|
+
require 'statements/server'
|
13
|
+
require 'statements/search'
|
14
|
+
|
15
|
+
require 'statements/models/transaction'
|
16
|
+
require 'statements/models/account'
|
17
|
+
require 'statements/models/document'
|