invoicing 0.2.1 → 1.0.0
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.
- checksums.yaml +7 -0
- data/LICENSE +1 -0
- data/README.md +57 -0
- data/Rakefile +16 -37
- data/lib/invoicing.rb +20 -10
- data/lib/invoicing/cached_record.rb +9 -6
- data/lib/invoicing/class_info.rb +34 -34
- data/lib/invoicing/connection_adapter_ext.rb +4 -4
- data/lib/invoicing/countries/uk.rb +6 -6
- data/lib/invoicing/currency_value.rb +39 -32
- data/lib/invoicing/find_subclasses.rb +40 -15
- data/lib/invoicing/ledger_item.rb +166 -145
- data/lib/invoicing/ledger_item/pdf_generator.rb +108 -0
- data/lib/invoicing/ledger_item/render_html.rb +76 -73
- data/lib/invoicing/ledger_item/render_ubl.rb +37 -35
- data/lib/invoicing/line_item.rb +43 -38
- data/lib/invoicing/price.rb +1 -1
- data/lib/invoicing/tax_rate.rb +3 -6
- data/lib/invoicing/taxable.rb +37 -32
- data/lib/invoicing/time_dependent.rb +40 -40
- data/lib/invoicing/version.rb +4 -4
- data/lib/rails/generators/invoicing/invoicing_generator.rb +14 -0
- data/lib/rails/generators/invoicing/ledger_item/ledger_item_generator.rb +17 -0
- data/lib/rails/generators/invoicing/ledger_item/templates/migration.rb +25 -0
- data/lib/rails/generators/invoicing/ledger_item/templates/model.rb +5 -0
- data/lib/rails/generators/invoicing/line_item/line_item_generator.rb +17 -0
- data/lib/rails/generators/invoicing/line_item/templates/migration.rb +20 -0
- data/lib/rails/generators/invoicing/line_item/templates/model.rb +5 -0
- data/lib/rails/generators/invoicing/tax_rate/tax_rate_generator.rb +17 -0
- data/lib/rails/generators/invoicing/tax_rate/templates/migration.rb +14 -0
- data/lib/rails/generators/invoicing/tax_rate/templates/model.rb +3 -0
- metadata +110 -153
- data.tar.gz.sig +0 -1
- data/History.txt +0 -31
- data/Manifest.txt +0 -62
- data/PostInstall.txt +0 -10
- data/README.rdoc +0 -58
- data/script/console +0 -10
- data/script/destroy +0 -14
- data/script/generate +0 -14
- data/tasks/rcov.rake +0 -4
- data/test/cached_record_test.rb +0 -100
- data/test/class_info_test.rb +0 -253
- data/test/connection_adapter_ext_test.rb +0 -79
- data/test/currency_value_test.rb +0 -209
- data/test/find_subclasses_test.rb +0 -120
- data/test/fixtures/README +0 -7
- data/test/fixtures/cached_record.sql +0 -22
- data/test/fixtures/class_info.sql +0 -28
- data/test/fixtures/currency_value.sql +0 -29
- data/test/fixtures/find_subclasses.sql +0 -43
- data/test/fixtures/ledger_item.sql +0 -39
- data/test/fixtures/line_item.sql +0 -33
- data/test/fixtures/price.sql +0 -4
- data/test/fixtures/tax_rate.sql +0 -4
- data/test/fixtures/taxable.sql +0 -14
- data/test/fixtures/time_dependent.sql +0 -35
- data/test/ledger_item_test.rb +0 -444
- data/test/line_item_test.rb +0 -139
- data/test/models/README +0 -4
- data/test/models/test_subclass_in_another_file.rb +0 -3
- data/test/models/test_subclass_not_in_database.rb +0 -6
- data/test/price_test.rb +0 -9
- data/test/ref-output/creditnote3.html +0 -82
- data/test/ref-output/creditnote3.xml +0 -89
- data/test/ref-output/invoice1.html +0 -93
- data/test/ref-output/invoice1.xml +0 -111
- data/test/ref-output/invoice2.html +0 -86
- data/test/ref-output/invoice2.xml +0 -98
- data/test/ref-output/invoice_null.html +0 -36
- data/test/render_html_test.rb +0 -70
- data/test/render_ubl_test.rb +0 -44
- data/test/setup.rb +0 -37
- data/test/tax_rate_test.rb +0 -9
- data/test/taxable_test.rb +0 -180
- data/test/test_helper.rb +0 -72
- data/test/time_dependent_test.rb +0 -180
- metadata.gz.sig +0 -4
| @@ -1,3 +1,5 @@ | |
| 1 | 
            +
            require "active_support/concern"
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module Invoicing
         | 
| 2 4 | 
             
              # = Input and output of monetary values
         | 
| 3 5 | 
             
              #
         | 
| @@ -56,9 +58,10 @@ module Invoicing | |
| 56 58 | 
             
              # The string returned by a +_formatted+ method is UTF-8 encoded -- remember most currency symbols (except $)
         | 
| 57 59 | 
             
              # are outside basic 7-bit ASCII.
         | 
| 58 60 | 
             
              module CurrencyValue
         | 
| 59 | 
            -
                
         | 
| 61 | 
            +
                extend ActiveSupport::Concern
         | 
| 62 | 
            +
             | 
| 60 63 | 
             
                # Data about currencies, indexed by ISO 4217 code. (Currently a very short list, in need of extending.)
         | 
| 61 | 
            -
                # The values are hashes, in which the following keys are recognised: | 
| 64 | 
            +
                # The values are hashes, in which the following keys are recognised:
         | 
| 62 65 | 
             
                # <tt>:round</tt>::  Smallest unit of the currency in normal use, to which values are rounded. Default is 0.01.
         | 
| 63 66 | 
             
                # <tt>:symbol</tt>:: Symbol or string usually used to denote the currency. Encoded as UTF-8. Default is ISO 4217 code.
         | 
| 64 67 | 
             
                # <tt>:suffix</tt>:: +true+ if the currency symbol appears after the number, +false+ if it appears before. Default +false+.
         | 
| @@ -72,7 +75,7 @@ module Invoicing | |
| 72 75 | 
             
                  'INR' => {:symbol => "\xE2\x82\xA8"},                   # Indian Rupee
         | 
| 73 76 | 
             
                  'JPY' => {:symbol => "\xC2\xA5",     :round  => 1}      # Japanese Yen
         | 
| 74 77 | 
             
                }
         | 
| 75 | 
            -
             | 
| 78 | 
            +
             | 
| 76 79 | 
             
                module ActMethods
         | 
| 77 80 | 
             
                  # Declares that the current model object has columns storing monetary amounts. Pass those attribute
         | 
| 78 81 | 
             
                  # names to +acts_as_currency_value+. By default, we try to find an attribute or method called +currency+,
         | 
| @@ -111,26 +114,30 @@ module Invoicing | |
| 111 114 | 
             
                  # (The example above is actually a real part of +LedgerItem+.)
         | 
| 112 115 | 
             
                  def acts_as_currency_value(*args)
         | 
| 113 116 | 
             
                    Invoicing::ClassInfo.acts_as(Invoicing::CurrencyValue, self, args)
         | 
| 114 | 
            -
             | 
| 115 | 
            -
                    # Register callback if this is the first time acts_as_currency_value has been called
         | 
| 116 | 
            -
                    before_save :write_back_currency_values if currency_value_class_info.previous_info.nil?
         | 
| 117 117 | 
             
                  end
         | 
| 118 118 | 
             
                end
         | 
| 119 119 |  | 
| 120 | 
            +
                included do
         | 
| 121 | 
            +
                  # Register callback if this is the first time acts_as_currency_value has been called
         | 
| 122 | 
            +
                  before_save :write_back_currency_values, :if => "currency_value_class_info.previous_info.nil?"
         | 
| 123 | 
            +
                end
         | 
| 124 | 
            +
             | 
| 120 125 | 
             
                # Format a numeric monetary value into a human-readable string, in the currency of the
         | 
| 121 126 | 
             
                # current model object.
         | 
| 122 127 | 
             
                def format_currency_value(value, options={})
         | 
| 123 128 | 
             
                  currency_value_class_info.format_value(self, value, options)
         | 
| 124 129 | 
             
                end
         | 
| 125 | 
            -
             | 
| 126 | 
            -
             | 
| 130 | 
            +
             | 
| 131 | 
            +
             | 
| 127 132 | 
             
                # Called automatically via +before_save+. Writes the result of converting +CurrencyValue+ attributes
         | 
| 128 133 | 
             
                # back to the actual attributes, so that they are saved in the database. (This doesn't happen in
         | 
| 129 134 | 
             
                # +convert_currency_values+ to avoid losing the +_before_type_cast+ attribute values.)
         | 
| 130 135 | 
             
                def write_back_currency_values
         | 
| 131 | 
            -
                  currency_value_class_info.all_args.each  | 
