Module: BillTrap::CLI

Extended by:
CLI, Helpers
Included in:
CLI
Defined in:
lib/billtrap/cli.rb,
lib/billtrap/cmd/in.rb,
lib/billtrap/cmd/new.rb,
lib/billtrap/cmd/set.rb,
lib/billtrap/cmd/show.rb,
lib/billtrap/cmd/entry.rb,
lib/billtrap/cmd/usage.rb,
lib/billtrap/cmd/client.rb,
lib/billtrap/cmd/export.rb,
lib/billtrap/cmd/import.rb,
lib/billtrap/cmd/payment.rb,
lib/billtrap/cmd/configure.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Helpers

format_date, format_money, is_i?

Instance Attribute Details

#argsObject

Returns the value of attribute args.



6
7
8
# File 'lib/billtrap/cli.rb', line 6

def args
  @args
end

Instance Method Details

#clientObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/billtrap/cmd/client.rb', line 3

def client
  opts = Trollop::options(args) do
    opt :add, "Add a new ID, reading from STDIN", :short => '-a'
    opt :delete, "Delete the client by ID", :type => Integer, :short => '-d'
  end

  if opts[:add]
    firstname = ask_value "First name"
    surname = ask_value "Surname"
    company = ask_value "Company"
    address = ask_value "Address", true
    mail = ask_value "Mail"
    rate = ask_value "Hourly rate"
    currency = ask_value "Use non-standard Currency? [Leave empty for #{BillTrap::Config['currency']}]"

    currency = currency.empty? ? BillTrap::Config['currency'] : currency
    puts "'#{currency}'"

    client = Client.create(
      :firstname => firstname, 
      :surname => surname, 
      :company => company,
      :address => address,
      :mail => mail,
      :rate => Money.parse(rate, currency).cents,
      :currency => currency
    )
      puts "Client #{firstname} #{surname} was created with id #{client.id}"
  elsif id = opts[:delete]
    if e = Client.get(id)
      if confirm "Are you sure you want to delete Client #{e.name} (##{e.id})"
        begin
          e.destroy
          puts "Client has been removed."
        rescue Sequel::ForeignKeyConstraintViolation
          warn 'Error: Client is still in use. Refusing to delete client'
        end
      else
        puts "Client has NOT been removed."
      end
    else
      warn "Can't find Client with id '#{id}'"
    end
  end
end

#commandsObject



42
43
44
# File 'lib/billtrap/cli.rb', line 42

def commands
  BillTrap::CLI.usage.scan(/\* \w+/).map{|s| s.gsub(/\* /, '')}
end

#configureObject



3
4
5
6
# File 'lib/billtrap/cmd/configure.rb', line 3

def configure
  BillTrap::Config.configure!
  puts "Config file written to: #{BillTrap::Config::CONFIG_PATH.inspect}"
end

#entryObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/billtrap/cmd/entry.rb', line 3

def entry
  opts = Trollop::options args do
    opt :add, "Manually add entry to current invoice", :short => '-a'
    opt :delete, "Delete entry from current invoice by ID", :type => :int, :short => '-d'
  end
  current = Invoice.current
  if opts[:add]
    title = ask_value 'Entry title'
    date  = ask_value 'Entry date (YYYY-MM-DD)'
    unit  = ask_value "Displayed unit (Defaults to 'h' for hours)" || 'h'
    count = ask_value "Quantity (Numeric)"
    price = ask_value "Price in #{current.currency} per unit (Numeric)"
    notes = ask_value 'Optional Notes', true

    e = InvoiceEntry.create(
      :invoice_id => current.id,
      :title => title,
      :date => Date.parse(date),
      :unit => unit,
      :count => count,
      :notes => notes,
      :cents => Money.parse(price).cents
    ) 

    puts "Added entry (##{e.id}) to current invoice (ID #{current.id})"
  elsif name = opts[:name]
    Invoice.current.update(:name => name)
    puts "Set current invoice (##{Invoice.current.id}) name to: #{name}"
  elsif id = opts[:delete]
    if e = InvoiceEntry[id]
      if confirm "Are you sure you want to delete InvoiceEntry ##{e.id}"
        e.destroy
        puts "Entry has been removed."
      else
        puts "Entry has NOT been removed."
      end
    else
      warn "Can't find entry with id '#{id}'"
    end
  end
