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,718 @@
1
+ module Invoicing
2
+ # = Ledger item objects
3
+ #
4
+ # This module implements a simple ledger, i.e. the record of all of the business transactions
5
+ # which are handled by your application. Each transaction is recorded as one +LedgerItem+ object,
6
+ # each of which may have one of the following three types:
7
+ #
8
+ # +Invoice+::
9
+ # When you send an invoice to someone (= a customer), this is a record of the fact
10
+ # that you have sold them something (a product, a service etc.), and how much you expect to be
11
+ # paid for it. An invoice can consist of a list of individual charges, but it is considered as
12
+ # one document for legal purposes. You can also create invoices from someone else to yourself,
13
+ # if you owe someone else money -- for example, if you need to pay commissions to a reseller of
14
+ # your application.
15
+ # +CreditNote+::
16
+ # This is basically a invoice for a negative amount; you should use it if you have previously
17
+ # sent a customer an invoice with an amount which was too great (i.e. you have overcharged them).
18
+ # The numeric values stored in the database for a credit note are negative, to make it easier to
19
+ # calculate account summaries, but they may be formatted as positive values when presented to
20
+ # users if that is customary in your country. For example, if you send a customer an invoice
21
+ # with a +total_amount+ of $20 and a credit note with a +total_amount+ of -$10, that means that
22
+ # overall you're asking them to pay $10.
23
+ # +Payment+::
24
+ # This is a record of the fact that a payment has been made. It's a simple object, in effect just
25
+ # saying that party A paid amount X to party B on date Y. This module does not implement any
26
+ # particular payment mechanism such as credit card handling, although it could be implemented on
27
+ # top of a +Payment+ object.
28
+ #
29
+ # == Important principles
30
+ #
31
+ # Note the distinction between Invoices/Credit Notes and Payments; to keep your accounts clean,
32
+ # it is important that you do not muddle these up.
33
+ #
34
+ # * <b>Invoices and Credit Notes</b> are the important documents for tax purposes in most
35
+ # jurisdictions. They record the date on which the sale is officially made, and that date
36
+ # determines which tax rates apply. An invoice often also represents the transfer of ownership from
37
+ # the supplier to the customer; for example, if you ask your customers to send payment in
38
+ # advance (such as 'topping up' their account), that money still belongs to your customer
39
+ # until the point where they have used your service, and you have charged them for your
40
+ # service by sending them an invoice. You should only invoice them for what they have actually
41
+ # used, then the remaining balance will automatically be retained on their account.
42
+ # * <b>Payments</b> are just what it says on the tin -- the transfer of money from one hand
43
+ # to another. A payment may occur before an invoice is issued (payment in advance), or
44
+ # after/at the same time as an invoice is issued to settle the debt (payment in arrears, giving
45
+ # your customers credit). You can choose whatever makes sense for your business.
46
+ # Payments may often be associated one-to-one with invoices, but not necessarily -- an invoice
47
+ # may be paid in instalments, or several invoices may be lumped together to one payment. Your
48
+ # customer may even refuse to pay some charges, in which case there is an invoice but no payment
49
+ # (until at some point you either reverse it with a credit note, or write it off as bad debt,
50
+ # but that's beyond our scope right now).
51
+ #
52
+ # Another very important principle is that once a piece of information has been added to the
53
+ # ledger, you <b>should not modify or delete it</b>. Particularly when you have 'sent' one of your
54
+ # customers or suppliers a document (which may mean simply that they have seen it on the web) you should
55
+ # not change it again, because they might have added that information to their own accounting system.
56
+ # Changing any information is guaranteed to lead to confusion. (The model objects do not restrict your
57
+ # editing capabilities because they might be necessary in specific circumstances, but you should be
58
+ # extremely careful when changing anything.)
59
+ #
60
+ # Of course you make mistakes or change your mind, but please deal with them cleanly:
61
+ # * If you create an invoice whose value is too small, don't amend the invoice, but send them
62
+ # another invoice to cover the remaining amount.
63
+ # * If you create an invoice whose value is too great (for example because you want to offer one
64
+ # customer a special discount), don't amend the invoice, but send them a credit note to waive
65
+ # your claim to the difference.
66
+ # * If you create a payment, mark it as +pending+ until it the money has actually arrived.
67
+ # If it never arrives, keep the record but mark it as +failed+ in case you need to investigate
68
+ # it later.
69
+ #
70
+ # The exception to the 'no modifications' rule are invoices on which you accumulate charges
71
+ # (e.g. over the course of a month)
72
+ # and then officially 'send' the invoice at the end of the period. In this gem we call such
73
+ # invoices +open+ while they may still be changed. It's ok to add charges to +open+ invoices
74
+ # as you go along; while it is +open+ it is not legally an invoice, but only a statement
75
+ # of accumulated charges. If you display it to users, make sure that you don't call it "invoice",
76
+ # to avoid confusion. Only when you set it to +closed+ at the end of the month does the
77
+ # statement become an invoice for legal purposes. Once it's +closed+ you must not add
78
+ # any further charges to it.
79
+ #
80
+ # Finally, each ledger item has a sender and a recipient; typically one of the two will be
81
+ # <b>you</b> (the person/organsation who owns/operates the application):
82
+ # * For invoices, credit notes and payments between you and your customers, set the sender
83
+ # to be yourself and the recipient to be your customer;
84
+ # * If you use this system to record suppliers, set the sender to be your supplier and the
85
+ # recipient to be yourself.
86
+ # (See below for details.) It is perfectly ok to have documents which are sent between your
87
+ # users, where you are neither sender nor recipient; this may be useful if you want to allow
88
+ # users to trade directly with each other.
89
+ #
90
+ # == Using invoices, credit notes and payments in your application
91
+ #
92
+ # All invoices, credit notes and payments (collectively called 'ledger items') are stored in a
93
+ # single database table. We use <b>single table inheritance</b> to distinguish the object types.
94
+ # You need to create at least the following four model classes in your application:
95
+ #
96
+ # class LedgerItem < ActiveRecord::Base
97
+ # acts_as_ledger_item
98
+ # end
99
+ #
100
+ # class Invoice < LedgerItem # Base class for all types of invoice
101
+ # acts_as_ledger_item :subtype => :invoice
102
+ # end
103
+ #
104
+ # class CreditNote < LedgerItem # Base class for all types of credit note
105
+ # acts_as_ledger_item :subtype => :credit_note
106
+ # end
107
+ #
108
+ # class Payment < LedgerItem # Base class for all types of payment
109
+ # acts_as_ledger_item :subtype => :payment
110
+ # end
111
+ #
112
+ # You may give the classes different names than these, and you can package them in modules if
113
+ # you wish, but they need to have the <tt>:subtype => ...</tt> option parameters as above.
114
+ #
115
+ # You can create as many subclasses as you like of each of Invoice, CreditNote and Payment. This
116
+ # provides a convenient mechanism for encapsulating different types of functionality which you
117
+ # may need for different types of transactions, but still keeping the accounts in one place. You
118
+ # may start with only one subclass of +Invoice+ (e.g. <tt>class MonthlyChargesInvoice < Invoice</tt>
119
+ # to bill users for their use of your application; but as you want to do more clever things, you
120
+ # can add other subclasses of +Invoice+ as and when you need them (such as +ConsultancyServicesInvoice+
121
+ # and +SalesCommissionInvoice+, for example). Similarly for payments, you may have subclasses
122
+ # representing credit card payments, cash payments, bank transfers etc.
123
+ #
124
+ # Please note that the +Payment+ ledger item type does not itself implement any particular
125
+ # payment methods such as credit card handling; however, for third-party libraries providing
126
+ # credit card handling, this would be a good place to integrate.
127
+ #
128
+ # The model classes must have a certain minimum set of columns and a few common methods, documented
129
+ # below (although you may rename any of them if you wish). Beyond those, you may add other methods and
130
+ # database columns for your application's own needs, provided they don't interfere with names used here.
131
+ #
132
+ # == Required methods/database columns
133
+ #
134
+ # The following methods/database columns are <b>required</b> for +LedgerItem+ objects (you may give them
135
+ # different names, but then you need to tell +acts_as_ledger_item+ about your custom names):
136
+ #
137
+ # +type+::
138
+ # String to store the class name, for ActiveRecord single table inheritance.
139
+ #
140
+ # +sender_id+::
141
+ # Integer-valued foreign key, used to refer to some other model object representing the party
142
+ # (person, company etc.) who is the sender of the transaction.
143
+ # - In the case of an invoice or credit note, the +sender_id+ identifies the supplier of the product or service,
144
+ # i.e. the person who is owed the amount specified on the invoice, also known as the creditor.
145
+ # - In the case of a payment record, the +sender_id+ identifies the payee, i.e. the person who sends the note
146
+ # confirming that they received payment.
147
+ # - This field may be +NULL+ to refer to yourself (i.e. the company/person who owns or
148
+ # operates this application), but you may also use non-+NULL+ values to refer to yourself. It's just
149
+ # important that you consistently refer to the same party by the same value in different ledger items.
150
+ #
151
+ # +recipient_id+::
152
+ # The counterpart to +sender_id+: foreign key to a model object which represents the
153
+ # party who is the recipient of the transaction.
154
+ # - In the case of an invoice or credit note, the +recipient_id+ identifies the customer/buyer of the product or
155
+ # service, i.e. the person who owes the amount specified on the invoice, also known as the debtor.
156
+ # - In the case of a payment record, the +recipient_id+ identifies the payer, i.e. the recipient of the
157
+ # payment receipt.
158
+ # - +NULL+ may be used as in +sender_id+.
159
+ #
160
+ # +sender_details+::
161
+ # A method (does not have to be a database column) which returns a hash with information
162
+ # about the party identified by +sender_id+. See the documentation of +sender_details+ for the expected
163
+ # contents of the hash. Must always return valid details, even if +sender_id+ is +NULL+.
164
+ #
165
+ # +recipient_details+::
166
+ # A method (does not have to be a database column) which returns a hash with information
167
+ # about the party identified by +recipient_id+. See the documentation of +sender_details+ for the expected
168
+ # contents of the hash (+recipient_details+ uses the same format as +sender_details+). Must always
169
+ # return valid details, even if +recipient_id+ is +NULL+.
170
+ #
171
+ # +identifier+::
172
+ # A number or string used to identify this record, i.e. the invoice number, credit note number or
173
+ # payment receipt number as appropriate.
174
+ # - There may be legal requirements in your country concerning its format, but as long as it uniquely identifies
175
+ # the document within your organisation you should be safe.
176
+ # - It's possible to simply make this an alias of the primary key, but it's strongly recommended that you use a
177
+ # separate database column. If you ever need to generate invoices on behalf of other people (i.e. where
178
+ # +sender_id+ is not you), you need to give the sender of the invoice the opportunity to enter their own
179
+ # +identifier+ (because it then must be unique within the sender's organisation, not yours).
180
+ #
181
+ # +issue_date+::
182
+ # A datetime column which indicates the date on which the document is issued, and which may also
183
+ # serve as the tax point (the date which determines which tax rate is applied). This should be a separate
184
+ # column, because it won't necessarily be the same as +created_at+ or +updated_at+. There may be business
185
+ # reasons for choosing particular dates, but the date at which you send the invoice or receive the payment
186
+ # should do unless your accountant advises you otherwise.
187
+ #
188
+ # +currency+::
189
+ # The 3-letter code which identifies the currency used in this transaction; must be one of the list
190
+ # of codes in ISO-4217[http://en.wikipedia.org/wiki/ISO_4217]. (Even if you only use one currency throughout
191
+ # your site, this is needed to format monetary amounts correctly.)
192
+ #
193
+ # +total_amount+::
194
+ # A decimal column containing the grand total monetary sum (of the invoice or credit note), or the monetary
195
+ # amount paid (of the payment record), including all taxes, charges etc. For invoices and credit notes, a
196
+ # +before_validation+ filter is automatically invoked, which adds up the +net_amount+ and +tax_amount+ values
197
+ # of all line items and assigns that sum to +total_amount+. For payment records, which do not usually have
198
+ # line items, you must assign the correct value to this column. See the documentation of the +CurrencyValue+
199
+ # module for notes on suitable datatypes for monetary values. +acts_as_currency_value+ is automatically applied
200
+ # to this attribute.
201
+ #
202
+ # +tax_amount+::
203
+ # If you're a small business you maybe don't need to add tax to your invoices; but if you are successful,
204
+ # you almost certainly will need to do so eventually. In most countries this takes the form of Value Added
205
+ # Tax (VAT) or Sales Tax. For invoices and credit notes, you must store the amount of tax in this table;
206
+ # a +before_validation+ filter is automatically invoked, which adds up the +tax_amount+ values of all
207
+ # line items and assigns that sum to +total_amount+. For payment records this should be zero (unless you
208
+ # use a cash accounting scheme, which is currently not supported). See the documentation of the
209
+ # +CurrencyValue+ module for notes on suitable datatypes for monetary values. +acts_as_currency_value+ is
210
+ # automatically applied to this attribute.
211
+ #
212
+ # +status+::
213
+ # A string column used to keep track of the status of ledger items. Currently the following values are defined
214
+ # (but future versions may add further +status+ values):
215
+ # +open+:: For invoices/credit notes: the document is not yet finalised, further line items may be added.
216
+ # +closed+:: For invoices/credit notes: the document has been sent to the recipient and will not be changed again.
217
+ # +cancelled+:: For invoices/credit notes: the document has been declared void and does not count towards accounts.
218
+ # (Use this sparingly; if you want to refund an invoice that has been sent, send a credit note.)
219
+ # +pending+:: For payments: payment is expected or has been sent, but has not yet been confirmed as received.
220
+ # +cleared+:: For payments: payment has completed successfully.
221
+ # +failed+:: For payments: payment did not succeed; this record is not counted towards accounts.
222
+ #
223
+ # +description+::
224
+ # A method which returns a short string describing what this invoice, credit note or payment is about.
225
+ # Can be a database column but doesn't have to be.
226
+ #
227
+ # +line_items+::
228
+ # You should define an association <tt>has_many :line_items, ...</tt> referring to the +LineItem+ objects
229
+ # associated with this ledger item.
230
+ #
231
+ #
232
+ # == Optional methods/database columns
233
+ #
234
+ # The following methods/database columns are <b>optional, but recommended</b> for +LedgerItem+ objects:
235
+ #
236
+ # +period_start+, +period_end+::
237
+ # Two datetime columns which define the period of time covered by an invoice or credit note. If the thing you
238
+ # are selling is a one-off, you can omit these columns or leave them as +NULL+. However, if there is any sort
239
+ # of duration associated with an invoice/credit note (e.g. charges incurred during a particular month, or
240
+ # an annual subscription, or a validity period of a license, etc.), please store that period here. It's
241
+ # important for accounting purposes. (For +Payment+ objects it usually makes most sense to just leave these
242
+ # as +NULL+.)
243
+ #
244
+ # +uuid+::
245
+ # A Universally Unique Identifier (UUID)[http://en.wikipedia.org/wiki/UUID] string for this invoice, credit
246
+ # note or payment. It may seem unnecessary now, but may help you to keep track of your data later on as
247
+ # your system grows. If you have the +uuid+ gem installed and this column is present, a UUID is automatically
248
+ # generated when you create a new ledger item.
249
+ #
250
+ # +due_date+::
251
+ # The date at which the invoice or credit note is due for payment. +nil+ on +Payment+ records.
252
+ #
253
+ # +created_at+, +updated_at+::
254
+ # The standard ActiveRecord datetime columns for recording when an object was created and last changed.
255
+ # The values are not directly used at the moment, but it's useful information in case you need to track down
256
+ # a particular transaction sometime; and ActiveRecord manages them for you anyway.
257
+ #
258
+ #
259
+ # == Generated methods
260
+ #
261
+ # In return for providing +LedgerItem+ with all the required information as documented above, you are given
262
+ # a number of class and instance methods which you will find useful sooner or later. In addition to those
263
+ # documented in this module (instance methods) and <tt>Invoicing::LedgerItem::ClassMethods</tt>
264
+ # (class methods), the following methods are generated dynamically:
265
+ #
266
+ # +sent_by+:: Named scope which takes a person/company ID and matches all ledger items whose
267
+ # +sender_id+ matches that value.
268
+ # +received_by+:: Named scope which takes a person/company ID and matches all ledger items whose
269
+ # +recipient_id+ matches that value.
270
+ # +sent_or_received_by+:: Union of +sent_by+ and +received_by+.
271
+ # +in_effect+:: Named scope which matches all closed invoices/credit notes (not open or cancelled)
272
+ # and all cleared payments (not pending or failed). You probably want to use this
273
+ # quite often, for all reporting purposes.
274
+ # +open_or_pending+:: Named scope which matches all open invoices/credit notes and all pending
275
+ # payments.
276
+ # +due_at+:: Named scope which takes a +DateTime+ argument and matches all ledger items whose
277
+ # +due_date+ value is either +NULL+ or is not after the given time. For example,
278
+ # you could run <tt>LedgerItem.due_at(Time.now).account_summaries</tt>
279
+ # once a day and process payment for all accounts whose balance is not zero.
280
+ # +sorted+:: Named scope which takes a column name as documented above (even if it has been
281
+ # renamed), and sorts the query by that column. If the column does not exist,
282
+ # silently falls back to sorting by the primary key.
283
+ # +exclude_empty_invoices+:: Named scope which excludes any invoices or credit notes which do not
284
+ # have any associated line items (payments without line items are
285
+ # included though). If you're chaining scopes it would be advantageous
286
+ # to put this one close to the beginning of your scope chain.
287
+ module LedgerItem
288
+
289
+ module ActMethods
290
+ # Declares that the current class is a model for ledger items (i.e. invoices, credit notes and
291
+ # payment notes).
292
+ #
293
+ # This method accepts a hash of options, all of which are optional:
294
+ # <tt>:subtype</tt>:: One of <tt>:invoice</tt>, <tt>:credit_note</tt> or <tt>:payment</tt>.
295
+ #
296
+ # Also, the name of any attribute or method required by +LedgerItem+ (as documented on the
297
+ # +LedgerItem+ module) may be used as an option, with the value being the name under which
298
+ # that particular method or attribute can be found. This allows you to use names other than
299
+ # the defaults. For example, if your database column storing the invoice value is called
300
+ # +gross_amount+ instead of +total_amount+:
301
+ #
302
+ # acts_as_ledger_item :total_amount => :gross_amount
303
+ def acts_as_ledger_item(*args)
304
+ Invoicing::ClassInfo.acts_as(Invoicing::LedgerItem, self, args)
305
+
306
+ info = ledger_item_class_info
307
+ return unless info.previous_info.nil? # Called for the first time?
308
+
309
+ before_validation :calculate_total_amount
310
+
311
+ # Set the 'amount' columns to act as currency values
312
+ acts_as_currency_value(info.method(:total_amount), info.method(:tax_amount),
313
+ :currency => info.method(:currency))
314
+
315
+ extend Invoicing::FindSubclasses
316
+ include Invoicing::LedgerItem::RenderHTML
317
+ 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
+ end # def acts_as_ledger_item
382
+
383
+ # Synonym for <tt>acts_as_ledger_item :subtype => :invoice</tt>. All options other than
384
+ # <tt>:subtype</tt> are passed on to +acts_as_ledger_item+. You should apply
385
+ # +acts_as_invoice+ only to a model which is a subclass of an +acts_as_ledger_item+ type.
386
+ def acts_as_invoice(options={})
387
+ acts_as_ledger_item(options.clone.update({:subtype => :invoice}))
388
+ end
389
+
390
+ # Synonym for <tt>acts_as_ledger_item :subtype => :credit_note</tt>. All options other than
391
+ # <tt>:subtype</tt> are passed on to +acts_as_ledger_item+. You should apply
392
+ # +acts_as_credit_note+ only to a model which is a subclass of an +acts_as_ledger_item+ type.
393
+ def acts_as_credit_note(options={})
394
+ acts_as_ledger_item(options.clone.update({:subtype => :credit_note}))
395
+ end
396
+
397
+ # Synonym for <tt>acts_as_ledger_item :subtype => :payment</tt>. All options other than
398
+ # <tt>:subtype</tt> are passed on to +acts_as_ledger_item+. You should apply
399
+ # +acts_as_payment+ only to a model which is a subclass of an +acts_as_ledger_item+ type.
400
+ def acts_as_payment(options={})
401
+ acts_as_ledger_item(options.clone.update({:subtype => :payment}))
402
+ end
403
+ end # module ActMethods
404
+
405
+ # Overrides the default constructor of <tt>ActiveRecord::Base</tt> when +acts_as_ledger_item+
406
+ # is called. If the +uuid+ gem is installed, this constructor creates a new UUID and assigns
407
+ # it to the +uuid+ property when a new ledger item model object is created.
408
+ def initialize(*args)
409
+ super
410
+ # Initialise uuid attribute if possible
411
+ info = ledger_item_class_info
412
+ if self.has_attribute?(info.method(:uuid)) && info.uuid_generator
413
+ write_attribute(info.method(:uuid), info.uuid_generator.generate)
414
+ end
415
+ end
416
+
417
+ # Calculate sum of net_amount and tax_amount across all line items, and assign it to total_amount;
418
+ # calculate sum of tax_amount across all line items, and assign it to tax_amount.
419
+ # Called automatically as a +before_validation+ callback.
420
+ def calculate_total_amount
421
+ net_total = tax_total = BigDecimal('0')
422
+ line_items = ledger_item_class_info.get(self, :line_items)
423
+
424
+ line_items.each do |line|
425
+ info = line.send(:line_item_class_info)
426
+
427
+ # Make sure ledger_item association is assigned -- the CurrencyValue
428
+ # getters depend on it to fetch the currency
429
+ info.set(line, :ledger_item, self)
430
+ net_amount = info.get(line, :net_amount)
431
+ tax_amount = info.get(line, :tax_amount)
432
+ net_total += net_amount unless net_amount.nil?
433
+ tax_total += tax_amount unless tax_amount.nil?
434
+ end
435
+
436
+ ledger_item_class_info.set(self, :total_amount, net_total + tax_total)
437
+ ledger_item_class_info.set(self, :tax_amount, tax_total)
438
+ end
439
+
440
+ # We don't actually implement anything using +method_missing+ at the moment, but use it to
441
+ # generate slightly more useful error messages in certain cases.
442
+ def method_missing(method_id, *args)
443
+ method_name = method_id.to_s
444
+ if ['line_items', ledger_item_class_info.method(:line_items)].include? method_name
445
+ raise RuntimeError, "You need to define an association like 'has_many :line_items' on #{self.class.name}. If you " +
446
+ "have defined the association with a different name, pass the option :line_items => :your_association_name to " +
447
+ "acts_as_ledger_item."
448
+ else
449
+ super
450
+ end
451
+ end
452
+
453
+ # The difference +total_amount+ minus +tax_amount+.
454
+ def net_amount
455
+ total_amount = ledger_item_class_info.get(self, :total_amount)
456
+ tax_amount = ledger_item_class_info.get(self, :tax_amount)
457
+ (total_amount && tax_amount) ? (total_amount - tax_amount) : nil
458
+ end
459
+
460
+ # +net_amount+ formatted in human-readable form using the ledger item's currency.
461
+ def net_amount_formatted
462
+ format_currency_value(net_amount)
463
+ end
464
+
465
+
466
+ # You must overwrite this method in subclasses of +Invoice+, +CreditNote+ and +Payment+ so that it returns
467
+ # details of the party sending the document. See +sender_id+ above for a detailed interpretation of
468
+ # sender and receiver.
469
+ #
470
+ # The methods +sender_details+ and +recipient_details+ are required to return hashes
471
+ # containing details about the sender and recipient of an invoice, credit note or payment. The reason we
472
+ # do this is that you probably already have your own system for handling users, customers and their personal
473
+ # or business details, and this framework shouldn't require you to change any of that.
474
+ #
475
+ # The invoicing framework currently uses these details only for rendering invoices and credit notes, but
476
+ # in future it may serve more advanced purposes, such as determining which tax rate to apply for overseas
477
+ # customers.
478
+ #
479
+ # In the hash returned by +sender_details+ and +recipient_details+, the following keys are recognised --
480
+ # please fill in as many as possible:
481
+ # <tt>:is_self</tt>:: +true+ if these details refer to yourself, i.e. the person or organsiation who owns/operates
482
+ # this application. +false+ if these details refer to any other party.
483
+ # <tt>:name</tt>:: The name of the person or organisation whose billing address is defined below.
484
+ # <tt>:contact_name</tt>:: The name of a person/department within the organisation named by <tt>:name</tt>.
485
+ # <tt>:address</tt>:: The body of the billing address (not including city, postcode, state and country); may be
486
+ # a multi-line string, with lines separated by '\n' line breaks.
487
+ # <tt>:city</tt>:: The name of the city or town in the billing address.
488
+ # <tt>:state</tt>:: The state/region/province/county of the billing address as appropriate.
489
+ # <tt>:postal_code</tt>:: The postal code of the billing address (e.g. ZIP code in the US).
490
+ # <tt>:country</tt>:: The billing address country (human-readable).
491
+ # <tt>:country_code</tt>:: The two-letter country code of the billing address, according to
492
+ # ISO-3166-1[http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2].
493
+ # <tt>:tax_number</tt>:: The Value Added Tax registration code of this person or organisation, if they have
494
+ # one, preferably including the country identifier at the beginning. This is important for
495
+ # transactions within the European Union.
496
+ def sender_details
497
+ raise 'overwrite this method'
498
+ end
499
+
500
+ # You must overwrite this method in subclasses of +Invoice+, +CreditNote+ and +Payment+ so that it returns
501
+ # details of the party receiving the document. See +recipient_id+ above for a detailed interpretation of
502
+ # sender and receiver. See +sender_details+ for a list of fields to return in the hash.
503
+ def recipient_details
504
+ raise 'overwrite this method'
505
+ end
506
+
507
+ # Returns +true+ if this document was sent by the user with ID +user_id+. If the argument is +nil+
508
+ # (indicating yourself), this also returns +true+ if <tt>sender_details[:is_self]</tt>.
509
+ def sent_by?(user_id)
510
+ (ledger_item_class_info.get(self, :sender_id) == user_id) ||
511
+ !!(user_id.nil? && ledger_item_class_info.get(self, :sender_details)[:is_self])
512
+ end
513
+
514
+ # Returns +true+ if this document was received by the user with ID +user_id+. If the argument is +nil+
515
+ # (indicating yourself), this also returns +true+ if <tt>recipient_details[:is_self]</tt>.
516
+ def received_by?(user_id)
517
+ (ledger_item_class_info.get(self, :recipient_id) == user_id) ||
518
+ !!(user_id.nil? && ledger_item_class_info.get(self, :recipient_details)[:is_self])
519
+ end
520
+
521
+ # Returns a boolean which specifies whether this transaction should be recorded as a debit (+true+)
522
+ # or a credit (+false+) on a particular ledger. Unless you know what you are doing, you probably
523
+ # do not need to touch this method.
524
+ #
525
+ # It takes an argument +self_id+, which should be equal to either +sender_id+ or +recipient_id+ of this
526
+ # object, and which determines from which perspective the account is viewed. The default behaviour is:
527
+ # * A sent invoice (<tt>self_id == sender_id</tt>) is a debit since it increases the recipient's
528
+ # liability; a sent credit note decreases the recipient's liability with a negative-valued
529
+ # debit; a sent payment receipt is a positive-valued credit and thus decreases the recipient's
530
+ # liability.
531
+ # * A received invoice (<tt>self_id == recipient_id</tt>) is a credit because it increases your own
532
+ # liability; a received credit note decreases your own liability with a negative-valued credit;
533
+ # a received payment receipt is a positive-valued debit and thus decreases your own liability.
534
+ #
535
+ # Note that accounting practices differ with regard to credit notes: some think that a sent
536
+ # credit note should be recorded as a positive credit (hence the name 'credit note'); others
537
+ # prefer to use a negative debit. We chose the latter because it allows you to calculate the
538
+ # total sale volume on an account simply by adding up all the debits. If there is enough demand
539
+ # for the positive-credit model, we may add support for it sometime in future.
540
+ def debit?(self_id)
541
+ sender_is_self = sent_by?(self_id)
542
+ recipient_is_self = received_by?(self_id)
543
+ raise ArgumentError, "self_id #{self_id.inspect} is neither sender nor recipient" unless sender_is_self || recipient_is_self
544
+ raise ArgumentError, "self_id #{self_id.inspect} is both sender and recipient" if sender_is_self && recipient_is_self
545
+ self.class.debit_when_sent_by_self ? sender_is_self : recipient_is_self
546
+ end
547
+
548
+
549
+ module ClassMethods
550
+ # Returns +true+ if this type of ledger item should be recorded as a debit when the party
551
+ # viewing the account is the sender of the document, and recorded as a credit when
552
+ # the party viewing the account is the recipient. Returns +false+ if those roles are
553
+ # reversed. This method implements default behaviour for invoices, credit notes and
554
+ # payments (see <tt>Invoicing::LedgerItem#debit?</tt>); if you define custom ledger item
555
+ # subtypes (other than +invoice+, +credit_note+ and +payment+), you should override this
556
+ # method accordingly in those subclasses.
557
+ def debit_when_sent_by_self
558
+ case ledger_item_class_info.subtype
559
+ when :invoice then true
560
+ when :credit_note then true
561
+ when :payment then false
562
+ else nil
563
+ end
564
+ end
565
+
566
+ # Returns +true+ if this type of ledger item is a +invoice+ subtype, and +false+ otherwise.
567
+ def is_invoice
568
+ ledger_item_class_info.subtype == :invoice
569
+ end
570
+
571
+ # Returns +true+ if this type of ledger item is a +credit_note+ subtype, and +false+ otherwise.
572
+ def is_credit_note
573
+ ledger_item_class_info.subtype == :credit_note
574
+ end
575
+
576
+ # Returns +true+ if this type of ledger item is a +payment+ subtype, and +false+ otherwise.
577
+ def is_payment
578
+ ledger_item_class_info.subtype == :payment
579
+ end
580
+
581
+ # Returns a summary of the customer or supplier account between two parties identified
582
+ # by +self_id+ (the party from whose perspective the account is seen, 'you') and +other_id+
583
+ # ('them', your supplier/customer). The return value is a hash with the following structure:
584
+ #
585
+ # { :GBP => { # ISO 4217 currency code for the following figures
586
+ # :sales => BigDecimal(...), # Sum of sales (invoices sent by self_id)
587
+ # :purchases => BigDecimal(...), # Sum of purchases (invoices received by self_id)
588
+ # :sale_receipts => BigDecimal(...), # Sum of payments received from customer
589
+ # :purchase_payments => BigDecimal(...), # Sum of payments made to supplier
590
+ # :balance => BigDecimal(...) # sales - purchases - sale_receipts + purchase_payments
591
+ # },
592
+ # :USD => { # Another block as above, if the account uses
593
+ # ... # multiple currencies
594
+ # }}
595
+ #
596
+ # The <tt>:balance</tt> fields indicate any outstanding money owed on the account: the value is
597
+ # positive if they owe you money, and negative if you owe them money.
598
+ def account_summary(self_id, other_id)
599
+ info = ledger_item_class_info
600
+ conditions = {info.method(:sender_id) => [self_id, other_id],
601
+ info.method(:recipient_id) => [self_id, other_id]}
602
+
603
+ with_scope :find => {:conditions => conditions} do
604
+ summaries = account_summaries(self_id)
605
+ summaries[other_id] || {}
606
+ end
607
+ end
608
+
609
+ # Returns a summary account status for all customers or suppliers with which a particular party
610
+ # has dealings. Takes into account all +closed+ invoices/credit notes and all +cleared+ payments
611
+ # which have +self_id+ as their +sender_id+ or +recipient_id+. Returns a hash whose keys are the
612
+ # other party of each account (i.e. the value of +sender_id+ or +recipient_id+ which is not
613
+ # +self_id+, as an integer), and whose values are again hashes, of the same form as returned by
614
+ # +account_summary+:
615
+ #
616
+ # LedgerItem.account_summaries(1)
617
+ # # => { 2 => { :USD => { :sales => ... }, :EUR => { :sales => ... } },
618
+ # # 3 => { :EUR => { :sales => ... } } }
619
+ #
620
+ # If you want to further restrict the ledger items taken into account in this calculation (e.g.
621
+ # include only data from a particular quarter) you can call this method within an ActiveRecord
622
+ # scope:
623
+ #
624
+ # q3_2008 = ['issue_date >= ? AND issue_date < ?', DateTime.parse('2008-07-01'), DateTime.parse('2008-10-01')]
625
+ # LedgerItem.scoped(:conditions => q3_2008).account_summaries(1)
626
+ #
627
+ def account_summaries(self_id)
628
+ info = ledger_item_class_info
629
+ ext = Invoicing::ConnectionAdapterExt
630
+ scope = scope(:find)
631
+
632
+ debit_classes = select_matching_subclasses(:debit_when_sent_by_self, true, self.table_name, self.inheritance_column).map{|c| c.name}
633
+ credit_classes = select_matching_subclasses(:debit_when_sent_by_self, false, self.table_name, self.inheritance_column).map{|c| c.name}
634
+ debit_when_sent = merge_conditions({info.method(:sender_id) => self_id, info.method(:type) => debit_classes})
635
+ debit_when_received = merge_conditions({info.method(:recipient_id) => self_id, info.method(:type) => credit_classes})
636
+ credit_when_sent = merge_conditions({info.method(:sender_id) => self_id, info.method(:type) => credit_classes})
637
+ credit_when_received = merge_conditions({info.method(:recipient_id) => self_id, info.method(:type) => debit_classes})
638
+
639
+ cols = {}
640
+ [:total_amount, :sender_id, :recipient_id, :status, :currency].each do |col|
641
+ cols[col] = connection.quote_column_name(info.method(col))
642
+ end
643
+
644
+ sender_is_self = merge_conditions({info.method(:sender_id) => self_id})
645
+ recipient_is_self = merge_conditions({info.method(:recipient_id) => self_id})
646
+ other_id_column = ext.conditional_function(sender_is_self, cols[:recipient_id], cols[:sender_id])
647
+ filter_conditions = "#{cols[:status]} IN ('closed','cleared') AND (#{sender_is_self} OR #{recipient_is_self})"
648
+
649
+
650
+
651
+ sql = "SELECT #{other_id_column} AS other_id, #{cols[:currency]} AS currency, " +
652
+ "SUM(#{ext.conditional_function(debit_when_sent, cols[:total_amount], 0)}) AS sales, " +
653
+ "SUM(#{ext.conditional_function(debit_when_received, cols[:total_amount], 0)}) AS purchase_payments, " +
654
+ "SUM(#{ext.conditional_function(credit_when_sent, cols[:total_amount], 0)}) AS sale_receipts, " +
655
+ "SUM(#{ext.conditional_function(credit_when_received, cols[:total_amount], 0)}) AS purchases " +
656
+ "FROM #{(scope && scope[:from]) || quoted_table_name} "
657
+
658
+ # Structure borrowed from ActiveRecord::Base.construct_finder_sql
659
+ add_joins!(sql, nil, scope)
660
+ add_conditions!(sql, filter_conditions, scope)
661
+
662
+ sql << " GROUP BY other_id, currency"
663
+
664
+ add_order!(sql, nil, scope)
665
+ add_limit!(sql, {}, scope)
666
+ add_lock!(sql, {}, scope)
667
+
668
+ rows = connection.select_all(sql)
669
+
670
+ results = {}
671
+ rows.each do |row|
672
+ row.symbolize_keys!
673
+ other_id = row[:other_id].to_i
674
+ currency = row[:currency].to_sym
675
+ summary = {:balance => BigDecimal('0')}
676
+
677
+ {:sales => 1, :purchases => -1, :sale_receipts => -1, :purchase_payments => 1}.each_pair do |field, factor|
678
+ summary[field] = BigDecimal(row[field])
679
+ summary[:balance] += BigDecimal(factor.to_s) * summary[field]
680
+ end
681
+
682
+ results[other_id] ||= {}
683
+ results[other_id][currency] = summary
684
+ end
685
+
686
+ results
687
+ end
688
+ end
689
+
690
+
691
+ # Stores state in the ActiveRecord class object
692
+ class ClassInfo < Invoicing::ClassInfo::Base #:nodoc:
693
+ attr_reader :subtype, :uuid_generator
694
+
695
+ def initialize(model_class, previous_info, args)
696
+ super
697
+ @subtype = all_options[:subtype]
698
+
699
+ begin # try to load the UUID gem
700
+ require 'uuid'
701
+ @uuid_generator = UUID.new
702
+ rescue LoadError, NameError # silently ignore if gem not found
703
+ @uuid_generator = nil
704
+ end
705
+ end
706
+
707
+ # Allow methods generated by +CurrencyValue+ to be renamed as well
708
+ def method(name)
709
+ if name.to_s =~ /^(.*)_formatted$/
710
+ "#{super($1)}_formatted"
711
+ else
712
+ super
713
+ end
714
+ end
715
+ end
716
+
717
+ end # module LedgerItem
718
+ end