| 136 | 
            +
                  currency_value_class_info.all_args.each do |attr|
         | 
| 137 | 
            +
                    write_attribute(attr, send(attr))
         | 
| 138 | 
            +
                  end
         | 
| 132 139 | 
             
                end
         | 
| 133 | 
            -
             | 
| 140 | 
            +
             | 
| 134 141 | 
             
                protected :write_back_currency_values
         | 
| 135 142 |  | 
| 136 143 |  | 
| @@ -138,7 +145,7 @@ module Invoicing | |
| 138 145 | 
             
                # These methods do not depend on ActiveRecord and can thus also be called externally.
         | 
| 139 146 | 
             
                module Formatter
         | 
| 140 147 | 
             
                  class << self
         | 
| 141 | 
            -
             | 
| 148 | 
            +
             | 
| 142 149 | 
             
                    # Given the three-letter ISO 4217 code of a currency, returns a hash with useful bits of information:
         | 
| 143 150 | 
             
                    # <tt>:code</tt>::   The ISO 4217 code of the currency.
         | 
| 144 151 | 
             
                    # <tt>:round</tt>::  Smallest unit of the currency in normal use, to which values are rounded. Default is 0.01.
         | 
| @@ -154,15 +161,15 @@ module Invoicing | |
| 154 161 | 
             
                        info.update(::Invoicing::CurrencyValue::CURRENCIES[code])
         | 
| 155 162 | 
             
                      end
         | 
| 156 163 | 
             
                      options.each_pair {|key, value| info[key] = value if valid_options.include? key }
         | 
| 157 | 
            -
             | 
| 164 | 
            +
             | 
| 158 165 | 
             
                      info[:suffix] ||= (info[:code] == info[:symbol]) && !info[:code].nil?
         | 
| 159 166 | 
             
                      info[:space]  ||= info[:suffix]
         | 
| 160 167 | 
             
                      info[:digits] = -Math.log10(info[:round]).floor if info[:digits].nil?
         | 
| 161 168 | 
             
                      info[:digits] = 0 if info[:digits] < 0
         | 
| 162 | 
            -
             | 
| 169 | 
            +
             | 
| 163 170 | 
             
                      info
         | 
| 164 171 | 
             
                    end
         | 
| 165 | 
            -
             | 
| 172 | 
            +
             | 
| 166 173 | 
             
                    # Given the three-letter ISO 4217 code of a currency and a BigDecimal value, returns the
         | 
| 167 174 | 
             
                    # value formatted as an UTF-8 string, ready for human consumption.
         | 
| 168 175 | 
             
                    #
         | 
| @@ -170,17 +177,17 @@ module Invoicing | |
| 170 177 | 
             
                    # as decimal separator and the comma as thousands separator.
         | 
| 171 178 | 
             
                    def format_value(currency_code, value, options={})
         | 
| 172 179 | 
             
                      info = currency_info(currency_code, options)
         | 
| 173 | 
            -
             | 
| 180 | 
            +
             | 
| 174 181 | 
             
                      negative = false
         | 
| 175 182 | 
             
                      if value < 0
         | 
| 176 183 | 
             
                        negative = true
         | 
| 177 184 | 
             
                        value = -value
         | 
| 178 185 | 
             
                      end
         | 
| 179 | 
            -
             | 
| 186 | 
            +
             | 
| 180 187 | 
             
                      value = "%.#{info[:digits]}f" % value
         | 
| 181 188 | 
             
                      while value.sub!(/(\d+)(\d\d\d)/, '\1,\2'); end
         | 
| 182 189 | 
             
                      value.sub!(/^\-/, '') # avoid displaying minus zero
         | 
| 183 | 
            -
             | 
| 190 | 
            +
             | 
| 184 191 | 
             
                      formatted = if ['', nil].include? info[:symbol]
         | 
| 185 192 | 
             
                        value
         | 
| 186 193 | 
             
                      elsif info[:space]
         | 
| @@ -188,26 +195,26 @@ module Invoicing | |
| 188 195 | 
             
                      else
         | 
| 189 196 | 
             
                        info[:suffix] ? "#{value}#{info[:symbol]}" : "#{info[:symbol]}#{value}"
         | 
| 190 197 | 
             
                      end
         | 
| 191 | 
            -
             | 
| 198 | 
            +
             | 
| 192 199 | 
             
                      if negative
         | 
| 193 200 | 
             
                        # default is to use proper unicode minus sign
         | 
| 194 201 | 
             
                        formatted = (options[:negative] == :brackets) ? "(#{formatted})" : (
         | 
| 195 202 | 
             
                          (options[:negative] == :hyphen) ? "-#{formatted}" : "\xE2\x88\x92#{formatted}"
         | 
| 196 203 | 
             
                        )
         | 
| 197 204 | 
             
                      end
         | 
| 198 | 
            -
                      formatted
         | 
| 205 | 
            +
                      formatted.force_encoding("utf-8")
         | 
| 199 206 | 
             
                    end
         | 
| 200 207 | 
             
                  end
         | 
| 201 208 | 
             
                end
         | 
| 202 209 |  | 
| 203 210 |  | 
| 204 211 | 
             
                class ClassInfo < Invoicing::ClassInfo::Base #:nodoc:
         | 
| 205 | 
            -
             | 
| 212 | 
            +
             | 
| 206 213 | 
             
                  def initialize(model_class, previous_info, args)
         | 
| 207 214 | 
             
                    super
         | 
| 208 215 | 
             
                    new_args.each{|attr| generate_attrs(attr)}
         | 
| 209 216 | 
             
                  end
         | 
| 210 | 
            -
             | 
| 217 | 
            +
             | 
| 211 218 | 
             
                  # Generates the getter and setter method for attribute +attr+.
         | 
| 212 219 | 
             
                  def generate_attrs(attr)
         | 
| 213 220 | 
             
                    model_class.class_eval do
         | 
| @@ -215,23 +222,23 @@ module Invoicing | |
| 215 222 | 
             
                        currency_info = currency_value_class_info.currency_info_for(self)
         | 
| 216 223 | 
             
                        return read_attribute(attr) if currency_info.nil?
         | 
| 217 224 | 
             
                        round_factor = BigDecimal(currency_info[:round].to_s)
         | 
| 218 | 
            -
             | 
| 225 | 
            +
             | 
| 219 226 | 
             
                        value = currency_value_class_info.attr_conversion_input(self, attr)
         | 
| 220 227 | 
             
                        value.nil? ? nil : (value / round_factor).round * round_factor
         | 
| 221 228 | 
             
                      end
         | 
| 222 | 
            -
             | 
| 229 | 
            +
             | 
| 223 230 | 
             
                      define_method("#{attr}=") do |new_value|
         | 
| 224 231 | 
             
                        write_attribute(attr, new_value)
         | 
| 225 232 | 
             
                      end
         | 
| 226 | 
            -
             | 
| 233 | 
            +
             | 
| 227 234 | 
             
                      define_method("#{attr}_formatted") do |*args|
         | 
| 228 235 | 
             
                        options = args.first || {}
         | 
| 229 236 | 
             
                        value_as_float = begin
         | 
| 230 | 
            -
                          Kernel.Float(send("#{attr}_before_type_cast")) | 
| 237 | 
            +
                          Kernel.Float(send("#{attr}_before_type_cast"))
         | 
| 231 238 | 
             
                        rescue ArgumentError, TypeError
         | 
| 232 239 | 
             
                          nil
         | 
| 233 240 | 
             
                        end
         | 
| 234 | 
            -
             | 
| 241 | 
            +
             | 
| 235 242 | 
             
                        if value_as_float.nil?
         | 
| 236 243 | 
             
                          ''
         | 
| 237 244 | 
             
                        else
         | 
| @@ -240,7 +247,7 @@ module Invoicing | |
| 240 247 | 
             
                      end
         | 
| 241 248 | 
             
                    end
         | 
| 242 249 | 
             
                  end
         | 
| 243 | 
            -
             | 
| 250 | 
            +
             | 
| 244 251 | 
             
                  # Returns the value of the currency code column of +object+, if available; otherwise the
         | 
| 245 252 | 
             
                  # default currency code (set by the <tt>:currency_code</tt> option), if available; +nil+ if all
         | 
| 246 253 | 
             
                  # else fails.
         | 
| @@ -251,12 +258,12 @@ module Invoicing | |
| 251 258 | 
             
                      all_options[:currency_code]
         | 
| 252 259 | 
             
                    end
         | 
| 253 260 | 
             
                  end
         | 
| 254 | 
            -
             | 
| 261 | 
            +
             | 
| 255 262 | 
             
                  # Returns a hash of information about the currency used by model +object+.
         | 
| 256 263 | 
             
                  def currency_info_for(object)
         | 
| 257 264 | 
             
                    ::Invoicing::CurrencyValue::Formatter.currency_info(currency_of(object), all_options)
         | 
