quotations + tax refactor, part 2
[freeside.git] / FS / FS / TaxEngine.pm
1 package FS::TaxEngine;
2
3 use strict;
4 use vars qw( $DEBUG );
5 use FS::Conf;
6 use FS::Record qw(qsearch qsearchs);
7
8 $DEBUG = 0;
9
10 =head1 NAME
11
12 FS::TaxEngine - Base class for tax calculation engines.
13
14 =head1 USAGE
15
16 1. At the start of creating an invoice, create an FS::TaxEngine object.
17 2. Each time a sale item is added to the invoice, call L</add_sale> on the 
18    TaxEngine.
19 3. Set the "pending" flag on the invoice.
20 4. Insert the invoice and its line items.
21
22 - If the TaxEngine is "batch" style (Billsoft):
23 5. After creating all invoices for the day, call 
24    FS::TaxEngine::process_tax_batch.  This will create the tax items for
25    all of the pending invoices, clear the "pending" flag, and call 
26    L<FS::cust_main::Billing/collect> on each of the billed customers.
27
28 - If not (the internal tax system, CCH):
29 5. After adding all sale items, call L</calculate_taxes> on the TaxEngine to
30    produce a list of tax line items.
31 6. Append the tax line items to the invoice.
32 7. Update the invoice with the new charged amount and clear the pending flag.
33
34 =head1 CLASS METHODS
35
36 =over 4
37
38 =item new 'cust_main' => CUST_MAIN, 'invoice_time' => TIME, OPTIONS...
39
40 Creates an L<FS::TaxEngine> object.  The subclass will be chosen by the 
41 'enable_taxproducts' configuration setting.
42
43 CUST_MAIN and TIME are required.  OPTIONS can include:
44
45 "cancel" => 1 to indicate that the package is being billed on cancellation.
46
47 "estimate" => 1 to indicate that this calculation is for tax estimation,
48 and isn't an actual sale invoice, in case that matters.
49
50 =cut
51
52 sub new {
53   my $class = shift;
54   my %opt = @_;
55   my $conf = FS::Conf->new;
56   if ($class eq 'FS::TaxEngine') {
57     my $subclass = $conf->config('enable_taxproducts') || 'internal';
58     $class .= "::$subclass";
59     local $@;
60     eval "use $class";
61     die "couldn't load $class: $@\n" if $@;
62   }
63   my $self = { items => [], taxes => {}, conf => $conf, %opt };
64   bless $self, $class;
65 }
66
67 =item info
68
69 Returns a hashref of metadata about this tax method, including:
70 - batch: whether this is a batch-style engine (requires different usage)
71 - override: whether this engine uses tax overrides
72 - manual_tax_location: whether this engine requires the user to select a "tax
73   location" separate from the address/city/state/zip fields
74 - rate_table: the table that stores the tax rates
75   (the 'taxline' method of that class will be used to calculate line-item
76    taxes)
77 - link_table: the table that links L<FS::cust_bill_pkg> records for taxes
78   to the C<rate_table> entry that generated them, and to the item they 
79   represent tax on.
80
81 =back
82
83 =head1 METHODS
84
85 =over 4
86
87 =item add_sale CUST_BILL_PKG
88
89 Adds the CUST_BILL_PKG object as a taxable sale on this invoice.
90
91 =item calculate_taxes INVOICE
92
93 Calculates the taxes on the taxable sales and returns a list of 
94 L<FS::cust_bill_pkg> objects to add to the invoice.  The base implementation
95 is to call L</make_taxlines> to produce a list of "raw" tax line items, 
96 then L</consolidate_taxlines> to combine those with the same itemdesc.
97
98 =cut
99
100 sub calculate_taxes {
101   my $self = shift;
102   my $cust_bill = shift;
103
104   my @raw_taxlines = $self->make_taxlines($cust_bill);
105
106   my @real_taxlines = $self->consolidate_taxlines(@raw_taxlines);
107
108   if ( $cust_bill and $cust_bill->get('invnum') ) {
109     $_->set('invnum', $cust_bill->get('invnum')) foreach @real_taxlines;
110   }
111   return \@real_taxlines;
112 }
113
114 sub make_taxlines {
115   my $self = shift;
116   my $conf = $self->{conf};
117
118   my $cust_bill = shift;
119
120   my @taxlines;
121
122   # For each distinct tax rate definition, calculate the tax and exemptions.
123   foreach my $taxnum ( keys %{ $self->{taxes} } ) {
124
125     my $taxables = $self->{taxes}{$taxnum};
126     my $tax_object = shift @$taxables;
127     # $tax_object is a cust_main_county or tax_rate 
128     # (with billpkgnum, pkgnum, locationnum set)
129     # the rest of @{ $taxlisthash->{$tax} } is cust_bill_pkg component objects
130     # (setup, recurring, usage classes)
131
132     my $taxline = $self->taxline('tax' => $tax_object, 'sales' => $taxables);
133     # taxline methods are now required to return real line items
134     # with their link records
135     die $taxline unless ref($taxline);
136
137     push @taxlines, $taxline;
138
139   } #foreach $taxnum
140
141   return @taxlines;
142 }
143
144 sub consolidate_taxlines {
145
146   my $self = shift;
147   my $conf = $self->{conf};
148
149   my @raw_taxlines = @_;
150   my @tax_line_items;
151
152   # keys are tax names (as printed on invoices / itemdesc )
153   # values are arrayrefs of taxlines
154   my %taxname;
155   # collate these by itemdesc
156   foreach my $taxline (@raw_taxlines) {
157     my $taxname = $taxline->itemdesc;
158     $taxname{$taxname} ||= [];
159     push @{ $taxname{$taxname} }, $taxline;
160   }
161
162   # keys are taxnums
163   # values are (cumulative) amounts
164   my %tax_amount;
165
166   my $link_table = $self->info->{link_table};
167
168   # Preconstruct cust_bill_pkg objects that will become the "final"
169   # taxlines for each name, so that we can reference them.
170   # (keys are taxnames)
171   my %real_taxline_named = map {
172     $_ => FS::cust_bill_pkg->new({
173         'pkgnum'    => 0,
174         'recur'     => 0,
175         'sdate'     => '',
176         'edate'     => '',
177         'itemdesc'  => $_
178     })
179   } keys %taxname;
180
181   # For each distinct tax name (the values set as $taxline->itemdesc),
182   # create a consolidated tax item with the total amount and all the links
183   # of all tax items that share that name.
184   foreach my $taxname ( keys %taxname ) {
185     my @tax_links;
186     my $tax_cust_bill_pkg = $real_taxline_named{$taxname};
187     $tax_cust_bill_pkg->set( $link_table => \@tax_links );
188
189     my $tax_total = 0;
190     warn "adding $taxname\n" if $DEBUG > 1;
191
192     foreach my $taxitem ( @{ $taxname{$taxname} } ) {
193       # then we need to transfer the amount and the links from the
194       # line item to the new one we're creating.
195       $tax_total += $taxitem->setup;
196       foreach my $link ( @{ $taxitem->get($link_table) } ) {
197         $link->set('tax_cust_bill_pkg', $tax_cust_bill_pkg);
198
199         # if the link represents tax on tax, also fix its taxable pointer
200         # to point to the "final" taxline
201         my $taxable_cust_bill_pkg = $link->get('taxable_cust_bill_pkg');
202         if (my $other_taxname = $taxable_cust_bill_pkg->itemdesc) {
203           $link->set('taxable_cust_bill_pkg',
204             $real_taxline_named{$other_taxname}
205           );
206         }
207
208         push @tax_links, $link;
209       }
210     } # foreach $taxitem
211     next unless $tax_total;
212
213     # we should really neverround this up...I guess it's okay if taxline 
214     # already returns amounts with 2 decimal places
215     $tax_total = sprintf('%.2f', $tax_total );
216     $tax_cust_bill_pkg->set('setup', $tax_total);
217
218     my $pkg_category = qsearchs( 'pkg_category', { 'categoryname' => $taxname,
219                                                    'disabled'     => '',
220                                                  },
221                                );
222
223     my @display = ();
224     if ( $pkg_category and
225          $conf->config('invoice_latexsummary') ||
226          $conf->config('invoice_htmlsummary')
227        )
228     {
229       my %hash = (  'section' => $pkg_category->categoryname );
230       push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
231     }
232     $tax_cust_bill_pkg->set('display', \@display);
233
234     push @tax_line_items, $tax_cust_bill_pkg;
235   }
236
237   @tax_line_items;
238 }
239
240 =head1 CLASS METHODS
241
242 =item cust_tax_locations LOCATION
243
244 Given an L<FS::cust_location> object (or a hash of location fields), 
245 returns a list of all tax jurisdiction locations that could possibly 
246 match it.  This is meant for interactive use: the location editing UI
247 displays the candidate locations to the user so they can choose the 
248 best match.
249
250 =cut
251
252 sub cust_tax_locations {
253   ();
254 } # shouldn't even get called unless info->{manual_tax_location} is true
255
256 =item add_taxproduct DESCRIPTION
257
258 If the module allows manually adding tax products (categories of taxable
259 items/services), this method will be called to do it. (If not, the UI in
260 browse/part_pkg_taxproduct/* should prevent adding an unlisted tax product.
261 That is the default behavior, so by default this method simply fails.)
262
263 DESCRIPTION is the contents of the taxproduct_description form input, which
264 will normally be filled in by browse/part_pkg_taxproduct/*.
265
266 Must return the newly inserted part_pkg_taxproduct object on success, or
267 a string on failure.
268
269 =cut
270
271 sub add_taxproduct {
272   my $class = shift;
273   "$class does not allow manually adding taxproducts";
274 }
275
276 =item transfer_batch (batch-style only)
277
278 Submits the pending transaction batch for processing, receives the 
279 results, and appends the calculated taxes to all invoices that were 
280 included in the batch.  Then clears their pending flags, and queues
281 a job to run C<FS::cust_main::Billing::collect> on each affected
282 customer.
283
284 =back
285
286 =cut
287
288 1;