billtrap 0.0.2

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.
@@ -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