| 258 265 | 
             
                  end
         | 
| 259 | 
            -
             | 
| 266 | 
            +
             | 
| 260 267 | 
             
                  # Formats a numeric value as a nice currency string in UTF-8 encoding.
         | 
| 261 268 | 
             
                  # +object+ is the model object carrying the value (used to determine the currency).
         | 
| 262 269 | 
             
                  def format_value(object, value, options={})
         | 
| @@ -267,16 +274,16 @@ module Invoicing | |
| 267 274 | 
             
                    end
         | 
| 268 275 | 
             
                    ::Invoicing::CurrencyValue::Formatter.format_value(currency_of(object), value, options)
         | 
| 269 276 | 
             
                  end
         | 
| 270 | 
            -
             | 
| 277 | 
            +
             | 
| 271 278 | 
             
                  # If other modules have registered callbacks for the event of reading a rounded attribute,
         | 
| 272 279 | 
             
                  # they are executed here. +attr+ is the name of the attribute being read.
         | 
| 273 280 | 
             
                  def attr_conversion_input(object, attr)
         | 
| 274 281 | 
             
                    value = nil
         | 
| 275 | 
            -
             | 
| 282 | 
            +
             | 
| 276 283 | 
             
                    if callback = all_options[:conversion_input]
         | 
| 277 284 | 
             
                      value = object.send(callback, attr)
         | 
| 278 285 | 
             
                    end
         | 
| 279 | 
            -
             | 
| 286 | 
            +
             | 
| 280 287 | 
             
                    unless value
         | 
| 281 288 | 
             
                      raw_value = object.read_attribute(attr)
         | 
| 282 289 | 
             
                      value = BigDecimal.new(raw_value.to_s) unless raw_value.nil?
         | 
| @@ -12,14 +12,14 @@ module Invoicing | |
| 12 12 | 
             
              #     extend Invoicing::FindSubclasses
         | 
| 13 13 | 
             
              #     def self.needs_refrigeration; false; end
         | 
| 14 14 | 
             
              #   end
         | 
| 15 | 
            -
              # | 
| 15 | 
            +
              #
         | 
| 16 16 | 
             
              #   class Food < Product; end
         | 
| 17 17 | 
             
              #   class Bread < Food; end
         | 
| 18 18 | 
             
              #   class Yoghurt < Food
         | 
| 19 19 | 
             
              #     def self.needs_refrigeration; true; end
         | 
| 20 20 | 
             
              #   end
         | 
| 21 21 | 
             
              #   class GreekYoghurt < Yoghurt; end
         | 
| 22 | 
            -
              # | 
| 22 | 
            +
              #
         | 
| 23 23 | 
             
              #   class Drink < Product; end
         | 
| 24 24 | 
             
              #   class SoftDrink < Drink; end
         | 
| 25 25 | 
             
              #   class Smoothie < Drink
         | 
| @@ -72,11 +72,11 @@ module Invoicing | |
| 72 72 | 
             
              # <tt>Class#inherited</tt> method; we use this to gather together a list of subclasses. Of course,
         | 
| 73 73 | 
             
              # we won't necessarily know about every class in the world which may subclass our class; in
         | 
| 74 74 | 
             
              # particular, <tt>Class#inherited</tt> won't be called until that subclass is loaded.
         | 
| 75 | 
            -
              # | 
| 75 | 
            +
              #
         | 
| 76 76 | 
             
              # If you're including the Ruby files with the subclass definitions using +require+, we will learn
         | 
| 77 | 
            -
              # about subclasses as soon as they are defined. However, if class loading is delayed until a | 
| 77 | 
            +
              # about subclasses as soon as they are defined. However, if class loading is delayed until a
         | 
| 78 78 | 
             
              # class is first used (for example, <tt>ActiveSupport::Dependencies</tt> does this with model
         | 
| 79 | 
            -
              # objects in Rails projects), we could run into a situation where we don't yet know about all | 
| 79 | 
            +
              # objects in Rails projects), we could run into a situation where we don't yet know about all
         | 
| 80 80 | 
             
              # subclasses used in a project at the point where we need to process a class method condition.
         | 
| 81 81 | 
             
              # This would cause us to omit some objects we should have found.
         | 
| 82 82 | 
             
              #
         | 
| @@ -89,7 +89,7 @@ module Invoicing | |
| 89 89 | 
             
              # to be on the safe side you can ensure all subclasses are loaded when your application
         | 
| 90 90 | 
             
              # initialises -- but that's not completely DRY ;-)
         | 
| 91 91 | 
             
              module FindSubclasses
         | 
| 92 | 
            -
             | 
| 92 | 
            +
             | 
| 93 93 | 
             
                # Overrides <tt>ActiveRecord::Base.sanitize_sql_hash_for_conditions</tt> since this is the method
         | 
| 94 94 | 
             
                # used to transform a hash of conditions into an SQL query fragment. This overriding method
         | 
| 95 95 | 
             
                # searches for class method conditions in the hash and transforms them into a condition on the
         | 
| @@ -103,14 +103,14 @@ module Invoicing | |
| 103 103 | 
             
                #   {:my_class_method => false}       # known_subclasses.reject{|cls| cls.my_class_method }
         | 
| 104 104 | 
             
                def sanitize_sql_hash_for_conditions(attrs, table_name = quoted_table_name)
         | 
| 105 105 | 
             
                  new_attrs = {}
         | 
| 106 | 
            -
             | 
| 106 | 
            +
             | 
| 107 107 | 
             
                  attrs.each_pair do |attr, value|
         | 
| 108 108 | 
             
                    attr = attr_base = attr.to_s
         | 
| 109 109 | 
             
                    attr_table_name = table_name
         | 
| 110 110 |  | 
| 111 111 | 
             
                    # Extract table name from qualified attribute names
         | 
| 112 112 | 
             
                    attr_table_name, attr_base = attr.split('.', 2) if attr.include?('.')
         | 
| 113 | 
            -
             | 
| 113 | 
            +
             | 
| 114 114 | 
             
                    if columns_hash.include?(attr_base) || ![self.table_name, quoted_table_name].include?(attr_table_name)
         | 
| 115 115 | 
             
                      new_attrs[attr] = value   # Condition on a table column, or another table -- pass through unmodified
         | 
| 116 116 | 
             
                    else
         | 
| @@ -125,10 +125,35 @@ module Invoicing | |
| 125 125 |  | 
| 126 126 | 
             
                  super(new_attrs, table_name)
         | 
| 127 127 | 
             
                end
         | 
| 128 | 
            -
             | 
| 128 | 
            +
             | 
| 129 | 
            +
                def expand_hash_conditions_for_aggregates(attrs)
         | 
| 130 | 
            +
                  new_attrs = {}
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                  attrs.each_pair do |attr, value|
         | 
| 133 | 
            +
                    attr = attr_base = attr.to_s
         | 
| 134 | 
            +
                    attr_table_name = table_name
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                    # Extract table name from qualified attribute names
         | 
| 137 | 
            +
                    attr_table_name, attr_base = attr.split('.', 2) if attr.include?('.')
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                    if columns_hash.include?(attr_base) || ![self.table_name, quoted_table_name].include?(attr_table_name)
         | 
| 140 | 
            +
                      new_attrs[attr] = value   # Condition on a table column, or another table -- pass through unmodified
         | 
| 141 | 
            +
                    else
         | 
| 142 | 
            +
                      begin
         | 
| 143 | 
            +
                        matching_classes = select_matching_subclasses(attr_base, value)
         | 
| 144 | 
            +
                        new_attrs["#{self.table_name}.#{inheritance_column}"] = matching_classes.map{|cls| cls.name.to_s}
         | 
| 145 | 
            +
                      rescue NoMethodError
         | 
| 146 | 
            +
                        new_attrs[attr] = value # If the class method doesn't exist, fall back to passing condition through unmodified
         | 
| 147 | 
            +
                      end
         | 
| 148 | 
            +
                    end
         | 
| 149 | 
            +
                  end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                  super(new_attrs)
         | 
| 152 | 
            +
                end
         | 
| 153 | 
            +
             | 
| 129 154 | 
             
                # Returns a list of those classes within +known_subclasses+ which match a condition
         | 
| 130 155 | 
             
                # <tt>method_name => value</tt>. May raise +NoMethodError+ if a class object does not
         | 
| 131 | 
            -
                # respond to +method_name+. | 
| 156 | 
            +
                # respond to +method_name+.
         | 
| 132 157 | 
             
                def select_matching_subclasses(method_name, value, table = table_name, type_column = inheritance_column)
         | 
| 133 158 | 
             
                  known_subclasses(table, type_column).select do |cls|
         | 
| 134 159 | 
             
                    returned = cls.send(method_name)
         | 
| @@ -139,28 +164,28 @@ module Invoicing | |
| 139 164 | 
             
                    end
         | 
| 140 165 | 
             
                  end
         | 
