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 +5 -0
- data/Rakefile +139 -0
- data/bin/paperless_to_xero +30 -0
- data/lib/paperless_to_xero.rb +2 -0
- data/lib/paperless_to_xero/converter.rb +135 -0
- data/lib/paperless_to_xero/decimal_helpers.rb +25 -0
- data/lib/paperless_to_xero/invoice.rb +50 -0
- data/lib/paperless_to_xero/invoice_item.rb +88 -0
- data/lib/paperless_to_xero/version.rb +12 -0
- data/spec/fixtures/end_to_end-input.csv +7 -0
- data/spec/fixtures/end_to_end-output.csv +12 -0
- data/spec/fixtures/multi-ex-vat.csv +2 -0
- data/spec/fixtures/multi-foreign.csv +2 -0
- data/spec/fixtures/multi-item.csv +2 -0
- data/spec/fixtures/single-1000.csv +2 -0
- data/spec/fixtures/single-basic.csv +2 -0
- data/spec/fixtures/single-dkk.csv +2 -0
- data/spec/fixtures/single-foreign.csv +2 -0
- data/spec/fixtures/single-no-vat.csv +2 -0
- data/spec/fixtures/single-zero_rated.csv +2 -0
- data/spec/paperless_to_xero/converter_spec.rb +218 -0
- data/spec/paperless_to_xero/invoice_item_spec.rb +162 -0
- data/spec/paperless_to_xero/invoice_spec.rb +131 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +26 -0
- metadata +94 -0
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,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,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
data/spec/spec_helper.rb
ADDED
@@ -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
|
+
|