tax engine refactoring for Avalara and Billsoft tax vendors, #25718
[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 C<add_sale> on the 
18    TaxEngine.
19
20 - If the TaxEngine is "batch" style (Billsoft):
21 3. Set the "pending" flag on the invoice.
22 4. Insert the invoice and its line items.
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    C<collect> on each of the billed customers.
27
28 - If not (the internal tax system, CCH):
29 3. After adding all sale items, call C<calculate_taxes> on the TaxEngine to
30    produce a list of tax line items.
31 4. Append the tax line items to the invoice.
32 5. Insert the invoice.
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 "cancel" => 1 to 
44 indicate that the package is being billed on cancellation.
45
46 =cut
47
48 sub new {
49   my $class = shift;
50   my %opt = @_;
51   if ($class eq 'FS::TaxEngine') {
52     my $conf = FS::Conf->new;
53     my $subclass = $conf->config('enable_taxproducts') || 'internal';
54     $class .= "::$subclass";
55     local $@;
56     eval "use $class";
57     die "couldn't load $class: $@\n" if $@;
58   }
59   my $self = { items => [], taxes => {}, %opt };
60   bless $self, $class;
61 }
62
63 =item info
64
65 Returns a hashref of metadata about this tax method, including:
66 - batch: whether this is a batch-style engine (requires different usage)
67 - override: whether this engine uses tax overrides
68 - manual_tax_location: whether this engine requires the user to select a "tax
69   location" separate from the address/city/state/zip fields
70 - rate_table: the table that stores the tax rates
71   (the 'taxline' method of that class will be used to calculate line-item
72    taxes)
73 - link_table: the table that links L<FS::cust_bill_pkg> records for taxes
74   to the C<rate_table> entry that generated them, and to the item they 
75   represent tax on.
76
77 =back
78
79 =head1 METHODS
80
81 =over 4
82
83 =item add_sale CUST_BILL_PKG
84
85 Adds the CUST_BILL_PKG object as a taxable sale on this invoice.
86
87 =item calculate_taxes CUST_BILL
88
89 Calculates the taxes on the taxable sales and returns a list of 
90 L<FS::cust_bill_pkg> objects to add to the invoice.  There is a base 
91 implementation of this, which calls the C<taxline> method to calculate
92 each individual tax.
93
94 =cut
95
96 sub calculate_taxes {
97   my $self = shift;
98   my $conf = FS::Conf->new;
99
100   my $cust_bill = shift;
101
102   my @tax_line_items;
103   # keys are tax names (as printed on invoices / itemdesc )
104   # values are arrayrefs of taxlines
105   my %taxname;
106
107   # keys are taxnums
108   # values are (cumulative) amounts
109   my %tax_amount;
110
111   # keys are taxnums
112   # values are arrayrefs of cust_tax_exempt_pkg objects
113   my %tax_exemption;
114
115   # For each distinct tax rate definition, calculate the tax and exemptions.
116   foreach my $taxnum ( keys %{ $self->{taxes} } ) {
117
118     my $taxables = $self->{taxes}{$taxnum};
119     my $tax_object = shift @$taxables;
120     # $tax_object is a cust_main_county or tax_rate 
121     # (with billpkgnum, pkgnum, locationnum set)
122     # the rest of @{ $taxlisthash->{$tax} } is cust_bill_pkg component objects
123     # (setup, recurring, usage classes)
124
125     my $taxline = $self->taxline('tax' => $tax_object, 'sales' => $taxables);
126     # taxline methods are now required to return real line items
127     # with their link records
128     die $taxline unless ref($taxline);
129
130     push @{ $taxname{ $taxline->itemdesc } }, $taxline;
131
132   } #foreach $taxnum
133
134   my $link_table = $self->info->{link_table};
135   # For each distinct tax name (the values set as $taxline->itemdesc),
136   # create a consolidated tax item with the total amount and all the links
137   # of all tax items that share that name.
138   foreach my $taxname ( keys %taxname ) {
139     my @tax_links;
140     my $tax_cust_bill_pkg = FS::cust_bill_pkg->new({
141         'invnum'    => $cust_bill->invnum,
142         'pkgnum'    => 0,
143         'recur'     => 0,
144         'sdate'     => '',
145         'edate'     => '',
146         'itemdesc'  => $taxname,
147         $link_table => \@tax_links,
148     });
149
150     my $tax_total = 0;
151     warn "adding $taxname\n" if $DEBUG > 1;
152
153     foreach my $taxitem ( @{ $taxname{$taxname} } ) {
154       # then we need to transfer the amount and the links from the
155       # line item to the new one we're creating.
156       $tax_total += $taxitem->setup;
157       foreach my $link ( @{ $taxitem->get($link_table) } ) {
158         $link->set('tax_cust_bill_pkg', $tax_cust_bill_pkg);
159         push @tax_links, $link;
160       }
161     } # foreach $taxitem
162     next unless $tax_total;
163
164     # we should really neverround this up...I guess it's okay if taxline 
165     # already returns amounts with 2 decimal places
166     $tax_total = sprintf('%.2f', $tax_total );
167     $tax_cust_bill_pkg->set('setup', $tax_total);
168
169     my $pkg_category = qsearchs( 'pkg_category', { 'categoryname' => $taxname,
170                                                    'disabled'     => '',
171                                                  },
172                                );
173
174     my @display = ();
175     if ( $pkg_category and
176          $conf->config('invoice_latexsummary') ||
177          $conf->config('invoice_htmlsummary')
178        )
179     {
180       my %hash = (  'section' => $pkg_category->categoryname );
181       push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
182     }
183     $tax_cust_bill_pkg->set('display', \@display);
184
185     push @tax_line_items, $tax_cust_bill_pkg;
186   }
187
188   \@tax_line_items;
189 }
190
191 =head1 CLASS METHODS
192
193 =item cust_tax_locations LOCATION
194
195 Given an L<FS::cust_location> object (or a hash of location fields), 
196 returns a list of all tax jurisdiction locations that could possibly 
197 match it.  This is meant for interactive use: the location editing UI
198 displays the candidate locations to the user so they can choose the 
199 best match.
200
201 =cut
202
203 sub cust_tax_locations {
204   ();
205 } # shouldn't even get called unless info->{manual_tax_location} is true
206
207 =item add_taxproduct DESCRIPTION
208
209 If the module allows manually adding tax products (categories of taxable
210 items/services), this method will be called to do it. (If not, the UI in
211 browse/part_pkg_taxproduct/* should prevent adding an unlisted tax product.
212 That is the default behavior, so by default this method simply fails.)
213
214 DESCRIPTION is the contents of the taxproduct_description form input, which
215 will normally be filled in by browse/part_pkg_taxproduct/*.
216
217 Must return the newly inserted part_pkg_taxproduct object on success, or
218 a string on failure.
219
220 =cut
221
222 sub add_taxproduct {
223   my $class = shift;
224   "$class does not allow manually adding taxproducts";
225 }
226
227 =item transfer_batch (batch-style only)
228
229 Submits the pending transaction batch for processing, receives the 
230 results, and appends the calculated taxes to all invoices that were 
231 included in the batch.  Then clears their pending flags, and queues
232 a job to run C<FS::cust_main::Billing::collect> on each affected
233 customer.
234
235 =back
236
237 =cut
238
239 1;