| 141 166 | 
             
                end
         | 
| 142 | 
            -
             | 
| 167 | 
            +
             | 
| 143 168 | 
             
                # Ruby callback which is invoked when a subclass is created. We use this to build a list of known
         | 
| 144 169 | 
             
                # subclasses.
         | 
| 145 170 | 
             
                def inherited(subclass)
         | 
| 146 171 | 
             
                  remember_subclass subclass
         | 
| 147 172 | 
             
                  super
         | 
| 148 173 | 
             
                end
         | 
| 149 | 
            -
             | 
| 174 | 
            +
             | 
| 150 175 | 
             
                # Add +subclass+ to the list of know subclasses of this class.
         | 
| 151 176 | 
             
                def remember_subclass(subclass)
         | 
| 152 177 | 
             
                  @known_subclasses ||= [self]
         | 
| 153 178 | 
             
                  @known_subclasses << subclass unless @known_subclasses.include? subclass
         | 
| 154 179 | 
             
                  self.superclass.remember_subclass(subclass) if self.superclass.respond_to? :remember_subclass
         | 
| 155 180 | 
             
                end
         | 
| 156 | 
            -
             | 
| 181 | 
            +
             | 
| 157 182 | 
             
                # Return the list of all known subclasses of this class, if necessary checking the database for
         | 
| 158 183 | 
             
                # classes which have not yet been loaded.
         | 
| 159 184 | 
             
                def known_subclasses(table = table_name, type_column = inheritance_column)
         | 
| 160 185 | 
             
                  load_all_subclasses_found_in_database(table, type_column)
         | 
| 161 186 | 
             
                  @known_subclasses ||= [self]
         | 
| 162 187 | 
             
                end
         | 
| 163 | 
            -
             | 
| 188 | 
            +
             | 
| 164 189 | 
             
              private
         | 
| 165 190 | 
             
                # Query the database for all qualified class names found in the +type_column+ column
         | 
| 166 191 | 
             
                # (called +type+ by default), and check that classes of that name have been loaded by the Ruby
         | 
| @@ -190,4 +215,4 @@ module Invoicing | |
| 190 215 | 
             
                  end
         | 
| 191 216 | 
             
                end
         | 
| 192 217 | 
             
              end
         | 
| 193 | 
            -
            end
         | 
| 218 | 
            +
            end
         | 
| @@ -1,3 +1,7 @@ | |
| 1 | 
            +
            require "active_support/concern"
         | 
| 2 | 
            +
            require "invoicing/ledger_item/render_html"
         | 
| 3 | 
            +
            require "invoicing/ledger_item/render_ubl"
         | 
| 4 | 
            +
             | 
| 1 5 | 
             
            module Invoicing
         | 
| 2 6 | 
             
              # = Ledger item objects
         | 
| 3 7 | 
             
              #
         | 
| @@ -285,7 +289,8 @@ module Invoicing | |
| 285 289 | 
             
              #                            included though). If you're chaining scopes it would be advantageous
         | 
| 286 290 | 
             
              #                            to put this one close to the beginning of your scope chain.
         | 
| 287 291 | 
             
              module LedgerItem
         | 
| 288 | 
            -
                
         | 
| 292 | 
            +
                extend ActiveSupport::Concern
         | 
| 293 | 
            +
             | 
| 289 294 | 
             
                module ActMethods
         | 
| 290 295 | 
             
                  # Declares that the current class is a model for ledger items (i.e. invoices, credit notes and
         | 
| 291 296 | 
             
                  # payment notes).
         | 
| @@ -302,98 +307,33 @@ module Invoicing | |
| 302 307 | 
             
                  #   acts_as_ledger_item :total_amount => :gross_amount
         | 
| 303 308 | 
             
                  def acts_as_ledger_item(*args)
         | 
| 304 309 | 
             
                    Invoicing::ClassInfo.acts_as(Invoicing::LedgerItem, self, args)
         | 
| 305 | 
            -
             | 
| 310 | 
            +
             | 
| 306 311 | 
             
                    info = ledger_item_class_info
         | 
| 307 312 | 
             
                    return unless info.previous_info.nil? # Called for the first time?
         | 
| 308 | 
            -
             | 
| 309 | 
            -
                    before_validation :calculate_total_amount
         | 
| 310 | 
            -
                    
         | 
| 313 | 
            +
             | 
| 311 314 | 
             
                    # Set the 'amount' columns to act as currency values
         | 
| 312 315 | 
             
                    acts_as_currency_value(info.method(:total_amount), info.method(:tax_amount),
         | 
| 313 316 | 
             
                      :currency => info.method(:currency), :value_for_formatting => :value_for_formatting)
         | 
| 314 | 
            -
             | 
| 315 | 
            -
                    extend | 
| 317 | 
            +
             | 
| 318 | 
            +
                    extend  Invoicing::FindSubclasses
         | 
| 316 319 | 
             
                    include Invoicing::LedgerItem::RenderHTML
         | 
| 317 320 | 
             
                    include Invoicing::LedgerItem::RenderUBL
         | 
| 318 | 
            -
                    
         | 
| 319 | 
            -
                    # Dynamically created named scopes
         | 
| 320 | 
            -
                    named_scope :sent_by, lambda{ |sender_id|
         | 
| 321 | 
            -
                      { :conditions => {info.method(:sender_id) => sender_id} }
         | 
| 322 | 
            -
                    }
         | 
| 323 | 
            -
                    
         | 
| 324 | 
            -
                    named_scope :received_by, lambda{ |recipient_id|
         | 
| 325 | 
            -
                      { :conditions => {info.method(:recipient_id) => recipient_id} }
         | 
| 326 | 
            -
                    }
         | 
| 327 | 
            -
                    
         | 
| 328 | 
            -
                    named_scope :sent_or_received_by, lambda{ |sender_or_recipient_id|
         | 
| 329 | 
            -
                      sender_col = connection.quote_column_name(info.method(:sender_id))
         | 
| 330 | 
            -
                      recipient_col = connection.quote_column_name(info.method(:recipient_id))
         | 
| 331 | 
            -
                      { :conditions => ["#{sender_col} = ? OR #{recipient_col} = ?",
         | 
| 332 | 
            -
                                        sender_or_recipient_id, sender_or_recipient_id] }
         | 
| 333 | 
            -
                    }
         | 
| 334 | 
            -
                    
         | 
| 335 | 
            -
                    named_scope :in_effect, :conditions => {info.method(:status) => ['closed', 'cleared']}
         | 
| 336 | 
            -
                    
         | 
| 337 | 
            -
                    named_scope :open_or_pending, :conditions => {info.method(:status) => ['open', 'pending']}
         | 
| 338 | 
            -
                    
         | 
| 339 | 
            -
                    named_scope :due_at, lambda{ |date|
         | 
| 340 | 
            -
                      due_date = connection.quote_column_name(info.method(:due_date))
         | 
| 341 | 
            -
                      {:conditions => ["#{due_date} <= ? OR #{due_date} IS NULL", date]}
         | 
| 342 | 
            -
                    }
         | 
| 343 | 
            -
                    
         | 
| 344 | 
            -
                    named_scope :sorted, lambda{|column|
         | 
| 345 | 
            -
                      column = ledger_item_class_info.method(column).to_s
         | 
| 346 | 
            -
                      if column_names.include?(column)
         | 
| 347 | 
            -
                        {:order => "#{connection.quote_column_name(column)}, #{connection.quote_column_name(primary_key)}"}
         | 
| 348 | 
            -
                      else
         | 
| 349 | 
            -
                        {:order => connection.quote_column_name(primary_key)}
         | 
| 350 | 
            -
                      end
         | 
| 351 | 
            -
                    }
         | 
| 352 | 
            -
                    
         | 
