summaryrefslogtreecommitdiff
path: root/FS/FS/TaxEngine.pm
blob: e92bf768c7a836217070e583bb291bb393bff4bf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
package FS::TaxEngine;

use strict;
use vars qw( $DEBUG );
use FS::Conf;
use FS::Record qw(qsearch qsearchs);

$DEBUG = 0;

=head1 NAME

FS::TaxEngine - Base class for tax calculation engines.

=head1 USAGE

1. At the start of creating an invoice, create an FS::TaxEngine object.
2. Each time a sale item is added to the invoice, call L</add_sale> on the 
   TaxEngine.
3. Set the "pending" flag on the invoice.
4. Insert the invoice and its line items.

- If the TaxEngine is "batch" style (Billsoft):
5. After creating all invoices for the day, call 
   FS::TaxEngine::process_tax_batch.  This will create the tax items for
   all of the pending invoices, clear the "pending" flag, and call 
   L<FS::cust_main::Billing/collect> on each of the billed customers.

- If not (the internal tax system, CCH):
5. After adding all sale items, call L</calculate_taxes> on the TaxEngine to
   produce a list of tax line items.
6. Append the tax line items to the invoice.
7. Update the invoice with the new charged amount and clear the pending flag.

=head1 CLASS METHODS

=over 4

=item class

Returns the class name for tax engines, according to the 'tax_data_vendor'
configuration setting.

=cut

sub class {
  my $conf = FS::Conf->new;
  my $subclass = $conf->config('tax_data_vendor') || 'internal';
  my $class = "FS::TaxEngine::$subclass";
  local $@;
  eval "use $class";
  die "couldn't load $class: $@\n" if $@;

  $class;
}

=item new 'cust_main' => CUST_MAIN, 'invoice_time' => TIME, OPTIONS...

Creates an L<FS::TaxEngine> object.  The subclass will be chosen by the 
'tax_data_vendor' configuration setting.

CUST_MAIN and TIME are required.  OPTIONS can include:

"cancel" => 1 to indicate that the package is being billed on cancellation.

"estimate" => 1 to indicate that this calculation is for tax estimation,
and isn't an actual sale invoice, in case that matters.

=cut

sub new {
  my $class = shift;
  my %opt = @_;
  my $conf = FS::Conf->new;
  if ($class eq 'FS::TaxEngine') {
    $class = $class->class;
  }
  my $self = { items => [], taxes => {}, conf => $conf, %opt };
  bless $self, $class;
}

=item info

Returns a hashref of metadata about this tax method, including:
- batch: whether this is a batch-style engine (requires different usage)
- override: whether this engine uses tax overrides
- manual_tax_location: whether this engine requires the user to select a "tax
  location" separate from the address/city/state/zip fields
- rate_table: the table that stores the tax rates
  (the 'taxline' method of that class will be used to calculate line-item
   taxes)
- link_table: the table that links L<FS::cust_bill_pkg> records for taxes
  to the C<rate_table> entry that generated them, and to the item they 
  represent tax on.

=back

=head1 METHODS

=over 4

=item add_sale CUST_BILL_PKG

Adds the CUST_BILL_PKG object as a taxable sale on this invoice.

=item calculate_taxes INVOICE

Calculates the taxes on the taxable sales and returns a list of 
L<FS::cust_bill_pkg> objects to add to the invoice.  The base implementation
is to call L</make_taxlines> to produce a list of "raw" tax line items, 
then L</consolidate_taxlines> to combine those with the same itemdesc.

If this fails, it will throw an exception. (Accordingly it should not trap
exceptions from internal methods that it calls, except to translate error 
messages into a more meaningful form.) If it succeeds, it MUST return an
arrayref (even if the arrayref is empty).

=cut

sub calculate_taxes {
  my $self = shift;
  my $cust_bill = shift;

  my @raw_taxlines = $self->make_taxlines($cust_bill);
  if ( !@raw_taxlines ) {
    return;
  } elsif ( !ref $raw_taxlines[0] ) { # error message
    #this isn't actually handled by our caller... better for make_taxlines to 
    # die, that'll be caught be the eval around us in cust_main/Billing.pm
    return $raw_taxlines[0];
  }

  my @real_taxlines = $self->consolidate_taxlines(@raw_taxlines);

  if ( $cust_bill and $cust_bill->get('invnum') ) {
    $_->set('invnum', $cust_bill->get('invnum')) foreach @real_taxlines;
  }
  return \@real_taxlines;
}

sub make_taxlines {
  # only used by FS::TaxEngine::internal; should just move there
  my $self = shift;
  my $conf = $self->{conf};

  my $cust_bill = shift;

  my @raw_taxlines;

  # For each distinct tax rate definition, calculate the tax and exemptions.
  foreach my $taxnum ( keys %{ $self->{taxes} } ) {

    my $taxables = $self->{taxes}{$taxnum};
    my $tax_object = shift @$taxables;
    # $tax_object is a cust_main_county or tax_rate 
    # (with billpkgnum, pkgnum, locationnum set)
    # the rest of @{ $taxlisthash->{$tax} } is cust_bill_pkg component objects
    # (setup, recurring, usage classes)

    my @taxlines = $self->taxline('tax' => $tax_object, 'sales' => $taxables);
    # taxline methods are now required to return the link records alone.
    # Consolidation will take care of the rest.
    next if !@taxlines;
    die $taxlines[0] unless ref($taxlines[0]);

    push @raw_taxlines, @taxlines;

  } #foreach $taxnum

  return @raw_taxlines;
}