end

#exportObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/billtrap/cmd/export.rb', line 3

def export
  opts = Trollop::options args do
    opt :adapter, "Set adapter", :type => :string, :short => '-a'
  end
  adapter = opts[:adapter] || 'ooffice'
  begin
    # Replace invoice number placeholders
    arg = {
      # Unique, auto-incremented invoice id (from database)
      :invoice_id => Invoice.current.id,
      # Unique, auto-incremented client id
      :client_id => Invoice.current.client_id
    }

    # Replace above parameters, then strfime parameters
    invoice_number = Invoice.current.created.strftime(Config['invoice_number_format'].gsub(/%\{(.*?)\}/) { arg[$1.to_sym] })

    attributes = {
      :invoice => Invoice.current,
      :invoice_number => invoice_number,
    }

    BillTrap::Adapters.load_adapter(adapter).new(attributes).generate
  rescue LoadError
    warn "Couldn't load adapter named #{adapter}.rb"
  end

end

#importObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/billtrap/cmd/import.rb', line 3

def import
  opts = Trollop::options args do
    opt :clear, "Clear entries before import", :short => '-c'
    opt :entry, "Import entries by ID", :type => :strings, :multi => true, :short => '-e'
    opt :round, "Round imported entries", :short => '-r'
    opt :sheet, "Import sheet by name", :type => :string, :short => '-s'
  end
  
  # Clear entries if --clear given
  if opts[:clear]
    InvoiceEntry.where(:invoice_id => Invoice.current.id).destroy
  end

  entries = 
  if opts[:sheet]
    Entry.filter(:sheet => opts[:sheet]).all
  elsif opts[:entry_given]
    Entry.where(:id => opts[:entry].first).all
  else 
    []
  end

  unless entries.length > 0
    warn "No matching entries found."
    return
  end

  entries.each do |e|
    Entry.round = opts[:round]
    # Ignore entry if (rounded) is empty
    next if e.duration == 0
    imported = InvoiceEntry.create(
      :invoice_id => Invoice.current.id,
      :title => e.sheet,
      :date => e.start.to_date,
      :unit => 'h',
      :count => (e.duration.to_f / 3600).round(2),
      :notes => e.note,
      :cents => Invoice.current.rate.cents
    ) 
    puts "Imported #{imported.count} hours from sheet #{e.sheet} as entry ##{imported.id}"
  end
end

#inObject



3
4
5
6
7
8
9
10
11
12
13
14
# File 'lib/billtrap/cmd/in.rb', line 3

def in
  key = args.shift || raise('Error: No ID/Name given')
  invoice = Invoice.get key

  if invoice
    puts "Activating invoice ##{invoice.id}"
    # set current id
    Invoice.current = invoice.id
  else
    puts "No Invoice found for input '#{key}'"
  end
end

#invokeObject



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/billtrap/cli.rb', line 8