| 353 | 
            -
                    named_scope :exclude_empty_invoices, lambda{
         | 
| 354 | 
            -
                      line_items_assoc_id = info.method(:line_items).to_sym
         | 
| 355 | 
            -
                      line_items_refl = reflections[line_items_assoc_id]
         | 
| 356 | 
            -
                      line_items_table = line_items_refl.quoted_table_name
         | 
| 357 | 
            -
                      
         | 
| 358 | 
            -
                      # e.g. `ledger_items`.`id`
         | 
| 359 | 
            -
                      ledger_items_id = quoted_table_name + "." + connection.quote_column_name(primary_key)
         | 
| 360 | 
            -
                      
         | 
| 361 | 
            -
                      # e.g. `line_items`.`id`
         | 
| 362 | 
            -
                      line_items_id = line_items_table + "." +
         | 
| 363 | 
            -
                        connection.quote_column_name(line_items_refl.klass.primary_key)
         | 
| 364 | 
            -
                      
         | 
| 365 | 
            -
                      # e.g. `line_items`.`ledger_item_id`
         | 
| 366 | 
            -
                      ledger_item_foreign_key = line_items_table + "." + connection.quote_column_name(
         | 
| 367 | 
            -
                        line_items_refl.klass.send(:line_item_class_info).method(:ledger_item_id))
         | 
| 368 | 
            -
                      
         | 
| 369 | 
            -
                      payment_classes = select_matching_subclasses(:is_payment, true).map{|c| c.name}
         | 
| 370 | 
            -
                      is_payment_class = merge_conditions({info.method(:type) => payment_classes})
         | 
| 371 | 
            -
                      
         | 
| 372 | 
            -
                      subquery = construct_finder_sql(
         | 
| 373 | 
            -
                        :select => "#{quoted_table_name}.*, COUNT(#{line_items_id}) AS number_of_line_items",
         | 
| 374 | 
            -
                        :joins => "LEFT JOIN #{line_items_table} ON #{ledger_item_foreign_key} = #{ledger_items_id}",
         | 
| 375 | 
            -
                        :group => Invoicing::ConnectionAdapterExt.group_by_all_columns(self)
         | 
| 376 | 
            -
                      )
         | 
| 377 | 
            -
                        
         | 
| 378 | 
            -
                      {:from => "(#{subquery}) AS #{quoted_table_name}",
         | 
| 379 | 
            -
                       :conditions => "number_of_line_items > 0 OR #{is_payment_class}"}
         | 
| 380 | 
            -
                    }
         | 
| 381 321 | 
             
                  end # def acts_as_ledger_item
         | 
| 382 | 
            -
             | 
| 322 | 
            +
             | 
| 383 323 | 
             
                  # Synonym for <tt>acts_as_ledger_item :subtype => :invoice</tt>. All options other than
         | 
| 384 324 | 
             
                  # <tt>:subtype</tt> are passed on to +acts_as_ledger_item+. You should apply
         | 
| 385 325 | 
             
                  # +acts_as_invoice+ only to a model which is a subclass of an +acts_as_ledger_item+ type.
         | 
| 386 326 | 
             
                  def acts_as_invoice(options={})
         | 
| 387 327 | 
             
                    acts_as_ledger_item(options.clone.update({:subtype => :invoice}))
         | 
| 388 328 | 
             
                  end
         | 
| 389 | 
            -
             | 
| 329 | 
            +
             | 
| 390 330 | 
             
                  # Synonym for <tt>acts_as_ledger_item :subtype => :credit_note</tt>. All options other than
         | 
| 391 331 | 
             
                  # <tt>:subtype</tt> are passed on to +acts_as_ledger_item+. You should apply
         | 
| 392 332 | 
             
                  # +acts_as_credit_note+ only to a model which is a subclass of an +acts_as_ledger_item+ type.
         | 
| 393 333 | 
             
                  def acts_as_credit_note(options={})
         | 
| 394 334 | 
             
                    acts_as_ledger_item(options.clone.update({:subtype => :credit_note}))
         | 
| 395 335 | 
             
                  end
         | 
| 396 | 
            -
             | 
| 336 | 
            +
             | 
| 397 337 | 
             
                  # Synonym for <tt>acts_as_ledger_item :subtype => :payment</tt>. All options other than
         | 
| 398 338 | 
             
                  # <tt>:subtype</tt> are passed on to +acts_as_ledger_item+. You should apply
         | 
| 399 339 | 
             
                  # +acts_as_payment+ only to a model which is a subclass of an +acts_as_ledger_item+ type.
         | 
| @@ -401,7 +341,73 @@ module Invoicing | |
| 401 341 | 
             
                    acts_as_ledger_item(options.clone.update({:subtype => :payment}))
         | 
| 402 342 | 
             
                  end
         | 
| 403 343 | 
             
                end # module ActMethods
         | 
| 404 | 
            -
             | 
| 344 | 
            +
             | 
| 345 | 
            +
                included do
         | 
| 346 | 
            +
                  before_validation :calculate_total_amount
         | 
| 347 | 
            +
             | 
| 348 | 
            +
                  # Dynamically created named scopes
         | 
| 349 | 
            +
                  scope :sent_by, lambda { |sender_id|
         | 
| 350 | 
            +
                    where(ledger_item_class_info.method(:sender_id) => sender_id)
         | 
| 351 | 
            +
                  }
         | 
| 352 | 
            +
             | 
| 353 | 
            +
                  scope :received_by, lambda {|recipient_id|
         | 
| 354 | 
            +
                    where(ledger_item_class_info.method(:recipient_id) => recipient_id)
         | 
| 355 | 
            +
                  }
         | 
| 356 | 
            +
             | 
| 357 | 
            +
                  scope :sent_or_received_by, lambda { |sender_or_recipient_id|
         | 
| 358 | 
            +
                    sender_col = connection.quote_column_name(ledger_item_class_info.method(:sender_id))
         | 
| 359 | 
            +
                    recipient_col = connection.quote_column_name(ledger_item_class_info.method(:recipient_id))
         | 
| 360 | 
            +
             | 
| 361 | 
            +
                    where(["#{sender_col} = ? OR #{recipient_col} = ?",
         | 
| 362 | 
            +
                            sender_or_recipient_id, sender_or_recipient_id])
         | 
| 363 | 
            +
                    }
         | 
| 364 | 
            +
             | 
| 365 | 
            +
                  scope :in_effect, lambda {
         | 
| 366 | 
            +
                    where(ledger_item_class_info.method(:status) => ['closed', 'cleared'])
         | 
| 367 | 
            +
                  }
         | 
| 368 | 
            +
             | 
| 369 | 
            +
                  scope :open_or_pending, lambda {
         | 
| 370 | 
            +
                    where(ledger_item_class_info.method(:status) => ['open', 'pending'])
         | 
| 371 | 
            +
                  }
         | 
| 372 | 
            +
             | 
| 373 | 
            +
                  scope :due_at, lambda { |date|
         | 
| 374 | 
            +
                    due_date = connection.quote_column_name(ledger_item_class_info.method(:due_date))
         | 
| 375 | 
            +
                    where(["#{due_date} <= ? OR #{due_date} IS NULL", date])
         | 
| 376 | 
            +
                  }
         | 
| 377 | 
            +
             | 
| 378 | 
            +
                  scope :sorted, lambda { |column|
         | 
| 379 | 
            +
                    column = ledger_item_class_info.method(column).to_s
         | 
| 380 | 
            +
                    if column_names.include?(column)
         | 
| 381 | 
            +
                      order("#{connection.quote_column_name(column)}, #{connection.quote_column_name(primary_key)}")
         | 
| 382 | 
            +
                    else
         | 
| 383 | 
            +
                      order(connection.quote_column_name(primary_key))
         | 
| 384 | 
            +
                    end
         | 
| 385 | 
            +
                  }
         | 
| 386 | 
            +
             | 
| 387 | 
            +
                  scope :exclude_empty_invoices, lambda {
         | 
| 388 | 
            +
                    line_items_assoc_id = ledger_item_class_info.method(:line_items).to_sym
         | 
| 389 | 
            +
                    line_items_refl = reflections[line_items_assoc_id]
         | 
| 390 | 
            +
                    line_items_table = line_items_refl.quoted_table_name
         | 
| 391 | 
            +
             | 
| 392 | 
            +
                    # e.g. `ledger_items`.`id`
         | 
| 393 | 
            +
                    ledger_items_id = quoted_table_name + "." + connection.quote_column_name(primary_key)
         | 
| 394 | 
            +
             | 
| 395 | 
            +
                    # e.g. `line_items`.`id`
         | 
| 396 | 
            +
                    line_items_id = line_items_table + "." +
         | 
| 397 | 
            +
                      connection.quote_column_name(line_items_refl.klass.primary_key)
         | 
| 398 | 
            +
             | 
| 399 | 
            +
                    # e.g. `line_items`.`ledger_item_id`
         | 
| 400 | 
            +
                    ledger_item_foreign_key = line_items_table + "." + connection.quote_column_name(
         | 
| 401 | 
            +
                                                                                                    line_items_refl.klass.send(:line_item_class_info).method(:ledger_item_id))
         | 
| 402 | 
            +
             | 
| 403 | 
            +
                    payment_classes = select_matching_subclasses(:is_payment, true).map{|c| c.name}
         | 
| 404 | 
            +
                    is_payment_class = merge_conditions({ledger_item_class_info.method(:type) => payment_classes})
         | 
| 405 | 
            +
             | 
| 406 | 
            +
                    joins("LEFT JOIN #{line_items_table} ON #{ledger_item_foreign_key} = #{ledger_items_id}").
         | 
| 407 | 
            +
                    where("(#{ledger_item_foreign_key} IS NULL) OR #{is_payment_class}")
         | 
| 408 | 
            +
                  }
         | 
| 409 | 
            +
                end
         | 
| 410 | 
            +
             | 
| 405 411 | 
             
                # Overrides the default constructor of <tt>ActiveRecord::Base</tt> when +acts_as_ledger_item+
         | 
