paperless_to_xero 1.1.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/README.rdoc ADDED
@@ -0,0 +1,5 @@
1
+ = Paperless-to-Xero
2
+
3
+ A simple translator which takes a CSV file from Mariner's Paperless receipt/document management software and makes a Xero accounts payable invoice CSV, for import into Xero.
4
+
5
+ Formatting in Paperless is very important, so you probably want to wait until I've written the docs
data/Rakefile ADDED
@@ -0,0 +1,139 @@
1
+ require 'rake'
2
+ require 'rake/rdoctask'
3
+ gem 'rspec'
4
+ require 'spec/rake/spectask'
5
+ require 'lib/paperless_to_xero/version.rb'
6
+
7
+ desc 'Generate documentation for Paperless to Xero.'
8
+ Rake::RDocTask.new(:rdoc) do |rdoc|
9
+ rdoc.rdoc_dir = 'rdoc'
10
+ rdoc.title = 'Paperless to Xero'
11
+ rdoc.options << '--line-numbers' << '--inline-source'
12
+ rdoc.rdoc_files.include('README.rdoc')
13
+ rdoc.rdoc_files.include('lib/**/*.rb')
14
+ end
15
+
16
+ task :default => :spec
17
+
18
+ desc "Run all specs in spec directory (excluding plugin specs)"
19
+ Spec::Rake::SpecTask.new(:spec) do |t|
20
+ t.spec_opts = ['--options', "\"#{Rake.original_dir}/spec/spec.opts\""]
21
+ t.spec_files = FileList['spec/**/*_spec.rb']
22
+ end
23
+
24
+ namespace :spec do
25
+ desc "Run all specs in spec directory with RCov (excluding plugin specs)"
26
+ Spec::Rake::SpecTask.new(:rcov) do |t|
27
+ t.spec_opts = ['--options', "\"#{Rake.original_dir}/spec/spec.opts\""]
28
+ t.spec_files = FileList['spec/**/*_spec.rb']
29
+ t.rcov = true
30
+ t.rcov_opts = lambda do
31
+ IO.readlines("#{Rake.original_dir}/spec/rcov.opts").map {|l| l.chomp.split " "}.flatten
32
+ end
33
+ end
34
+
35
+ desc "Print Specdoc for all specs (excluding plugin specs)"
36
+ Spec::Rake::SpecTask.new(:doc) do |t|
37
+ t.spec_opts = ["--format", "specdoc", "--dry-run"]
38
+ t.spec_files = FileList['spec/**/*_spec.rb']
39
+ end
40
+ end
41
+
42
+ require "rubygems"
43
+ require "rake/gempackagetask"
44
+
45
+ # This builds the actual gem. For details of what all these options
46
+ # mean, and other ones you can add, check the documentation here:
47
+ #
48
+ # http://rubygems.org/read/chapter/20
49
+ #
50
+ spec = Gem::Specification.new do |s|
51
+
52
+ # Change these as appropriate
53
+ s.name = "paperless_to_xero"
54
+ s.version = PaperlessToXero::Version()
55
+ s.summary = "Convert Paperless CSV exports to Xero invoice import CSV"
56
+ s.description = File.read('README.rdoc')
57
+ s.author = "Matt Patterson"
58
+ s.email = "matt@reprocessed.org"
59
+ s.homepage = "http://reprocessed.org/"
60
+
61
+ s.has_rdoc = true
62
+ s.extra_rdoc_files = %w(README.rdoc)
63
+ s.rdoc_options = %w(--main README.rdoc)
64
+
65
+ # Add any extra files to include in the gem
66
+ s.files = %w(Rakefile README.rdoc) + Dir.glob("{bin,spec,lib}/**/*")
67
+ s.executables = FileList["bin/**"].map { |f| File.basename(f) }
68
+
69
+ s.require_paths = ["lib"]
70
+
71
+ # If you want to depend on other gems, add them here, along with any
72
+ # relevant versions
73
+
74
+ s.add_development_dependency("rspec") # add any other gems for testing/development
75
+
76
+ # If you want to publish automatically to rubyforge, you'll may need
77
+ # to tweak this, and the publishing task below too.
78
+ s.rubyforge_project = "paperless_to_xero"
79
+ end
80
+
81
+ # This task actually builds the gem. We also regenerate a static
82
+ # .gemspec file, which is useful if something (i.e. GitHub) will
83
+ # be automatically building a gem for this project. If you're not
84
+ # using GitHub, edit as appropriate.
85
+ Rake::GemPackageTask.new(spec) do |pkg|
86
+ pkg.gem_spec = spec
87
+
88
+ # Generate the gemspec file for github.
89
+ file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
90
+ File.open(file, "w") {|f| f << spec.to_ruby }
91
+ end
92
+
93
+ desc 'Clear out RDoc and generated packages'
94
+ task :clean => [:clobber_rdoc, :clobber_package] do
95
+ rm "#{spec.name}.gemspec"
96
+ end
97
+
98
+ # If you want to publish to RubyForge automatically, here's a simple
99
+ # task to help do that. If you don't, just get rid of this.
100
+ # Be sure to set up your Rubyforge account details with the Rubyforge
101
+ # gem; you'll need to run `rubyforge setup` and `rubyforge config` at
102
+ # the very least.
103
+ begin
104
+ require "rake/contrib/sshpublisher"
105
+ namespace :rubyforge do
106
+
107
+ desc "Release gem and RDoc documentation to RubyForge"
108
+ task :release => ["rubyforge:release:gem", "rubyforge:release:docs"]
109
+
110
+ namespace :release do
111
+ desc "Release a new version of this gem"
112
+ task :gem => [:package] do
113
+ require 'rubyforge'
114
+ rubyforge = RubyForge.new
115
+ rubyforge.configure
116
+ rubyforge.login
117
+ rubyforge.userconfig['release_notes'] = spec.summary
118
+ path_to_gem = File.join(File.dirname(__FILE__), "pkg", "#{spec.name}-#{spec.version}.gem")
119
+ puts "Publishing #{spec.name}-#{spec.version.to_s} to Rubyforge..."
120
+ rubyforge.add_release(spec.rubyforge_project, spec.name, spec.version.to_s, path_to_gem)
121
+ end
122
+
123
+ desc "Publish RDoc to RubyForge."
124
+ task :docs => [:rdoc] do
125
+ config = YAML.load(
126
+ File.read(File.expand_path('~/.rubyforge/user-config.yml'))
127
+ )
128
+
129
+ host = "#{config['username']}@rubyforge.org"
130
+ remote_dir = "/var/www/gforge-projects/coop_to_ofx/" # Should be the same as the rubyforge project name
131
+ local_dir = 'rdoc'
132
+
133
+ Rake::SshDirPublisher.new(host, remote_dir, local_dir).upload
134
+ end
135
+ end
136
+ end
137
+ rescue LoadError
138
+ puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
139
+ end
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.push(File.expand_path(File.dirname(__FILE__) + '/../lib'))
4
+ require 'optparse'
5
+ require 'paperless_to_xero'
6
+
7
+ OptionParser.new do |opts|
8
+ opts.banner = "Usage: paperless_to_xero [opts] /path/to/input.csv /path/to/output.csv"
9
+ opts.separator ""
10
+ opts.separator "Common options:"
11
+
12
+ opts.on_tail("-h", "--help", "Show this message") do
13
+ puts opts
14
+ exit
15
+ end
16
+
17
+ opts.on_tail("--version", "Show version") do
18
+ require 'paperless_to_xero/version'
19
+ puts PaperlessToXero::Version()
20
+ puts "Copyright (c) 2009, Matt Patterson. Released under the MIT license"
21
+ exit
22
+ end
23
+ end.parse!
24
+
25
+ input = ARGV[0]
26
+ output = ARGV[1]
27
+
28
+ converter = PaperlessToXero::Converter.new(input, output)
29
+ converter.convert!
30
+
@@ -0,0 +1,2 @@
1
+ require 'paperless_to_xero/invoice'
2
+ require 'paperless_to_xero/converter'
@@ -0,0 +1,135 @@
1
+ require 'csv'
2
+ require 'date'
3
+
4
+ module PaperlessToXero
5
+ class Converter
6
+ attr_reader :input_path, :output_path
7
+
8
+ def initialize(input_path, output_path)
9
+ @input_path, @output_path = input_path, output_path
10
+ end
11
+
12
+ def invoices
13
+ @invoices ||= []
14
+ end
15
+
16
+ def parse
17
+ input_csv = CSV.read(input_path)
18
+ # remove Paperless header row
19
+ input_csv.shift
20
+
21
+ input_csv.each do |row|
22
+ date, merchant, paperless_currency, amount, vat, category, payment_method, notes_field, description, reference, status, *extras = row
23
+ negative = amount.index('--') == 0
24
+ category = category[0..2] unless category.nil?
25
+ unless negative # negative stuff ought to be a credit note. not sure if that works...
26
+ # process amounts for commas added by Paperless
27
+ amount = amount.tr(',', '') unless amount.nil?
28
+ vat = vat.tr(',', '') unless vat.nil?
29
+ notes = extract_notes(notes_field)
30
+ total_vat = vat.nil? ? "0.00" : vat
31
+ invoice = PaperlessToXero::Invoice.new(extract_date(date), merchant, reference, amount, total_vat, inc_vat?(notes), extract_currency(notes))
32
+ if extras.empty?
33
+ invoice.add_item(description, amount, vat, category, extract_vat_note(vat, notes))
34
+ else
35
+ raise RangeError, "input CSV row is badly formatted" unless extras.size % 6 == 0
36
+ items = chunk_extras(extras)
37
+ items.each do |item|
38
+ description, paperless_currency, amount, unknown, category, notes_field = item
39
+ category = category[0..2]
40
+ notes = extract_notes(notes_field)
41
+ vat_amount = extract_vat_amount(notes)
42
+ vat_note = extract_vat_note(vat_amount, notes)
43
+ invoice.add_item(description, amount, vat_amount, category, vat_note)
44
+ end
45
+ end
46
+ invoices << invoice
47
+
48
+ # currency fudging
49
+ # actual_currency_match = notes.nil? ? nil : notes.match(/(\$|€|DKK|USD|EUR)/)
50
+ # actual_currency = actual_currency_match.nil? ? nil : actual_currency_match[1]
51
+ #
52
+ # description = description + " (#{actual_currency})" unless actual_currency.nil?
53
+ end
54
+ end
55
+ end
56
+
57
+ def convert!
58
+ # grab the input
59
+ parse
60
+ # open the output CSV
61
+ CSV.open(output_path, 'w') do |writer|
62
+ # Xero header row
63
+ writer << ['ContactName','InvoiceNumber','InvoiceDate','DueDate','SubTotal',
64
+ 'TotalTax','Total','Description','Quantity','UnitAmount','AccountCode','TaxType','TaxAmount',
65
+ 'TrackingName1','TrackingOption1','TrackingName2','TrackingOption2']
66
+
67
+ # body rows
68
+ invoices.each do |invoice|
69
+ invoice.serialise_to_csv(writer)
70
+ end
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def inc_vat?(notes)
77
+ notes.each do |item|
78
+ return false if item.match(/^Ex[ -]?VAT$/i)
79
+ end
80
+ true
81
+ end
82
+
83
+ def extract_date(date_string)
84
+ ds, day, month, year = date_string.match(/([0-9]{2})\/([0-9]{2})\/([0-9]{4})/).to_a
85
+ Date.parse("#{year}-#{month}-#{day}")
86
+ end
87
+
88
+ def chunk_extras(extras)
89
+ duped_extras = extras.dup
90
+ (1..(extras.size / 6)).inject([]) do |chunked, i|
91
+ chunked << duped_extras.slice!(0..5)
92
+ end
93
+ end
94
+
95
+ def extract_notes(notes_field)
96
+ notes = notes_field.nil? ? [] : notes_field.split(';')
97
+ notes.collect { |item| item.strip }
98
+ end
99
+
100
+ def extract_currency(notes)
101
+ notes.each do |item|
102
+ return item if item.match(/^[A-Z]{3}$/)
103
+ case item
104
+ when "€"
105
+ return "EUR"
106
+ when "$"
107
+ return "USD"
108
+ end
109
+ end
110
+ "GBP"
111
+ end
112
+
113
+ def extract_vat_amount(notes)
114
+ notes.each do |item|
115
+ return item if item.match(/^[0-9]+\.[0-9]{1,2}$/)
116
+ end
117
+ nil
118
+ end
119
+
120
+ def extract_vat_note(vat_amount, notes)
121
+ notes.each do |item|
122
+ return item if item.match(/^VAT/)
123
+ end
124
+
125
+ case vat_amount
126
+ when "0.00"
127
+ 'VAT - 0%'
128
+ when nil
129
+ 'No VAT'
130
+ else
131
+ 'VAT - 15%'
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,25 @@
1
+ module PaperlessToXero
2
+ module DecimalHelpers
3
+ def amounts_when_vat_inclusive(decimal_inc_vat_amount, decimal_vat_amount)
4
+ vat_inclusive_amount = formatted_decimal(decimal_inc_vat_amount)
5
+ vat_amount = formatted_decimal(decimal_vat_amount)
6
+ decimal_ex_vat_amount = decimal_inc_vat_amount - decimal_vat_amount
7
+ vat_exclusive_amount = formatted_decimal(decimal_ex_vat_amount)
8
+ [vat_exclusive_amount, vat_amount, vat_inclusive_amount]
9
+ end
10
+
11
+ def amounts_when_vat_exclusive(decimal_ex_vat_amount, decimal_vat_amount)
12
+ vat_exclusive_amount = formatted_decimal(decimal_ex_vat_amount)
13
+ vat_amount = formatted_decimal(decimal_vat_amount)
14
+ decimal_inc_vat_amount = decimal_ex_vat_amount + decimal_vat_amount
15
+ vat_inclusive_amount = formatted_decimal(decimal_inc_vat_amount)
16
+ [vat_exclusive_amount, vat_amount, vat_inclusive_amount]
17
+ end
18
+
19
+ def formatted_decimal(value)
20
+ value = value.to_s('F')
21
+ value = value + '0' unless value.index('.') < value.size - 2
22
+ value
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,50 @@
1
+ require 'bigdecimal'
2
+ require 'paperless_to_xero/decimal_helpers'
3
+ require 'paperless_to_xero/invoice_item'
4
+
5
+ module PaperlessToXero
6
+ class Invoice
7
+ include PaperlessToXero::DecimalHelpers
8
+
9
+ attr_reader :date, :merchant, :reference_id, :currency, :total, :ex_vat_total, :inc_vat_total, :vat_total
10
+
11
+ def initialize(date, merchant, reference_id, total, vat, vat_inclusive = true, currency = 'GBP')
12
+ @date, @merchant, @reference_id = date, merchant, reference_id
13
+ @total, @vat_inclusive, @currency = total, vat_inclusive, currency
14
+ decimal_total = BigDecimal.new(total)
15
+ decimal_vat = BigDecimal.new(vat)
16
+ @ex_vat_total, @vat_total, @inc_vat_total = amounts_when_vat_inclusive(decimal_total, decimal_vat)
17
+ end
18
+
19
+ def items
20
+ @items ||= []
21
+ end
22
+
23
+ def vat_inclusive?
24
+ @vat_inclusive
25
+ end
26
+
27
+ def add_item(description, amount, vat_amount, category, vat_note)
28
+ items << PaperlessToXero::InvoiceItem.new(description, amount, vat_amount, category, vat_note, @vat_inclusive)
29
+ end
30
+
31
+ def serialise_to_csv(csv)
32
+ serialising_items = items.dup
33
+ first_item = serialising_items.shift
34
+
35
+ marked_merchant = currency != 'GBP' ? merchant + " (#{currency})" : merchant
36
+ unless first_item.nil?
37
+ csv << [marked_merchant, reference_id, date.strftime('%d/%m/%Y'), date.strftime('%d/%m/%Y'),
38
+ ex_vat_total, vat_total, inc_vat_total, first_item.description, '1',
39
+ first_item.vat_exclusive_amount, first_item.category, first_item.vat_type, first_item.vat_amount,
40
+ nil, nil, nil, nil]
41
+ end
42
+ serialising_items.each do |item|
43
+ csv << [nil, reference_id, nil, nil,
44
+ nil, nil, nil, item.description, '1',
45
+ item.vat_exclusive_amount, item.category, item.vat_type, item.vat_amount,
46
+ nil, nil, nil, nil]
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,88 @@
1
+ require 'bigdecimal'
2
+ require 'bigdecimal/util'
3
+ require 'paperless_to_xero/decimal_helpers'
4
+
5
+ module PaperlessToXero
6
+ class InvoiceItem
7
+ include PaperlessToXero::DecimalHelpers
8
+
9
+ class << self
10
+ def fetch_vat_rate(vat_type)
11
+ @vat_rates ||= {
12
+ '5.5% (France, VAT on expenses)' => 1.055.to_d,
13
+ '19.6% (France, VAT on expenses)' => 1.196.to_d,
14
+ '7% (Germany, VAT on expenses)' => 1.07.to_d,
15
+ '19% (Germany, VAT on expenses)' => 1.19.to_d,
16
+ '25% (Denmark, VAT on expenses)' => 1.25.to_d,
17
+ '21.5% (Ireland, VAT on expenses)' => 1.215.to_d,
18
+ '15% (EU VAT ID)' => 1.15.to_d,
19
+ '15% (VAT on expenses)' => 1.15.to_d,
20
+ 'Zero Rated Expenses' => 0,
21
+ '15% (Luxembourg, VAT on expenses)' => 1.15.to_d
22
+ }
23
+ @vat_rates[vat_type]
24
+ end
25
+ end
26
+
27
+ attr_reader :description, :amount, :category, :vat_inclusive, :vat_inclusive_amount, :vat_exclusive_amount, :vat_amount, :vat_type
28
+
29
+ def initialize(description, amount, vat_amount, category, vat_note = 'VAT - 15%', vat_inclusive = true)
30
+ @amount, @vat_amount, @description, @category, @vat_inclusive = amount, vat_amount, description, category, vat_inclusive
31
+ @vat_type = extract_vat_type(vat_note)
32
+
33
+ vat_rate = fetch_vat_rate(vat_type)
34
+ case vat_rate
35
+ when BigDecimal
36
+ decimal_amount = BigDecimal.new(@amount)
37
+ decimal_vat_amount = @vat_amount.nil? ? nil : BigDecimal.new(@vat_amount)
38
+ amounts_method = vat_inclusive ? :amounts_when_vat_inclusive : :amounts_when_vat_exclusive
39
+ @vat_exclusive_amount, @vat_amount, @vat_inclusive_amount = self.send(amounts_method, decimal_amount, decimal_vat_amount)
40
+ else
41
+ amounts_when_zero_rated_or_non_vat(vat_rate)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def amounts_when_zero_rated_or_non_vat(vat_rate)
48
+ @vat_inclusive_amount = @amount
49
+ @vat_exclusive_amount = @amount
50
+ @vat_amount = vat_rate.nil? ? nil : "0.00"
51
+ end
52
+
53
+ def fetch_vat_rate(vat_type)
54
+ self.class.fetch_vat_rate(vat_type)
55
+ end
56
+
57
+ def extract_vat_type(vat_note)
58
+ case vat_note
59
+ when 'VAT - France - 5.5%'
60
+ '5.5% (France, VAT on expenses)'
61
+ when /Fr/
62
+ '19.6% (France, VAT on expenses)'
63
+ when 'VAT - Germany - 7%'
64
+ '7% (Germany, VAT on expenses)'
65
+ when /Germany/
66
+ '19% (Germany, VAT on expenses)'
67
+ when /Den/
68
+ '25% (Denmark, VAT on expenses)'
69
+ when /Irel/
70
+ '21.5% (Ireland, VAT on expenses)'
71
+ when /Sweden/
72
+ '25% (Sweden, VAT on expenses)'
73
+ when /Lux/
74
+ '15% (Luxembourg, VAT on expenses)'
75
+ when /VAT - EU/
76
+ '15% (EU VAT ID)'
77
+ when 'VAT - 15%'
78
+ '15% (VAT on expenses)'
79
+ when 'VAT - 0%'
80
+ 'Zero Rated Expenses'
81
+ when 'No VAT'
82
+ 'No VAT'
83
+ when 'VAT'
84
+ '15% (VAT on expenses)'
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,12 @@
1
+ module PaperlessToXero
2
+ def self.Version
3
+ PaperlessToXero::Version::FULL
4
+ end
5
+
6
+ module Version
7
+ MAJOR = 1
8
+ MINOR = 1
9
+ POINT = 1
10
+ FULL = [PaperlessToXero::Version::MAJOR, PaperlessToXero::Version::MINOR, PaperlessToXero::Version::POINT].join('.')
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ "Date","Merchant","Currency","Amount","Tax","Category","Payment Method","Notes","Description","Reference #","Status"
2
+ 22/06/2009,"Geberer","£","2.80","0.33","494 - Travel International","Cash","€ ","Food & Coffee","2009-06-22-02",,"Coffee","£","1.50",,"494 - Travel International","0.24; VAT - Germany - 19%","Food","£","1.30",,"494 - Travel International","0.09; VAT - Germany - 7%"
3
+ 29/05/2009,"FDIH","£","250.00","50.00","480 - Staff Training","Credit Card","€; VAT - Denmark - 25%","Reboot 11 ticket","2009-05-29-02",
4
+ 18/05/2009,"Apple Store, Regent Street","£","617.95","80.60",,"Debit Card",,"Mac Mini, iWork, VMWare Fusion","2009-05-18-09",,"Mac Mini","£","499.00",,"720 - Computer Equipment","65.09; VAT - 15%","iWork 09","£","70.00",,"463 - IT Software and Consumables","9.13; VAT - 15%","VMWare Fusion","£","48.95",,"463 - IT Software and Consumables","6.38; VAT - 15%"
5
+ 18/05/2009,"Apple Store, Regent Street","£","14.95","1.95","429 - General Expenses","Debit Card",,"Phone case","2009-05-18-05",
6
+ 23/04/2009,"Rexel Senate","£","81.42","10.62","325 - Direct Expenses",,,"Cat 6 modules","2009-04-23-04",
7
+ 18/04/2009,"Apple Store, Regent Street","£","617.95","80.60",,"Debit Card","Ex VAT","Mac Mini, iWork, VMWare Fusion","2009-04-18-09",,"Mac Mini","£","433.91",,"720 - Computer Equipment","65.09; VAT - 15%","iWork 09","£","60.87",,"463 - IT Software and Consumables","9.13; VAT - 15%","VMWare Fusion","£","42.57",,"463 - IT Software and Consumables","6.38; VAT - 15%"
@@ -0,0 +1,12 @@
1
+ ContactName,InvoiceNumber,InvoiceDate,DueDate,SubTotal,TotalTax,Total,Description,Quantity,UnitAmount,AccountCode,TaxType,TaxAmount,TrackingName1,TrackingOption1,TrackingName2,TrackingOption2
2
+ Geberer (EUR),2009-06-22-02,22/06/2009,22/06/2009,2.47,0.33,2.80,Coffee,1,1.26,494,"19% (Germany, VAT on expenses)",0.24,,,,
3
+ ,2009-06-22-02,,,,,,Food,1,1.21,494,"7% (Germany, VAT on expenses)",0.09,,,,
4
+ FDIH (EUR),2009-05-29-02,29/05/2009,29/05/2009,200.00,50.00,250.00,Reboot 11 ticket,1,200.00,480,"25% (Denmark, VAT on expenses)",50.00,,,,
5
+ "Apple Store, Regent Street",2009-05-18-09,18/05/2009,18/05/2009,537.35,80.60,617.95,Mac Mini,1,433.91,720,15% (VAT on expenses),65.09,,,,
6
+ ,2009-05-18-09,,,,,,iWork 09,1,60.87,463,15% (VAT on expenses),9.13,,,,
7
+ ,2009-05-18-09,,,,,,VMWare Fusion,1,42.57,463,15% (VAT on expenses),6.38,,,,
8
+ "Apple Store, Regent Street",2009-05-18-05,18/05/2009,18/05/2009,13.00,1.95,14.95,Phone case,1,13.00,429,15% (VAT on expenses),1.95,,,,
9
+ Rexel Senate,2009-04-23-04,23/04/2009,23/04/2009,70.80,10.62,81.42,Cat 6 modules,1,70.80,325,15% (VAT on expenses),10.62,,,,
10
+ "Apple Store, Regent Street",2009-04-18-09,18/04/2009,18/04/2009,537.35,80.60,617.95,Mac Mini,1,433.91,720,15% (VAT on expenses),65.09,,,,
11
+ ,2009-04-18-09,,,,,,iWork 09,1,60.87,463,15% (VAT on expenses),9.13,,,,
12
+ ,2009-04-18-09,,,,,,VMWare Fusion,1,42.57,463,15% (VAT on expenses),6.38,,,,
@@ -0,0 +1,2 @@
1
+ "Date","Merchant","Currency","Amount","Tax","Category","Payment Method","Notes","Description","Reference #","Status"
2
+ 18/05/2009,"Apple Store, Regent Street","£","617.95","80.60",,"Debit Card","Ex VAT","Mac Mini, iWork, VMWare Fusion","2009-05-18-09",,"Mac Mini","£","433.91",,"720 - Computer Equipment","65.09; VAT - 15%","iWork 09","£","60.87",,"463 - IT Software and Consumables","9.13; VAT - 15%","VMWare Fusion","£","42.57",,"463 - IT Software and Consumables","6.38; VAT - 15%"
@@ -0,0 +1,2 @@
1
+ "Date","Merchant","Currency","Amount","Tax","Category","Payment Method","Notes","Description","Reference #","Status"
2
+ 22/06/2009,"Geberer","£","2.80","0.33","494 - Travel International","Cash","€ ","Food & Coffee","2009-06-22-02",,"Coffee","£","1.50",,"494 - Travel International","0.24; VAT - Germany - 19%","Food","£","1.30",,"494 - Travel International","0.09; VAT - Germany - 7%"
@@ -0,0 +1,2 @@
1
+ "Date","Merchant","Currency","Amount","Tax","Category","Payment Method","Notes","Description","Reference #","Status"
2
+ 18/05/2009,"Apple Store, Regent Street","£","617.95","80.60",,"Debit Card",,"Mac Mini, iWork, VMWare Fusion","2009-05-18-09",,"Mac Mini","£","499.00",,"720 - Computer Equipment","65.09; VAT - 15%","iWork 09","£","70.00",,"463 - IT Software and Consumables","9.13; VAT - 15%","VMWare Fusion","£","48.95",,"463 - IT Software and Consumables","6.38; VAT - 15%"
@@ -0,0 +1,2 @@
1
+ "Date","Merchant","Currency","Amount","Tax","Category","Payment Method","Notes","Description","Reference #","Status"
2
+ 31/05/2009,"Bertrams Hotel Guldsmeden","£","2,235.00","447.00","494 - Travel International","Debit Card","VAT - Denmark - 25%","Reboot hotel booking","2009-05-31-02",
@@ -0,0 +1,2 @@
1
+ "Date","Merchant","Currency","Amount","Tax","Category","Payment Method","Notes","Description","Reference #","Status"
2
+ 18/05/2009,"Apple Store, Regent Street","£","14.95","1.95","429 - General Expenses","Debit Card",,"Phone case","2009-05-18-05",
@@ -0,0 +1,2 @@
1
+ "Date","Merchant","Currency","Amount","Tax","Category","Payment Method","Notes","Description","Reference #","Status"
2
+ 24/06/2009,"Halvandet","£","73.00","14.60","494 - Travel International","Credit Card","DKK; VAT - Denmark - 25%","Food & drink","2009-06-24-03",
@@ -0,0 +1,2 @@
1
+ "Date","Merchant","Currency","Amount","Tax","Category","Payment Method","Notes","Description","Reference #","Status"
2
+ 29/05/2009,"FDIH","£","250.00","50.00","480 - Staff Training","Credit Card","€; VAT - Denmark - 25%","Reboot 11 ticket","2009-05-29-02",
@@ -0,0 +1,2 @@
1
+ "Date","Merchant","Currency","Amount","Tax","Category","Payment Method","Notes","Description","Reference #","Status"
2
+ 26/01/2009,"Fastlane Cabs","£","4.50",,"493 - Travel National","Cash",,"Taxi in Nottingham","2009-01-26-03",
@@ -0,0 +1,2 @@
1
+ "Date","Merchant","Currency","Amount","Tax","Category","Payment Method","Notes","Description","Reference #","Status"
2
+ 10/03/2009,"Transport For London","£","20.00","0.00","493 - Travel National","Credit Card",,"Oyster card auto top-up","2009-03-10-06",
@@ -0,0 +1,218 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+ require 'tempfile'
3
+
4
+ Spec::Matchers.define :have_detail_matching do |key, value|
5
+ match do |object|
6
+ object.send(key) == value
7
+ end
8
+ failure_message_for_should do |object|
9
+ "Expected <#{object.class.name}>.#{key} to match '#{value}'. Instead, it was '#{object.send(key)}'"
10
+ end
11
+ failure_message_for_should_not do |object|
12
+ "Expected <#{object.class.name}>.#{key} NOT to match '#{value}'"
13
+ end
14
+ description do
15
+ "have detail #{key.inspect} matching '#{value}'"
16
+ end
17
+ end
18
+
19
+ describe PaperlessToXero::Converter do
20
+ before(:each) do
21
+ @converter = PaperlessToXero::Converter.new('/input/path', '/output/path')
22
+ end
23
+
24
+ def fixture_path(name)
25
+ File.expand_path(File.dirname(__FILE__) + "/../fixtures/#{name}.csv")
26
+ end
27
+
28
+ def verify_invoice_details(details)
29
+ invoice = @converter.invoices.first
30
+ invoice_details = details[:invoice]
31
+
32
+ invoice_details.each do |key, value|
33
+ invoice.should have_detail_matching(key, value)
34
+ end
35
+
36
+ invoice.should be_vat_inclusive if details[:vat_inclusive]
37
+ invoice.should_not be_vat_inclusive if details[:vat_exclusive]
38
+
39
+ line_items_details = {:description => 'Phone case', :category => '429', :vat_inclusive_amount => '14.95', :vat_exclusive_amount => '13.00', :vat_amount => '1.95', :vat_type => '15% (VAT on expenses)'}
40
+ line_items = invoice.items.dup
41
+ if details[:line_items]
42
+ details[:line_items].each do |line_item_details|
43
+ line_item = line_items.shift
44
+ line_item_details.each do |key, value|
45
+ line_item.should have_detail_matching(key, value)
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ describe "single item inputs" do
52
+ it "should able to create an invoice for a basic single-item invoice" do
53
+ @converter.stubs(:input_path).returns(fixture_path('single-basic'))
54
+ @converter.parse
55
+
56
+ verify_invoice_details(
57
+ :invoice => {:date => Date.parse('2009-05-18'), :merchant => 'Apple Store, Regent Street',
58
+ :reference_id => '2009-05-18-05', :inc_vat_total => '14.95', :vat_total => '1.95',
59
+ :ex_vat_total => '13.00', :currency => 'GBP'},
60
+ :vat_inclusive => true,
61
+ :line_items => [
62
+ {:description => 'Phone case', :category => '429', :vat_type => '15% (VAT on expenses)',
63
+ :vat_inclusive_amount => '14.95', :vat_exclusive_amount => '13.00', :vat_amount => '1.95'}
64
+ ]
65
+ )
66
+ end
67
+
68
+ it "should able to create an invoice for a single-item invoice with an amount over 1,000 (Paperless likes to add the commas)" do
69
+ @converter.stubs(:input_path).returns(fixture_path('single-1000'))
70
+ @converter.parse
71
+ verify_invoice_details(
72
+ :invoice => {:inc_vat_total => '2235.00'},
73
+ :line_items => [
74
+ {:vat_inclusive_amount => '2235.00'}
75
+ ]
76
+ )
77
+ end
78
+
79
+ it "should able to create an invoice for a zero-rated single-item invoice" do
80
+ @converter.stubs(:input_path).returns(fixture_path('single-zero_rated'))
81
+ @converter.parse
82
+
83
+ verify_invoice_details(
84
+ :invoice => {:merchant => 'Transport For London', :inc_vat_total => '20.00', :vat_total => '0.00',
85
+ :ex_vat_total => '20.00'},
86
+ :vat_inclusive => true,
87
+ :line_items => [
88
+ {:vat_type => 'Zero Rated Expenses', :vat_inclusive_amount => '20.00', :vat_exclusive_amount => '20.00',
89
+ :vat_amount => '0.00'}
90
+ ]
91
+ )
92
+ end
93
+
94
+ it "should able to create an invoice for a foreign currency single-item invoice" do
95
+ @converter.stubs(:input_path).returns(fixture_path('single-foreign'))
96
+ @converter.parse
97
+
98
+ verify_invoice_details(
99
+ :invoice => {:currency => 'EUR', :inc_vat_total => '250.00', :vat_total => '50.00',
100
+ :ex_vat_total => '200.00'},
101
+ :vat_inclusive => true,
102
+ :line_items => [
103
+ {:vat_type => '25% (Denmark, VAT on expenses)', :vat_inclusive_amount => '250.00', :vat_exclusive_amount => '200.00',
104
+ :vat_amount => '50.00'}
105
+ ]
106
+ )
107
+ end
108
+
109
+ it "should able to create an invoice for a foreign currency (not € or $) single-item invoice" do
110
+ @converter.stubs(:input_path).returns(fixture_path('single-dkk'))
111
+ @converter.parse
112
+
113
+ verify_invoice_details(
114
+ :invoice => {:merchant => 'Halvandet', :currency => 'DKK'},
115
+ :vat_inclusive => true,
116
+ :line_items => [
117
+ {:vat_type => '25% (Denmark, VAT on expenses)', :vat_inclusive_amount => '73.00', :vat_exclusive_amount => '58.40',
118
+ :vat_amount => '14.60'}
119
+ ]
120
+ )
121
+ end
122
+
123
+ it "should cope with a single item invoice with no VAT" do
124
+ @converter.stubs(:input_path).returns(fixture_path('single-no-vat'))
125
+ @converter.parse
126
+
127
+ verify_invoice_details(
128
+ :invoice => {:inc_vat_total => '4.50', :vat_total => '0.00', :ex_vat_total => '4.50'},
129
+ :vat_inclusive => true,
130
+ :line_items => [
131
+ {:vat_type => 'No VAT', :vat_inclusive_amount => '4.50', :vat_exclusive_amount => '4.50',
132
+ :vat_amount => nil}
133
+ ]
134
+ )
135
+ end
136
+ end
137
+
138
+ describe "multi-item inputs" do
139
+ it "should able to create an Invoice for a basic multi-item invoice" do
140
+ @converter.stubs(:input_path).returns(fixture_path('multi-item'))
141
+ @converter.parse
142
+
143
+ verify_invoice_details(
144
+ :invoice => {:date => Date.parse('2009-05-18'), :merchant => 'Apple Store, Regent Street',
145
+ :reference_id => '2009-05-18-09', :inc_vat_total => '617.95', :vat_total => '80.60',
146
+ :ex_vat_total => '537.35', :currency => 'GBP'},
147
+ :vat_inclusive => true,
148
+ :line_items => [
149
+ {:description => 'Mac Mini', :category => '720', :vat_type => '15% (VAT on expenses)',
150
+ :vat_inclusive_amount => '499.00', :vat_exclusive_amount => '433.91', :vat_amount => '65.09'},
151
+ {:description => 'iWork 09', :category => '463', :vat_type => '15% (VAT on expenses)',
152
+ :vat_inclusive_amount => '70.00', :vat_exclusive_amount => '60.87', :vat_amount => '9.13'},
153
+ {:description => 'VMWare Fusion', :category => '463', :vat_type => '15% (VAT on expenses)',
154
+ :vat_inclusive_amount => '48.95', :vat_exclusive_amount => '42.57', :vat_amount => '6.38'}
155
+ ]
156
+ )
157
+ end
158
+
159
+ it "should able to create an Invoice for a foreign currency multi-item invoice" do
160
+ @converter.stubs(:input_path).returns(fixture_path('multi-foreign'))
161
+ @converter.parse
162
+
163
+ verify_invoice_details(
164
+ :invoice => {:currency => 'EUR', :inc_vat_total => '2.80', :vat_total => '0.33',
165
+ :ex_vat_total => '2.47'},
166
+ :vat_inclusive => true,
167
+ :line_items => [
168
+ {:description => 'Coffee', :vat_type => '19% (Germany, VAT on expenses)',
169
+ :vat_inclusive_amount => '1.50', :vat_amount => '0.24'},
170
+ {:description => 'Food', :vat_type => '7% (Germany, VAT on expenses)',
171
+ :vat_inclusive_amount => '1.30', :vat_amount => '0.09'}
172
+ ]
173
+ )
174
+ end
175
+
176
+ it "should cope with a VAT-exclusive invoice" do
177
+ @converter.stubs(:input_path).returns(fixture_path('multi-ex-vat'))
178
+ @converter.parse
179
+
180
+ verify_invoice_details(
181
+ :invoice => {:date => Date.parse('2009-05-18'), :merchant => 'Apple Store, Regent Street',
182
+ :reference_id => '2009-05-18-09', :inc_vat_total => '617.95', :vat_total => '80.60',
183
+ :ex_vat_total => '537.35', :currency => 'GBP'},
184
+ :vat_inclusive => false,
185
+ :line_items => [
186
+ {:description => 'Mac Mini', :category => '720', :vat_type => '15% (VAT on expenses)',
187
+ :vat_inclusive_amount => '499.00', :vat_exclusive_amount => '433.91', :vat_amount => '65.09'},
188
+ {:description => 'iWork 09', :category => '463', :vat_type => '15% (VAT on expenses)',
189
+ :vat_inclusive_amount => '70.00', :vat_exclusive_amount => '60.87', :vat_amount => '9.13'},
190
+ {:description => 'VMWare Fusion', :category => '463', :vat_type => '15% (VAT on expenses)',
191
+ :vat_inclusive_amount => '48.95', :vat_exclusive_amount => '42.57', :vat_amount => '6.38'}
192
+ ]
193
+ )
194
+ end
195
+ end
196
+
197
+ describe "end-to-end" do
198
+ before(:each) do
199
+ @tempfile_path = Tempfile.new(['output', 'csv']).path
200
+ end
201
+
202
+ it "should produce exactly the output we expect" do
203
+ converter = PaperlessToXero::Converter.new(fixture_path('end_to_end-input'), @tempfile_path)
204
+ converter.convert!
205
+
206
+ expected = File.readlines(fixture_path('end_to_end-output'))
207
+ actual = File.readlines(@tempfile_path)
208
+
209
+ (0..expected.size).each do |i|
210
+ actual[i].should == expected[i]
211
+ end
212
+ end
213
+
214
+ after(:each) do
215
+ File.unlink(@tempfile_path)
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,162 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe PaperlessToXero::InvoiceItem do
4
+ describe "the creation basics" do
5
+ it "should be able to be instantiated" do
6
+ # amount, vat, category, payment_method, notes, description, reference, status
7
+ PaperlessToXero::InvoiceItem.new('description', '34.50', '4.50', '123 - Some stuff', 'VAT - 15%', true).
8
+ should be_instance_of(PaperlessToXero::InvoiceItem)
9
+ end
10
+ end
11
+
12
+ describe "instances" do
13
+ before(:each) do
14
+ @item = PaperlessToXero::InvoiceItem.new('description', '34.50', '4.50', '123 - Some stuff', 'VAT - 15%', true)
15
+ end
16
+
17
+ it "should be able to report their description" do
18
+ @item.description.should == 'description'
19
+ end
20
+
21
+ it "should be able to report their amount" do
22
+ @item.amount.should == "34.50"
23
+ end
24
+
25
+ it "should be able to report their VAT amount" do
26
+ @item.vat_amount.should == "4.50"
27
+ end
28
+
29
+ it "should be able to report their VAT rate" do
30
+ @item.vat_type.should == '15% (VAT on expenses)'
31
+ end
32
+
33
+ it "should be able to report whether the amount is VAT inclusive" do
34
+ @item.vat_inclusive.should be_true
35
+ end
36
+
37
+ it "should be able to report whether their category" do
38
+ @item.category.should == "123 - Some stuff"
39
+ end
40
+
41
+ describe "where items are VAT inclusive" do
42
+ it "should be able to report the amount of VAT" do
43
+ @item.vat_amount.should == "4.50"
44
+ end
45
+
46
+ it "should be able to report the VAT inclusive amount" do
47
+ @item.vat_inclusive_amount.should == "34.50"
48
+ end
49
+
50
+ it "should be able to report the VAT exclusive amount" do
51
+ @item.vat_exclusive_amount.should == "30.00"
52
+ end
53
+ end
54
+
55
+ describe "where items are VAT exclusive" do
56
+ before(:each) do
57
+ @item = PaperlessToXero::InvoiceItem.new('description', '30.00', '4.50', '123 - Some stuff', 'VAT - 15%', false)
58
+ end
59
+
60
+ it "should be able to report the amount of VAT" do
61
+ @item.vat_amount.should == "4.50"
62
+ end
63
+
64
+ it "should be able to report the VAT inclusive amount" do
65
+ @item.vat_inclusive_amount.should == "34.50"
66
+ end
67
+
68
+ it "should be able to report the VAT exclusive amount" do
69
+ @item.vat_exclusive_amount.should == "30.00"
70
+ end
71
+ end
72
+
73
+ describe "where items are zero-rated for VAT" do
74
+ describe "and £0.00 VAT is reported for them" do
75
+ before(:each) do
76
+ @item = PaperlessToXero::InvoiceItem.new('description', '30.00', '0.00', '123 - Some stuff', 'VAT - 0%', false)
77
+ end
78
+
79
+ it "should be able to report the amount of VAT" do
80
+ @item.vat_amount.should == "0.00"
81
+ end
82
+
83
+ it "should be able to report the VAT inclusive amount" do
84
+ @item.vat_inclusive_amount.should == "30.00"
85
+ end
86
+
87
+ it "should be able to report the VAT exclusive amount" do
88
+ @item.vat_exclusive_amount.should == "30.00"
89
+ end
90
+ end
91
+
92
+ describe "and no VAT is reported for them" do
93
+ before(:each) do
94
+ @item = PaperlessToXero::InvoiceItem.new('description', '30.00', nil, '123 - Some stuff', 'VAT - 0%', false)
95
+ end
96
+
97
+ it "should be able to report the amount of VAT" do
98
+ @item.vat_amount.should == "0.00"
99
+ end
100
+
101
+ it "should be able to report the VAT inclusive amount" do
102
+ @item.vat_inclusive_amount.should == "30.00"
103
+ end
104
+
105
+ it "should be able to report the VAT exclusive amount" do
106
+ @item.vat_exclusive_amount.should == "30.00"
107
+ end
108
+ end
109
+ end
110
+
111
+ describe "where items do not have a VAT receipt" do
112
+ before(:each) do
113
+ @item = PaperlessToXero::InvoiceItem.new('description', '30.00', nil, '123 - Some stuff', 'No VAT', false)
114
+ end
115
+
116
+ it "should be able to report the amount of VAT" do
117
+ @item.vat_amount.should == nil
118
+ end
119
+
120
+ it "should be able to report the VAT inclusive amount" do
121
+ @item.vat_inclusive_amount.should == "30.00"
122
+ end
123
+
124
+ it "should be able to report the VAT exclusive amount" do
125
+ @item.vat_exclusive_amount.should == "30.00"
126
+ end
127
+ end
128
+
129
+ describe "VAT extraction" do
130
+ def self.vat_pairs
131
+ {'VAT - Germany - 7%' => '7% (Germany, VAT on expenses)',
132
+ 'VAT - Germany - 19%' => '19% (Germany, VAT on expenses)',
133
+ 'VAT - Germany' => '19% (Germany, VAT on expenses)',
134
+ 'VAT - France - 5.5%' => '5.5% (France, VAT on expenses)',
135
+ 'VAT - France - 19.6%' => '19.6% (France, VAT on expenses)',
136
+ 'VAT - France' => '19.6% (France, VAT on expenses)',
137
+ 'VAT - Denmark - 25%' => '25% (Denmark, VAT on expenses)',
138
+ 'VAT - Denmark' => '25% (Denmark, VAT on expenses)',
139
+ 'VAT - Sweden - 25%' => '25% (Sweden, VAT on expenses)',
140
+ 'VAT - Sweden' => '25% (Sweden, VAT on expenses)',
141
+ 'VAT - Ireland - 21.5%' => '21.5% (Ireland, VAT on expenses)',
142
+ 'VAT - Ireland' => '21.5% (Ireland, VAT on expenses)',
143
+ 'VAT - Luxembourg - 15%' => '15% (Luxembourg, VAT on expenses)',
144
+ 'VAT - Luxembourg' => '15% (Luxembourg, VAT on expenses)',
145
+ 'VAT - EU' => '15% (EU VAT ID)',
146
+ 'VAT - EU - EU372000063' => '15% (EU VAT ID)',
147
+ 'VAT - 15%' => '15% (VAT on expenses)',
148
+ 'VAT - 0%' => 'Zero Rated Expenses',
149
+ 'VAT' => '15% (VAT on expenses)',
150
+ 'No VAT' => 'No VAT'}
151
+ end
152
+
153
+ vat_pairs.each do |input, expected|
154
+ it "should convert '#{input}' to '#{expected}'" do
155
+ PaperlessToXero::InvoiceItem.publicize_methods do
156
+ @item.extract_vat_type(input).should == expected
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,131 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe PaperlessToXero::Invoice do
4
+ describe "the creation basics" do
5
+ it "should be able to be instantiated" do
6
+ PaperlessToXero::Invoice.new(Date.parse("2009-07-20"), 'Merchant', 'reference UID', '23.00', '3.00', true, 'GBP').
7
+ should be_instance_of(PaperlessToXero::Invoice)
8
+ end
9
+ end
10
+
11
+ describe "instances" do
12
+ describe "basics" do
13
+ before(:each) do
14
+ @invoice = PaperlessToXero::Invoice.new(Date.parse("2009-07-20"), 'Merchant', 'reference UID', '23.00', '3.00', true, 'GBP')
15
+ end
16
+
17
+ it "should be able to report their merchant" do
18
+ @invoice.merchant.should == 'Merchant'
19
+ end
20
+
21
+ it "should be able to report their currency" do
22
+ @invoice.currency.should == 'GBP'
23
+ end
24
+
25
+ it "should be able to report their reference UID" do
26
+ @invoice.reference_id.should == 'reference UID'
27
+ end
28
+
29
+ it "should be able to report their date" do
30
+ @invoice.date.should == Date.parse("2009-07-20")
31
+ end
32
+
33
+ it "should be able to report their total" do
34
+ @invoice.total.should == '23.00'
35
+ end
36
+
37
+ it "should be able to report VAT" do
38
+ @invoice.vat_total.should == '3.00'
39
+ end
40
+
41
+ it "should be able to report their ex-VAT total" do
42
+ @invoice.ex_vat_total.should == '20.00'
43
+ end
44
+
45
+ it "should be able to report their inc-VAT total" do
46
+ @invoice.inc_vat_total.should == '23.00'
47
+ end
48
+
49
+ it "should be able to report that it's VAT inclusive" do
50
+ @invoice.vat_inclusive?.should be_true
51
+ end
52
+
53
+ describe "adding items to an invoice" do
54
+ it "should be able to add an item" do
55
+ PaperlessToXero::InvoiceItem.expects(:new).with('description', '30.00', nil, '123 - Some stuff', 'No VAT', true).returns(:item)
56
+ @invoice.add_item('description', '30.00', nil, '123 - Some stuff', 'No VAT')
57
+
58
+ @invoice.items.should == [:item]
59
+ end
60
+ end
61
+
62
+ describe "ex-VAT invoices" do
63
+ before(:each) do
64
+ @invoice = PaperlessToXero::Invoice.new(Date.parse("2009-07-20"), 'Merchant', 'reference UID', '23.00', '3.00', false, 'GBP')
65
+ end
66
+
67
+ it "should be able to report VAT" do
68
+ @invoice.vat_total.should == '3.00'
69
+ end
70
+
71
+ it "should be able to report their ex-VAT total" do
72
+ @invoice.ex_vat_total.should == '20.00'
73
+ end
74
+
75
+ it "should be able to report their inc-VAT total" do
76
+ @invoice.inc_vat_total.should == '23.00'
77
+ end
78
+
79
+ it "should be pass on its ex-vat-ness to invoice items" do
80
+ PaperlessToXero::InvoiceItem.expects(:new).with('description', '30.00', nil, '123 - Some stuff', 'No VAT', false).returns(:item)
81
+ @invoice.add_item('description', '30.00', nil, '123 - Some stuff', 'No VAT')
82
+ end
83
+ end
84
+ end
85
+
86
+ describe "serializing an invoice" do
87
+ before(:each) do
88
+ @fake_csv = mock()
89
+ end
90
+
91
+ def make_invoice(total, vat, currency = 'GBP')
92
+ PaperlessToXero::Invoice.new(Date.parse("2009-07-20"), 'Merchant', 'reference UID', total, vat, true, currency)
93
+ end
94
+
95
+ describe "single-item invoices" do
96
+ it "should produce sensible Xero-pleasing output" do
97
+ invoice = make_invoice('45.00', '5.00')
98
+ invoice.add_item('description', '30.00', nil, '123 - Some stuff', 'No VAT')
99
+
100
+ @fake_csv.expects(:<<).with(['Merchant', 'reference UID', '20/07/2009', '20/07/2009', '40.00', '5.00', '45.00', 'description', '1', '30.00', '123 - Some stuff', 'No VAT', nil, nil, nil, nil, nil])
101
+
102
+ invoice.serialise_to_csv(@fake_csv)
103
+ end
104
+ end
105
+
106
+ describe "multi-item invoices" do
107
+ it "should produce sensible Xero-pleasing output" do
108
+ invoice = make_invoice('75.00', '5.00')
109
+ invoice.add_item('thing', '30.00', nil, '123 - Some stuff', 'No VAT')
110
+ invoice.add_item('other thing', '23.00', '3.00', '234 - Some other stuff', 'VAT - 15%')
111
+
112
+ @fake_csv.expects(:<<).with(['Merchant', 'reference UID', '20/07/2009', '20/07/2009', '70.00', '5.00', '75.00', 'thing', '1', '30.00', '123 - Some stuff', 'No VAT', nil, nil, nil, nil, nil])
113
+ @fake_csv.expects(:<<).with([nil, 'reference UID', nil, nil, nil, nil, nil, 'other thing', '1', '20.00', '234 - Some other stuff', '15% (VAT on expenses)', '3.00', nil, nil, nil, nil])
114
+
115
+ invoice.serialise_to_csv(@fake_csv)
116
+ end
117
+ end
118
+
119
+ describe "foreign-currency invoices" do
120
+ it "should stick the currency after the merchant so they can be picked out after import" do
121
+ invoice = make_invoice('45.00', '5.00', 'EUR')
122
+ invoice.add_item('description', '30.00', nil, '123 - Some stuff', 'No VAT')
123
+
124
+ @fake_csv.expects(:<<).with(['Merchant (EUR)', 'reference UID', '20/07/2009', '20/07/2009', '40.00', '5.00', '45.00', 'description', '1', '30.00', '123 - Some stuff', 'No VAT', nil, nil, nil, nil, nil])
125
+
126
+ invoice.serialise_to_csv(@fake_csv)
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --format specdoc
@@ -0,0 +1,26 @@
1
+ require 'rubygems'
2
+ require 'mocha'
3
+ $:.push(File.expand_path(File.dirname(__FILE__) + '/../lib'))
4
+ Spec::Runner.configure do |config|
5
+ config.mock_with :mocha
6
+ end
7
+
8
+ require 'paperless_to_xero'
9
+
10
+ class Class
11
+ def publicize_methods
12
+ saved_private_instance_methods = self.private_instance_methods
13
+ saved_protected_instance_methods = self.protected_instance_methods
14
+ self.class_eval do
15
+ public *saved_private_instance_methods
16
+ public *saved_protected_instance_methods
17
+ end
18
+
19
+ yield
20
+
21
+ self.class_eval do
22
+ private *saved_private_instance_methods
23
+ protected *saved_protected_instance_methods
24
+ end
25
+ end
26
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: paperless_to_xero
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Matt Patterson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-07-22 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rspec
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: |-
26
+ = Paperless-to-Xero
27
+
28
+ A simple translator which takes a CSV file from Mariner's Paperless receipt/document management software and makes a Xero accounts payable invoice CSV, for import into Xero.
29
+
30
+ Formatting in Paperless is very important, so you probably want to wait until I've written the docs
31
+ email: matt@reprocessed.org
32
+ executables:
33
+ - paperless_to_xero
34
+ extensions: []
35
+
36
+ extra_rdoc_files:
37
+ - README.rdoc
38
+ files:
39
+ - Rakefile
40
+ - README.rdoc
41
+ - bin/paperless_to_xero
42
+ - spec/fixtures/end_to_end-input.csv
43
+ - spec/fixtures/end_to_end-output.csv
44
+ - spec/fixtures/multi-ex-vat.csv
45
+ - spec/fixtures/multi-foreign.csv
46
+ - spec/fixtures/multi-item.csv
47
+ - spec/fixtures/single-1000.csv
48
+ - spec/fixtures/single-basic.csv
49
+ - spec/fixtures/single-dkk.csv
50
+ - spec/fixtures/single-foreign.csv
51
+ - spec/fixtures/single-no-vat.csv
52
+ - spec/fixtures/single-zero_rated.csv
53
+ - spec/paperless_to_xero/converter_spec.rb
54
+ - spec/paperless_to_xero/invoice_item_spec.rb
55
+ - spec/paperless_to_xero/invoice_spec.rb
56
+ - spec/spec.opts
57
+ - spec/spec_helper.rb
58
+ - lib/paperless_to_xero/converter.rb
59
+ - lib/paperless_to_xero/decimal_helpers.rb
60
+ - lib/paperless_to_xero/invoice.rb
61
+ - lib/paperless_to_xero/invoice_item.rb
62
+ - lib/paperless_to_xero/version.rb
63
+ - lib/paperless_to_xero.rb
64
+ has_rdoc: true
65
+ homepage: http://reprocessed.org/
66
+ licenses: []
67
+
68
+ post_install_message:
69
+ rdoc_options:
70
+ - --main
71
+ - README.rdoc
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: "0"
79
+ version:
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: "0"
85
+ version:
86
+ requirements: []
87
+
88
+ rubyforge_project: paperless_to_xero
89
+ rubygems_version: 1.3.4
90
+ signing_key:
91
+ specification_version: 3
92
+ summary: Convert Paperless CSV exports to Xero invoice import CSV
93
+ test_files: []
94
+