paperless_to_xero 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
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
+