| 406 412 | 
             
                # is called. If the +uuid+ gem is installed, this constructor creates a new UUID and assigns
         | 
| 407 413 | 
             
                # it to the +uuid+ property when a new ledger item model object is created.
         | 
| @@ -413,7 +419,7 @@ module Invoicing | |
| 413 419 | 
             
                    write_attribute(info.method(:uuid), info.uuid_generator.generate)
         | 
| 414 420 | 
             
                  end
         | 
| 415 421 | 
             
                end
         | 
| 416 | 
            -
             | 
| 422 | 
            +
             | 
| 417 423 | 
             
                # Calculate sum of net_amount and tax_amount across all line items, and assign it to total_amount;
         | 
| 418 424 | 
             
                # calculate sum of tax_amount across all line items, and assign it to tax_amount.
         | 
| 419 425 | 
             
                # Called automatically as a +before_validation+ callback. If the LedgerItem subtype is +payment+
         | 
| @@ -423,25 +429,26 @@ module Invoicing | |
| 423 429 | 
             
                  return if self.class.is_payment && line_items.empty?
         | 
| 424 430 |  | 
| 425 431 | 
             
                  net_total = tax_total = BigDecimal('0')
         | 
| 426 | 
            -
             | 
| 432 | 
            +
             | 
| 427 433 | 
             
                  line_items.each do |line|
         | 
| 428 434 | 
             
                    info = line.send(:line_item_class_info)
         | 
| 429 | 
            -
             | 
| 435 | 
            +
             | 
| 430 436 | 
             
                    # Make sure ledger_item association is assigned -- the CurrencyValue
         | 
| 431 437 | 
             
                    # getters depend on it to fetch the currency
         | 
| 432 438 | 
             
                    info.set(line, :ledger_item, self)
         | 
| 433 439 | 
             
                    line.valid? # Ensure any before_validation hooks are called
         | 
| 434 | 
            -
             | 
| 440 | 
            +
             | 
| 435 441 | 
             
                    net_amount = info.get(line, :net_amount)
         | 
| 436 442 | 
             
                    tax_amount = info.get(line, :tax_amount)
         | 
| 437 443 | 
             
                    net_total += net_amount unless net_amount.nil?
         | 
| 438 444 | 
             
                    tax_total += tax_amount unless tax_amount.nil?
         | 
| 439 445 | 
             
                  end
         | 
| 440 | 
            -
             | 
| 446 | 
            +
             | 
| 441 447 | 
             
                  ledger_item_class_info.set(self, :total_amount, net_total + tax_total)
         | 
| 442 448 | 
             
                  ledger_item_class_info.set(self, :tax_amount,   tax_total)
         | 
| 449 | 
            +
                  return net_total
         | 
| 443 450 | 
             
                end
         | 
| 444 | 
            -
             | 
| 451 | 
            +
             | 
| 445 452 | 
             
                # We don't actually implement anything using +method_missing+ at the moment, but use it to
         | 
| 446 453 | 
             
                # generate slightly more useful error messages in certain cases.
         | 
| 447 454 | 
             
                def method_missing(method_id, *args)
         | 
| @@ -461,13 +468,13 @@ module Invoicing | |
| 461 468 | 
             
                  tax_amount   = ledger_item_class_info.get(self, :tax_amount)
         | 
| 462 469 | 
             
                  (total_amount && tax_amount) ? (total_amount - tax_amount) : nil
         | 
| 463 470 | 
             
                end
         | 
| 464 | 
            -
             | 
| 471 | 
            +
             | 
| 465 472 | 
             
                # +net_amount+ formatted in human-readable form using the ledger item's currency.
         | 
| 466 473 | 
             
                def net_amount_formatted
         | 
| 467 474 | 
             
                  format_currency_value(net_amount)
         | 
| 468 475 | 
             
                end
         | 
| 469 | 
            -
             | 
| 470 | 
            -
             | 
| 476 | 
            +
             | 
| 477 | 
            +
             | 
| 471 478 | 
             
                # You must overwrite this method in subclasses of +Invoice+, +CreditNote+ and +Payment+ so that it returns
         | 
| 472 479 | 
             
                # details of the party sending the document. See +sender_id+ above for a detailed interpretation of
         | 
| 473 480 | 
             
                # sender and receiver.
         | 
| @@ -501,28 +508,28 @@ module Invoicing | |
| 501 508 | 
             
                def sender_details
         | 
| 502 509 | 
             
                  raise 'overwrite this method'
         | 
| 503 510 | 
             
                end
         | 
| 504 | 
            -
             | 
| 511 | 
            +
             | 
| 505 512 | 
             
                # You must overwrite this method in subclasses of +Invoice+, +CreditNote+ and +Payment+ so that it returns
         | 
| 506 513 | 
             
                # details of the party receiving the document. See +recipient_id+ above for a detailed interpretation of
         | 
| 507 514 | 
             
                # sender and receiver. See +sender_details+ for a list of fields to return in the hash.
         | 
| 508 515 | 
             
                def recipient_details
         | 
| 509 516 | 
             
                  raise 'overwrite this method'
         | 
| 510 517 | 
             
                end
         | 
| 511 | 
            -
             | 
| 518 | 
            +
             | 
| 512 519 | 
             
                # Returns +true+ if this document was sent by the user with ID +user_id+. If the argument is +nil+
         | 
| 513 520 | 
             
                # (indicating yourself), this also returns +true+ if <tt>sender_details[:is_self]</tt>.
         | 
| 514 521 | 
             
                def sent_by?(user_id)
         | 
| 515 522 | 
             
                  (ledger_item_class_info.get(self, :sender_id) == user_id) ||
         | 
| 516 523 | 
             
                    !!(user_id.nil? && ledger_item_class_info.get(self, :sender_details)[:is_self])
         | 
| 517 524 | 
             
                end
         | 
| 518 | 
            -
             | 
| 525 | 
            +
             | 
| 519 526 | 
             
                # Returns +true+ if this document was received by the user with ID +user_id+. If the argument is +nil+
         | 
| 520 527 | 
             
                # (indicating yourself), this also returns +true+ if <tt>recipient_details[:is_self]</tt>.
         | 
| 521 528 | 
             
                def received_by?(user_id)
         | 
| 522 529 | 
             
                  (ledger_item_class_info.get(self, :recipient_id) == user_id) ||
         | 
| 523 530 | 
             
                    !!(user_id.nil? && ledger_item_class_info.get(self, :recipient_details)[:is_self])
         | 
| 524 531 | 
             
                end
         | 
| 525 | 
            -
             | 
| 532 | 
            +
             | 
| 526 533 | 
             
                # Returns a boolean which specifies whether this transaction should be recorded as a debit (+true+)
         | 
| 527 534 | 
             
                # or a credit (+false+) on a particular ledger. Unless you know what you are doing, you probably
         | 
| 528 535 | 
             
                # do not need to touch this method.
         | 
| @@ -560,8 +567,8 @@ module Invoicing | |
| 560 567 | 
             
                  value = -value if (options[:credit] == :negative) && !debit?(options[:self_id])
         | 
| 561 568 | 
             
                  value
         | 
| 562 569 | 
             
                end
         | 
| 563 | 
            -
             | 
| 564 | 
            -
             | 
| 570 | 
            +
             | 
| 571 | 
            +
             | 
| 565 572 | 
             
                module ClassMethods
         | 
| 566 573 | 
             
                  # Returns +true+ if this type of ledger item should be recorded as a debit when the party
         | 
| 567 574 | 
             
                  # viewing the account is the sender of the document, and recorded as a credit when
         | 
| @@ -578,22 +585,22 @@ module Invoicing | |
| 578 585 | 
             
                      else nil
         | 
| 579 586 | 
             
                    end
         | 
| 580 587 | 
             
                  end
         | 
| 581 | 
            -
             | 
| 588 | 
            +
             | 
| 582 589 | 
             
                  # Returns +true+ if this type of ledger item is a +invoice+ subtype, and +false+ otherwise.
         | 
| 583 590 | 
             
                  def is_invoice
         | 
| 584 591 | 
             
                    ledger_item_class_info.subtype == :invoice
         | 
| 585 592 | 
             
                  end
         | 
| 586 | 
            -
             | 
| 593 | 
            +
             | 
| 587 594 | 
             
                  # Returns +true+ if this type of ledger item is a +credit_note+ subtype, and +false+ otherwise.
         | 
| 588 595 | 
             
                  def is_credit_note
         | 
| 589 596 | 
             
                    ledger_item_class_info.subtype == :credit_note
         | 
| 590 597 | 
             
                  end
         | 
| 591 | 
            -
             | 
| 598 | 
            +
             | 
| 592 599 | 
             
                  # Returns +true+ if this type of ledger item is a +payment+ subtype, and +false+ otherwise.
         | 
| 593 600 | 
             
                  def is_payment
         | 