sub consolidate_taxlines {

  my $self = shift;
  my $conf = $self->{conf};

  my @raw_taxlines = @_;
  return if !@raw_taxlines; # shouldn't even be here

  my @tax_line_items;

  # keys are tax names (as printed on invoices / itemdesc )
  # values are arrayrefs of tax links ("raw taxlines")
  my %taxname;
  # collate these by itemdesc
  foreach my $taxline (@raw_taxlines) {
    my $taxname = $taxline->taxname;
    $taxname{$taxname} ||= [];
    push @{ $taxname{$taxname} }, $taxline;
  }

  # keys are taxnums
  # values are (cumulative) amounts
  my %tax_amount;

  my $link_table = $raw_taxlines[0]->table;

  # Preconstruct cust_bill_pkg objects that will become the "final"
  # taxlines for each name, so that we can reference them.
  # (keys are taxnames)
  my %real_taxline_named = map {
    $_ => FS::cust_bill_pkg->new({
        'pkgnum'    => 0,
        'recur'     => 0,
        'sdate'     => '',
        'edate'     => '',
        'itemdesc'  => $_
    })
  } keys %taxname;

  # For each distinct tax name (the values set as $taxline->itemdesc),
  # create a consolidated tax item with the total amount and all the links
  # of all tax items that share that name.
  foreach my $taxname ( keys %taxname ) {
    my $tax_links = $taxname{$taxname};
    my $tax_cust_bill_pkg = $real_taxline_named{$taxname};
    $tax_cust_bill_pkg->set( $link_table => $tax_links );

    my $tax_total = 0;
    warn "adding $taxname\n" if $DEBUG > 1;

    foreach my $link ( @$tax_links ) {
      # then we need to transfer the amount and the links from the
      # line item to the new one we're creating.
      $tax_total += $link->amount;
      $link->set('tax_cust_bill_pkg', $tax_cust_bill_pkg);

      # if the link represents tax on tax, also fix its taxable pointer
      # to point to the "final" taxline
      my $taxable_cust_bill_pkg = $link->get('taxable_cust_bill_pkg');
      if ( $taxable_cust_bill_pkg and
           my $other_taxname = $taxable_cust_bill_pkg->itemdesc) {
        $link->set('taxable_cust_bill_pkg',
          $real_taxline_named{$other_taxname}
        );
      }

    } # foreach $link
    next unless $tax_total;

    # we should really neverround this up...I guess it's okay if taxline 
    # already returns amounts with 2 decimal places
    $tax_total = sprintf('%.2f', $tax_total );
    $tax_cust_bill_pkg->set('setup', $tax_total);

    my $pkg_category = qsearchs( 'pkg_category', { 'categoryname' => $taxname,
                                                   'disabled'     => '',
                                                 },
                               );

    my @display = ();
    if ( $pkg_category and
         $conf->config('invoice_latexsummary') ||
         $conf->config('invoice_htmlsummary')
       )
    {
      my %hash = (  'section' => $pkg_category->categoryname );
      push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
    }
    $tax_cust_bill_pkg->set('display', \@display);

    push @tax_line_items, $tax_cust_bill_pkg;
  }

  @tax_line_items;
}

=head1 CLASS METHODS

=item cust_tax_locations LOCATION

Given an L<FS::cust_location> object (or a hash of location fields), 
returns a list of all tax jurisdiction locations that could possibly 
match it.  This is meant for interactive use: the location editing UI
displays the candidate locations to the user so they can choose the 
best match.

=cut

sub cust_tax_locations {
  ();
} # shouldn't even get called unless info->{manual_tax_location} is true

=item add_taxproduct DESCRIPTION

If the module allows manually adding tax products (categories of taxable
items/services), this method will be called to do it. (If not, the UI in
browse/part_pkg_taxproduct/* should prevent adding an unlisted tax product.
That is the default behavior, so by default this method simply fails.)

DESCRIPTION is the contents of the taxproduct_description form input, which
will normally be filled in by browse/part_pkg_taxproduct/*.

Must return the newly inserted part_pkg_taxproduct object on success, or
a string on failure.

=cut

sub add_taxproduct {
  my $class = shift;
  #my $classname = ref($class);
  #my $vendor = (split('::',$classname))[2];
  my $vendor = ref($class) || $class;
  "$vendor does not allow manually adding taxproducts";
}

=item transfer_batch (batch-style only)

Submits the pending transaction batch for processing, receives the 
results, and appends the calculated taxes to all invoices that were 
included in the batch.  Then clears their pending flags, and queues
a job to run C<FS::cust_main::Billing::collect> on each affected
customer.

=back

=cut

1;