text-invoice 0.0.1
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.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +66 -0
- data/Rakefile +1 -0
- data/bin/text-invoice +5 -0
- data/lib/text-invoice.rb +11 -0
- data/lib/text-invoice/cli.rb +37 -0
- data/lib/text-invoice/invoice.rb +13 -0
- data/lib/text-invoice/invoice.yaml +25 -0
- data/lib/text-invoice/summary.rb +34 -0
- data/lib/text-invoice/tasks.rb +62 -0
- data/lib/text-invoice/template.rb +20 -0
- data/lib/text-invoice/totals.rb +33 -0
- data/lib/text-invoice/version.rb +3 -0
- data/spec/cli_spec.rb +150 -0
- data/spec/invoice_spec.rb +8 -0
- data/spec/summary_spec.rb +39 -0
- data/spec/tasks_spec.rb +69 -0
- data/spec/template_spec.rb +29 -0
- data/spec/totals_spec.rb +49 -0
- data/templates/invoice.html +261 -0
- data/text-invoice.gemspec +23 -0
- metadata +96 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# Text Invoice
|
2
|
+
|
3
|
+
Some tools and helpers for creating, summarising and transforming invoices stored as [YAML][yaml] documents.
|
4
|
+
|
5
|
+
The tools are designed to be used in a UNIX-like environment.
|
6
|
+
|
7
|
+
## Typical uses
|
8
|
+
|
9
|
+
Create a new invoice
|
10
|
+
|
11
|
+
text-invoice new > some-job.yaml
|
12
|
+
|
13
|
+
Update totals, transform to HTML and write to disk
|
14
|
+
|
15
|
+
cat some-job.yaml | text-invoice update | text-invoice html > some-job.html
|
16
|
+
|
17
|
+
Get a summary of all invoices
|
18
|
+
|
19
|
+
find -name "*.yaml" | xargs text-invoice summary
|
20
|
+
|
21
|
+
Get a summarised list of all invoices
|
22
|
+
|
23
|
+
find -name "*.yaml" | xargs text-invoice list
|
24
|
+
|
25
|
+
Get a summarised list of all invoices which contain a word
|
26
|
+
|
27
|
+
grep -r -l "some pattern" * | xargs text-invoice list
|
28
|
+
|
29
|
+
You get the idea.
|
30
|
+
|
31
|
+
## Custom templates
|
32
|
+
|
33
|
+
It's just YAML so it's easy template. I use [Mustache templates][mustache] for the default HTML invoice, and added an option to support custom Mustache templates
|
34
|
+
|
35
|
+
cat some-job.yml | text-invoice template my-template.mustache > some-job.something
|
36
|
+
|
37
|
+
Of course if you have the Mustache gem installed (which is a dependency of this gem!) you can just use that!
|
38
|
+
|
39
|
+
cat some-job.yml | mustache - template.mustache > some-job.something
|
40
|
+
|
41
|
+
## PDF
|
42
|
+
|
43
|
+
I transform my invoices in PDF using [wkhtmltopdf][wkhtmltopdf]. There is a [wkpdf gem][wkpdf] but it's only for Max OS X, so that wasn't getting included.
|
44
|
+
|
45
|
+
cat some-job.yaml | text-invoice update | text-invoice html > /tmp/invoice.html && wkhtmltopdf /tmp/invoice.html some-job.pdf
|
46
|
+
|
47
|
+
Use whatever makes you happy.
|
48
|
+
|
49
|
+
## This seems stupid
|
50
|
+
|
51
|
+
I like having invoices in text so I can update them in another Vim buffer as I'm doing work. If I ever think I'm doing to much typing I can always wrap the commands in shell scripts or Vim mappings.
|
52
|
+
|
53
|
+
I prefer this to context switching to another application/website or trying to work out what I did from git logs.
|
54
|
+
|
55
|
+
## Acknowledgements
|
56
|
+
|
57
|
+
The template I'm using is a modified version of the pretty-neat-but-not-what-I-wanted [Editable, Printable Invoice by Chris Coyer and others][editable-invoice].
|
58
|
+
|
59
|
+
This is the first gem I've written, and [this guide][gem-dev] was helpful.
|
60
|
+
|
61
|
+
[editable-invoice]: http://css-tricks.com/editable-invoice-v2/
|
62
|
+
[wkhtmltopdf]: http://code.google.com/p/wkhtmltopdf/
|
63
|
+
[mustache]: http://mustache.github.com/
|
64
|
+
[gem-dev]: https://github.com/radar/guides/blob/master/gem-development.md
|
65
|
+
[yaml]: http://yaml.org/
|
66
|
+
[wkpdf]: http://rubygems.org/gems/wkpdf
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/text-invoice
ADDED
data/lib/text-invoice.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "text-invoice/version"
|
2
|
+
require "text-invoice/totals"
|
3
|
+
require "text-invoice/summary"
|
4
|
+
require "text-invoice/cli"
|
5
|
+
require "text-invoice/tasks"
|
6
|
+
require "text-invoice/invoice"
|
7
|
+
require "text-invoice/template"
|
8
|
+
require 'yaml'
|
9
|
+
|
10
|
+
module TextInvoice
|
11
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'text-invoice'
|
2
|
+
|
3
|
+
module TextInvoice
|
4
|
+
class CLI
|
5
|
+
attr_accessor :tasks, :argv, :stdin
|
6
|
+
|
7
|
+
def initialize()
|
8
|
+
@tasks = TextInvoice::Tasks.new
|
9
|
+
@argv = ARGV
|
10
|
+
@stdin = $stdin
|
11
|
+
end
|
12
|
+
|
13
|
+
def run()
|
14
|
+
mode = @argv.shift
|
15
|
+
if mode == "update"
|
16
|
+
@tasks.totals(@stdin.read)
|
17
|
+
elsif mode == "summary"
|
18
|
+
@tasks.summary(@argv)
|
19
|
+
elsif mode == "list"
|
20
|
+
@tasks.list(@argv)
|
21
|
+
elsif mode == "new"
|
22
|
+
@tasks.new_invoice()
|
23
|
+
elsif mode == "html"
|
24
|
+
@tasks.html(@stdin.read)
|
25
|
+
elsif mode == "template"
|
26
|
+
template = @argv.shift
|
27
|
+
if template
|
28
|
+
@tasks.template(@stdin.read, template)
|
29
|
+
else
|
30
|
+
@tasks.usage()
|
31
|
+
end
|
32
|
+
else
|
33
|
+
@tasks.usage()
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
---
|
2
|
+
invoice:
|
3
|
+
date:
|
4
|
+
from:
|
5
|
+
name:
|
6
|
+
abn:
|
7
|
+
address1:
|
8
|
+
address2:
|
9
|
+
phone:
|
10
|
+
email:
|
11
|
+
to:
|
12
|
+
attn:
|
13
|
+
for:
|
14
|
+
name:
|
15
|
+
address1:
|
16
|
+
address2:
|
17
|
+
address3:
|
18
|
+
phone:
|
19
|
+
items:
|
20
|
+
- description: item
|
21
|
+
quantity: 0
|
22
|
+
unit: 0
|
23
|
+
paid: 0
|
24
|
+
terms:
|
25
|
+
---
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module TextInvoice
|
2
|
+
class Summary
|
3
|
+
def summary(invoices)
|
4
|
+
paid = 0
|
5
|
+
due = 0
|
6
|
+
total = 0
|
7
|
+
count = 0
|
8
|
+
invoices.each do |invoice|
|
9
|
+
data = load(invoice)
|
10
|
+
total += data["total"]
|
11
|
+
paid += data["paid"]
|
12
|
+
due += data["due"]
|
13
|
+
count += 1
|
14
|
+
end
|
15
|
+
headings = ["Count", "Total", "Paid", "Due"].join("\t")
|
16
|
+
results = [count, total, paid, due].join("\t")
|
17
|
+
[headings, results].join("\n")
|
18
|
+
end
|
19
|
+
|
20
|
+
def list(invoices)
|
21
|
+
response = [["Invoice","Date", "Total", "Paid", "Due"].join("\t")]
|
22
|
+
invoices.each do |invoice|
|
23
|
+
data = load(invoice)
|
24
|
+
response << ([data["invoice"], data["date"], data["total"], data["paid"], data["due"]].join("\t"))
|
25
|
+
end
|
26
|
+
response.join("\n")
|
27
|
+
end
|
28
|
+
|
29
|
+
def load(invoice)
|
30
|
+
calculator = TextInvoice::Totals.new
|
31
|
+
YAML.load(calculator.process(open(invoice)))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'text-invoice'
|
2
|
+
|
3
|
+
module TextInvoice
|
4
|
+
class Tasks
|
5
|
+
attr_accessor :totals_calculator, :summary_calculator, :invoice, :template
|
6
|
+
|
7
|
+
def initialize()
|
8
|
+
@totals_calculator = TextInvoice::Totals.new
|
9
|
+
@summary_calculator = TextInvoice::Summary.new
|
10
|
+
@invoice = TextInvoice::Invoice
|
11
|
+
@template = TextInvoice::Template.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def totals(input)
|
15
|
+
@totals_calculator.process(input)
|
16
|
+
end
|
17
|
+
|
18
|
+
def summary(input)
|
19
|
+
@summary_calculator.summary(input)
|
20
|
+
end
|
21
|
+
|
22
|
+
def list(input)
|
23
|
+
@summary_calculator.list(input)
|
24
|
+
end
|
25
|
+
|
26
|
+
def new_invoice()
|
27
|
+
@invoice.blank()
|
28
|
+
end
|
29
|
+
|
30
|
+
def html(invoice)
|
31
|
+
@template.html(invoice)
|
32
|
+
end
|
33
|
+
|
34
|
+
def template(invoice, template)
|
35
|
+
@template.custom(invoice, template)
|
36
|
+
end
|
37
|
+
|
38
|
+
def usage()
|
39
|
+
text = """
|
40
|
+
Usage: text-invoice [new|totals|summary|list|html|template] [additional arguments..]
|
41
|
+
|
42
|
+
new Writes an empty invoice to STDOUT
|
43
|
+
|
44
|
+
update Calculates subtotals, totals and due amounts
|
45
|
+
Reads an invoice from STDIN and writes an updated invoice to STDOUT
|
46
|
+
|
47
|
+
summary Returns a summary of invoice files.
|
48
|
+
Summarises invoice files from additional arguments
|
49
|
+
|
50
|
+
list Returns a summarised list of invoices
|
51
|
+
Lists invoice files from additional arguments
|
52
|
+
|
53
|
+
html Transforms an invoice to HTML using a built-in template
|
54
|
+
Reads invoice from STDIN
|
55
|
+
|
56
|
+
template Transforms an invoice to HTML using a custom Mustache template
|
57
|
+
Reads invoice from STDIN. File path of a Mustache template must
|
58
|
+
be provided as an additional argument.
|
59
|
+
"""
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'text-invoice'
|
2
|
+
require 'mustache'
|
3
|
+
|
4
|
+
module TextInvoice
|
5
|
+
class Template
|
6
|
+
def html(invoice)
|
7
|
+
parsed = YAML.load(invoice)
|
8
|
+
Mustache.render(open(root + "templates/invoice.html").read(), parsed)
|
9
|
+
end
|
10
|
+
|
11
|
+
def custom(invoice, template)
|
12
|
+
parsed = YAML.load(invoice)
|
13
|
+
Mustache.render(open(template).read(), parsed)
|
14
|
+
end
|
15
|
+
|
16
|
+
def root
|
17
|
+
File.dirname(__FILE__) + "/../../"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module TextInvoice
|
2
|
+
class Totals
|
3
|
+
def process(invoice)
|
4
|
+
# parse
|
5
|
+
parsed = YAML.load(invoice)
|
6
|
+
|
7
|
+
# total accumulator
|
8
|
+
total = 0
|
9
|
+
|
10
|
+
# process line items
|
11
|
+
if not parsed["items"] == nil
|
12
|
+
for item in parsed["items"]
|
13
|
+
quantity = item["quantity"]
|
14
|
+
unit = item["unit"]
|
15
|
+
subtotal = quantity * unit
|
16
|
+
item["subtotal"] = subtotal
|
17
|
+
total += subtotal
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# update totals
|
22
|
+
parsed["total"] = total
|
23
|
+
|
24
|
+
if parsed["paid"]
|
25
|
+
parsed["due"] = total - parsed["paid"]
|
26
|
+
else
|
27
|
+
parsed["due"] = total
|
28
|
+
end
|
29
|
+
|
30
|
+
parsed.to_yaml
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/spec/cli_spec.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'text-invoice'
|
2
|
+
|
3
|
+
describe TextInvoice::CLI do
|
4
|
+
it "should return usage details with no parameters" do
|
5
|
+
tasks = double("tasks")
|
6
|
+
tasks.should_receive(:usage).and_return("usage details")
|
7
|
+
|
8
|
+
cli = TextInvoice::CLI.new
|
9
|
+
cli.tasks = tasks
|
10
|
+
cli.run().should eql("usage details")
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should return usage details with unknow parameters" do
|
14
|
+
argv = double("argv")
|
15
|
+
argv.should_receive(:shift).and_return("something we don't do")
|
16
|
+
|
17
|
+
tasks = double("tasks")
|
18
|
+
tasks.should_receive(:usage).and_return("usage details")
|
19
|
+
|
20
|
+
cli = TextInvoice::CLI.new
|
21
|
+
cli.argv = argv
|
22
|
+
cli.tasks = tasks
|
23
|
+
cli.run().should eql("usage details")
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should return summary task with argv parameter" do
|
27
|
+
argv = double("argv")
|
28
|
+
argv.should_receive(:shift).and_return("summary")
|
29
|
+
|
30
|
+
tasks = double("tasks")
|
31
|
+
tasks.should_receive(:summary).with(argv).and_return("summary results")
|
32
|
+
|
33
|
+
cli = TextInvoice::CLI.new
|
34
|
+
cli.tasks = tasks
|
35
|
+
cli.argv = argv
|
36
|
+
|
37
|
+
cli.run().should eql("summary results")
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should return summary task with argv parameters" do
|
41
|
+
argv = double("argv")
|
42
|
+
argv.should_receive(:shift).and_return("summary")
|
43
|
+
|
44
|
+
tasks = double("tasks")
|
45
|
+
tasks.should_receive(:summary).with(argv).and_return("summary results")
|
46
|
+
|
47
|
+
cli = TextInvoice::CLI.new
|
48
|
+
cli.tasks = tasks
|
49
|
+
cli.argv = argv
|
50
|
+
|
51
|
+
cli.run().should eql("summary results")
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should return list task with argv parameter" do
|
55
|
+
argv = double("argv")
|
56
|
+
argv.should_receive(:shift).and_return("list")
|
57
|
+
|
58
|
+
tasks = double("tasks")
|
59
|
+
tasks.should_receive(:list).with(argv).and_return("list results")
|
60
|
+
|
61
|
+
cli = TextInvoice::CLI.new
|
62
|
+
cli.tasks = tasks
|
63
|
+
cli.argv = argv
|
64
|
+
|
65
|
+
cli.run().should eql("list results")
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should return totals task with stdin parameter" do
|
69
|
+
argv = double("argv")
|
70
|
+
argv.should_receive(:shift).and_return("update")
|
71
|
+
|
72
|
+
stdin = double("stdin")
|
73
|
+
stdin.should_receive(:read).and_return("stdin")
|
74
|
+
|
75
|
+
tasks = double("tasks")
|
76
|
+
tasks.should_receive(:totals).with("stdin").and_return("totals results")
|
77
|
+
|
78
|
+
cli = TextInvoice::CLI.new
|
79
|
+
cli.tasks = tasks
|
80
|
+
cli.stdin = stdin
|
81
|
+
cli.argv = argv
|
82
|
+
|
83
|
+
cli.run().should eql("totals results")
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should return new invoice" do
|
87
|
+
argv = double("argv")
|
88
|
+
argv.should_receive(:shift).and_return("new")
|
89
|
+
|
90
|
+
tasks = double("tasks")
|
91
|
+
tasks.should_receive(:new_invoice).with(no_args()).and_return("new invoice yaml")
|
92
|
+
|
93
|
+
cli = TextInvoice::CLI.new
|
94
|
+
cli.tasks = tasks
|
95
|
+
cli.argv = argv
|
96
|
+
|
97
|
+
cli.run().should eql("new invoice yaml")
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should html template invoices" do
|
101
|
+
argv = double("argv")
|
102
|
+
argv.should_receive(:shift).and_return("html")
|
103
|
+
|
104
|
+
stdin = double("stdin")
|
105
|
+
stdin.should_receive(:read).and_return("stdin")
|
106
|
+
|
107
|
+
tasks = double("tasks")
|
108
|
+
tasks.should_receive(:html).with("stdin").and_return("invoice html")
|
109
|
+
|
110
|
+
cli = TextInvoice::CLI.new
|
111
|
+
cli.tasks = tasks
|
112
|
+
cli.argv = argv
|
113
|
+
cli.stdin = stdin
|
114
|
+
|
115
|
+
cli.run().should eql("invoice html")
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should custom template invoices" do
|
119
|
+
argv = double("argv")
|
120
|
+
argv.should_receive(:shift).and_return("template", "template_path")
|
121
|
+
|
122
|
+
stdin = double("stdin")
|
123
|
+
stdin.should_receive(:read).and_return("stdin")
|
124
|
+
|
125
|
+
tasks = double("tasks")
|
126
|
+
tasks.should_receive(:template).with("stdin","template_path").and_return("invoice html")
|
127
|
+
|
128
|
+
cli = TextInvoice::CLI.new
|
129
|
+
cli.tasks = tasks
|
130
|
+
cli.argv = argv
|
131
|
+
cli.stdin = stdin
|
132
|
+
|
133
|
+
cli.run().should eql("invoice html")
|
134
|
+
end
|
135
|
+
|
136
|
+
it "should return usage if custom template path not provided" do
|
137
|
+
argv = double("argv")
|
138
|
+
argv.should_receive(:shift).and_return("template", nil)
|
139
|
+
|
140
|
+
tasks = double("tasks")
|
141
|
+
tasks.should_receive(:usage)
|
142
|
+
|
143
|
+
cli = TextInvoice::CLI.new
|
144
|
+
cli.tasks = tasks
|
145
|
+
cli.argv = argv
|
146
|
+
|
147
|
+
cli.run()
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'text-invoice'
|
2
|
+
|
3
|
+
describe TextInvoice::Summary do
|
4
|
+
it "should open a yaml file and update totals" do
|
5
|
+
summary = TextInvoice::Summary.new
|
6
|
+
calculate = TextInvoice::Totals.new
|
7
|
+
|
8
|
+
invoice = summary.load("lib/text-invoice/invoice.yaml").to_yaml
|
9
|
+
|
10
|
+
blank = calculate.process(TextInvoice::Invoice.blank)
|
11
|
+
invoice.should eql(blank)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should return invoice summary" do
|
15
|
+
summary = TextInvoice::Summary.new
|
16
|
+
invoice1 = { 'due' => 5, 'total' => 10, 'paid' => 5 }
|
17
|
+
invoice2 = { 'due' => 1, 'total' => 10, 'paid' => 9 }
|
18
|
+
summary.stub(:load).and_return(invoice1, invoice2)
|
19
|
+
|
20
|
+
response = summary.summary(['file1','file2'])
|
21
|
+
|
22
|
+
headings = response.split("\n")[0].should eql(['Count','Total','Paid','Due'].join("\t"))
|
23
|
+
results = response.split("\n")[1].should eql(['2','20','14','6'].join("\t"))
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should return invoice list" do
|
27
|
+
summary = TextInvoice::Summary.new
|
28
|
+
invoice1 = { 'date' => '1/3/2012', 'invoice' => "1", 'due' => 5, 'total' => 10, 'paid' => 5 }
|
29
|
+
invoice2 = { 'date' => '2/3/2012', 'invoice' => "2", 'due' => 1, 'total' => 10, 'paid' => 9 }
|
30
|
+
summary.stub(:load).and_return(invoice1, invoice2)
|
31
|
+
|
32
|
+
response = summary.list(['file1','file2'])
|
33
|
+
|
34
|
+
response.split("\n")[0].should eql(['Invoice','Date','Total','Paid','Due'].join("\t"))
|
35
|
+
response.split("\n")[1].should eql(['1','1/3/2012','10','5','5'].join("\t"))
|
36
|
+
response.split("\n")[2].should eql(['2','2/3/2012','10','9','1'].join("\t"))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
data/spec/tasks_spec.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'text-invoice'
|
2
|
+
|
3
|
+
describe TextInvoice::Tasks do
|
4
|
+
it "should return usage details" do
|
5
|
+
tasks = TextInvoice::Tasks.new
|
6
|
+
tasks.usage().should include("Usage")
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should return summary" do
|
10
|
+
summary_calculator = double("summary")
|
11
|
+
summary_calculator.should_receive(:summary).with([]).and_return("summary results")
|
12
|
+
|
13
|
+
tasks = TextInvoice::Tasks.new
|
14
|
+
tasks.summary_calculator = summary_calculator
|
15
|
+
|
16
|
+
tasks.summary([]).should include("summary results")
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should return list" do
|
20
|
+
summary_calculator = double("summary")
|
21
|
+
summary_calculator.should_receive(:list).with([]).and_return("list results")
|
22
|
+
|
23
|
+
tasks = TextInvoice::Tasks.new
|
24
|
+
tasks.summary_calculator = summary_calculator
|
25
|
+
|
26
|
+
tasks.list([]).should include("list results")
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should return totals" do
|
30
|
+
totals_calculator = double("summary")
|
31
|
+
totals_calculator.should_receive(:process).with([]).and_return("totals results")
|
32
|
+
|
33
|
+
tasks = TextInvoice::Tasks.new
|
34
|
+
tasks.totals_calculator = totals_calculator
|
35
|
+
|
36
|
+
tasks.totals([]).should include("totals results")
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should return blank invoice yaml" do
|
40
|
+
invoice = double("invoice")
|
41
|
+
invoice.should_receive(:blank).and_return("new invoice yaml")
|
42
|
+
|
43
|
+
tasks = TextInvoice::Tasks.new
|
44
|
+
tasks.invoice = invoice
|
45
|
+
|
46
|
+
tasks.new_invoice().should eql("new invoice yaml")
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should return html" do
|
50
|
+
template = double("template")
|
51
|
+
template.should_receive(:html).with({}).and_return("invoice html")
|
52
|
+
|
53
|
+
tasks = TextInvoice::Tasks.new
|
54
|
+
tasks.template = template
|
55
|
+
|
56
|
+
tasks.html({}).should eql("invoice html")
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should return template" do
|
60
|
+
template = double("template")
|
61
|
+
template.should_receive(:custom).with({},"").and_return("invoice html")
|
62
|
+
|
63
|
+
tasks = TextInvoice::Tasks.new
|
64
|
+
tasks.template = template
|
65
|
+
|
66
|
+
tasks.template({},"").should eql("invoice html")
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'text-invoice'
|
2
|
+
|
3
|
+
describe TextInvoice::Template do
|
4
|
+
it "should return invoice html the using default template" do
|
5
|
+
template = TextInvoice::Template.new
|
6
|
+
invoice = YAML.load(TextInvoice::Invoice.blank)
|
7
|
+
invoice["invoice"] = "123"
|
8
|
+
|
9
|
+
html = template.html(invoice.to_yaml)
|
10
|
+
|
11
|
+
# check for something from the template
|
12
|
+
html.should include("html")
|
13
|
+
# check for something from the data
|
14
|
+
html.should include("123")
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should return invoice html the using a custom template" do
|
18
|
+
template = TextInvoice::Template.new
|
19
|
+
invoice = YAML.load(TextInvoice::Invoice.blank)
|
20
|
+
invoice["invoice"] = "123"
|
21
|
+
|
22
|
+
html = template.custom(invoice.to_yaml, "templates/invoice.html")
|
23
|
+
|
24
|
+
# check for something from the template
|
25
|
+
html.should include("html")
|
26
|
+
# check for something from the data
|
27
|
+
html.should include("123")
|
28
|
+
end
|
29
|
+
end
|
data/spec/totals_spec.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'text-invoice'
|
2
|
+
|
3
|
+
describe TextInvoice::Totals do
|
4
|
+
it "should add totals for empty invoice" do
|
5
|
+
# empty invoice
|
6
|
+
invoice = {}.to_yaml
|
7
|
+
calculator = TextInvoice::Totals.new
|
8
|
+
|
9
|
+
# process
|
10
|
+
result = YAML.load(calculator.process(invoice))
|
11
|
+
|
12
|
+
# validate
|
13
|
+
result["total"].should eql(0)
|
14
|
+
result["due"].should eql(0)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should update invoice totals and subtotals" do
|
18
|
+
|
19
|
+
# setup invoice
|
20
|
+
invoice = { 'items' =>
|
21
|
+
[ { 'quantity' => 1, 'unit' => 4 },
|
22
|
+
{ 'quantity' => 2, 'unit' => 3 } ] }.to_yaml
|
23
|
+
calculator = TextInvoice::Totals.new
|
24
|
+
|
25
|
+
# process
|
26
|
+
result = YAML.load(calculator.process(invoice))
|
27
|
+
|
28
|
+
# validate
|
29
|
+
result["items"][0]["subtotal"].should eql(4)
|
30
|
+
result["items"][1]["subtotal"].should eql(6)
|
31
|
+
result["total"].should eql(10)
|
32
|
+
result["due"].should eql(10)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should update 'due' amount to be 'total' minus 'paid'" do
|
36
|
+
|
37
|
+
# setup invoice
|
38
|
+
invoice = { 'items' => [ { 'quantity' => 1, 'unit' => 10 } ], 'paid' => 5 }.to_yaml
|
39
|
+
calculator = TextInvoice::Totals.new
|
40
|
+
|
41
|
+
# process
|
42
|
+
result = YAML.load(calculator.process(invoice))
|
43
|
+
|
44
|
+
# validate
|
45
|
+
result["total"].should eql(10)
|
46
|
+
result["paid"].should eql(5)
|
47
|
+
result["due"].should eql(5)
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,261 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html xmlns="http://www.w3.org/1999/xhtml">
|
3
|
+
<head>
|
4
|
+
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />
|
5
|
+
|
6
|
+
<title>Invoice</title>
|
7
|
+
|
8
|
+
<style>
|
9
|
+
* {
|
10
|
+
margin: 0;
|
11
|
+
padding: 0;
|
12
|
+
}
|
13
|
+
|
14
|
+
body {
|
15
|
+
font: 14px/1.4 Georgia, serif;
|
16
|
+
}
|
17
|
+
|
18
|
+
#page-wrap {
|
19
|
+
width: 800px;
|
20
|
+
margin: 0 auto;
|
21
|
+
}
|
22
|
+
|
23
|
+
table {
|
24
|
+
border-collapse: collapse;
|
25
|
+
}
|
26
|
+
|
27
|
+
table td, table th {
|
28
|
+
border: 1px solid black;
|
29
|
+
padding: 5px;
|
30
|
+
}
|
31
|
+
|
32
|
+
#header {
|
33
|
+
height: 15px;
|
34
|
+
width: 100%;
|
35
|
+
margin: 20px 0;
|
36
|
+
background: #222;
|
37
|
+
text-align: center;
|
38
|
+
color: white;
|
39
|
+
font: bold 15px Helvetica, Sans-Serif;
|
40
|
+
text-decoration: uppercase;
|
41
|
+
padding: 8px 0px;
|
42
|
+
}
|
43
|
+
|
44
|
+
#identity {
|
45
|
+
width: 300px;
|
46
|
+
float: left
|
47
|
+
}
|
48
|
+
|
49
|
+
#customer {
|
50
|
+
width: 300px;
|
51
|
+
float: right
|
52
|
+
}
|
53
|
+
|
54
|
+
#customer table {
|
55
|
+
width: 100%
|
56
|
+
}
|
57
|
+
|
58
|
+
#customer table th {
|
59
|
+
text-align: left;
|
60
|
+
background: #eee;
|
61
|
+
}
|
62
|
+
|
63
|
+
#address {
|
64
|
+
width: 250px;
|
65
|
+
height: 150px;
|
66
|
+
float: left;
|
67
|
+
}
|
68
|
+
|
69
|
+
#meta {
|
70
|
+
margin-top: 30px;
|
71
|
+
margin-bottom: 30px;
|
72
|
+
width: 300px;
|
73
|
+
float: right;
|
74
|
+
}
|
75
|
+
|
76
|
+
#meta td {
|
77
|
+
text-align: right;
|
78
|
+
}
|
79
|
+
|
80
|
+
#meta td.meta-head {
|
81
|
+
text-align: left;
|
82
|
+
background: #eee;
|
83
|
+
}
|
84
|
+
|
85
|
+
#meta td textarea {
|
86
|
+
width: 100%;
|
87
|
+
height: 20px;
|
88
|
+
text-align: right;
|
89
|
+
}
|
90
|
+
|
91
|
+
#items {
|
92
|
+
clear: both;
|
93
|
+
width: 100%;
|
94
|
+
margin: 30px 0 0 0;
|
95
|
+
border: 1px none black;
|
96
|
+
}
|
97
|
+
|
98
|
+
#items th {
|
99
|
+
background: #eee;
|
100
|
+
}
|
101
|
+
|
102
|
+
#items tr.item-row td {
|
103
|
+
vertical-align: top;
|
104
|
+
border: none;
|
105
|
+
}
|
106
|
+
|
107
|
+
#items tr.item-row {
|
108
|
+
border-left: 1px solid black;
|
109
|
+
border-right: 1px solid black;
|
110
|
+
}
|
111
|
+
|
112
|
+
#items tr.totals {
|
113
|
+
border-right: 1px solid black;
|
114
|
+
border-top: 1px solid black;
|
115
|
+
}
|
116
|
+
|
117
|
+
#items tr.total {
|
118
|
+
border-right: 1px solid black;
|
119
|
+
}
|
120
|
+
|
121
|
+
#items tr.due {
|
122
|
+
background: #eee;
|
123
|
+
}
|
124
|
+
|
125
|
+
#items td.description {
|
126
|
+
width: 300px;
|
127
|
+
}
|
128
|
+
|
129
|
+
#items td.blank {
|
130
|
+
border: none;
|
131
|
+
}
|
132
|
+
|
133
|
+
#items td.money {
|
134
|
+
border-right: 0;
|
135
|
+
text-align: left;
|
136
|
+
padding-left: 50px;
|
137
|
+
}
|
138
|
+
|
139
|
+
#terms {
|
140
|
+
text-align: center;
|
141
|
+
margin: 20px 0 0 0;
|
142
|
+
}
|
143
|
+
|
144
|
+
#terms h5 {
|
145
|
+
text-transform: uppercase;
|
146
|
+
font: 13px Helvetica, Sans-Serif;
|
147
|
+
border-bottom: 1px solid black;
|
148
|
+
padding: 0 0 8px 0;
|
149
|
+
margin: 0 0 8px 0;
|
150
|
+
}
|
151
|
+
|
152
|
+
#terms textarea {
|
153
|
+
width: 100%;
|
154
|
+
text-align: center;
|
155
|
+
}
|
156
|
+
</style>
|
157
|
+
</head>
|
158
|
+
|
159
|
+
<body>
|
160
|
+
|
161
|
+
<div id="page-wrap">
|
162
|
+
|
163
|
+
<div id="header">INVOICE</div>
|
164
|
+
|
165
|
+
<div id="identity">
|
166
|
+
<div>{{from.name}}</div>
|
167
|
+
<div>{{from.abn}}</div>
|
168
|
+
<br/>
|
169
|
+
<div>{{from.address1}}</div>
|
170
|
+
<div>{{from.address2}}</div>
|
171
|
+
<div>{{from.phone}}</div>
|
172
|
+
<div>{{from.email}}</div>
|
173
|
+
</div>
|
174
|
+
|
175
|
+
|
176
|
+
<div id="customer">
|
177
|
+
<table>
|
178
|
+
<tr>
|
179
|
+
<th>
|
180
|
+
To
|
181
|
+
</th>
|
182
|
+
</tr>
|
183
|
+
<tr>
|
184
|
+
<td>
|
185
|
+
<div>{{to.attn}}</div>
|
186
|
+
<div>{{to.name}}</div>
|
187
|
+
<div>{{to.address1}}</div>
|
188
|
+
<div>{{to.address2}}</div>
|
189
|
+
<div>{{to.address3}}</div>
|
190
|
+
<div>{{to.phone}}</div>
|
191
|
+
</td>
|
192
|
+
</tr>
|
193
|
+
</table>
|
194
|
+
</div>
|
195
|
+
<div style="clear:both"></div>
|
196
|
+
|
197
|
+
<div id="details">
|
198
|
+
<table id="meta">
|
199
|
+
<tr>
|
200
|
+
<td class="meta-head">Invoice #</td>
|
201
|
+
<td>{{invoice}}</td>
|
202
|
+
</tr>
|
203
|
+
<tr>
|
204
|
+
|
205
|
+
<td class="meta-head">Date</td>
|
206
|
+
<td>{{date}}</td>
|
207
|
+
</tr>
|
208
|
+
<tr>
|
209
|
+
<td class="meta-head">Amount Due</td>
|
210
|
+
<td><div class="due">${{due}}</div></td>
|
211
|
+
</tr>
|
212
|
+
|
213
|
+
</table>
|
214
|
+
|
215
|
+
</div>
|
216
|
+
|
217
|
+
|
218
|
+
<table id="items">
|
219
|
+
|
220
|
+
<tr>
|
221
|
+
<th>Description</th>
|
222
|
+
<th>Unit Cost</th>
|
223
|
+
<th>Quantity</th>
|
224
|
+
<th>Price</th>
|
225
|
+
</tr>
|
226
|
+
|
227
|
+
{{#items}}
|
228
|
+
<tr class="item-row">
|
229
|
+
<td class="description">{{description}}</td>
|
230
|
+
<td class="money">${{unit}}</td>
|
231
|
+
<td class="money">{{quantity}}</td>
|
232
|
+
<td class="money">${{subtotal}}</td>
|
233
|
+
</tr>
|
234
|
+
{{/items}}
|
235
|
+
|
236
|
+
<tr class="totals">
|
237
|
+
<td colspan="2" class="blank"> </td>
|
238
|
+
<td colspan="1" class="total-line">Total</td>
|
239
|
+
<td class="money"><div id="total">${{total}}</div></td>
|
240
|
+
</tr>
|
241
|
+
<tr class="total">
|
242
|
+
<td colspan="2" class="blank"> </td>
|
243
|
+
<td colspan="1" class="total-line">Amount Paid</td>
|
244
|
+
|
245
|
+
<td class="money">${{paid}}</td>
|
246
|
+
</tr>
|
247
|
+
<tr class="total">
|
248
|
+
<td colspan="2" class="blank"> </td>
|
249
|
+
<td colspan="1" class="total-line balance">Balance Due</td>
|
250
|
+
<td class="money"><div class="due">{{due}}</div></td>
|
251
|
+
</tr>
|
252
|
+
|
253
|
+
</table>
|
254
|
+
|
255
|
+
<div id="terms">
|
256
|
+
<h5>Terms</h5>
|
257
|
+
{{terms}}
|
258
|
+
</div>
|
259
|
+
</div>
|
260
|
+
</body>
|
261
|
+
</html>
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "text-invoice/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "text-invoice"
|
7
|
+
s.version = TextInvoice::VERSION
|
8
|
+
s.authors = ["tarn"]
|
9
|
+
s.email = ["tarn.barford@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = "invoicing tools"
|
12
|
+
s.description = ""
|
13
|
+
|
14
|
+
s.rubyforge_project = "text-invoice"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_development_dependency "rspec"
|
22
|
+
s.add_dependency "mustache"
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: text-invoice
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- tarn
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-25 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &70476390 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70476390
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: mustache
|
27
|
+
requirement: &70475980 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70475980
|
36
|
+
description: ''
|
37
|
+
email:
|
38
|
+
- tarn.barford@gmail.com
|
39
|
+
executables:
|
40
|
+
- text-invoice
|
41
|
+
extensions: []
|
42
|
+
extra_rdoc_files: []
|
43
|
+
files:
|
44
|
+
- .gitignore
|
45
|
+
- Gemfile
|
46
|
+
- README.md
|
47
|
+
- Rakefile
|
48
|
+
- bin/text-invoice
|
49
|
+
- lib/text-invoice.rb
|
50
|
+
- lib/text-invoice/cli.rb
|
51
|
+
- lib/text-invoice/invoice.rb
|
52
|
+
- lib/text-invoice/invoice.yaml
|
53
|
+
- lib/text-invoice/summary.rb
|
54
|
+
- lib/text-invoice/tasks.rb
|
55
|
+
- lib/text-invoice/template.rb
|
56
|
+
- lib/text-invoice/totals.rb
|
57
|
+
- lib/text-invoice/version.rb
|
58
|
+
- spec/cli_spec.rb
|
59
|
+
- spec/invoice_spec.rb
|
60
|
+
- spec/summary_spec.rb
|
61
|
+
- spec/tasks_spec.rb
|
62
|
+
- spec/template_spec.rb
|
63
|
+
- spec/totals_spec.rb
|
64
|
+
- templates/invoice.html
|
65
|
+
- text-invoice.gemspec
|
66
|
+
homepage: ''
|
67
|
+
licenses: []
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ! '>='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
requirements: []
|
85
|
+
rubyforge_project: text-invoice
|
86
|
+
rubygems_version: 1.8.10
|
87
|
+
signing_key:
|
88
|
+
specification_version: 3
|
89
|
+
summary: invoicing tools
|
90
|
+
test_files:
|
91
|
+
- spec/cli_spec.rb
|
92
|
+
- spec/invoice_spec.rb
|
93
|
+
- spec/summary_spec.rb
|
94
|
+
- spec/tasks_spec.rb
|
95
|
+
- spec/template_spec.rb
|
96
|
+
- spec/totals_spec.rb
|