invoicing 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/CHANGELOG +3 -0
  2. data/LICENSE +20 -0
  3. data/Manifest +60 -0
  4. data/README +48 -0
  5. data/Rakefile +75 -0
  6. data/invoicing.gemspec +41 -0
  7. data/lib/invoicing.rb +9 -0
  8. data/lib/invoicing/cached_record.rb +107 -0
  9. data/lib/invoicing/class_info.rb +187 -0
  10. data/lib/invoicing/connection_adapter_ext.rb +44 -0
  11. data/lib/invoicing/countries/uk.rb +24 -0
  12. data/lib/invoicing/currency_value.rb +212 -0
  13. data/lib/invoicing/find_subclasses.rb +193 -0
  14. data/lib/invoicing/ledger_item.rb +718 -0
  15. data/lib/invoicing/ledger_item/render_html.rb +515 -0
  16. data/lib/invoicing/ledger_item/render_ubl.rb +268 -0
  17. data/lib/invoicing/line_item.rb +246 -0
  18. data/lib/invoicing/price.rb +9 -0
  19. data/lib/invoicing/tax_rate.rb +9 -0
  20. data/lib/invoicing/taxable.rb +355 -0
  21. data/lib/invoicing/time_dependent.rb +388 -0
  22. data/lib/invoicing/version.rb +21 -0
  23. data/test/cached_record_test.rb +100 -0
  24. data/test/class_info_test.rb +253 -0
  25. data/test/connection_adapter_ext_test.rb +71 -0
  26. data/test/currency_value_test.rb +184 -0
  27. data/test/find_subclasses_test.rb +120 -0
  28. data/test/fixtures/README +7 -0
  29. data/test/fixtures/cached_record.sql +22 -0
  30. data/test/fixtures/class_info.sql +28 -0
  31. data/test/fixtures/currency_value.sql +29 -0
  32. data/test/fixtures/find_subclasses.sql +43 -0
  33. data/test/fixtures/ledger_item.sql +39 -0
  34. data/test/fixtures/line_item.sql +33 -0
  35. data/test/fixtures/price.sql +4 -0
  36. data/test/fixtures/tax_rate.sql +4 -0
  37. data/test/fixtures/taxable.sql +14 -0
  38. data/test/fixtures/time_dependent.sql +35 -0
  39. data/test/ledger_item_test.rb +352 -0
  40. data/test/line_item_test.rb +139 -0
  41. data/test/models/README +4 -0
  42. data/test/models/test_subclass_in_another_file.rb +3 -0
  43. data/test/models/test_subclass_not_in_database.rb +6 -0
  44. data/test/price_test.rb +9 -0
  45. data/test/ref-output/creditnote3.html +82 -0
  46. data/test/ref-output/creditnote3.xml +89 -0
  47. data/test/ref-output/invoice1.html +93 -0
  48. data/test/ref-output/invoice1.xml +111 -0
  49. data/test/ref-output/invoice2.html +86 -0
  50. data/test/ref-output/invoice2.xml +98 -0
  51. data/test/ref-output/invoice_null.html +36 -0
  52. data/test/render_html_test.rb +69 -0
  53. data/test/render_ubl_test.rb +32 -0
  54. data/test/setup.rb +37 -0
  55. data/test/tax_rate_test.rb +9 -0
  56. data/test/taxable_test.rb +180 -0
  57. data/test/test_helper.rb +48 -0
  58. data/test/time_dependent_test.rb +180 -0
  59. data/website/curvycorners.js +1 -0
  60. data/website/screen.css +149 -0
  61. data/website/template.html.erb +43 -0
  62. metadata +180 -0
