invoices 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +41 -0
- data/Rakefile +5 -0
- data/bin/invoices +8 -0
- data/db/schema.rb +68 -0
- data/lib/invoices/controllers/application_controller.rb +73 -0
- data/lib/invoices/controllers/billers_controller.rb +22 -0
- data/lib/invoices/controllers/clients_controller.rb +24 -0
- data/lib/invoices/controllers/commits_controller.rb +10 -0
- data/lib/invoices/controllers/invoices_controller.rb +26 -0
- data/lib/invoices/controllers/line_items_controller.rb +26 -0
- data/lib/invoices/global.rb +12 -0
- data/lib/invoices/models/biller.rb +23 -0
- data/lib/invoices/models/client.rb +27 -0
- data/lib/invoices/models/commit.rb +18 -0
- data/lib/invoices/models/invoice.rb +75 -0
- data/lib/invoices/models/line_item.rb +27 -0
- data/lib/invoices/views/helpers/views_helper.rb +22 -0
- data/lib/invoices/views/invoices_view.rb +78 -0
- data/spec/controllers_spec.rb +60 -0
- data/spec/models_spec.rb +237 -0
- data/spec/spec_helper.rb +8 -0
- metadata +69 -0
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# About
|
2
|
+
My first gem. Also my first offline foray into OOB.
|
3
|
+
|
4
|
+
### Installation
|
5
|
+
<code>$ gem install invoices</code>
|
6
|
+
|
7
|
+
### Configuration
|
8
|
+
<code>$ invoices biller -n</code>
|
9
|
+
<code>$ invoices client -n</code>
|
10
|
+
<code>$ invoices invoice -c "Client Name"</code>
|
11
|
+
|
12
|
+
### License
|
13
|
+
The MIT License (MIT)
|
14
|
+
Copyright (c) 2013 Aaron Macy (aaronmacy.com)
|
15
|
+
|
16
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
17
|
+
|
18
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
19
|
+
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
## TO DO LIST
|
23
|
+
- Verify that there are records in the database
|
24
|
+
- Write Controller tests
|
25
|
+
- Prevent invalid data from being saved
|
26
|
+
- Improve CLI
|
27
|
+
- Validate data before stored to db
|
28
|
+
- Support MySQL & PostgreSQL
|
29
|
+
- Add custom error messages throughout (see comments)
|
30
|
+
- Client names need to be in quotes (raise exception if they aren't)
|
31
|
+
- Allow commit messages to be > 40 characters
|
32
|
+
- Universalize testing of #project_root
|
33
|
+
- [Review sqlite3 gem methods](http://sqlite-ruby.rubyforge.org/sqlite3/)
|
34
|
+
- Allow multiple git repos per invoice
|
35
|
+
- Provide control over which commits get added to the invoice
|
36
|
+
- Add ability to regenerate invoices
|
37
|
+
- Add ability to preview invoices @ the command line
|
38
|
+
- Allow users to select where they want their invoices to be stored
|
39
|
+
- Export invoices as PDFs
|
40
|
+
|
41
|
+
### Bugs
|
data/Rakefile
ADDED
data/bin/invoices
ADDED
data/db/schema.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
class Schema
|
2
|
+
def initalize
|
3
|
+
INVOICES_DB
|
4
|
+
TEST_DB
|
5
|
+
end
|
6
|
+
def create_all_tables(db)
|
7
|
+
begin
|
8
|
+
create_billers_table(db)
|
9
|
+
create_clients_table(db)
|
10
|
+
create_invoices_table(db)
|
11
|
+
create_line_items_table(db)
|
12
|
+
rescue SQLite3::SQLException
|
13
|
+
end
|
14
|
+
end
|
15
|
+
def create_billers_table(db)
|
16
|
+
db.execute <<-SQL
|
17
|
+
create table billers (
|
18
|
+
name varchar(30),
|
19
|
+
street1 varchar(30),
|
20
|
+
street2 varchar(30),
|
21
|
+
city varchar(30),
|
22
|
+
state varchar(2),
|
23
|
+
zip varchar(5),
|
24
|
+
phone varchar(14),
|
25
|
+
email varchar(30)
|
26
|
+
);
|
27
|
+
SQL
|
28
|
+
end
|
29
|
+
def create_clients_table(db)
|
30
|
+
db.execute <<-SQL
|
31
|
+
create table clients (
|
32
|
+
name varchar(30),
|
33
|
+
street1 varchar(30),
|
34
|
+
street2 varchar(30),
|
35
|
+
city varchar(30),
|
36
|
+
state varchar(2),
|
37
|
+
zip varchar(5),
|
38
|
+
phone varchar(14),
|
39
|
+
email varchar(30),
|
40
|
+
rate int
|
41
|
+
);
|
42
|
+
SQL
|
43
|
+
end
|
44
|
+
def create_invoices_table(db)
|
45
|
+
db.execute <<-SQL
|
46
|
+
create table invoices (
|
47
|
+
invoice_number int,
|
48
|
+
date varchar(10),
|
49
|
+
client_id int,
|
50
|
+
total_hrs int,
|
51
|
+
total_cost int
|
52
|
+
);
|
53
|
+
SQL
|
54
|
+
end
|
55
|
+
def create_line_items_table(db)
|
56
|
+
db.execute <<-SQL
|
57
|
+
create table line_items (
|
58
|
+
invoice_number int,
|
59
|
+
line_number int,
|
60
|
+
commit_date varchar(10),
|
61
|
+
commit_msg varchar,
|
62
|
+
hrs int,
|
63
|
+
rate int,
|
64
|
+
cost int
|
65
|
+
);
|
66
|
+
SQL
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require_relative '../global'
|
3
|
+
require_relative 'invoices_controller'
|
4
|
+
require_relative 'billers_controller'
|
5
|
+
require_relative 'clients_controller'
|
6
|
+
require_relative 'line_items_controller'
|
7
|
+
require_relative 'commits_controller'
|
8
|
+
require_relative '../models/invoice'
|
9
|
+
require_relative '../models/biller'
|
10
|
+
require_relative '../models/client'
|
11
|
+
require_relative '../models/line_item'
|
12
|
+
require_relative '../models/commit'
|
13
|
+
require_relative '../views/invoices_view'
|
14
|
+
require_relative '../../../db/schema'
|
15
|
+
|
16
|
+
class ApplicationController
|
17
|
+
def initialize
|
18
|
+
Schema.new.create_all_tables(INVOICES_DB)
|
19
|
+
Dir.mkdir(INVOICES_FOLDER) unless File.directory?(INVOICES_FOLDER)
|
20
|
+
end
|
21
|
+
def parse_options
|
22
|
+
options = {}
|
23
|
+
subcommand_help = "\nExamples:\nCreate an invoice: invoices invoice -c 'Client Name'\nCreate a biller: invoices biller -n\nCreate a client: invoices client -n"
|
24
|
+
@global = OptionParser.new do |opt|
|
25
|
+
opt.banner = "Usage: invoices options [subcommand [options]]"
|
26
|
+
opt.on("-v", "--version", "Check the version of Invoices") do# |v|
|
27
|
+
#options[:version] = v
|
28
|
+
puts "v#{INVOICES_VERSION}"
|
29
|
+
end
|
30
|
+
opt.on("-h", "--help", "Get some help") do# |v|
|
31
|
+
#options[:help] = v
|
32
|
+
puts @global
|
33
|
+
puts subcommand_help
|
34
|
+
end
|
35
|
+
end
|
36
|
+
subcommands = {
|
37
|
+
'invoice' => OptionParser.new do |opt|
|
38
|
+
opt.on("-c", "--client CLIENT", "Select the client for this invoice") do |v|
|
39
|
+
#option[:client] = v
|
40
|
+
client = Client.new.find_by_name(v)
|
41
|
+
InvoicesController.new(client)
|
42
|
+
end
|
43
|
+
end,
|
44
|
+
'biller' => OptionParser.new do |opt|
|
45
|
+
opt.on("-n", "--new", "Add a new biller to the database") do# |v|
|
46
|
+
#option[:biller_new] = v
|
47
|
+
BillersController.new
|
48
|
+
end
|
49
|
+
end,
|
50
|
+
'client' => OptionParser.new do |opt|
|
51
|
+
opt.on("-n", "--new", "Add a new client to the database") do# |v|
|
52
|
+
#option[:client_new] = v
|
53
|
+
ClientsController.new
|
54
|
+
end
|
55
|
+
end
|
56
|
+
}
|
57
|
+
@global.order!
|
58
|
+
cmd = ARGV.shift
|
59
|
+
if cmd #&& subcommands.key?(cmd.to_sym)
|
60
|
+
subcommands[cmd].order!
|
61
|
+
else
|
62
|
+
puts @global
|
63
|
+
puts subcommand_help
|
64
|
+
end
|
65
|
+
end
|
66
|
+
def parse_commands
|
67
|
+
case ARGV[0]
|
68
|
+
when "invoice"
|
69
|
+
when "biller"
|
70
|
+
when "client"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class BillersController
|
2
|
+
def initialize
|
3
|
+
biller = Biller.new
|
4
|
+
puts "your name >"
|
5
|
+
biller.name = $stdin.gets.chomp
|
6
|
+
puts "street1 >"
|
7
|
+
biller.street1 = $stdin.gets.chomp
|
8
|
+
puts "street2 >"
|
9
|
+
biller.street2 = $stdin.gets.chomp
|
10
|
+
puts "city >"
|
11
|
+
biller.city = $stdin.gets.chomp
|
12
|
+
puts "state (2 letters) >"
|
13
|
+
biller.state = $stdin.gets.chomp
|
14
|
+
puts "zip (5 digits) >"
|
15
|
+
biller.zip = $stdin.gets.chomp
|
16
|
+
puts "phone >"
|
17
|
+
biller.phone = $stdin.gets.chomp
|
18
|
+
puts "email >"
|
19
|
+
biller.email = $stdin.gets.chomp
|
20
|
+
biller.save
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class ClientsController
|
2
|
+
def initialize
|
3
|
+
client = Client.new
|
4
|
+
puts "client name >"
|
5
|
+
client.name = $stdin.gets.chomp
|
6
|
+
puts "street1 >"
|
7
|
+
client.street1 = $stdin.gets.chomp
|
8
|
+
puts "street2 >"
|
9
|
+
client.street2 = $stdin.gets.chomp
|
10
|
+
puts "city >"
|
11
|
+
client.city = $stdin.gets.chomp
|
12
|
+
puts "state (2 letters) >"
|
13
|
+
client.state = $stdin.gets.chomp
|
14
|
+
puts "zip (5 digits) >"
|
15
|
+
client.zip = $stdin.gets.chomp
|
16
|
+
puts "phone >"
|
17
|
+
client.phone = $stdin.gets.chomp
|
18
|
+
puts "email >"
|
19
|
+
client.email = $stdin.gets.chomp
|
20
|
+
puts "hourly rate you'll charge this client >"
|
21
|
+
client.rate = $stdin.gets.chomp
|
22
|
+
client.save
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class InvoicesController
|
2
|
+
def initialize(client)
|
3
|
+
@biller = Biller.new.default
|
4
|
+
@invoice = Invoice.new
|
5
|
+
@client = client
|
6
|
+
@invoice.client_id = @client.id
|
7
|
+
get_root
|
8
|
+
add_line_items
|
9
|
+
create_file
|
10
|
+
end
|
11
|
+
def get_root
|
12
|
+
puts "Where is the project root (the parent directory of the git repo)?"
|
13
|
+
@invoice.project_root($stdin.gets.chomp)
|
14
|
+
@invoice.git_root
|
15
|
+
end
|
16
|
+
def add_line_items
|
17
|
+
commits = CommitsController.new(@invoice.git_log)
|
18
|
+
LineItemsController.new(@invoice, commits.index, @client)
|
19
|
+
end
|
20
|
+
def create_file
|
21
|
+
@invoice.save
|
22
|
+
view = InvoicesView.new(@invoice, @biller, @client)
|
23
|
+
File.open("#{INVOICES_FOLDER}/invoice#{@invoice.format_number}.txt", 'w') { |f| f.write(view.render) }
|
24
|
+
puts "generated invoice#{@invoice.format_number}.txt"
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class LineItemsController
|
2
|
+
def initialize(invoice, commits, client)
|
3
|
+
@invoice, @commits_index, @client = invoice, commits, client
|
4
|
+
compile_line_items
|
5
|
+
end
|
6
|
+
def compile_line_items
|
7
|
+
puts "Would you like to enter a different rate for each commit? (y/n)"
|
8
|
+
custom_rate = true if $stdin.gets.chomp == "y"
|
9
|
+
i = 0
|
10
|
+
line_item = nil
|
11
|
+
@commits_index.each do |commit|
|
12
|
+
puts "\ncommit #{i + 1}: " + commit.msg
|
13
|
+
puts "how long did this take?"
|
14
|
+
item_hrs = $stdin.gets.chomp
|
15
|
+
if custom_rate
|
16
|
+
puts "how much will you charge?"
|
17
|
+
line_item = LineItem.new(@invoice.number, i + 1, commit.date, commit.msg, item_hrs, $stdin.gets.chomp)
|
18
|
+
else
|
19
|
+
line_item = LineItem.new(@invoice.number, i + 1, commit.date, commit.msg, item_hrs, @client.rate)
|
20
|
+
end
|
21
|
+
line_item.save
|
22
|
+
@invoice.add_line_item(line_item)
|
23
|
+
i += 1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
INVOICES_VERSION = '0.1.0'
|
2
|
+
INVOICES_FOLDER = File.expand_path('~/Invoices')
|
3
|
+
INVOICES_DB = SQLite3::Database.new('db/invoices.db')
|
4
|
+
TEST_DB = SQLite3::Database.new('db/test.db')
|
5
|
+
|
6
|
+
def choose_db(*boolean)
|
7
|
+
if boolean.first
|
8
|
+
TEST_DB
|
9
|
+
else
|
10
|
+
INVOICES_DB
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class Biller
|
2
|
+
attr_accessor :name, :street1, :street2, :city,
|
3
|
+
:state, :zip, :phone, :email
|
4
|
+
def default(*boolean)
|
5
|
+
biller = choose_db(*boolean).execute("select * from billers").first
|
6
|
+
@name = biller[0].to_s
|
7
|
+
@street1 = biller[1].to_s
|
8
|
+
@street2 = biller[2].to_s
|
9
|
+
@city = biller[3].to_s
|
10
|
+
@state = biller[4].to_s
|
11
|
+
@zip = biller[5].to_s
|
12
|
+
@phone = biller[6].to_s
|
13
|
+
@email = biller[7].to_s
|
14
|
+
return self
|
15
|
+
end
|
16
|
+
def save(*boolean)
|
17
|
+
# Should raise error unless all fields except for street2 are filled
|
18
|
+
choose_db(*boolean).execute("INSERT INTO billers
|
19
|
+
(name, street1, street2, city, state, zip, phone, email)
|
20
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
21
|
+
[@name, @street1, @street2, @city, @state, @zip, @phone, @email])
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Client
|
2
|
+
attr_accessor :id, :name, :street1, :street2, :city,
|
3
|
+
:state, :zip, :phone, :rate, :email
|
4
|
+
def find_by_name(name, *boolean)
|
5
|
+
client = choose_db(*boolean).execute("select * from clients
|
6
|
+
where name = '#{name}'").first
|
7
|
+
@id = choose_db(*boolean).execute("select rowid from clients
|
8
|
+
where name = '#{name}'").first
|
9
|
+
@name = client[0].to_s
|
10
|
+
@street1 = client[1].to_s
|
11
|
+
@street2 = client[2].to_s
|
12
|
+
@city = client[3].to_s
|
13
|
+
@state = client[4].to_s
|
14
|
+
@zip = client[5].to_s
|
15
|
+
@phone = client[6].to_s
|
16
|
+
@email = client[7].to_s
|
17
|
+
@rate = client[8]
|
18
|
+
return self
|
19
|
+
end
|
20
|
+
def save(*boolean)
|
21
|
+
choose_db(*boolean).execute("INSERT INTO clients
|
22
|
+
(name, street1, street2, city, state,
|
23
|
+
zip, phone, email, rate)
|
24
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
25
|
+
[@name, @street1, @street2, @city, @state, @zip, @phone, @email, @rate])
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Commit
|
2
|
+
attr_accessor :date, :msg
|
3
|
+
def initialize(line)
|
4
|
+
parse_date(line)
|
5
|
+
parse_msg(line)
|
6
|
+
end
|
7
|
+
def parse_date(line)
|
8
|
+
timestamp = line.split(/> /).last.slice(0, 10).to_i
|
9
|
+
@date = Time.at(timestamp) # Convert Unix timestamp
|
10
|
+
end
|
11
|
+
def parse_msg(line)
|
12
|
+
if line.include?("commit:")
|
13
|
+
@msg = line.split(/commit:/).last.strip.slice(0, 40)
|
14
|
+
elsif line.include?("commit (initial):")
|
15
|
+
@msg = line.split(/commit \(initial\):/).last.strip.slice(0, 40)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
class Invoice
|
2
|
+
attr_reader :hours, :rate, :format_number, :line_items_array, :root, :git_log
|
3
|
+
attr_accessor :client_id, :total_hrs, :total_cost, :number, :date
|
4
|
+
def initialize
|
5
|
+
calculate_number
|
6
|
+
@line_items_array = []
|
7
|
+
@git_log = []
|
8
|
+
@total_hrs = 0
|
9
|
+
@total_cost = 0
|
10
|
+
@date = Time.now.strftime("%m/%d/%y")
|
11
|
+
end
|
12
|
+
def calculate_number
|
13
|
+
def format(x)
|
14
|
+
if x >= 1 && x < 10 then "000#{x}"
|
15
|
+
elsif x >= 10 && x < 100 then "00#{x}"
|
16
|
+
elsif x >= 100 && x < 1000 then "0#{x}"
|
17
|
+
elsif x >= 100 && x < 10000 then "#{x}"
|
18
|
+
# raise an exception for x >= 10000
|
19
|
+
end
|
20
|
+
end
|
21
|
+
i = 1
|
22
|
+
Dir.foreach(File.expand_path('~/invoices')) do |filename|
|
23
|
+
if filename.include?("invoice#{format(i)}.txt")
|
24
|
+
i += 1
|
25
|
+
else
|
26
|
+
format(i)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
@number = i
|
30
|
+
@format_number = format(i)
|
31
|
+
end
|
32
|
+
def project_root(file)
|
33
|
+
@root = File.expand_path(file)
|
34
|
+
end
|
35
|
+
def git_root
|
36
|
+
File.open("#{@root}/.git/logs/HEAD", "r") do |file|
|
37
|
+
file.each_line do |line|
|
38
|
+
@git_log << line if line.include?("commit")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
def calculate_total_hrs
|
43
|
+
@line_items_array.each do |line_item|
|
44
|
+
@total_hrs += line_item.hrs
|
45
|
+
end
|
46
|
+
@total_hrs
|
47
|
+
end
|
48
|
+
def calculate_total_cost
|
49
|
+
@line_items_array.each do |line_item|
|
50
|
+
@total_cost += line_item.cost
|
51
|
+
end
|
52
|
+
@total_cost
|
53
|
+
end
|
54
|
+
def save(*boolean)
|
55
|
+
calculate_total_hrs
|
56
|
+
calculate_total_cost
|
57
|
+
choose_db(*boolean).execute("INSERT INTO invoices
|
58
|
+
(invoice_number, date, client_id, total_hrs, total_cost)
|
59
|
+
VALUES (?, ?, ?, ?, ?)",
|
60
|
+
[@number, @date, @client_id, @total_hrs, @total_cost])
|
61
|
+
end
|
62
|
+
def add_line_item(line_item)
|
63
|
+
@line_items_array << line_item
|
64
|
+
end
|
65
|
+
def find_by_invoice_number(invoice_number, *boolean)
|
66
|
+
invoice = choose_db(*boolean).execute("select * from invoices where
|
67
|
+
invoice_number = #{invoice_number}").first
|
68
|
+
@number = invoice[0]
|
69
|
+
@date = invoice[1]
|
70
|
+
@client_id = invoice[2]
|
71
|
+
@total_hrs = invoice[3]
|
72
|
+
@total_cost = invoice[4]
|
73
|
+
return self
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class LineItem
|
2
|
+
attr_accessor :invoice_number, :line_number, :date,
|
3
|
+
:msg, :hrs, :rate, :cost
|
4
|
+
def initialize(invoice_number, line_number, date, msg, hrs, rate)
|
5
|
+
@invoice_number = invoice_number
|
6
|
+
@line_number = line_number
|
7
|
+
@date = date.to_s
|
8
|
+
@msg = msg
|
9
|
+
@hrs = hrs.to_i
|
10
|
+
@rate = rate.to_i
|
11
|
+
@cost = @hrs * @rate
|
12
|
+
end
|
13
|
+
def find_by_invoice_number(invoice_number, *boolean)
|
14
|
+
items = choose_db(*boolean).execute("select * from line_items where
|
15
|
+
invoice_number = #{invoice_number}")
|
16
|
+
items.map! do |line|
|
17
|
+
LineItem.new(line[0], line[1], line[2], line[3], line[4].to_s, line[5].to_s)
|
18
|
+
end
|
19
|
+
items
|
20
|
+
end
|
21
|
+
def save(*boolean)
|
22
|
+
choose_db(*boolean).execute("INSERT INTO line_items
|
23
|
+
(invoice_number, line_number, commit_date, commit_msg, hrs, rate, cost)
|
24
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)",
|
25
|
+
[@invoice_number, @line_number, @date, @msg, @hrs, @rate, @cost])
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ViewsHelpers
|
2
|
+
def compare_length(string, max_length)
|
3
|
+
# raise error if string.length < 0 || string.length > max_length
|
4
|
+
string = string.to_s unless string.instance_of?(String)
|
5
|
+
if string.length < max_length
|
6
|
+
difference = 0
|
7
|
+
difference = max_length - string.length
|
8
|
+
string + (" " * difference)
|
9
|
+
else
|
10
|
+
string
|
11
|
+
end
|
12
|
+
end
|
13
|
+
def format_hrs(h)
|
14
|
+
compare_length(h, 3)
|
15
|
+
end
|
16
|
+
def format_rate(amt)
|
17
|
+
compare_length("$" + amt.to_s, 4)
|
18
|
+
end
|
19
|
+
def divider
|
20
|
+
" | "
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'time'
|
2
|
+
require_relative 'helpers/views_helper'
|
3
|
+
|
4
|
+
class InvoicesView
|
5
|
+
include ViewsHelpers
|
6
|
+
def initialize(invoice, biller, client)
|
7
|
+
@invoice, @biller, @client = invoice, biller, client
|
8
|
+
end
|
9
|
+
def header
|
10
|
+
def space(chars)
|
11
|
+
" " * (72 - chars) # 72 chars in page width was,
|
12
|
+
end # traditionally, the most common
|
13
|
+
def line(left, right)
|
14
|
+
s = space(left.length + right.length)
|
15
|
+
l = left + s + right + "\n"
|
16
|
+
if l.strip.empty?
|
17
|
+
""
|
18
|
+
else
|
19
|
+
l
|
20
|
+
end
|
21
|
+
end
|
22
|
+
def address(person)
|
23
|
+
line(person.name, " ") +
|
24
|
+
line(person.street1, " ") +
|
25
|
+
line(person.street2, " ") +
|
26
|
+
line(person.city + ", " + person.state + " " + person.zip, " ") +
|
27
|
+
line(person.phone, " ") +
|
28
|
+
line(person.email, " ")
|
29
|
+
end
|
30
|
+
line("INVOICE #" + @invoice.format_number, @invoice.date) + "\n" +
|
31
|
+
address(@biller) + "\n" * 2 +
|
32
|
+
line("BILL TO:", "") + address(@client) + "\n" * 2
|
33
|
+
end
|
34
|
+
def grid
|
35
|
+
def border_top
|
36
|
+
"----+-- DATE --+------------- COMMIT MESSAGE -------------+ HRS + RATE +" +
|
37
|
+
("\n" * 2)
|
38
|
+
end
|
39
|
+
def border_bottom
|
40
|
+
("\n" * 2) +
|
41
|
+
"----+----------+------------------------------------------+-----+------+" + "\n"
|
42
|
+
end
|
43
|
+
def total
|
44
|
+
"TOTALS:" + (" " * 53) + "#{format_hrs(@invoice.total_hrs)}" +
|
45
|
+
divider + "#{format_rate(@invoice.total_cost)}" + "\n"
|
46
|
+
end
|
47
|
+
border_top + LineItemsView.new.prepare(@invoice.line_items_array).join("\n") +
|
48
|
+
border_bottom + total
|
49
|
+
end
|
50
|
+
def render
|
51
|
+
header + grid
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class LineItemsView
|
56
|
+
include ViewsHelpers
|
57
|
+
def format_number(n)
|
58
|
+
compare_length(n, 3)
|
59
|
+
end
|
60
|
+
def format_date(d)
|
61
|
+
d = Time.parse(d)
|
62
|
+
d = d.strftime("%m/%d/%y")
|
63
|
+
compare_length(d, 8)
|
64
|
+
end
|
65
|
+
def format_msg(m)
|
66
|
+
compare_length(m, 40)
|
67
|
+
end
|
68
|
+
def prepare(line_items)
|
69
|
+
# Receives an array from LineItemsController
|
70
|
+
line_items.map do |item|
|
71
|
+
format_number(item.line_number) + divider +
|
72
|
+
format_date(item.date) + divider +
|
73
|
+
format_msg(item.msg) + divider +
|
74
|
+
format_hrs(item.hrs) + divider +
|
75
|
+
format_rate(item.rate)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
require_relative '../lib/invoices/controllers/application_controller'
|
3
|
+
require_relative '../lib/invoices/controllers/invoices_controller'
|
4
|
+
require_relative '../lib/invoices/controllers/billers_controller'
|
5
|
+
require_relative '../lib/invoices/controllers/clients_controller'
|
6
|
+
require_relative '../lib/invoices/controllers/line_items_controller'
|
7
|
+
require_relative '../lib/invoices/controllers/commits_controller'
|
8
|
+
=begin
|
9
|
+
describe ApplicationController do
|
10
|
+
before do
|
11
|
+
@app = ApplicationController.new
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#initalize" do
|
15
|
+
it "should create a db & invoice folder" do
|
16
|
+
File.directory?(INVOICES_FOLDER).must_equal true
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should create invoices.db" do
|
20
|
+
File.exists?(INVOICES_DB).must_equal true
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should create test.db" do
|
24
|
+
File.exists?(TEST_DB).must_equal true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
#describe "#parse_options" do
|
29
|
+
#end
|
30
|
+
|
31
|
+
#describe "#parse_commands" do
|
32
|
+
#end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe InvoicesController do
|
36
|
+
before do
|
37
|
+
@invoices_controller = InvoicesController.new(Client.new)
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#initialize" do
|
41
|
+
it "should set some instance variables" do
|
42
|
+
@invoices_controller.biller.wont_be_nil
|
43
|
+
@invoices_controller.biller.must_be_instance_of Biller
|
44
|
+
@invoices_controller.invoice.must_be_instance_of Invoice
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe BillersController do
|
50
|
+
end
|
51
|
+
|
52
|
+
describe ClientsController do
|
53
|
+
end
|
54
|
+
|
55
|
+
describe LineItemsController do
|
56
|
+
end
|
57
|
+
|
58
|
+
describe CommitsController do
|
59
|
+
end
|
60
|
+
=end
|
data/spec/models_spec.rb
ADDED
@@ -0,0 +1,237 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
require_relative '../lib/invoices/models/invoice'
|
3
|
+
require_relative '../lib/invoices/models/biller'
|
4
|
+
require_relative '../lib/invoices/models/client'
|
5
|
+
require_relative '../lib/invoices/models/line_item'
|
6
|
+
require_relative '../lib/invoices/models/commit'
|
7
|
+
|
8
|
+
describe Invoice do
|
9
|
+
before do
|
10
|
+
TEST_DB.execute("DELETE FROM billers") # Combining into 1 SQL command
|
11
|
+
TEST_DB.execute("DELETE FROM clients") # was not working
|
12
|
+
TEST_DB.execute("DELETE FROM invoices")
|
13
|
+
TEST_DB.execute("DELETE FROM line_items")
|
14
|
+
@invoice = Invoice.new
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#initialize" do
|
18
|
+
it "should set some instance variables" do
|
19
|
+
@invoice.number.must_be :>, 0 # Also tests #calculate_number
|
20
|
+
@invoice.format_number.must_be_instance_of String
|
21
|
+
@invoice.date.must_equal Time.now.strftime("%m/%d/%y")
|
22
|
+
@invoice.git_log.must_be_instance_of Array
|
23
|
+
@invoice.line_items_array.must_be_instance_of Array
|
24
|
+
@invoice.total_hrs.must_equal 0
|
25
|
+
@invoice.total_cost.must_equal 0
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#project_root" do
|
30
|
+
before do
|
31
|
+
@invoice.project_root('~') # Universalize
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should set the file path" do
|
35
|
+
@invoice.root.must_equal File.expand_path('~')
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "#git_root" do
|
39
|
+
before do
|
40
|
+
@invoice.git_root
|
41
|
+
end
|
42
|
+
|
43
|
+
specify { @invoice.git_log.must_be_instance_of Array }
|
44
|
+
specify { @invoice.git_log.each { |line| line.must_include "commit" }}
|
45
|
+
|
46
|
+
describe "#add_line_item" do
|
47
|
+
before do
|
48
|
+
item1 = LineItem.new(@invoice.number, 1, @invoice.date, "Lorem ipsum", 7, 20)
|
49
|
+
item2 = LineItem.new(@invoice.number, 2, @invoice.date, "Lipsum", 5, 15)
|
50
|
+
@invoice.add_line_item(item1)
|
51
|
+
@invoice.add_line_item(item2)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should create an array of LineItem objects" do
|
55
|
+
@invoice.line_items_array[0].must_be_instance_of LineItem
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "#save" do
|
59
|
+
before do
|
60
|
+
@invoice.client_id = 3
|
61
|
+
@invoice.save(true)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should not save empty data" do
|
65
|
+
@invoice.number.must_be :>, 0
|
66
|
+
@invoice.date.wont_be_empty
|
67
|
+
@invoice.client_id.must_be :>, 0
|
68
|
+
@invoice.total_hrs.must_be :>, 0
|
69
|
+
@invoice.total_cost.must_be :>, 0
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should raise an error for invalid data"
|
73
|
+
|
74
|
+
it "should call #calculate_total_hrs" do
|
75
|
+
@invoice.total_hrs.must_equal 12
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should call #calculate_total_cost" do
|
79
|
+
@invoice.total_cost.must_equal 215
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "#find_by_invoice_number" do
|
83
|
+
before do
|
84
|
+
@invoice_query = Invoice.new.find_by_invoice_number(@invoice.number, true)
|
85
|
+
@invoice_query.must_be_instance_of Invoice
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should set instance variables from the database" do
|
89
|
+
@invoice.number.must_equal @invoice_query.number
|
90
|
+
@invoice.date.must_equal @invoice_query.date
|
91
|
+
@invoice.client_id.must_equal @invoice_query.client_id
|
92
|
+
@invoice.total_hrs.must_equal @invoice_query.total_hrs
|
93
|
+
@invoice.total_cost.must_equal @invoice_query.total_cost
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe Biller do
|
103
|
+
before do
|
104
|
+
@biller = Biller.new
|
105
|
+
@biller.name = "Aaron Burr"
|
106
|
+
@biller.street1 = "VP"
|
107
|
+
@biller.street2 = ""
|
108
|
+
@biller.city = "Washington"
|
109
|
+
@biller.state = "DC"
|
110
|
+
@biller.zip = "12345"
|
111
|
+
@biller.phone = "Telegram"
|
112
|
+
@biller.email = "aburr@example.com"
|
113
|
+
end
|
114
|
+
|
115
|
+
describe "#save" do
|
116
|
+
before do
|
117
|
+
@biller.save(true)
|
118
|
+
end
|
119
|
+
|
120
|
+
describe "#default" do # Write a test for the if/else
|
121
|
+
before do
|
122
|
+
@biller_query = Biller.new.default(true)
|
123
|
+
@biller_query.must_be_instance_of Biller
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should set instance variables from the database" do
|
127
|
+
@biller.name.must_equal @biller_query.name
|
128
|
+
@biller.street1.must_equal @biller_query.street1
|
129
|
+
@biller.street2.must_equal @biller_query.street2
|
130
|
+
@biller.city.must_equal @biller_query.city
|
131
|
+
@biller.state.must_equal @biller_query.state
|
132
|
+
@biller.zip.must_equal @biller_query.zip
|
133
|
+
@biller.phone.must_equal @biller_query.phone
|
134
|
+
@biller.email.must_equal @biller_query.email
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
describe Client do
|
141
|
+
before do
|
142
|
+
@client = Client.new
|
143
|
+
@client.name = "Alexander Hamilton"
|
144
|
+
@client.street1 = "Wall St"
|
145
|
+
@client.street2 = ""
|
146
|
+
@client.city = "Dover"
|
147
|
+
@client.state = "DE"
|
148
|
+
@client.zip = "55555"
|
149
|
+
@client.phone = "555-555-5555"
|
150
|
+
@client.rate = 100
|
151
|
+
@client.email = "ah@example.com"
|
152
|
+
end
|
153
|
+
|
154
|
+
describe "#save" do
|
155
|
+
before do
|
156
|
+
@client.save(true)
|
157
|
+
end
|
158
|
+
# Write a test for #all & the if/else
|
159
|
+
describe "#find_by_name" do
|
160
|
+
before do
|
161
|
+
@client_query = Client.new.find_by_name("Alexander Hamilton", true)
|
162
|
+
@client_query.must_be_instance_of Client
|
163
|
+
end
|
164
|
+
|
165
|
+
it "should set instance variables from the database" do
|
166
|
+
@client.name.must_equal @client_query.name
|
167
|
+
@client.street1.must_equal @client_query.street1
|
168
|
+
@client.street2.must_equal @client_query.street2
|
169
|
+
@client.city.must_equal @client_query.city
|
170
|
+
@client.state.must_equal @client_query.state
|
171
|
+
@client.zip.must_equal @client_query.zip
|
172
|
+
@client.phone.must_equal @client_query.phone
|
173
|
+
@client.email.must_equal @client_query.email
|
174
|
+
@client.rate.must_equal @client_query.rate
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
describe LineItem do
|
181
|
+
before do
|
182
|
+
@line_item1 = LineItem.new(7, 1, Time.now, "Lorem ipsum", 7, 20)
|
183
|
+
@line_item2 = LineItem.new(7, 2, Time.now, "Lipsum", 5, 15)
|
184
|
+
end
|
185
|
+
|
186
|
+
describe "#save" do
|
187
|
+
before do
|
188
|
+
@line_item1.save(true)
|
189
|
+
@line_item2.save(true)
|
190
|
+
end
|
191
|
+
|
192
|
+
describe "#find_by_invoice_number" do
|
193
|
+
before do
|
194
|
+
line_item_query = @line_item1.find_by_invoice_number(7, true)
|
195
|
+
@line1 = line_item_query[0]
|
196
|
+
@line2 = line_item_query[1]
|
197
|
+
end
|
198
|
+
|
199
|
+
it "should set instance variables from the database" do
|
200
|
+
@line_item1.invoice_number.must_equal @line1.invoice_number
|
201
|
+
@line_item1.line_number.must_equal @line1.line_number
|
202
|
+
@line_item1.date.must_equal @line1.date
|
203
|
+
@line_item1.msg.must_equal @line1.msg
|
204
|
+
@line_item1.hrs.must_equal @line1.hrs
|
205
|
+
@line_item1.rate.must_equal @line1.rate
|
206
|
+
@line_item1.cost.must_equal @line1.cost
|
207
|
+
@line_item2.invoice_number.must_equal @line2.invoice_number
|
208
|
+
@line_item2.line_number.must_equal @line2.line_number
|
209
|
+
@line_item2.date.must_equal @line2.date
|
210
|
+
@line_item2.msg.must_equal @line2.msg
|
211
|
+
@line_item2.hrs.must_equal @line2.hrs
|
212
|
+
@line_item2.rate.must_equal @line2.rate
|
213
|
+
@line_item2.cost.must_equal @line2.cost
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
describe Commit do
|
220
|
+
before do
|
221
|
+
line = "0000000000000000000000000000000000000000 847657c5752fb6de037f7ed1da964c702952867d Aaron Macy <aaronmacy@gmail.com> 1355558298 -0800 commit (initial): initial commit"
|
222
|
+
@commit = Commit.new(line)
|
223
|
+
end
|
224
|
+
|
225
|
+
describe "#parse_date" do
|
226
|
+
it "should convert the Unix timestamp to Time object" do
|
227
|
+
@commit.date.must_be_instance_of Time
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
describe "#parse_msg" do
|
232
|
+
it "should strip the commit message to a string of <= 40 chars" do
|
233
|
+
@commit.msg.must_be_instance_of String
|
234
|
+
@commit.msg.length.must_be :<=, 40
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: invoices
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Aaron Macy
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-01-10 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: ! "Generate monospaced .txt invoices at the command line using\n Git
|
15
|
+
Commits."
|
16
|
+
email: aaronmacy@gmail.com
|
17
|
+
executables:
|
18
|
+
- invoices
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- lib/invoices/global.rb
|
23
|
+
- lib/invoices/controllers/application_controller.rb
|
24
|
+
- lib/invoices/controllers/billers_controller.rb
|
25
|
+
- lib/invoices/controllers/clients_controller.rb
|
26
|
+
- lib/invoices/controllers/commits_controller.rb
|
27
|
+
- lib/invoices/controllers/invoices_controller.rb
|
28
|
+
- lib/invoices/controllers/line_items_controller.rb
|
29
|
+
- lib/invoices/models/biller.rb
|
30
|
+
- lib/invoices/models/client.rb
|
31
|
+
- lib/invoices/models/commit.rb
|
32
|
+
- lib/invoices/models/invoice.rb
|
33
|
+
- lib/invoices/models/line_item.rb
|
34
|
+
- lib/invoices/views/helpers/views_helper.rb
|
35
|
+
- lib/invoices/views/invoices_view.rb
|
36
|
+
- bin/invoices
|
37
|
+
- Rakefile
|
38
|
+
- README.md
|
39
|
+
- db/schema.rb
|
40
|
+
- spec/controllers_spec.rb
|
41
|
+
- spec/models_spec.rb
|
42
|
+
- spec/spec_helper.rb
|
43
|
+
homepage: http://github.com/amacy/invoices
|
44
|
+
licenses:
|
45
|
+
- MIT
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
requirements:
|
63
|
+
- sqlite3
|
64
|
+
rubyforge_project:
|
65
|
+
rubygems_version: 1.8.24
|
66
|
+
signing_key:
|
67
|
+
specification_version: 3
|
68
|
+
summary: Generate invoices at the command line using Git Commits.
|
69
|
+
test_files: []
|