vatcalc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e13c738c08997c3f9eed7520fca91a0ce0d8396e
4
+ data.tar.gz: 03c1ab0f632e87dd5211887c5a09376b57e6925c
5
+ SHA512:
6
+ metadata.gz: 77a06947b030edb11ce582f640d589318de1d99c37159088491348bcaf4ef9945c81ab3a556a8a9b64df4ecfdfeb263a267b905db99a418f34c474368a0b6eaa
7
+ data.tar.gz: 86fbacf991bdb51c976e6a3d8e9b036390d9df8baf15a6113d2f48e85dbf57f054409d0d32851097eea552f18edd090e2a48cbfaf5cf988d455a8aebd56bb34d
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.15.4
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at TODO: Write your email address. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in vatcalc.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 TODO: Write your name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,189 @@
1
+ # Vatcalc
2
+
3
+ A gem to calculate VAT of multiple products with differently VAT percentages.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'vatcalc'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install vatcalc
20
+
21
+ ## Include
22
+
23
+ Include Vatcalc::ActsAsBillElement in your models / Classes
24
+
25
+ ```ruby
26
+
27
+ class Product < ActiveRecord::Base
28
+ include Vatcalc::ActsAsBillElement
29
+
30
+ acts_as_bill_element(amount: price, vat_percentage: :my_vat_percentage_field, currency: "EUR", prefix: :bill)
31
+
32
+ #you can pass the following key value pairs
33
+
34
+ # amount: the price for the product [required]
35
+ # you can pass:
36
+ # symbol => for a method
37
+ # lambda => ->(obj) {obj.price * 3}
38
+ # Float, Integer for a fix amount.
39
+ # NOTE:
40
+ # if the value is an Integer it is assumed that the amount is given in cents.
41
+ #
42
+ #
43
+ # currency:
44
+ # self-descriptive [optional]
45
+ # standard value is the money default currency
46
+ # => https://github.com/RubyMoney/money
47
+ #
48
+ # you can pass:
49
+ # symbol => for a method
50
+ # lambda => ->(obj) {obj.other_object.find_my_currency}
51
+ # String => "EUR", "USD"
52
+ #
53
+ #
54
+ # vat_percentage:
55
+ # the VAT percentage for the given product. [optional]
56
+ # This option will be ignored if you set the service option to true. +see below+
57
+ # Default is Vatcalc.vat_percentage => @see section "Configuration"
58
+ # you can pass:
59
+ # symbol => for a method
60
+ # lambda => ->(obj) {obj.find_my_currency}
61
+ # String => "EUR", "USD"
62
+ # NOTE:
63
+ # if the value is between 1 and 100 the value will be divided by 100.
64
+ # For example if you pass a value like 19 it is assumed that you mean 19%
65
+ # if the value is between 0 and 100 the value won't be divided.
66
+ #
67
+ # prefix: the prefix to call gross,vat,net,vat_splitted, and vat_percentage on your object.
68
+ # @see below.
69
+ #
70
+ #
71
+  # net:
72
+ # is the amount given as net amount ? [optional]
73
+ # Default => false
74
+ #
75
+ #
76
+ #
77
+ # service:
78
+ # is the object a service like a Coupon or a Fee ? If this option is set to true
79
+ # the object has not a fix VAT percentage.
80
+ # the vat will be calculated by the non-service net rates in a bill.
81
+
82
+
83
+ ....
84
+
85
+ #now you can call
86
+ product.bill_gross #=> #<Money fractional:1000 currency:EUR>
87
+ product.bill_net #=> #<Money fractional:840 currency:EUR>
88
+ product.bill_vat #=> #<Money fractional:160 currency:EUR>
89
+
90
+
91
+ product.bill_vat_splitted #=> {#<Vatcalc::VATPercentage vat_percentage:19%> => #<Money fractional:160 currency:EUR>}
92
+ # the key in the result hash is a Vatcalc::VATPercentage object it responds to to_f, to_s, to_d
93
+ # @example
94
+ # vp.to_s => "19%"
95
+ # vp.to_f => 1.19
96
+ # vp.to_d => #<BigDecimal:7f7ee20989a0,'0.119E1',18(36)>
97
+
98
+
99
+ end
100
+ ```
101
+
102
+ ## Bill
103
+
104
+ Creating a new bill object
105
+ ```ruby
106
+
107
+ bill = Vatcalc::Bill.new(elements: [product1,product2,fee])
108
+
109
+ #NOTE:
110
+ # If you pass an Array of 2D arrays it is assumed that the first element in 2D Array is the object
111
+ # and the second element is the quantity.
112
+ # @example elements:
113
+ # [
114
+ # [ product1, 2 ],
115
+ # [ product2, 1 ]
116
+ # ]
117
+
118
+ #now you can call
119
+ bill.gross #=> #<Money fractional:15000 currency:EUR>
120
+ bill.vat #=> #<Money fractional:1925 currency:EUR>
121
+ bill.net #=> #<Money fractional:13075 currency:EUR>
122
+
123
+ # if you only want to get amounts of the base.
124
+ bill.base.gross
125
+ ...
126
+
127
+ # if you only want to get amounts of the services.
128
+ bill.services.gross
129
+ ...
130
+
131
+
132
+
133
+ bill.vat_splitted #=>
134
+ #
135
+ # {
136
+ # #<Vatcalc::VATPercentage vat_percentage:7%> => #<Money fractional:325 currency:EUR>,
137
+ # #<Vatcalc::VATPercentage vat_percentage:19%> => #<Money fractional:1600 currency:EUR>
138
+ # }
139
+
140
+ # To get the vat rates
141
+ # These vat rates are the net sums of the non-service elements grouped by vat_percentage and divided by total
142
+ # non-service elements net amount
143
+
144
+ bill.vat_rates # =>
145
+ # {
146
+ # #<Vatcalc::VATPercentage vat_percentage:19%>=>0.6424,
147
+ # #<Vatcalc::VATPercentage vat_percentage:7%>=>0.3576,
148
+ # }
149
+
150
+ ## NOTE: Each service Element will be taxed by these rates.
151
+
152
+
153
+
154
+
155
+ bill.each do |obj, quantity, gross, vat, net|
156
+ # NOTE: gross, vat, net are already multiplied by quantity
157
+ # do stuff ..
158
+ end
159
+
160
+
161
+
162
+
163
+
164
+ ```
165
+
166
+
167
+ ## Configuration
168
+
169
+ ```ruby
170
+ gem 'vatcalc'
171
+ ```
172
+
173
+ ## Development
174
+
175
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
176
+
177
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
178
+
179
+ ## Contributing
180
+
181
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/vatcalc. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
182
+
183
+ ## License
184
+
185
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
186
+
187
+ ## Code of Conduct
188
+
189
+ Everyone interacting in the Vatcalc project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/vatcalc/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "vatcalc"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/vatcalc.rb ADDED
@@ -0,0 +1,51 @@
1
+ require "vatcalc/version"
2
+ require 'active_support'
3
+ require 'active_support/core_ext'
4
+
5
+ require "vatcalc/util"
6
+ require "vatcalc/vat_percentage"
7
+
8
+ require "vatcalc/gnv"
9
+ require "vatcalc/base_element"
10
+
11
+ require "vatcalc/service_element"
12
+ require "vatcalc/bill"
13
+
14
+ require "vatcalc/acts_as_bill_element"
15
+
16
+ module Vatcalc
17
+ mattr_accessor :currency
18
+
19
+ class << self
20
+
21
+ def vat_percentage
22
+ @vat_percentage
23
+ end
24
+
25
+ def vat_percentage=(v)
26
+ @vat_percentage = VATPercentage.new(v)
27
+ end
28
+
29
+ alias :percentage :vat_percentage
30
+ alias :percentage= :vat_percentage=
31
+
32
+
33
+ def vat_of(v,**args)
34
+ BaseElement.new(v,**args).vat
35
+ end
36
+
37
+ def net_of(v,**args)
38
+ BaseElement.new(v,**args).net
39
+ end
40
+
41
+ def gross_of(v,**args)
42
+ BaseElement.new(v,**args).gross
43
+ end
44
+
45
+ end
46
+
47
+ #German Standard Settings
48
+ self.currency = "EUR"
49
+ self.vat_percentage= 19.00
50
+
51
+ end
@@ -0,0 +1,57 @@
1
+ module Vatcalc
2
+
3
+
4
+ def self.acts_as_bill_element?
5
+ @acts_as_bill_element ||= ->(obj) { obj.class.respond_to?(:acts_as_bill_element) && obj.respond_to?(:as_vatcalc_bill_element) }
6
+ end
7
+
8
+
9
+ module ActsAsBillElement
10
+
11
+ def self.included(mod)
12
+ mod.extend(ClassMethods)
13
+ end
14
+
15
+ module ClassMethods
16
+ def acts_as_bill_element(amount:, service: false, currency: nil, vat_percentage: nil, prefix: :bill, net: false)
17
+
18
+ args_to_convert = {amount: amount,currency: currency,net: net}
19
+ delegators = [:gross,:net,:vat,:vat_splitted]
20
+
21
+ if service
22
+ klass = Vatcalc::ServiceElement
23
+ else
24
+ klass = Vatcalc::BaseElement
25
+ args_to_convert[:vat_percentage] = vat_percentage
26
+ delegators << :vat_percentage
27
+ end
28
+
29
+
30
+ delegate *delegators, prefix: prefix, to: :as_vatcalc_bill_element
31
+ v_name = :@as_vatcalc_bill_element
32
+
33
+ define_method(:as_vatcalc_bill_element) do
34
+ unless instance_variable_get(v_name)
35
+ args = args_to_convert.inject({}) do |h,(k,v)|
36
+ case v
37
+ when Proc
38
+ h[k] = v.call(self)
39
+ when Symbol
40
+ h[k] = send(v)
41
+ else
42
+ h[k] = v
43
+ end
44
+ h
45
+ end
46
+ instance_variable_set v_name, klass.new( args.delete(:amount), **args)
47
+ end
48
+ instance_variable_get(v_name)
49
+ end
50
+ end
51
+
52
+
53
+ end
54
+
55
+
56
+ end
57
+ end
@@ -0,0 +1,56 @@
1
+ # A Base Element Object inherits from GNV
2
+ #@see Vatcalc::GNV
3
+ #
4
+ # A BaseElement always needs an VAT percentage and an amount
5
+ # if no VAT Percentage is given it takes the default VAT Percentage
6
+ module Vatcalc
7
+ class BaseElement < GNV
8
+
9
+ include Comparable
10
+
11
+
12
+ attr_reader :vat_percentage
13
+ alias_method :percentage, :vat_percentage
14
+ alias_method :vat_p, :vat_percentage
15
+
16
+
17
+ #Initalizes a new Object of an BaseElement
18
+ #@param amount = [Money,Numeric]
19
+ #@param options = [Hash]
20
+ #
21
+ # Assumes that the amount is a gross value but you can pass a net value as well if you pass the
22
+ # option net: true
23
+ #
24
+ #@example
25
+ #
26
+ # => b = BaseElement.new 10.00, vat_percentage: 19, currency: "EUR"
27
+ # b.net.to_f = 8.40
28
+ # => b = BaseElement.new 10.00, vat_percentage: 7, currency: "USD"
29
+ # b.net.to_f = 9.35
30
+ # => b = BaseElement.new 10.00, vat_percentage: 7, currency: "USD", net: true
31
+ # => b.gross = 10.70
32
+ def initialize(amount,currency: nil, vat_percentage: nil, net: false)
33
+ @currency = currency || Vatcalc.currency
34
+ amount = Util.to_money(amount,@currency)
35
+ vp = Util.to_vat_percentage(vat_percentage)
36
+ @vector = net ? Vector[amount * vp, amount] : Vector[amount, amount / vp]
37
+ @vat_percentage = vp
38
+ @vat_splitted = {@vat_percentage => vat}
39
+ end
40
+
41
+ def hash
42
+ #vector comes from GNV
43
+ [@vector,@vat_percentage].hash
44
+ end
45
+
46
+ def ==(oth)
47
+ oth.is_a?(BaseElement) && (oth.vector == @vector) && (vat_p == oth.vat_p)
48
+ end
49
+
50
+
51
+ def inspect
52
+ "#<#{self.class.name} vat_percentage:#{vat_p} gross:#{gross} net: #{net} vat:#{vat} currency:#{@currency}>"
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,227 @@
1
+
2
+ module Vatcalc
3
+ class Bill
4
+
5
+ include Enumerable
6
+
7
+ attr_reader :currency
8
+ attr_reader :services
9
+ attr_reader :base
10
+
11
+ alias_method :service_elements, :services
12
+ alias_method :base_elements, :base
13
+
14
+ delegate :rates,:rates!,:human_rates,:vat_percentages, to: :@base
15
+ delegate :each, to: :all
16
+
17
+ def initialize(elements: [],currency: nil)
18
+ @base = Base.new #defined at the bottom
19
+ @services = Services.new #defined at the bottom
20
+ @currency = currency
21
+ insert(elements)
22
+ end
23
+
24
+
25
+ #inserts one or more element which included the Vatcalc::ActsAsBillElement module
26
+ #@param obj = [Array,Object]
27
+ #@result = [Vatcalc::Bill]
28
+ def insert(obj, quantity = 1)
29
+ case obj
30
+ when Array
31
+ return (obj.each { |obj, quantity| insert(obj, quantity)}.last || self)
32
+ when Vatcalc.acts_as_bill_element?
33
+ gnv = obj.as_vatcalc_bill_element
34
+ else raise ArgumentError.new ("Can't insert a #{obj.class} into #{self}. #{obj.class} must include Vatcalc::ActsAsBillElement")
35
+ end
36
+ if (quantity ||= 1) > 0
37
+ gnv.source = obj
38
+ @currency = gnv.currency
39
+ case gnv
40
+ when BaseElement
41
+ @base.insert(gnv,quantity)
42
+ # if an base element is inserted after services already in here.
43
+ @services.rates_changed!(@base.rates) if @services.length > 0
44
+ when ServiceElement
45
+ @services.insert(gnv,quantity)
46
+ # the service gets now the rates of the base
47
+ gnv.change_rates(@base.rates)
48
+ end
49
+
50
+ @base.currency = @currency
51
+ @services.currency = @currency
52
+ end
53
+ self
54
+ end
55
+
56
+ [:gross,:vat,:net].each do |m|
57
+ define_method(m) { base.send(m) + services.send(m) }
58
+ end
59
+
60
+ def vat_splitted
61
+ all.vat_splitted
62
+ end
63
+
64
+ def all
65
+ (base + services)
66
+ end
67
+
68
+
69
+ RoundPrecision = 4
70
+ #will only be used in rspec for test
71
+ Tolerance = BigDecimal("1E-#{RoundPrecision}")
72
+ #@see +rates+
73
+
74
+ alias_method :percentages, :vat_percentages
75
+ alias_method :vat_rates, :rates
76
+ alias_method :elements, :all
77
+
78
+ # A GNVCollection consists basically of a an 2D Array of GNV
79
+ # GNV Objects +@see Vatcalc::GNV+
80
+ # It's a helper class to calculate amounts and iterate through
81
+ # specific GNV objects.
82
+ class GNVCollection
83
+ include Enumerable
84
+
85
+ attr_reader :collection
86
+ attr_accessor :currency
87
+
88
+ delegate :length, :first, :last, to: :@collection
89
+
90
+ # which class can be inserted in the collection
91
+ def self.for
92
+ GNV
93
+ end
94
+
95
+ def initialize(col=[],currency = nil)
96
+ @collection = col
97
+ @currency = currency
98
+ end
99
+
100
+ def insert(gnv,quantity)
101
+ raise(TypeError.new) unless gnv.is_a?(self.class.for)
102
+ @currency = gnv.currency
103
+ @vat_splitted = nil
104
+ @collection << [gnv,quantity]
105
+ @gross, @vat, @net = [nil] * 3
106
+ self
107
+ end
108
+
109
+ def <<(arg)
110
+ insert(arg,1)
111
+ end
112
+
113
+ def vat_splitted
114
+ @vat_splitted ||= @collection.inject(GNV.new(0,0,@currency)){|sum,(gnv,q)| sum += (gnv * q) }.vat_splitted
115
+ end
116
+
117
+ [:gross,:vat,:net].each do |it|
118
+ define_method(it) { instance_variable_get("@#{it}") || instance_variable_set( "@#{it}", @collection.inject(new_money) {|sum,(gnv,q)| sum += (gnv.send(it) * q) } ) }
119
+ end
120
+
121
+ def +(other)
122
+ raise(TypeError.new) unless other.is_a?(GNVCollection)
123
+ GNVCollection.new(@collection + other.collection,@currency)
124
+ end
125
+
126
+ def each
127
+ result = []
128
+ @collection.each do |gnv,quantity|
129
+ arr = [gnv.source, quantity, gross*quantity, net*quantity, vat*quantity]
130
+ result << arr
131
+ yield *arr
132
+ end
133
+ result
134
+ end
135
+
136
+ def each_gnv
137
+ @collection.each {|gnv,quantity| yield gnv, quantity }
138
+ end
139
+
140
+ private
141
+
142
+ def money_hash
143
+ Hash.new(new_money)
144
+ end
145
+
146
+ def new_money
147
+ Money.new(0,@currency)
148
+ end
149
+
150
+
151
+ end
152
+
153
+
154
+ class Base < GNVCollection
155
+
156
+ attr_reader :vat_percentages
157
+
158
+ def initialize(*args)
159
+ super
160
+ @vat_percentages = Set.new
161
+ end
162
+
163
+ def self.for
164
+ BaseElement
165
+ end
166
+
167
+ def insert(gnv,quantity)
168
+ super
169
+ @rates = nil
170
+ @vat_percentages << gnv.vat_p
171
+ self
172
+ end
173
+
174
+ # Output of rates in form of
175
+ # key is VAT Percentage and Value is the rate
176
+ # "{1.0=>0.0092, 1.19=>0.8804, 1.07=>0.1104}"
177
+ def rates
178
+ @rates ||= rates!
179
+ end
180
+
181
+ def rates!
182
+ @rates = Hash.new(0.00)
183
+ if net.to_f != 0
184
+ left_over = 1.00
185
+ grouped_amounts = @collection.inject(money_hash){ |h,(gnv,q)| h[gnv.vat_p] += gnv.net * q; h}.sort
186
+
187
+ grouped_amounts.each_with_index do |(vp,amount),i|
188
+ if i == (grouped_amounts.length - 1)
189
+ #last element
190
+ @rates[vp] = left_over.round(4)
191
+ else
192
+ @rates[vp] = (amount / net).round(4)
193
+ left_over -= @rates[vp]
194
+ end
195
+ end
196
+ else
197
+ max_p = @vat_percentages.max
198
+ @rates[max_p] = 1.00 if max_p
199
+ end
200
+ @rates = @rates.sort.reverse.to_h #sorted by vat percentage
201
+ end
202
+
203
+ # Output of rates in form of
204
+ # key is VAT Percentage and Value is the rate in decimal form
205
+ # {"19%"=>"69.81%", "7%"=>"21.74%", "0%"=>"8.45%"}
206
+ def human_rates
207
+ #example ((1.19 - 1.00)*100).round(2) => 19.0
208
+ rates.inject({}){|h,(pr,v)| h[pr.to_s] = Util.human_percentage_value(v,4); h}
209
+ end
210
+
211
+ end
212
+
213
+ class Services < GNVCollection
214
+ def self.for
215
+ ServiceElement
216
+ end
217
+
218
+ def rates_changed!(rates)
219
+ @vat_splitted = nil
220
+ @gross, @vat, @net = [nil] * 3
221
+ each_gnv {|gnv,_| gnv.change_rates(rates)}
222
+ end
223
+ end
224
+
225
+
226
+ end
227
+ end
@@ -0,0 +1,122 @@
1
+ require "matrix"
2
+
3
+ # A GNV Object consists basically of a 2D Vector
4
+ # First value is gross, second is net.
5
+ # Vat is calculated by gross - net
6
+ #
7
+ # GNV is an abstract Object and should only used for internal calculations in this library.
8
+ #
9
+ #@example
10
+ # GNV.new(10.00,9.00)
11
+ #
12
+ # You can add or subtract two GNVs
13
+ # GNV.new(10.00,9.00) + GNV.new(9.00,0.00)
14
+ module Vatcalc
15
+ class GNV
16
+
17
+ include Comparable
18
+
19
+ attr_reader :vector,:currency
20
+ attr_accessor :source
21
+
22
+ def initialize(gross,net,cur=nil)
23
+ @currency ||= (cur || Vatcalc.currency)
24
+ init_vector(gross,net)
25
+ end
26
+
27
+ [:+,:-].each do |m_name|
28
+ define_method(m_name) do |oth|
29
+ gnv = oth.is_a?(GNV) ? to_gnv(@vector.send(m_name,oth.vector)) : raise(TypeError.new)
30
+ gnv.tap do |g|
31
+ g.vat_splitted = oth.vat_splitted.merge(self.vat_splitted) {|vp,m1,m2| m1 + m2}
32
+ end
33
+ end
34
+ end
35
+
36
+ def *(oth)
37
+ gnv = oth.is_a?(Numeric) ? to_gnv(@vector * oth) : raise(TypeError.new)
38
+ gnv.tap do |g|
39
+ h = new_money_hash
40
+ self.vat_splitted.each {|vp,m| h[vp] = m*oth}
41
+ g.vat_splitted = h
42
+ end
43
+ end
44
+
45
+ #For usage of => - GNV.new(100.00,90.00)
46
+ def -@
47
+ to_gnv(-@vector)
48
+ end
49
+
50
+
51
+ def ==(oth)
52
+ oth.is_a?(GNV) && oth.vector == @vector
53
+ end
54
+
55
+ alias_method :eql?, :==
56
+
57
+ def <=>(other)
58
+ if other.respond_to?(:net)
59
+ net <=> other.net
60
+ else
61
+ net <=> other
62
+ end
63
+ end
64
+
65
+
66
+
67
+ #@see https://www.mutuallyhuman.com/blog/2011/01/25/class-coercion-in-ruby
68
+ def coerce(oth)
69
+ [self,oth]
70
+ end
71
+
72
+ def gross
73
+ @vector[0]
74
+ end
75
+
76
+ def net
77
+ @vector[1]
78
+ end
79
+
80
+ def vat_splitted
81
+ @vat_splitted ||= new_money_hash
82
+ end
83
+
84
+ # Returns a Integer hash value based on the +value+
85
+ # in order to use functions like & (intersection), group_by, etc.
86
+ #
87
+ # @return [Integer]
88
+ #
89
+ # @example
90
+ # GNV.new(19,11).hash #=> 908351
91
+ def hash
92
+ @vector.hash
93
+ end
94
+
95
+ #Always gross - net
96
+ def vat
97
+ gross - net
98
+ end
99
+
100
+ def to_gnv(v=@vector)
101
+ GNV.new(v[0],v[1],@currency)
102
+ end
103
+
104
+ protected
105
+
106
+ def vat_splitted=(h)
107
+ @vat_splitted = h
108
+ end
109
+
110
+ private
111
+
112
+ def init_vector(g,n)
113
+ @vector = Vector[*[g,n].map{|i| Util.to_money(i,@currency)}]
114
+ end
115
+
116
+ def new_money_hash
117
+ Hash.new(Money.new(0,@currency))
118
+ end
119
+
120
+
121
+ end
122
+ end
@@ -0,0 +1,93 @@
1
+ module Vatcalc
2
+ class ServiceElement < GNV
3
+
4
+
5
+ attr_reader :vat_splitted,:rates
6
+
7
+ #Initalizes a new Object of an ServiceElement
8
+ #@param amount = [Money,Numeric]
9
+ #@param options = [Hash]
10
+ #
11
+ # Assumes that the amount is a gross value but you can pass a net value as well if you pass the
12
+ # option net: true
13
+ #
14
+ #@example
15
+ # => b = ServiceElement.new 10.00, currency: "EUR"
16
+ # b.net.to_f = 8.40
17
+ # => b = ServiceElement.new 10.00, currency: "USD"
18
+ # b.net.to_f = 9.35
19
+ # => b = ServiceElement.new 10.00, currency: "USD", net: true
20
+ # => b.gross = 10.70
21
+ def initialize(amount,net: false, currency: nil, rates: {})
22
+ @net_service = net
23
+ #if an service element is initialized # => gross equals net
24
+ super amount, amount, currency
25
+ change_rates(rates)
26
+ end
27
+
28
+ # Allocates net or gross by new vat_percentage rates and calculates the vat splitted by given rates
29
+ # @param rates [Hash]
30
+ # =>
31
+ #
32
+ # @return [Hash]
33
+ # @example
34
+ # => {#<Vatcalc::VATPercentage vat_percentage:19%>=>#<Money fractional:64 currency:EUR>,
35
+ # #<Vatcalc::VATPercentage vat_percentage:7%>=>#<Money fractional:39 currency:EUR>}
36
+ #
37
+ def change_rates(new_rates)
38
+ if new_rates.is_a? Hash
39
+ if !new_rates.empty?
40
+ # Using basically the allocate function of the Money gem here.
41
+ # EXPLANATION FROM MONEY GEM:
42
+ #
43
+ # Allocates money between different parties without losing pennies.
44
+ # After the mathematical split has been performed, leftover pennies will
45
+ # be distributed round-robin amongst the parties. This means that parties
46
+ # listed first will likely receive more pennies than ones that are listed later
47
+
48
+ # @param [Array<Numeric>] splits [0.50, 0.25, 0.25] to give 50% of the cash to party1, 25% to party2, and 25% to party3.
49
+
50
+ # @return [Array<Money>]
51
+
52
+ # @example
53
+ # Money.new(5, "USD").allocate([0.3, 0.7]) #=> [Money.new(2), Money.new(3)]
54
+ # Money.new(100, "USD").allocate([0.33, 0.33, 0.33]) #=> [Money.new(34), Money.new(33), Money.new(33)]
55
+ #
56
+ allocated = (@net_service ? net : gross).allocate(new_rates.values)
57
+ # Init new vector after the allocate calculation
58
+ # Comes from superclass GNV
59
+ init_vector(0,0)
60
+ @vat_splitted = {}
61
+ new_rates.keys.zip(allocated).each do |vp,splitted|
62
+ #creating a new base element
63
+ b = BaseElement.new(splitted, net: @net_service,vat_percentage: vp, currency: @currency)
64
+ @vector += b.vector
65
+ @vat_splitted[b.vat_percentage] = b.vat
66
+ end
67
+ @rates = new_rates
68
+ else
69
+ @vat_splitted = {}
70
+ end
71
+ @rates = new_rates
72
+ else
73
+ ArgumentError.new "Hash must be given not #{arg.class}"
74
+ end
75
+ end
76
+
77
+ def hash
78
+ #vector comes from GNV
79
+ [@vector,@vat_splitted].hash
80
+ end
81
+
82
+ def ==(oth)
83
+ oth.is_a?(ServiceElement) && oth.gross == gross && oth.net == net && (@vat_splitted == oth.vat_splitted)
84
+ end
85
+
86
+
87
+ def inspect
88
+ "#<#{self.class.name} vat_splitted:#{vat_splitted} gross:#{gross} net: #{net} vat:#{vat} currency: #{@currency}>"
89
+ end
90
+
91
+
92
+ end
93
+ end
@@ -0,0 +1,67 @@
1
+
2
+ require "money"
3
+
4
+
5
+ module Vatcalc
6
+ class Util
7
+ class << self
8
+
9
+ #Converts an Object into a Money object
10
+ #@return [Money]
11
+ #@example
12
+ # => Vatcalc::Util.convert_to_money(10.00)
13
+ def convert_to_money(obj,curr=nil)
14
+ curr ||= Vatcalc.currency
15
+ case obj
16
+ when Money
17
+ obj
18
+ when Fixnum
19
+ Money.new(obj,curr)
20
+ when Numeric
21
+ Money.new(obj*100,curr)
22
+ else
23
+ raise InvalidAmountError.new "Can't convert #{obj.class} to an Money instance"
24
+ end
25
+ end
26
+
27
+ # ALIAS for convert_to_money method
28
+ alias_method :conv_to_money, :convert_to_money
29
+ alias_method :conv_to_m, :convert_to_money
30
+ alias_method :to_money, :convert_to_money
31
+
32
+ #Converts an Object into an VATPercentage Object
33
+ #@return [VATPercentage]
34
+ #
35
+ #@example
36
+ # => Vatcalc::Util.to_vat_percentage
37
+ def convert_to_vat_percentage(vat_percentage)
38
+ case vat_percentage
39
+ when VATPercentage
40
+ vat_percentage
41
+ when nil
42
+ Vatcalc.vat_percentage
43
+ else
44
+ VATPercentage.new(vat_percentage)
45
+ end
46
+ end
47
+
48
+ # ALIAS for convert_to_vat_percentage method
49
+ alias_method :to_vat_percentage, :convert_to_vat_percentage
50
+ alias_method :to_vat_p, :convert_to_vat_percentage
51
+
52
+ #Returns a human friendly percentage value
53
+ #@param value = [Float,Integer,String]
54
+ # => human_percentage_value(0.19) => 19%
55
+ def human_percentage_value(value,precision=2)
56
+ full, fraction = ((value.to_f)*100).to_f.round(precision).divmod(1)
57
+ full.to_s + (fraction > 0.00 ? ("," + fraction.round(precision).to_s[2..-1]) : "") + "%"
58
+ end
59
+
60
+ end
61
+
62
+ end
63
+
64
+
65
+ class InvalidAmountError < TypeError
66
+ end
67
+ end
@@ -0,0 +1,110 @@
1
+ module Vatcalc
2
+ class VATPercentage < Numeric
3
+
4
+ include Comparable
5
+
6
+ attr_reader :value
7
+
8
+ #Init a VATPercentage object by
9
+ # => Integer
10
+ # => VATPercentage.new 19
11
+ # => Float
12
+ # => VATPercentage.new 0.19
13
+ # => String
14
+ # => VATPercentage.new 19%
15
+ # => VATPercentage.new 19,1%
16
+ # => VATPercentage.new 19,1%
17
+ # => VATPercentage.new 19.1%
18
+ def initialize(obj)
19
+ @value = case obj
20
+ when VATPercentage
21
+ obj.value
22
+ when 0.00..0.99
23
+ as_d(obj.to_f + 1.00)
24
+ when 1..100.00
25
+ as_d((obj.to_f / 100 ) + 1.00)
26
+ else
27
+ if obj.is_a?(String) && obj.match(/[0-9]{0,2}\.|\,{0,1}[0-9]{0,2}/)
28
+ as_d((obj.gsub("," , ".").to_f / 100) + 1.00)
29
+ else
30
+ raise TypeError.new("Can't convert #{obj.class} #{obj} to an valid #{self.class}")
31
+ end
32
+ end
33
+ end
34
+
35
+ delegate :to_i,:to_f, to: :to_d
36
+
37
+
38
+ #For comparisaon between a value or a +VATPercentage+
39
+ #@return [Intger]
40
+ #@see module Comparable
41
+ def <=>(other)
42
+ to_d <=> as_d(other)
43
+ end
44
+
45
+ #Returns a gross value
46
+ #@return [Money]
47
+ #@example
48
+ # => 10.00 * VATPercentage.new(19) #=> #<Money fractional:1190 currency:EUR>
49
+ def *(other)
50
+ case other
51
+ when Money
52
+ other * @value
53
+ when Numeric
54
+ Util.convert_to_money(other) * @value
55
+ when VATPercentage
56
+ raise TypeError.new "Can't multiply a VATPercentage by another VATPercentage"
57
+ else
58
+ if other.respond_to?(:coerce)
59
+ a,b = other.coerce(self)
60
+ a * b
61
+ else
62
+ raise TypeError.new "Can't multiply #{other.class} by VATPercentage"
63
+ end
64
+ end
65
+ end
66
+
67
+ #@see https://www.mutuallyhuman.com/blog/2011/01/25/class-coercion-in-ruby
68
+ #Basic usage of coerce. Now you can write:
69
+ # + 10.00 * VATPercentage.new(19) +
70
+ # and:
71
+ # + VATPercentage.new(19) * 10.00 +
72
+ def coerce(other)
73
+ [self,other]
74
+ end
75
+
76
+ def to_d
77
+ @value
78
+ end
79
+
80
+ def inspect
81
+ "#<#{self.class.name} vat_percentage:#{to_s}>"
82
+ end
83
+
84
+ # Returns a Integer hash value based on the +value+
85
+ # in order to use functions like & (intersection), group_by, etc.
86
+ #
87
+ # @return [Integer]
88
+ #
89
+ # @example
90
+ # VATPercentage.new(19).hash #=> 908351
91
+ def hash
92
+ [@value,self.class].hash
93
+ end
94
+
95
+ def to_s
96
+ Util.human_percentage_value(@value-1.00)
97
+ end
98
+
99
+
100
+ private
101
+ def as_d(num)
102
+ if num.respond_to?(:to_d)
103
+ num.is_a?(Rational) ? num.to_d(5) : num.to_d
104
+ else
105
+ BigDecimal.new(num.to_s.empty? ? 0 : num.to_s)
106
+ end
107
+ end
108
+
109
+ end
110
+ end
@@ -0,0 +1,3 @@
1
+ module Vatcalc
2
+ VERSION = "0.1.0"
3
+ end
data/vatcalc.gemspec ADDED
@@ -0,0 +1,39 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "vatcalc/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "vatcalc"
8
+ spec.version = Vatcalc::VERSION
9
+ spec.authors = ["Christopher Geduhn"]
10
+ spec.email = ["christopher.geduhn@googlemail.com"]
11
+
12
+ spec.summary = %q{ A gem to calculate VAT of multiple products with differently VAT percentages. }
13
+ spec.description = %q{ A gem to calculate VAT of multiple products with differently VAT percentages. }
14
+ spec.homepage = "https://github.com/cgeduhn/vatcalc"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ # if spec.respond_to?(:metadata)
20
+ # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
21
+ # else
22
+ # raise "RubyGems 2.0 or newer is required to protect against " \
23
+ # "public gem pushes."
24
+ # end
25
+
26
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
27
+ f.match(%r{^(test|spec|features)/})
28
+ end
29
+ spec.bindir = "exe"
30
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ["lib"]
32
+
33
+ spec.add_dependency "money"
34
+ spec.add_dependency 'activesupport'
35
+
36
+ spec.add_development_dependency "bundler", "~> 1.15"
37
+ spec.add_development_dependency "rake", "~> 10.0"
38
+ spec.add_development_dependency "rspec", "~> 3.0"
39
+ end
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vatcalc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Christopher Geduhn
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-10-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: money
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.15'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.15'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ description: " A gem to calculate VAT of multiple products with differently VAT percentages. "
84
+ email:
85
+ - christopher.geduhn@googlemail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rspec"
92
+ - ".travis.yml"
93
+ - CODE_OF_CONDUCT.md
94
+ - Gemfile
95
+ - LICENSE.txt
96
+ - README.md
97
+ - Rakefile
98
+ - bin/console
99
+ - bin/setup
100
+ - lib/vatcalc.rb
101
+ - lib/vatcalc/acts_as_bill_element.rb
102
+ - lib/vatcalc/base_element.rb
103
+ - lib/vatcalc/bill.rb
104
+ - lib/vatcalc/gnv.rb
105
+ - lib/vatcalc/service_element.rb
106
+ - lib/vatcalc/util.rb
107
+ - lib/vatcalc/vat_percentage.rb
108
+ - lib/vatcalc/version.rb
109
+ - vatcalc.gemspec
110
+ homepage: https://github.com/cgeduhn/vatcalc
111
+ licenses:
112
+ - MIT
113
+ metadata: {}
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ requirements: []
129
+ rubyforge_project:
130
+ rubygems_version: 2.6.13
131
+ signing_key:
132
+ specification_version: 4
133
+ summary: A gem to calculate VAT of multiple products with differently VAT percentages.
134
+ test_files: []