invoicing 0.1.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.
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