@@ -0,0 +1,44 @@
1
+ module Invoicing
2
+ # Extensions specific to certain database adapters. Currently only MySQL and PostgreSQL are
3
+ # supported.
4
+ class ConnectionAdapterExt
5
+
6
+ # Creates a database-specific SQL fragment for evaluating a three-legged conditional function
7
+ # in a query.
8
+ def self.conditional_function(condition, value_if_true, value_if_false)
9
+ case ActiveRecord::Base.connection.adapter_name
10
+ when "MySQL"
11
+ "IF(#{condition}, #{value_if_true}, #{value_if_false})"
12
+ when "PostgreSQL"
13
+ "CASE WHEN #{condition} THEN #{value_if_true} ELSE #{value_if_false} END"
14
+ else
15
+ raise "Database adapter #{ActiveRecord::Base.connection.adapter_name} not supported by invoicing gem"
16
+ end
17
+ end
18
+
19
+ # Suppose <tt>A has_many B</tt>, and you want to select all As, counting for each A how many
20
+ # Bs it has. In MySQL you can just say:
21
+ # SELECT A.*, COUNT(B.id) AS number_of_bs FROM A LEFT JOIN B on A.id = B.a_id GROUP BY A.id
22
+ # PostgreSQL, however, doesn't like you selecting a column from A if that column is neither
23
+ # in the <tt>GROUP BY</tt> clause nor wrapped in an aggregation function (even though it is
24
+ # implicitly grouped by through the fact that <tt>A.id</tt> is unique per row). Therefore
25
+ # for PostgreSQL, we need to explicitly list all of A's columns in the <tt>GROUP BY</tt>
26
+ # clause.
27
+ #
28
+ # This method takes a model class (a subclass of <tt>ActiveRecord::Base</tt>) and returns
29
+ # a string suitable to be used as the contents of the <tt>GROUP BY</tt> clause.
30
+ def self.group_by_all_columns(model_class)
31
+ case ActiveRecord::Base.connection.adapter_name
32
+ when "MySQL"
33
+ model_class.quoted_table_name + "." +
34
+ ActiveRecord::Base.connection.quote_column_name(model_class.primary_key)
35
+ when "PostgreSQL"
36
+ model_class.column_names.map{ |column|
37
+ model_class.quoted_table_name + "." + ActiveRecord::Base.connection.quote_column_name(column)
38
+ }.join(', ')
39
+ else
40
+ raise "Database adapter #{ActiveRecord::Base.connection.adapter_name} not supported by invoicing gem"
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,24 @@
1
+ module Invoicing
2
+ module Countries
3
+ module UK
4
+ # Extremely simplistic implementation of UK VAT. This needs to be fixed.
5
+ class VAT
6
+ def apply_tax(params)
7
+ params[:value] * BigDecimal('1.15')
8
+ end
9
+
10
+ def remove_tax(params)
11
+ params[:value] / BigDecimal('1.15')
12
+ end
13
+
14
+ def tax_info(params)
15
+ "(inc. VAT)"
16
+ end
17
+
18
+ def tax_details(params)
19
+ "(including VAT at 15%)"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,212 @@
1
+ module Invoicing
2
+ # = Input and output of monetary values
3
+ #
4
+ # This module simplifies model objects which need to store monetary values. It automatically takes care
5
+ # of currency rounding conventions and formatting values for output.
6
+ #
7
+ # == General notes on currency precision and rounding
8
+ #
9
+ # It is important to deal carefully with rounding errors in accounts. If the figures don't add up exactly,
10
+ # you may have to pay for expensive accountant hours while they try to find out where the missing pennies or
11
+ # cents have gone -- better to avoid this trouble from the start. Because of this, it is strongly recommended
12
+ # that you use fixed-point or decimal datatypes to store any sort of currency amounts, never floating-point
13
+ # numbers.
14
+ #
15
+ # Keep in mind that not all currencies subdivide their main unit into 100 smaller units; storing four digits
16
+ # after the decimal point should be enough to allow you to expand into other currencies in future. Also leave
17
+ # enough headroom in case you ever need to use an inflated currency. For example,
18
+ # if you are using MySQL, <tt>decimal(20,4)</tt> may be a good choice for all your columns which store
19
+ # monetary amounts. The extra few bytes aren't going to cost you anything.
20
+ #
21
+ # On the other hand, it doesn't usually make sense to store monetary values with a higher precision than is
22
+ # conventional for a particular currency (usually this is related to the value of the smallest coin in
23
+ # circulation, but conventions may differ). For example, if your currency rounds to two decimal places, then
24
+ # you should also round every monetary amount to two decimal places before storing it. If you store values
25
+ # at a higher precision than you display, your numbers may appear to not add up correctly when you present
26
+ # them to users. Fortunately, this module automatically performs currency-specific rounding for you.
27
+ #
28
+ # == Using +acts_as_currency_value+
29
+ #
30
+ # This module simplifies model objects which need to store monetary values, by automatically taking care
31
+ # of currency rounding and formatting conventions. In a typical set-up, every model object which has one or
32
+ # more attributes storing monetary amounts (a price, a fee, a tax amount, a payment value, etc.) should also
33
+ # have a +currency+ column, which stores the ISO 4217 three-letter upper-case code identifying the currency.
34
+ # Annotate your model class with +acts_as_currency_value+, passing it a list of attribute names which store
35
+ # monetary amounts. If you refuse to store a +currency+ attribute, you may instead specify a default currency
36
+ # by passing a <tt>:currency_code => CODE</tt> option to +acts_as_currency_value+, but this is not recommended:
37
+ # even if you are only using one currency now, you may well expand into other currencies later. It is not
38
+ # possible to have multiple different currencies in the same model object.
39
+ #
40
+ # The +CurrencyValue+ module knows how to handle a set of default currencies (see +CURRENCIES+ below). If your
41
+ # currency is not supported in the way you want, you can extend/modify the hash yourself (please also send us
42
+ # a patch so that we can extend our list of inbuilt currencies):
43
+ # Invoicing::CurrencyValue::CURRENCIES['HKD'] = {:symbol => 'HK$', :round => 0.10, :digits => 2}
44
+ # This specifies that the Hong Kong Dollar should be displayed using the 'HK$' symbol and two digits after the
45
+ # decimal point, but should always be rounded to the nearest 10 cents since the 10 cent coin is the smallest
46
+ # in circulation (therefore the second digit after the decimal point will always be zero).
47
+ #
48
+ # When that is done, you can use the model object normally, and rounding will occur automatically:
49
+ # invoice.currency = 'HKD'
50
+ # invoice.tax_amount = invoice.net_amount * TaxRates.default_rate_now # 1234.56789
51
+ # invoice.tax_amount == BigDecimal('1234.6') # true - rounded to nearest 0.01
52
+ #
53
+ # Moreover, you can just append +_formatted+ to your attribute name and get the value formatted for including
54
+ # in your views:
55
+ # invoice.tax_amount_formatted # 'HK$1,234.60'
56
+ # The string returned by a +_formatted+ method is UTF-8 encoded -- remember most currency symbols (except $)
57
+ # are outside basic 7-bit ASCII.
58
+ module CurrencyValue
59
+
60
+ # 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:
62
+ # <tt>:round</tt>:: Smallest unit of the currency in normal use, to which values are rounded. Default is 0.01.
63
+ # <tt>:symbol</tt>:: Symbol or string usually used to denote the currency. Encoded as UTF-8. Default is ISO 4217 code.
64
+ # <tt>:suffix</tt>:: +true+ if the currency symbol appears after the number, +false+ if it appears before. Default +false+.
65
+ CURRENCIES = {
66
+ 'EUR' => {:symbol => "\xE2\x82\xAC"}, # Euro
67
+ 'GBP' => {:symbol => "\xC2\xA3"}, # Pound Sterling
68
+ 'USD' => {:symbol => "$"}, # United States Dollar
69
+ 'CAD' => {:symbol => "$"}, # Canadian Dollar
70
+ 'AUD' => {:symbol => "$"}, # Australian Dollar
71
+ 'CNY' => {:symbol => "\xE5\x85\x83", :suffix => true}, # Chinese Yuan (RMB)
72
+ 'INR' => {:symbol => "\xE2\x82\xA8"}, # Indian Rupee
73
+ 'JPY' => {:symbol => "\xC2\xA5", :round => 1} # Japanese Yen
74
+ }
75
+
76
+ module ActMethods
77
+ # Declares that the current model object has columns storing monetary amounts. Pass those attribute
78
+ # names to +acts_as_currency_value+. By default, we try to find an attribute or method called +currency+,
79
+ # which stores the 3-letter ISO 4217 currency code for a record; if that attribute has a different name,
80
+ # specify the name using the <tt>:currency</tt> option. For example:
81
+ #
82
+ # class Price < ActiveRecord::Base
83
+ # validates_numericality_of :net_amount, :tax_amount
84
+ # validates_inclusion_of :currency_code, %w( USD GBP EUR JPY )
85
+ # acts_as_currency_value :net_amount, :tax_amount, :currency => :currency_code
86
+ # end
87
+ def acts_as_currency_value(*args)
88
+ Invoicing::ClassInfo.acts_as(Invoicing::CurrencyValue, self, args)
89
+
90
+ # Register callback if this is the first time acts_as_currency_value has been called
91
+ before_save :write_back_currency_values if currency_value_class_info.previous_info.nil?
92
+ end
93
+ end
94
+
95
+ # Format a numeric monetary value into a human-readable string, in the currency of the
96
+ # current model object.
97
+ def format_currency_value(value)
98
+ currency_value_class_info.format_value(self, value)
99
+ end
100
+
101
+
102
+ # Called automatically via +before_save+. Writes the result of converting +CurrencyValue+ attributes
103
+ # back to the actual attributes, so that they are saved in the database. (This doesn't happen in
104
+ # +convert_currency_values+ to avoid losing the +_before_type_cast+ attribute values.)
105
+ def write_back_currency_values
106
+ currency_value_class_info.all_args.each {|attr| write_attribute(attr, send(attr)) }
107
+ end
108
+
109
+ protected :write_back_currency_values
110
+
111
+
112
+ class ClassInfo < Invoicing::ClassInfo::Base #:nodoc:
113
+
114
+ def initialize(model_class, previous_info, args)
115
+ super
116
+ new_args.each{|attr| generate_attrs(attr)}
117
+ end
118
+
119
+ # Generates the getter and setter method for attribute +attr+.
120
+ def generate_attrs(attr)
121
+ model_class.class_eval do
122
+ define_method(attr) do
123
+ currency_info = currency_value_class_info.currency_info_for(self)
124
+ return read_attribute(attr) if currency_info.nil?
125
+ round_factor = BigDecimal(currency_info[:round].to_s)
126
+
127
+ value = currency_value_class_info.attr_conversion_input(self, attr)
128
+ value.nil? ? nil : (value / round_factor).round * round_factor
129
+ end
130
+
131
+ define_method("#{attr}=") do |new_value|
132
+ write_attribute(attr, new_value)
133
+ end
134
+
135
+ define_method("#{attr}_formatted") do
136
+ begin
137
+ format_currency_value(Kernel.Float(send("#{attr}_before_type_cast")))
138
+ rescue ArgumentError, TypeError
139
+ '' # if <attr>_before_type_cast could not be converted to float
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ # Returns the value of the currency code column of +object+, if available; otherwise the
146
+ # default currency code (set by the <tt>:currency_code</tt> option), if available; +nil+ if all
147
+ # else fails.
148
+ def currency_of(object)
149
+ if object.attributes.has_key?(method(:currency)) || object.respond_to?(method(:currency))
150
+ get(object, :currency)
151
+ else
152
+ all_options[:currency_code]
153
+ end
154
+ end
155
+
156
+ # Returns a hash of information about the currency used by model +object+. Contains the following keys:
157
+ # <tt>:code</tt>:: The ISO 4217 code of the currency.
158
+ # <tt>:round</tt>:: Smallest unit of the currency in normal use, to which values are rounded. Default is 0.01.
159
+ # <tt>:symbol</tt>:: Symbol or string usually used to denote the currency. Encoded as UTF-8. Default is ISO 4217 code.
160
+ # <tt>:suffix</tt>:: +true+ if the currency symbol appears after the number, +false+ if it appears before.
161
+ # <tt>:space</tt>:: Whether or not to leave a space between the number and the currency symbol.
162
+ # <tt>:digits</tt>:: Number of digits to display after the decimal point.
163
+ def currency_info_for(object)
164
+ valid_options = [:symbol, :round, :suffix, :space, :digits, :format]
165
+ code = currency_of(object)
166
+ info = {:code => code, :symbol => code, :round => 0.01, :suffix => nil, :space => nil, :digits => nil}
167
+ if ::Invoicing::CurrencyValue::CURRENCIES.has_key? code
168
+ info.update(::Invoicing::CurrencyValue::CURRENCIES[code])
169
+ end
170
+ all_options.each_pair {|key, value| info[key] = value if valid_options.include? key }
171
+
172
+ info[:suffix] = true if info[:suffix].nil? && (info[:code] == info[:symbol]) && !info[:code].nil?
173
+ info[:space] = true if info[:space].nil? && info[:suffix]
174
+ info[:digits] = -Math.log10(info[:round]).floor if info[:digits].nil?
175
+ info[:digits] = 0 if info[:digits] < 0
176
+
177
+ info
178
+ end
179
+
180
+ # Formats a numeric value as a nice currency string in UTF-8 encoding.
181
+ # +object+ is the model object carrying the value (used to determine the currency).
182
+ def format_value(object, value)
183
+ info = currency_info_for(object)
184
+
185
+ # FIXME: take locale into account
186
+ value = "%.#{info[:digits]}f" % value
187
+ while value.sub!(/(\d+)(\d\d\d)/,'\1,\2'); end
188
+ if info[:space]
189
+ info[:suffix] ? "#{value} #{info[:symbol]}" : "#{info[:symbol]} #{value}"
190
+ else
191
+ info[:suffix] ? "#{value}#{info[:symbol]}" : "#{info[:symbol]}#{value}"
192
+ end
193
+ end
194
+
195
+ # If other modules have registered callbacks for the event of reading a rounded attribute,
196
+ # they are executed here. +attr+ is the name of the attribute being read.
197
+ def attr_conversion_input(object, attr)
198
+ value = nil
199
+
200
+ if callback = all_options[:conversion_input]
201
+ value = object.send(callback, attr)
202
+ end
203
+
204
+ unless value
205
+ raw_value = object.read_attribute(attr)
206
+ value = BigDecimal.new(raw_value.to_s) unless raw_value.nil?
207
+ end
208
+ value
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,193 @@
1
+ module Invoicing
2
+ # = Subclass-aware filtering by class methods
3
+ #
4
+ # Utility module which can be mixed into <tt>ActiveRecord::Base</tt> subclasses which use
5
+ # single table inheritance. It enables you to query the database for model objects based
6
+ # on static class properties without having to instantiate more model objects than necessary.
7
+ # Its methods should be used as class methods, so the module should be mixed in using +extend+.
8
+ #
9
+ # For example:
10
+ #
11
+ # class Product < ActiveRecord::Base
12
+ # extend Invoicing::FindSubclasses
13
+ # def self.needs_refrigeration; false; end
14
+ # end
15
+ #
16
+ # class Food < Product; end
17
+ # class Bread < Food; end
18
+ # class Yoghurt < Food
19
+ # def self.needs_refrigeration; true; end
20
+ # end
21
+ # class GreekYoghurt < Yoghurt; end
22
+ #
23
+ # class Drink < Product; end
24
+ # class SoftDrink < Drink; end
25
+ # class Smoothie < Drink
26
+ # def self.needs_refrigeration; true; end
27
+ # end
28
+ #
29
+ # So we know that all +Yoghurt+ and all +Smoothie+ objects need refrigeration (including subclasses
30
+ # of +Yoghurt+ and +Smoothly+, unless they override +needs_refrigeration+ again), and the others
31
+ # don't. This fact is defined through a class method and not stored in the database. It needn't
32
+ # necessarily be constant -- you could make +needs_refrigeration+ return +true+ or +false+
33
+ # depending on the current temperature, for example.
34
+ #
35
+ # Now assume that in your application you need to query all objects which need refrigeration
36
+ # (and maybe also satisfy some other conditions). Since the database knows nothing about
37
+ # +needs_refrigeration+, what you would have to do traditionally is to instantiate all objects
38
+ # and then to filter them yourself, i.e.
39
+ #
40
+ # Product.find(:all).select{|p| p.class.needs_refrigeration}
41
+ #
42
+ # However, if only a small proportion of your products needs refrigeration, this requires you to
43
+ # load many more objects than necessary, putting unnecessary load on your application. With the
44
+ # +FindSubclasses+ module you can let the database do the filtering instead:
45
+ #
46
+ # Product.find(:all, :conditions => {:needs_refrigeration => true})
47
+ #
48
+ # You could even define a named scope to do the same thing:
49
+ #
50
+ # class Product
51
+ # named_scope :refrigerated_products, :conditions => {:needs_refrigeration => true})
52
+ # end
53
+ #
54
+ # Much nicer! The condition looks precisely like a condition on a database table column, even
55
+ # though it actually refers to a class method. Under the hood, this query translates into:
56
+ #
57
+ # Product.find(:all, :conditions => {:type => ['Yoghurt', 'GreekYoghurt', 'Smoothie']})
58
+ #
59
+ # And of course you can combine it with normal conditions on database table columns. If there
60
+ # is a table column and a class method with the same name, +FindSublasses+ remains polite and lets
61
+ # the table column take precedence.
62
+ #
63
+ # == How it works
64
+ #
65
+ # +FindSubclasses+ relies on having a list of all subclasses of your single-table-inheritance
66
+ # base class; then, if you specify a condition with a key which has no corresponding database
67
+ # table column, +FindSubclasses+ will check all subclasses for the return value of a class
68
+ # method with that name, and search for the names of classes which match the condition.
69
+ #
70
+ # Purists of object-oriented programming will most likely find this appalling, and it's important
71
+ # to know the limitations. In Ruby, a class can be notified if it subclassed, by defining the
72
+ # <tt>Class#inherited</tt> method; we use this to gather together a list of subclasses. Of course,
73
+ # we won't necessarily know about every class in the world which may subclass our class; in
74
+ # particular, <tt>Class#inherited</tt> won't be called until that subclass is loaded.
75
+ #
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
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
80
+ # subclasses used in a project at the point where we need to process a class method condition.
81
+ # This would cause us to omit some objects we should have found.
82
+ #
83
+ # To prevent this from happening, this module searches for all types of object currently stored
84
+ # in the table (along the lines of <tt>SELECT DISTINCT type FROM table_name</tt>), and makes sure
85
+ # all class names mentioned there are loaded before evaluating a class method condition. Note that
86
+ # this doesn't necessarily load all subclasses, but at least it loads those which currently have
87
+ # instances stored in the database, so we won't omit any objects when selecting from the table.
88
+ # There is still room for race conditions to occur, but otherwise it should be fine. If you want
89
+ # to be on the safe side you can ensure all subclasses are loaded when your application
90
+ # initialises -- but that's not completely DRY ;-)
91
+ module FindSubclasses
92
+
93
+ # Overrides <tt>ActiveRecord::Base.sanitize_sql_hash_for_conditions</tt> since this is the method
94
+ # used to transform a hash of conditions into an SQL query fragment. This overriding method
95
+ # searches for class method conditions in the hash and transforms them into a condition on the
96
+ # class name. All further work is delegated back to the superclass method.
97
+ #
98
+ # Condition formats are very similar to those accepted by +ActiveRecord+:
99
+ # {:my_class_method => 'value'} # known_subclasses.select{|cls| cls.my_class_method == 'value' }
100
+ # {:my_class_method => [1, 2]} # known_subclasses.select{|cls| [1, 2].include?(cls.my_class_method) }
101
+ # {:my_class_method => 3..6} # known_subclasses.select{|cls| (3..6).include?(cls.my_class_method) }
102
+ # {:my_class_method => true} # known_subclasses.select{|cls| cls.my_class_method }
103
+ # {:my_class_method => false} # known_subclasses.reject{|cls| cls.my_class_method }
104
+ def sanitize_sql_hash_for_conditions(attrs, table_name = quoted_table_name)
105
+ new_attrs = {}
106
+
107
+ attrs.each_pair do |attr, value|
108
+ attr = attr_base = attr.to_s
109
+ attr_table_name = table_name
110
+
111
+ # Extract table name from qualified attribute names
112
+ attr_table_name, attr_base = attr.split('.', 2) if attr.include?('.')
113
+
114
+ if columns_hash.include?(attr_base) || ![self.table_name, quoted_table_name].include?(attr_table_name)
115
+ new_attrs[attr] = value # Condition on a table column, or another table -- pass through unmodified
116
+ else
117
+ begin
118
+ matching_classes = select_matching_subclasses(attr_base, value)
119
+ new_attrs["#{self.table_name}.#{inheritance_column}"] = matching_classes.map{|cls| cls.name.to_s}
120
+ rescue NoMethodError
121
+ new_attrs[attr] = value # If the class method doesn't exist, fall back to passing condition through unmodified
122
+ end
123
+ end
124
+ end
125
+
126
+ super(new_attrs, table_name)
127
+ end
128
+
129
+ # Returns a list of those classes within +known_subclasses+ which match a condition
130
+ # <tt>method_name => value</tt>. May raise +NoMethodError+ if a class object does not
131
+ # respond to +method_name+.
132
+ def select_matching_subclasses(method_name, value, table = table_name, type_column = inheritance_column)
133
+ known_subclasses(table, type_column).select do |cls|
134
+ returned = cls.send(method_name)
135
+ (returned == value) or case value
136
+ when true then !!returned
137
+ when false then !returned
138
+ when Array, Range then value.include?(returned)
139
+ end
140
+ end
141
+ end
142
+
143
+ # Ruby callback which is invoked when a subclass is created. We use this to build a list of known
144
+ # subclasses.
145
+ def inherited(subclass)
146
+ remember_subclass subclass
147
+ super
148
+ end
149
+
150
+ # Add +subclass+ to the list of know subclasses of this class.
151
+ def remember_subclass(subclass)
152
+ @known_subclasses ||= [self]
153
+ @known_subclasses << subclass unless @known_subclasses.include? subclass
154
+ self.superclass.remember_subclass(subclass) if self.superclass.respond_to? :remember_subclass
155
+ end
156
+
157
+ # Return the list of all known subclasses of this class, if necessary checking the database for
158
+ # classes which have not yet been loaded.
159
+ def known_subclasses(table = table_name, type_column = inheritance_column)
160
+ load_all_subclasses_found_in_database(table, type_column)
161
+ @known_subclasses ||= [self]
162
+ end
163
+
164
+ private
165
+ # Query the database for all qualified class names found in the +type_column+ column
166
+ # (called +type+ by default), and check that classes of that name have been loaded by the Ruby
167
+ # interpreter. If a type name is encountered which cannot be loaded,
168
+ # <tt>ActiveRecord::SubclassNotFound</tt> is raised.
169
+ #
170
+ # TODO: Cache this somehow, to avoid querying for class names more often than necessary. It's not
171
+ # obvious though how to do this best -- a different Ruby instance may insert a row into the
172
+ # database with a type which is not yet loaded in this interpreter. Maybe reloading the list
173
+ # of types from the database every 30-60 seconds or so would be a compromise?
174
+ def load_all_subclasses_found_in_database(table = table_name, type_column = inheritance_column)
175
+ quoted_table_name = connection.quote_table_name(table)
176
+ quoted_inheritance_column = connection.quote_column_name(type_column)
177
+ query = "SELECT DISTINCT #{quoted_inheritance_column} FROM #{quoted_table_name}"
178
+ for subclass_name in connection.select_all(query).map{|record| record[type_column]}
179
+ unless subclass_name.blank? # empty string or nil means base class
180
+ begin
181
+ compute_type(subclass_name)
182
+ rescue NameError
183
+ raise ActiveRecord::SubclassNotFound, # Error message borrowed from ActiveRecord::Base
184
+ "The single-table inheritance mechanism failed to locate the subclass: '#{subclass_name}'. " +
185
+ "This error is raised because the column '#{type_column}' is reserved for storing the class in case of inheritance. " +
186
+ "Please rename this column if you didn't intend it to be used for storing the inheritance class " +
187
+ "or overwrite #{self.to_s}.inheritance_column to use another column for that information."
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end