def invoke
  require 'cmd/usage'

  case args.first when '-h', '--help', '--usage', '-?', 'help', nil
    puts BillTrap::CLI.usage
    exit 0
  when '-v', '--version'
    puts "BillTrap version #{BillTrap::VERSION}"
    exit 0
  end

  # Grab global options, then stop
  flags = Trollop::options args do
    opt :debug
    stop_on_unknown
  end

  command = args.shift
  # Complete command
  available = commands.select{ |key| key.match(/^#{command}/) }
  if available.size == 1
    require "cmd/#{available[0]}"
    send available[0]
  elsif available.size > 1
    warn "Error: Ambiguous command '#{command}'"
    warn "Matching commands are: #{available.join(", ")}"
  else
    warn "Error: Invalid command #{command.inspect}"
  end
rescue StandardError, LoadError => e
  raise e if flags && flags[:debug]
  warn e.message
end

#newObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# File 'lib/billtrap/cmd/new.rb', line 3

def new
  opts = Trollop::options args do
    opt :client, "Optional Client ID", :type => :string, :short => '-c'
    opt :date, "Optional invoice date", :type => :string, :short => '-d'
    opt :name, "Optional invoice name", :type => :string, :short => '-n'
  end

  date =
    if opts[:date]
      Chronic.parse(opts[:date])
    else
      Date.today
    end


  invoice = Invoice.create(
    :name => opts[:name],
    :created => date,
    :client => Client.get(opts[:client])
  )
  # Make active
  Invoice.current = invoice.id
  puts "Created invoice ##{invoice.id}"
end

#paymentObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/billtrap/cmd/payment.rb', line 3

def payment
  opts = Trollop::options args do
    opt :add, "Add payment to current invoice", :type => :strings, :multi => true, :short => '-a'
    opt :delete, "Delete payment by ID from current invoice", :type => :int, :short => '-d'
  end
  if opts[:add_given] && opts[:add][0].length > 1
    # If the invoice has no total
    if Invoice.current.total.cents == 0
      warn "Can't add payment. Invoice ##{Invoice.current.id} has no total"
      return
    end

    # Test if payment would add more than the remaining amount
    payment = Money.parse(opts[:add][0].shift, Invoice.current.currency)
    if (Invoice.current.received_amount + payment > Invoice.current.total)
      warn 'With this payment, the received amount surpasses its total.'
      cropped = Invoice.current.total - Invoice.current.received_amount
      if ask_value "Do you want to add the remaining payment of #{format_money(cropped)}"
        payment = cropped
      else
        puts "Payment has NOT been added."
        return
      end
    end
    Invoice.current.add_payment(:cents => payment.cents, :note => opts[:add][0].shift)
    puts "Added #{format_money(payment)} to current invoice"
  elsif opts[:delete]
    if e = Payment[opts[:delete]]
      if confirm "Are you sure you want to delete Payment ##{e.id}"
        e.destroy
        puts "Payment has been removed."
      else
        puts "Payment has NOT been removed."
      end        
    else
      warn "Error: No Payment found for id ##{opts[:delete]}"
    end
  else
    warn "Error: Invalid command"
  end
end

#setObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/billtrap/cmd/set.rb', line 3

def set
  # Grab subcommand
  k = args.shift
  case
  when k == 'client'
    id = args.shift
    if e = Client.get(id)
      Invoice.current.update(:client_id => e.id)
      puts "SET client to #{e.name} (##{e.id})"
    else
      warn "Error: Can't find Client with id '#{id}'"
    end
  when k == 'date'
    if d = args.shift
      new_date = Date.parse d
    else
      new_date = Date.today
    end
    Invoice.current.update(:created => new_date)
    puts "SET created date to #{format_date(new_date)}"
  when k == 'name'
    if n = args.shift
      Invoice.current.update(:name => n)
      puts "SET name to '#{n}'"
    else
      warn "Error: Missing required attributed for token 'name'"
    end
  when k == 'sent'
    if d = args.shift
      Invoice.current.update(:sent => Date.parse(d))
      puts "SET invoice sent date to #{d}"
    else
      Invoice.current.update(:sent => nil)
      puts "UNSET invoice sent date"
    end
  when k.respond_to?(:to_s)
    Invoice.current.set_attr(k.to_str, args.shift)
    puts "Setting attribute #{k}"
  else
    warn "Error: Missing / unrecognized TOKEN #{k}"
  end
end

#showObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/billtrap/cmd/show.rb', line 3

def show
  opts = Trollop::options args do
    opt :completed, "Show only completed (i.e., sent and paid) invoices", :short => '-c'
    opt :detail, "Show details (including entries) of a particular invoice", :type => :string, :short => '-d'
  end
  if opts[:detail]
    # Display details of invoice with id/name from args
    if invoice = Invoice.get(opts[:detail])

      puts "%-12s%s" % ["Invoice: ", "#{invoice.name || 'unnamed'} (##{invoice.id})"]
      puts "%-12s%s" % ["Created on: ", format_date(invoice.created)]
      if invoice.sent
        puts "%-12s%s" % ["Sent on: ", format_date(invoice.sent)]
      end

      puts '-' * 22
      if invoice.invoice_entries.size > 0
        puts 'Invoice entries'
        # Determine length of entry titles
        width = invoice.invoice_entries.sort_by{|inv| inv.title.to_s.length }.last.title.to_s.length + 4
        width = 12 if width < 12
        puts "  %-#{width}s%-12s%-12s%-12s%s" % ["Title", "Date", "Quantity", "Price", "Notes"]
        invoice.invoice_entries.each do |e|
          puts "  %-#{width}s%-12s%-12s%-12s%s" % [
            e.title,
            e.date,
            e.typed_amount,
            format_money(e.total),
            e.notes
          ]
        end
      else
        puts 'No InvoiceEntries'
      end

      if invoice.payments.size > 0
        puts '-' * 22
        puts 'Received payments'
        puts "  %12s    %s" % ["Payment", "Notes"]
        invoice.payments.each do |e|
          puts "  %12s    %s" % [
            format_money(e.amount),
            e.note
          ]
        end
      else
      end

    else
      puts "No Invoice found for input '#{detail_id}'"
    end
  elsif opts[:completed]
    puts 'Showing only completed invoices'
    print_invoices Invoice.completed
  else
    puts 'Showing open invoices'
    print_invoices Invoice.open
  end
end

#usageObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/billtrap/cmd/usage.rb', line 3

def usage 
  <<-EOF

Billtrap - Manage invoices, import time slices from Timetrap

Usage: bt COMMAND [OPTIONS] [ARGS...]

COMMAND can be abbreviated. For example `bt edit --delete 1` and `bt e -d 1` are equivalent.

COMMAND is one of:

  * configure - Write out a YAML config file to HOME/.billtrap.yml.
usage: bt configure

  * client - Manage clients (adding, deleting and connecting to current invoice)
usage: bt client [--add] [--delete [ID]] [--set [ID]]
-a, --add         Manually add a client, reads from STDIN
-d, --delete      Delete a client. If no ID is given, prints all clients.
                  Note: Clients cannot be deleted if a non-archived invoiced is linked to it.

  * entry - Edit the active invoice, adding or deleting invoice entries manually 
usage: bt edit [--add] [--delete [ID]]
-a, --add         Manually add an invoice entry (timeslice or product), reads from STDIN
-d, --delete      Delete an invoice entry. If no ID is given, prints all entries and asks for an ID.

  * export - Export active invoice, using the default adapter (Serenity)
-a, --adapter     Override the default adapter

  * in - Switch to another invoice, making it active for edits
usage: bt in [ID | NAME]

  * import - Import data from Timetrap. Sets invoice title to the sheet name and description to notes
usage: bt import [--clear] [--sheet NAME] [--entry ID [ID ..]]
-c, --clear       Clears ALL invoice entries before import
-e, --entry       Import the given entries.
-s, --sheet       Import all entries from the given sheet. 

  * new - Create a new invoice, activating it for edits
usage: bt new [--name NAME] [--date DATE] [--client ID | NAME]
-c, --client      Tie the invoice to a client
-d, --date        Set to override invoice date (defaults to today)
-n, --name        Set invoice reference name

  * payment - add, remove payments to current id
usage: bt payment [--add AMOUNT ['NOTES']] [--delete ID]
-a, --add         Add a payment to current invoice
-d, --delete      Delete a payment by ID from current invoice

  * set - Set variables on the current invoice
usage: bt set TOKEN [VALUE]
Where TOKEN is one of
  client          Set a client by id or surname
  date            Set the `created on` date (YYYY-MM-DD), leave empty for today
  name            Set name of current invoice
  sent            Set the `sent on` date (YYYY-MM-DD), leave empty for today
  other           Add { 'other' => VALUE } to the invoice's custom attributes.
                  Use this to set attributes for populating templates

  * show - Display a list invoices. Shows pending invoices by default (open or unpaid)
usage: bt show [--details ID | NAME] [--completed]
-d, --detail      Show details (including entries) of a particular invoice
-c, --completed   Show only completed (i.e., sent and paid) invoices

  GLOBAL OPTIONS
  Use global options by prepending them before any command.
  --debug         Display stack traces for errors.
  usage: bt --debug COMMAND [ARGS]

  OTHER OPTIONS
  -h, --help      Display this help.

  EXAMPLES

  Please submit bugs and feature requests to http://github.com/oliverguenther/billtrap/issues
  EOF
end