| 594 601 | 
             
                    ledger_item_class_info.subtype == :payment
         | 
| 595 602 | 
             
                  end
         | 
| 596 | 
            -
             | 
| 603 | 
            +
             | 
| 597 604 | 
             
                  # Returns a summary of the customer or supplier account between two parties identified
         | 
| 598 605 | 
             
                  # by +self_id+ (the party from whose perspective the account is seen, 'you') and +other_id+
         | 
| 599 606 | 
             
                  # ('them', your supplier/customer). The return value is a hash with ISO 4217 currency codes
         | 
| @@ -627,7 +634,7 @@ module Invoicing | |
| 627 634 | 
             
                    info = ledger_item_class_info
         | 
| 628 635 | 
             
                    self_id = self_id.to_i
         | 
| 629 636 | 
             
                    other_id = [nil, ''].include?(other_id) ? nil : other_id.to_i
         | 
| 630 | 
            -
             | 
| 637 | 
            +
             | 
| 631 638 | 
             
                    if other_id.nil?
         | 
| 632 639 | 
             
                      result = {}
         | 
| 633 640 | 
             
                      # Sum over all others, grouped by currency
         | 
| @@ -641,16 +648,13 @@ module Invoicing | |
| 641 648 | 
             
                        end
         | 
| 642 649 | 
             
                      end
         | 
| 643 650 | 
             
                      result
         | 
| 644 | 
            -
                      
         | 
| 645 651 | 
             
                    else
         | 
| 646 652 | 
             
                      conditions = {info.method(:sender_id)    => [self_id, other_id],
         | 
| 647 653 | 
             
                                    info.method(:recipient_id) => [self_id, other_id]}
         | 
| 648 | 
            -
                       | 
| 649 | 
            -
                        account_summaries(self_id, options)[other_id] || {}
         | 
| 650 | 
            -
                      end
         | 
| 654 | 
            +
                      where(conditions).account_summaries(self_id, options)[other_id] || {}
         | 
| 651 655 | 
             
                    end
         | 
| 652 656 | 
             
                  end
         | 
| 653 | 
            -
             | 
| 657 | 
            +
             | 
| 654 658 | 
             
                  # Returns a summary account status for all customers or suppliers with which a particular party
         | 
| 655 659 | 
             
                  # has dealings. Takes into account all +closed+ invoices/credit notes and all +cleared+ payments
         | 
| 656 660 | 
             
                  # which have +self_id+ as their +sender_id+ or +recipient_id+. Returns a hash whose keys are the
         | 
| @@ -678,44 +682,38 @@ module Invoicing | |
| 678 682 | 
             
                  def account_summaries(self_id, options={})
         | 
| 679 683 | 
             
                    info = ledger_item_class_info
         | 
| 680 684 | 
             
                    ext = Invoicing::ConnectionAdapterExt
         | 
| 681 | 
            -
             | 
| 682 | 
            -
                    
         | 
| 685 | 
            +
             | 
| 683 686 | 
             
                    debit_classes  = select_matching_subclasses(:debit_when_sent_by_self, true,  self.table_name, self.inheritance_column).map{|c| c.name}
         | 
| 684 687 | 
             
                    credit_classes = select_matching_subclasses(:debit_when_sent_by_self, false, self.table_name, self.inheritance_column).map{|c| c.name}
         | 
| 685 | 
            -
             | 
| 686 | 
            -
                     | 
| 687 | 
            -
                     | 
| 688 | 
            -
                     | 
| 688 | 
            +
             | 
| 689 | 
            +
                    # rails 3 idiocricies. in case of STI, type of base class is nil. Need special handling
         | 
| 690 | 
            +
                    debit_when_sent      = merge_conditions(inheritance_condition(debit_classes),  info.method(:sender_id)    => self_id)
         | 
| 691 | 
            +
                    debit_when_received  = merge_conditions(inheritance_condition(credit_classes), info.method(:recipient_id) => self_id)
         | 
| 692 | 
            +
                    credit_when_sent     = merge_conditions(inheritance_condition(credit_classes), info.method(:sender_id)    => self_id)
         | 
| 693 | 
            +
                    credit_when_received = merge_conditions(inheritance_condition(debit_classes),  info.method(:recipient_id) => self_id)
         | 
| 689 694 |  | 
| 690 695 | 
             
                    cols = {}
         | 
| 691 696 | 
             
                    [:total_amount, :sender_id, :recipient_id, :status, :currency].each do |col|
         | 
| 692 697 | 
             
                      cols[col] = connection.quote_column_name(info.method(col))
         | 
| 693 698 | 
             
                    end
         | 
| 694 | 
            -
             | 
| 699 | 
            +
             | 
| 695 700 | 
             
                    sender_is_self    = merge_conditions({info.method(:sender_id)    => self_id})
         | 
| 696 701 | 
             
                    recipient_is_self = merge_conditions({info.method(:recipient_id) => self_id})
         | 
| 697 702 | 
             
                    other_id_column = ext.conditional_function(sender_is_self, cols[:recipient_id], cols[:sender_id])
         | 
| 698 | 
            -
                    accept_status =  | 
| 703 | 
            +
                    accept_status = merge_conditions(info.method(:status) => (options[:with_status] || %w(closed cleared)))
         | 
| 699 704 | 
             
                    filter_conditions = "#{accept_status} AND (#{sender_is_self} OR #{recipient_is_self})"
         | 
| 700 705 |  | 
| 701 | 
            -
                    sql = " | 
| 706 | 
            +
                    sql = select("#{other_id_column} AS other_id, #{cols[:currency]} AS currency, " +
         | 
| 702 707 | 
             
                      "SUM(#{ext.conditional_function(debit_when_sent,      cols[:total_amount], 0)}) AS sales, " +
         | 
| 703 708 | 
             
                      "SUM(#{ext.conditional_function(debit_when_received,  cols[:total_amount], 0)}) AS purchase_payments, " +
         | 
| 704 709 | 
             
                      "SUM(#{ext.conditional_function(credit_when_sent,     cols[:total_amount], 0)}) AS sale_receipts, " +
         | 
| 705 | 
            -
                      "SUM(#{ext.conditional_function(credit_when_received, cols[:total_amount], 0)}) AS purchases " | 
| 706 | 
            -
             | 
| 707 | 
            -
                    
         | 
| 708 | 
            -
                     | 
| 709 | 
            -
             | 
| 710 | 
            -
                     | 
| 711 | 
            -
                    
         | 
| 712 | 
            -
                    sql << " GROUP BY other_id, currency"
         | 
| 713 | 
            -
             | 
| 714 | 
            -
                    add_order!(sql, nil, scope)
         | 
| 715 | 
            -
                    add_limit!(sql, {}, scope)
         | 
| 716 | 
            -
                    add_lock!(sql, {}, scope)
         | 
| 717 | 
            -
                    
         | 
| 718 | 
            -
                    rows = connection.select_all(sql)
         | 
| 710 | 
            +
                      "SUM(#{ext.conditional_function(credit_when_received, cols[:total_amount], 0)}) AS purchases ")
         | 
| 711 | 
            +
             | 
| 712 | 
            +
                    sql = sql.where(filter_conditions)
         | 
| 713 | 
            +
                    sql = sql.group("other_id, currency")
         | 
| 714 | 
            +
             | 
| 715 | 
            +
                    # add order, limit, and lock from outside
         | 
| 716 | 
            +
                    rows = connection.execute(sql.to_sql).to_a
         | 
| 719 717 |  | 
| 720 718 | 
             
                    results = {}
         | 
| 721 719 | 
             
                    rows.each do |row|
         | 
| @@ -723,16 +721,16 @@ module Invoicing | |
| 723 721 | 
             
                      other_id = row[:other_id].to_i
         | 
| 724 722 | 
             
                      currency = row[:currency].to_sym
         | 
| 725 723 | 
             
                      summary = {:balance => BigDecimal('0'), :currency => currency}
         | 
| 726 | 
            -
             | 
| 724 | 
            +
             | 
| 727 725 | 
             
                      {:sales => 1, :purchases => -1, :sale_receipts => -1, :purchase_payments => 1}.each_pair do |field, factor|
         | 
| 728 | 
            -
                        summary[field] = BigDecimal(row[field])
         | 
| 726 | 
            +
                        summary[field] = BigDecimal(row[field].to_s)
         | 
| 729 727 | 
             
                        summary[:balance] += BigDecimal(factor.to_s) * summary[field]
         | 
| 730 728 | 
             
                      end
         | 
| 731 | 
            -
             | 
| 729 | 
            +
             | 
| 732 730 | 
             
                      results[other_id] ||= {}
         | 
| 733 731 | 
             
                      results[other_id][currency] = AccountSummary.new summary
         | 
| 734 732 | 
             
                    end
         | 
| 735 | 
            -
             | 
| 733 | 
            +
             | 
| 736 734 | 
             
                    results
         | 
