paperless_to_xero 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +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
|
+
|