billtrap 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,73 @@
1
+ # encoding: UTF-8
2
+ module BillTrap
3
+ module CLI
4
+ extend Helpers
5
+ extend self
6
+ attr_accessor :args
7
+
8
+ def invoke
9
+ require 'cmd/usage'
10
+
11
+ case args.first when '-h', '--help', '--usage', '-?', 'help', nil
12
+ puts BillTrap::CLI.usage
13
+ exit 0
14
+ when '-v', '--version'
15
+ puts "BillTrap version #{BillTrap::VERSION}"
16
+ exit 0
17
+ end
18
+
19
+ # Grab global options, then stop
20
+ flags = Trollop::options args do
21
+ opt :debug
22
+ stop_on_unknown
23
+ end
24
+
25
+ command = args.shift
26
+ # Complete command
27
+ available = commands.select{ |key| key.match(/^#{command}/) }
28
+ if available.size == 1
29
+ require "cmd/#{available[0]}"
30
+ send available[0]
31
+ elsif available.size > 1
32
+ warn "Error: Ambiguous command '#{command}'"
33
+ warn "Matching commands are: #{available.join(", ")}"
34
+ else
35
+ warn "Error: Invalid command #{command.inspect}"
36
+ end
37
+ rescue StandardError, LoadError => e
38
+ raise e if flags && flags[:debug]
39
+ warn e.message
40
+ end
41
+
42
+ def commands
43
+ BillTrap::CLI.usage.scan(/\* \w+/).map{|s| s.gsub(/\* /, '')}
44
+ end
45
+
46
+ private
47
+
48
+ def confirm question
49
+ print "#{question} ? "
50
+ $stdin.gets =~ /\Aye?s?\Z/i
51
+ rescue Interrupt
52
+ # Avoid ugly trace
53
+ warn "\nCaught Interrupt. Exiting"
54
+ exit 1
55
+ end
56
+
57
+ def ask_value name, multiline=false
58
+ print "#{name}: "
59
+ if multiline
60
+ puts "(Multiline input, type Ctrl-D or insert END and return to exit)"
61
+ val = ($stdin.gets("END") || '').chomp("END")
62
+ puts
63
+ else
64
+ val = ($stdin.gets || '').chomp
65
+ end
66
+ return val
67
+ rescue Interrupt
68
+ # Avoid ugly trace
69
+ warn "\nCaught Interrupt. Exiting"
70
+ exit 1
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,50 @@
1
+ module BillTrap
2
+ module CLI
3
+ def client
4
+ opts = Trollop::options(args) do
5
+ opt :add, "Add a new ID, reading from STDIN", :short => '-a'
6
+ opt :delete, "Delete the client by ID", :type => Integer, :short => '-d'
7
+ end
8
+
9
+ if opts[:add]
10
+ firstname = ask_value "First name"
11
+ surname = ask_value "Surname"
12
+ company = ask_value "Company"
13
+ address = ask_value "Address", true
14
+ mail = ask_value "Mail"
15
+ rate = ask_value "Hourly rate"
16
+ currency = ask_value "Use non-standard Currency? [Leave empty for #{BillTrap::Config['currency']}]"
17
+
18
+ currency = currency.empty? ? BillTrap::Config['currency'] : currency
19
+ puts "'#{currency}'"
20
+
21
+ client = Client.create(
22
+ :firstname => firstname,
23
+ :surname => surname,
24
+ :company => company,
25
+ :address => address,
26
+ :mail => mail,
27
+ :rate => Money.parse(rate, currency).cents,
28
+ :currency => currency
29
+ )
30
+ puts "Client #{firstname} #{surname} was created with id #{client.id}"
31
+ elsif id = opts[:delete]
32
+ if e = Client.get(id)
33
+ if confirm "Are you sure you want to delete Client #{e.name} (##{e.id})"
34
+ begin
35
+ e.destroy
36
+ puts "Client has been removed."
37
+ rescue Sequel::ForeignKeyConstraintViolation
38
+ warn 'Error: Client is still in use. Refusing to delete client'
39
+ end
40
+ else
41
+ puts "Client has NOT been removed."
42
+ end
43
+ else
44
+ warn "Can't find Client with id '#{id}'"
45
+ end
46
+ end
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,8 @@
1
+ module BillTrap
2
+ module CLI
3
+ def configure
4
+ BillTrap::Config.configure!
5
+ puts "Config file written to: #{BillTrap::Config::CONFIG_PATH.inspect}"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,45 @@
1
+ module BillTrap
2
+ module CLI
3
+ def entry
4
+ opts = Trollop::options args do
5
+ opt :add, "Manually add entry to current invoice", :short => '-a'
6
+ opt :delete, "Delete entry from current invoice by ID", :type => :int, :short => '-d'
7
+ end
8
+ current = Invoice.current
9
+ if opts[:add]
10
+ title = ask_value 'Entry title'
11
+ date = ask_value 'Entry date (YYYY-MM-DD)'
12
+ unit = ask_value "Displayed unit (Defaults to 'h' for hours)" || 'h'
13
+ count = ask_value "Quantity (Numeric)"
14
+ price = ask_value "Price in #{current.currency} per unit (Numeric)"
15
+ notes = ask_value 'Optional Notes', true
16
+
17
+ e = InvoiceEntry.create(
18
+ :invoice_id => current.id,
19
+ :title => title,
20
+ :date => Date.parse(date),
21
+ :unit => unit,
22
+ :count => count,
23
+ :notes => notes,
24
+ :cents => Money.parse(price).cents
25
+ )
26
+
27
+ puts "Added entry (##{e.id}) to current invoice (ID #{current.id})"
28
+ elsif name = opts[:name]
29
+ Invoice.current.update(:name => name)
30
+ puts "Set current invoice (##{Invoice.current.id}) name to: #{name}"
31
+ elsif id = opts[:delete]
32
+ if e = InvoiceEntry[id]
33
+ if confirm "Are you sure you want to delete InvoiceEntry ##{e.id}"
34
+ e.destroy
35
+ puts "Entry has been removed."
36
+ else
37
+ puts "Entry has NOT been removed."
38
+ end
39
+ else
40
+ warn "Can't find entry with id '#{id}'"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,32 @@
1
+ module BillTrap
2
+ module CLI
3
+ def export
4
+ opts = Trollop::options args do
5
+ opt :adapter, "Set adapter", :type => :string, :short => '-a'
6
+ end
7
+ adapter = opts[:adapter] || 'ooffice'
8
+ begin
9
+ # Replace invoice number placeholders
10
+ arg = {
11
+ # Unique, auto-incremented invoice id (from database)
12
+ :invoice_id => Invoice.current.id,
13
+ # Unique, auto-incremented client id
14
+ :client_id => Invoice.current.client_id
15
+ }
16
+
17
+ # Replace above parameters, then strfime parameters
18
+ invoice_number = Invoice.current.created.strftime(Config['invoice_number_format'].gsub(/%\{(.*?)\}/) { arg[$1.to_sym] })
19
+
20
+ attributes = {
21
+ :invoice => Invoice.current,
22
+ :invoice_number => invoice_number,
23
+ }
24
+
25
+ BillTrap::Adapters.load_adapter(adapter).new(attributes).generate
26
+ rescue LoadError
27
+ warn "Couldn't load adapter named #{adapter}.rb"
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,47 @@
1
+ module BillTrap
2
+ module CLI
3
+ def import
4
+ opts = Trollop::options args do
5
+ opt :clear, "Clear entries before import", :short => '-c'
6
+ opt :entry, "Import entries by ID", :type => :strings, :multi => true, :short => '-e'
7
+ opt :round, "Round imported entries", :short => '-r'
8
+ opt :sheet, "Import sheet by name", :type => :string, :short => '-s'
9
+ end
10
+
11
+ # Clear entries if --clear given
12
+ if opts[:clear]
13
+ InvoiceEntry.where(:invoice_id => Invoice.current.id).destroy
14
+ end
15
+
16
+ entries =
17
+ if opts[:sheet]
18
+ Entry.filter(:sheet => opts[:sheet]).all
19
+ elsif opts[:entry_given]
20
+ Entry.where(:id => opts[:entry].first).all
21
+ else
22
+ []
23
+ end
24
+
25
+ unless entries.length > 0
26
+ warn "No matching entries found."
27
+ return
28
+ end
29
+
30
+ entries.each do |e|
31
+ Entry.round = opts[:round]
32
+ # Ignore entry if (rounded) is empty
33
+ next if e.duration == 0
34
+ imported = InvoiceEntry.create(
35
+ :invoice_id => Invoice.current.id,
36
+ :title => e.sheet,
37
+ :date => e.start.to_date,
38
+ :unit => 'h',
39
+ :count => (e.duration.to_f / 3600).round(2),
40
+ :notes => e.note,
41
+ :cents => Invoice.current.rate.cents
42
+ )
43
+ puts "Imported #{imported.count} hours from sheet #{e.sheet} as entry ##{imported.id}"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,16 @@
1
+ module BillTrap
2
+ module CLI
3
+ def in
4
+ key = args.shift || raise('Error: No ID/Name given')
5
+ invoice = Invoice.get key
6
+
7
+ if invoice
8
+ puts "Activating invoice ##{invoice.id}"
9
+ # set current id
10
+ Invoice.current = invoice.id
11
+ else
12
+ puts "No Invoice found for input '#{key}'"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,28 @@
1
+ module BillTrap
2
+ module CLI
3
+ def new
4
+ opts = Trollop::options args do
5
+ opt :client, "Optional Client ID", :type => :string, :short => '-c'
6
+ opt :date, "Optional invoice date", :type => :string, :short => '-d'
7
+ opt :name, "Optional invoice name", :type => :string, :short => '-n'
8
+ end
9
+
10
+ date =
11
+ if opts[:date]
12
+ Chronic.parse(opts[:date])
13
+ else
14
+ Date.today
15
+ end
16
+
17
+
18
+ invoice = Invoice.create(
19
+ :name => opts[:name],
20
+ :created => date,
21
+ :client => Client.get(opts[:client])
22
+ )
23
+ # Make active
24
+ Invoice.current = invoice.id
25
+ puts "Created invoice ##{invoice.id}"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,45 @@
1
+ module BillTrap
2
+ module CLI
3
+ def payment
4
+ opts = Trollop::options args do
5
+ opt :add, "Add payment to current invoice", :type => :strings, :multi => true, :short => '-a'
6
+ opt :delete, "Delete payment by ID from current invoice", :type => :int, :short => '-d'
7
+ end
8
+ if opts[:add_given] && opts[:add][0].length > 1
9
+ # If the invoice has no total
10
+ if Invoice.current.total.cents == 0
11
+ warn "Can't add payment. Invoice ##{Invoice.current.id} has no total"
12
+ return
13
+ end
14
+
15
+ # Test if payment would add more than the remaining amount
16
+ payment = Money.parse(opts[:add][0].shift, Invoice.current.currency)
17
+ if (Invoice.current.received_amount + payment > Invoice.current.total)
18
+ warn 'With this payment, the received amount surpasses its total.'
19
+ cropped = Invoice.current.total - Invoice.current.received_amount
20
+ if ask_value "Do you want to add the remaining payment of #{format_money(cropped)}"
21
+ payment = cropped
22
+ else
23
+ puts "Payment has NOT been added."
24
+ return
25
+ end
26
+ end
27
+ Invoice.current.add_payment(:cents => payment.cents, :note => opts[:add][0].shift)
28
+ puts "Added #{format_money(payment)} to current invoice"
29
+ elsif opts[:delete]
30
+ if e = Payment[opts[:delete]]
31
+ if confirm "Are you sure you want to delete Payment ##{e.id}"
32
+ e.destroy
33
+ puts "Payment has been removed."
34
+ else
35
+ puts "Payment has NOT been removed."
36
+ end
37
+ else
38
+ warn "Error: No Payment found for id ##{opts[:delete]}"
39
+ end
40
+ else
41
+ warn "Error: Invalid command"
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,46 @@
1
+ module BillTrap
2
+ module CLI
3
+ def set
4
+ # Grab subcommand
5
+ k = args.shift
6
+ case
7
+ when k == 'client'
8
+ id = args.shift
9
+ if e = Client.get(id)
10
+ Invoice.current.update(:client_id => e.id)
11
+ puts "SET client to #{e.name} (##{e.id})"
12
+ else
13
+ warn "Error: Can't find Client with id '#{id}'"
14
+ end
15
+ when k == 'date'
16
+ if d = args.shift
17
+ new_date = Date.parse d
18
+ else
19
+ new_date = Date.today
20
+ end
21
+ Invoice.current.update(:created => new_date)
22
+ puts "SET created date to #{format_date(new_date)}"
23
+ when k == 'name'
24
+ if n = args.shift
25
+ Invoice.current.update(:name => n)
26
+ puts "SET name to '#{n}'"
27
+ else
28
+ warn "Error: Missing required attributed for token 'name'"
29
+ end
30
+ when k == 'sent'
31
+ if d = args.shift
32
+ Invoice.current.update(:sent => Date.parse(d))
33
+ puts "SET invoice sent date to #{d}"
34
+ else
35
+ Invoice.current.update(:sent => nil)
36
+ puts "UNSET invoice sent date"
37
+ end
38
+ when k.respond_to?(:to_s)
39
+ Invoice.current.set_attr(k.to_str, args.shift)
40
+ puts "Setting attribute #{k}"
41
+ else
42
+ warn "Error: Missing / unrecognized TOKEN #{k}"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,87 @@
1
+ module BillTrap
2
+ module CLI
3
+ def show
4
+ opts = Trollop::options args do
5
+ opt :completed, "Show only completed (i.e., sent and paid) invoices", :short => '-c'
6
+ opt :detail, "Show details (including entries) of a particular invoice", :type => :string, :short => '-d'
7
+ end
8
+ if opts[:detail]
9
+ # Display details of invoice with id/name from args
10
+ if invoice = Invoice.get(opts[:detail])
11
+
12
+ puts "%-12s%s" % ["Invoice: ", "#{invoice.name || 'unnamed'} (##{invoice.id})"]
13
+ puts "%-12s%s" % ["Created on: ", format_date(invoice.created)]
14
+ if invoice.sent
15
+ puts "%-12s%s" % ["Sent on: ", format_date(invoice.sent)]
16
+ end
17
+
18
+ puts '-' * 22
19
+ if invoice.invoice_entries.size > 0
20
+ puts 'Invoice entries'
21
+ # Determine length of entry titles
22
+ width = invoice.invoice_entries.sort_by{|inv| inv.title.to_s.length }.last.title.to_s.length + 4
23
+ width = 12 if width < 12
24
+ puts " %-#{width}s%-12s%-12s%-12s%s" % ["Title", "Date", "Quantity", "Price", "Notes"]
25
+ invoice.invoice_entries.each do |e|
26
+ puts " %-#{width}s%-12s%-12s%-12s%s" % [
27
+ e.title,
28
+ e.date,
29
+ e.typed_amount,
30
+ format_money(e.total),
31
+ e.notes
32
+ ]
33
+ end
34
+ else
35
+ puts 'No InvoiceEntries'
36
+ end
37
+
38
+ if invoice.payments.size > 0
39
+ puts '-' * 22
40
+ puts 'Received payments'
41
+ puts " %12s %s" % ["Payment", "Notes"]
42
+ invoice.payments.each do |e|
43
+ puts " %12s %s" % [
44
+ format_money(e.amount),
45
+ e.note
46
+ ]
47
+ end
48
+ else
49
+ end
50
+
51
+ else
52
+ puts "No Invoice found for input '#{detail_id}'"
53
+ end
54
+ elsif opts[:completed]
55
+ puts 'Showing only completed invoices'
56
+ print_invoices Invoice.completed
57
+ else
58
+ puts 'Showing open invoices'
59
+ print_invoices Invoice.open
60
+ end
61
+ end
62
+
63
+ private
64
+ def print_invoices invoices
65
+ if invoices.empty?
66
+ warn 'No matching invoices found'
67
+ return
68
+ end
69
+
70
+ # Determine length of invoice names
71
+ width = invoices.sort_by{|inv| inv.name.to_s.length }.last.name.to_s.length + 4
72
+ width = 12 if width < 12
73
+ puts " %-6s%-#{width}s%-24s%-16s%s" % ["ID", "Name", "Client", "Created", "Payments / Total"]
74
+ invoices.each do |i|
75
+ active = (Invoice.current.id == i.id) ? '>>' : ''
76
+ puts " %-6s%-#{width}s%-24s%-16s%s" % [
77
+ "#{active}#{i.id}",
78
+ i.name || ' - ',
79
+ i.client ? i.client.name : ' - ',
80
+ i.created,
81
+ "#{format_money(i.received_amount)} / #{format_money(i.total)}"
82
+ ]
83
+ end
84
+ end
85
+
86
+ end
87
+ end