| 737 735 | 
             
                  end
         | 
| 738 736 |  | 
| @@ -750,7 +748,7 @@ module Invoicing | |
| 750 748 | 
             
                    sender_recipient_to_ledger_item_ids = {}
         | 
| 751 749 | 
             
                    result_map = {}
         | 
| 752 750 | 
             
                    info = ledger_item_class_info
         | 
| 753 | 
            -
             | 
| 751 | 
            +
             | 
| 754 752 | 
             
                    # Find the most recent occurrence of each ID, first in the sender_id column, then in recipient_id
         | 
| 755 753 | 
             
                    [:sender_id, :recipient_id].each do |column|
         | 
| 756 754 | 
             
                      column = info.method(column)
         | 
| @@ -758,19 +756,19 @@ module Invoicing | |
| 758 756 | 
             
                      sql = "SELECT MAX(#{primary_key}) AS id, #{quoted_column} AS ref FROM #{quoted_table_name} WHERE "
         | 
| 759 757 | 
             
                      sql << merge_conditions({column => sender_recipient_ids})
         | 
| 760 758 | 
             
                      sql << " GROUP BY #{quoted_column}"
         | 
| 761 | 
            -
             | 
| 759 | 
            +
             | 
| 762 760 | 
             
                      ActiveRecord::Base.connection.select_all(sql).each do |row|
         | 
| 763 761 | 
             
                        sender_recipient_to_ledger_item_ids[row['ref'].to_i] = row['id'].to_i
         | 
| 764 762 | 
             
                      end
         | 
| 765 | 
            -
             | 
| 763 | 
            +
             | 
| 766 764 | 
             
                      sender_recipient_ids -= sender_recipient_to_ledger_item_ids.keys
         | 
| 767 765 | 
             
                    end
         | 
| 768 | 
            -
             | 
| 766 | 
            +
             | 
| 769 767 | 
             
                    # Load all the ledger items needed to get one representative of each name
         | 
| 770 768 | 
             
                    find(sender_recipient_to_ledger_item_ids.values.uniq).each do |ledger_item|
         | 
| 771 769 | 
             
                      sender_id = info.get(ledger_item, :sender_id)
         | 
| 772 770 | 
             
                      recipient_id = info.get(ledger_item, :recipient_id)
         | 
| 773 | 
            -
             | 
| 771 | 
            +
             | 
| 774 772 | 
             
                      if sender_recipient_to_ledger_item_ids.include? sender_id
         | 
| 775 773 | 
             
                        details = info.get(ledger_item, :sender_details)
         | 
| 776 774 | 
             
                        result_map[sender_id] = details[:name]
         | 
| @@ -780,25 +778,48 @@ module Invoicing | |
| 780 778 | 
             
                        result_map[recipient_id] = details[:name]
         | 
| 781 779 | 
             
                      end
         | 
| 782 780 | 
             
                    end
         | 
| 783 | 
            -
             | 
| 781 | 
            +
             | 
| 784 782 | 
             
                    result_map
         | 
| 785 783 | 
             
                  end
         | 
| 786 | 
            -
             | 
| 784 | 
            +
             | 
| 785 | 
            +
                  def inheritance_condition(classes)
         | 
| 786 | 
            +
                    segments = []
         | 
| 787 | 
            +
                    segments << sanitize_sql(inheritance_column => classes)
         | 
| 788 | 
            +
             | 
| 789 | 
            +
                    if classes.include?(self.to_s) && self.new.send(inheritance_column).nil?
         | 
| 790 | 
            +
                      segments << sanitize_sql(type: nil)
         | 
| 791 | 
            +
                    end
         | 
| 792 | 
            +
             | 
| 793 | 
            +
                    "(#{segments.join(') OR (')})" unless segments.empty?
         | 
| 794 | 
            +
                  end
         | 
| 795 | 
            +
             | 
| 796 | 
            +
                  def merge_conditions(*conditions)
         | 
| 797 | 
            +
                    segments = []
         | 
| 798 | 
            +
             | 
| 799 | 
            +
                    conditions.each do |condition|
         | 
| 800 | 
            +
                      unless condition.blank?
         | 
| 801 | 
            +
                        sql = sanitize_sql(condition)
         | 
| 802 | 
            +
                        segments << sql unless sql.blank?
         | 
| 803 | 
            +
                      end
         | 
| 804 | 
            +
                    end
         | 
| 805 | 
            +
             | 
| 806 | 
            +
                    "(#{segments.join(') AND (')})" unless segments.empty?
         | 
| 807 | 
            +
                  end
         | 
| 787 808 | 
             
                end # module ClassMethods
         | 
| 788 | 
            -
             | 
| 789 | 
            -
             | 
| 809 | 
            +
             | 
| 810 | 
            +
             | 
| 790 811 | 
             
                # Very simple class for representing the sum of all sales, purchases and payments on
         | 
| 791 812 | 
             
                # an account.
         | 
| 792 813 | 
             
                class AccountSummary #:nodoc:
         | 
| 793 814 | 
             
                  NUM_FIELDS = [:sales, :purchases, :sale_receipts, :purchase_payments, :balance]
         | 
| 794 815 | 
             
                  attr_reader *([:currency] + NUM_FIELDS)
         | 
| 795 | 
            -
             | 
| 816 | 
            +
             | 
| 796 817 | 
             
                  def initialize(hash)
         | 
| 797 818 | 
             
                    @currency = hash[:currency]; @sales = hash[:sales]; @purchases = hash[:purchases]
         | 
| 798 819 | 
             
                    @sale_receipts = hash[:sale_receipts]; @purchase_payments = hash[:purchase_payments]
         | 
| 799 820 | 
             
                    @balance = hash[:balance]
         | 
| 800 821 | 
             
                  end
         | 
| 801 | 
            -
             | 
| 822 | 
            +
             | 
| 802 823 | 
             
                  def method_missing(name, *args)
         | 
| 803 824 | 
             
                    if name.to_s =~ /(.*)_formatted$/
         | 
| 804 825 | 
             
                      ::Invoicing::CurrencyValue::Formatter.format_value(currency, send($1))
         | 
| @@ -806,13 +827,13 @@ module Invoicing | |
| 806 827 | 
             
                      super
         | 
| 807 828 | 
             
                    end
         | 
| 808 829 | 
             
                  end
         | 
| 809 | 
            -
             | 
| 830 | 
            +
             | 
| 810 831 | 
             
                  def +(other)
         | 
| 811 832 | 
             
                    hash = {:currency => currency}
         | 
| 812 833 | 
             
                    NUM_FIELDS.each {|field| hash[field] = send(field) + other.send(field) }
         | 
| 813 834 | 
             
                    AccountSummary.new hash
         | 
| 814 835 | 
             
                  end
         | 
| 815 | 
            -
             | 
| 836 | 
            +
             | 
| 816 837 | 
             
                  def to_s
         | 
| 817 838 | 
             
                    NUM_FIELDS.map do |field|
         | 
| 818 839 | 
             
                      val = send("#{field}_formatted")
         | 
| @@ -820,24 +841,24 @@ module Invoicing | |
| 820 841 | 
             
                    end.join('; ')
         | 
| 821 842 | 
             
                  end
         | 
| 822 843 | 
             
                end
         | 
| 823 | 
            -
             | 
| 824 | 
            -
             | 
| 844 | 
            +
             | 
| 845 | 
            +
             | 
| 825 846 | 
             
                # Stores state in the ActiveRecord class object
         | 
| 826 847 | 
             
                class ClassInfo < Invoicing::ClassInfo::Base #:nodoc:
         | 
| 827 848 | 
             
                  attr_reader :subtype, :uuid_generator
         | 
| 828 | 
            -
             | 
| 849 | 
            +
             | 
| 829 850 | 
             
                  def initialize(model_class, previous_info, args)
         | 
| 830 851 | 
             
                    super
         | 
| 831 852 | 
             
                    @subtype = all_options[:subtype]
         | 
| 832 | 
            -
             | 
| 853 | 
            +
             | 
| 833 854 | 
             
                    begin # try to load the UUID gem
         | 
| 834 855 | 
             
                      require 'uuid'
         | 
| 835 856 | 
             
                      @uuid_generator = UUID.new
         | 
| 836 | 
            -
                    rescue LoadError, NameError # silently ignore if gem not found | 
| 857 | 
            +
                    rescue LoadError, NameError # silently ignore if gem not found
         | 
| 837 858 | 
             
                      @uuid_generator = nil
         | 
| 838 859 | 
             
                    end
         | 
| 839 860 | 
             
                  end
         | 
| 840 | 
            -
             | 
| 861 | 
            +
             | 
| 841 862 | 
             
                  # Allow methods generated by +CurrencyValue+ to be renamed as well
         | 
| 842 863 | 
             
                  def method(name)
         | 
| 843 864 | 
             
                    if name.to_s =~ /^(.*)_formatted$/
         |