delete fees, RT#81713
[freeside.git] / FS / FS / cust_bill_pkg.pm
1 package FS::cust_bill_pkg;
2 use base qw( FS::TemplateItem_Mixin FS::cust_main_Mixin FS::Record );
3
4 use strict;
5 use vars qw( @ISA $DEBUG $me );
6 use Carp;
7 use List::Util qw( sum min );
8 use Text::CSV_XS;
9 use FS::Record qw( qsearch qsearchs dbh );
10 use FS::cust_pkg;
11 use FS::cust_bill_pkg_detail;
12 use FS::cust_bill_pkg_display;
13 use FS::cust_bill_pkg_discount;
14 use FS::cust_bill_pkg_fee;
15 use FS::cust_bill_pay_pkg;
16 use FS::cust_credit_bill_pkg;
17 use FS::cust_tax_exempt_pkg;
18 use FS::cust_bill_pkg_tax_location;
19 use FS::cust_bill_pkg_tax_rate_location;
20 use FS::cust_tax_adjustment;
21 use FS::cust_bill_pkg_void;
22 use FS::cust_bill_pkg_detail_void;
23 use FS::cust_bill_pkg_display_void;
24 use FS::cust_bill_pkg_discount_void;
25 use FS::cust_bill_pkg_tax_location_void;
26 use FS::cust_bill_pkg_tax_rate_location_void;
27 use FS::cust_tax_exempt_pkg_void;
28 use FS::cust_bill_pkg_fee_void;
29 use FS::reason;
30 use FS::reason_type;
31
32 use FS::Cursor;
33
34 $DEBUG = 0;
35 $me = '[FS::cust_bill_pkg]';
36
37 =head1 NAME
38
39 FS::cust_bill_pkg - Object methods for cust_bill_pkg records
40
41 =head1 SYNOPSIS
42
43   use FS::cust_bill_pkg;
44
45   $record = new FS::cust_bill_pkg \%hash;
46   $record = new FS::cust_bill_pkg { 'column' => 'value' };
47
48   $error = $record->insert;
49
50   $error = $record->check;
51
52 =head1 DESCRIPTION
53
54 An FS::cust_bill_pkg object represents an invoice line item.
55 FS::cust_bill_pkg inherits from FS::Record.  The following fields are
56 currently supported:
57
58 =over 4
59
60 =item billpkgnum
61
62 primary key
63
64 =item invnum
65
66 invoice (see L<FS::cust_bill>)
67
68 =item pkgnum
69
70 package (see L<FS::cust_pkg>) or 0 for the special virtual sales tax package, or -1 for the virtual line item (itemdesc is used for the line)
71
72 =item pkgpart_override
73
74 optional package definition (see L<FS::part_pkg>) override
75
76 =item setup
77
78 setup fee
79
80 =item recur
81
82 recurring fee
83
84 =item sdate
85
86 starting date of recurring fee
87
88 =item edate
89
90 ending date of recurring fee
91
92 =item itemdesc
93
94 Line item description (overrides normal package description)
95
96 =item quantity
97
98 If not set, defaults to 1
99
100 =item unitsetup
101
102 If not set, defaults to setup
103
104 =item unitrecur
105
106 If not set, defaults to recur
107
108 =item hidden
109
110 If set to Y, indicates data should not appear as separate line item on invoice
111
112 =back
113
114 sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">.  Also
115 see L<Time::Local> and L<Date::Parse> for conversion functions.
116
117 =head1 METHODS
118
119 =over 4
120
121 =item new HASHREF
122
123 Creates a new line item.  To add the line item to the database, see
124 L<"insert">.  Line items are normally created by calling the bill method of a
125 customer object (see L<FS::cust_main>).
126
127 =cut
128
129 sub table { 'cust_bill_pkg'; }
130
131 sub detail_table            { 'cust_bill_pkg_detail'; }
132 sub display_table           { 'cust_bill_pkg_display'; }
133 sub discount_table          { 'cust_bill_pkg_discount'; }
134 #sub tax_location_table      { 'cust_bill_pkg_tax_location'; }
135 #sub tax_rate_location_table { 'cust_bill_pkg_tax_rate_location'; }
136 #sub tax_exempt_pkg_table    { 'cust_tax_exempt_pkg'; }
137
138 =item insert
139
140 Adds this line item to the database.  If there is an error, returns the error,
141 otherwise returns false.
142
143 =cut
144
145 sub insert {
146   my $self = shift;
147
148   local $SIG{HUP} = 'IGNORE';
149   local $SIG{INT} = 'IGNORE';
150   local $SIG{QUIT} = 'IGNORE';
151   local $SIG{TERM} = 'IGNORE';
152   local $SIG{TSTP} = 'IGNORE';
153   local $SIG{PIPE} = 'IGNORE';
154
155   my $oldAutoCommit = $FS::UID::AutoCommit;
156   local $FS::UID::AutoCommit = 0;
157   my $dbh = dbh;
158
159   my $error = $self->SUPER::insert;
160   if ( $error ) {
161     $dbh->rollback if $oldAutoCommit;
162     return $error;
163   }
164
165   if ( $self->get('details') ) {
166     foreach my $detail ( @{$self->get('details')} ) {
167       $detail->billpkgnum($self->billpkgnum);
168       $error = $detail->insert;
169       if ( $error ) {
170         $dbh->rollback if $oldAutoCommit;
171         return "error inserting cust_bill_pkg_detail: $error";
172       }
173     }
174   }
175
176   if ( $self->get('display') ) {
177     foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
178       $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
179       $error = $cust_bill_pkg_display->insert;
180       if ( $error ) {
181         $dbh->rollback if $oldAutoCommit;
182         return "error inserting cust_bill_pkg_display: $error";
183       }
184     }
185   }
186
187   if ( $self->get('discounts') ) {
188     foreach my $cust_bill_pkg_discount ( @{$self->get('discounts')} ) {
189       $cust_bill_pkg_discount->billpkgnum($self->billpkgnum);
190       $error = $cust_bill_pkg_discount->insert;
191       if ( $error ) {
192         $dbh->rollback if $oldAutoCommit;
193         return "error inserting cust_bill_pkg_discount: $error";
194       }
195     }
196   }
197
198   foreach my $cust_tax_exempt_pkg ( @{$self->cust_tax_exempt_pkg} ) {
199     $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
200     $error = $cust_tax_exempt_pkg->insert;
201     if ( $error ) {
202       $dbh->rollback if $oldAutoCommit;
203       return "error inserting cust_tax_exempt_pkg: $error";
204     }
205   }
206
207   foreach my $tax_link_table (qw(cust_bill_pkg_tax_location
208                                  cust_bill_pkg_tax_rate_location))
209   {
210     my $tax_location = $self->get($tax_link_table) || [];
211     foreach my $link ( @$tax_location ) {
212       my $pkey = $link->primary_key;
213       next if $link->get($pkey); # don't try to double-insert
214       # This cust_bill_pkg can be linked on either side (i.e. it can be the
215       # tax or the taxed item).  If the other side is already inserted, 
216       # then set billpkgnum to ours, and insert the link.  Otherwise,
217       # set billpkgnum to ours and pass the link off to the cust_bill_pkg
218       # on the other side, to be inserted later.
219
220       my $tax_cust_bill_pkg = $link->get('tax_cust_bill_pkg');
221       if ( $tax_cust_bill_pkg && $tax_cust_bill_pkg->billpkgnum ) {
222         $link->set('billpkgnum', $tax_cust_bill_pkg->billpkgnum);
223         # break circular links when doing this
224         $link->set('tax_cust_bill_pkg', '');
225       }
226       my $taxable_cust_bill_pkg = $link->get('taxable_cust_bill_pkg');
227       if ( $taxable_cust_bill_pkg && $taxable_cust_bill_pkg->billpkgnum ) {
228         $link->set('taxable_billpkgnum', $taxable_cust_bill_pkg->billpkgnum);
229         # XXX pkgnum is zero for tax on tax; it might be better to use
230         # the underlying package?
231         $link->set('pkgnum', $taxable_cust_bill_pkg->pkgnum);
232         $link->set('locationnum', $taxable_cust_bill_pkg->tax_locationnum);
233         $link->set('taxable_cust_bill_pkg', '');
234       }
235
236       if ( $link->billpkgnum and $link->taxable_billpkgnum ) {
237         $error = $link->insert;
238         if ( $error ) {
239           $dbh->rollback if $oldAutoCommit;
240           return "error inserting cust_bill_pkg_tax_location: $error";
241         }
242       } else { # handoff
243         my $other; # the as yet uninserted cust_bill_pkg
244         $other = $link->billpkgnum ? $link->get('taxable_cust_bill_pkg')
245                                    : $link->get('tax_cust_bill_pkg');
246         my $link_array = $other->get( $tax_link_table ) || [];
247         push @$link_array, $link;
248         $other->set( $tax_link_table => $link_array);
249       }
250     } #foreach my $link
251   }
252
253   # someday you will be as awesome as cust_bill_pkg_tax_location...
254   # and today is that day
255   #my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
256   #if ( $tax_rate_location ) {
257   #  foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
258   #    $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
259   #    $error = $cust_bill_pkg_tax_rate_location->insert;
260   #    if ( $error ) {
261   #      $dbh->rollback if $oldAutoCommit;
262   #      return "error inserting cust_bill_pkg_tax_rate_location: $error";
263   #    }
264   #  }
265   #}
266
267   my $fee_links = $self->get('cust_bill_pkg_fee');
268   if ( $fee_links ) {
269     foreach my $link ( @$fee_links ) {
270       # very similar to cust_bill_pkg_tax_location, for obvious reasons
271       next if $link->billpkgfeenum; # don't try to double-insert
272
273       my $target = $link->get('cust_bill_pkg'); # the line item of the fee
274       my $base = $link->get('base_cust_bill_pkg'); # line item it was based on
275
276       if ( $target and $target->billpkgnum ) {
277         $link->set('billpkgnum', $target->billpkgnum);
278         # base_invnum => null indicates that the fee is based on its own
279         # invoice
280         $link->set('base_invnum', $target->invnum) unless $link->base_invnum;
281         $link->set('cust_bill_pkg', '');
282       }
283
284       if ( $base and $base->billpkgnum ) {
285         $link->set('base_billpkgnum', $base->billpkgnum);
286         $link->set('base_cust_bill_pkg', '');
287       } elsif ( $base ) {
288         # it's based on a line item that's not yet inserted
289         my $link_array = $base->get('cust_bill_pkg_fee') || [];
290         push @$link_array, $link;
291         $base->set('cust_bill_pkg_fee' => $link_array);
292         next; # don't insert the link yet
293       }
294
295       $error = $link->insert;
296       if ( $error ) {
297         $dbh->rollback if $oldAutoCommit;
298         return "error inserting cust_bill_pkg_fee: $error";
299       }
300     } # foreach my $link
301   }
302
303   if ( my $fee_origin = $self->get('fee_origin') ) {
304     $fee_origin->set('billpkgnum' => $self->billpkgnum);
305     $error = $fee_origin->replace;
306     if ( $error ) {
307       $dbh->rollback if $oldAutoCommit;
308       return "error updating fee origin record: $error";
309     }
310   }
311
312   my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
313   if ( $cust_tax_adjustment ) {
314     $cust_tax_adjustment->billpkgnum($self->billpkgnum);
315     $error = $cust_tax_adjustment->replace;
316     if ( $error ) {
317       $dbh->rollback if $oldAutoCommit;
318       return "error replacing cust_tax_adjustment: $error";
319     }
320   }
321
322   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
323   '';
324
325 }
326
327 =item void [ REASON [ , REPROCESS_CDRS ] ]
328
329 Voids this line item: deletes the line item and adds a record of the voided
330 line item to the FS::cust_bill_pkg_void table (and related tables).
331
332 =cut
333
334 sub void {
335   my $self = shift;
336   my $reason = scalar(@_) ? shift : '';
337   my $reprocess_cdrs = scalar(@_) ? shift : '';
338
339   unless (ref($reason) || !$reason) {
340     $reason = FS::reason->new_or_existing(
341       'class'  => 'I',
342       'type'   => 'Invoice void',
343       'reason' => $reason
344     );
345   }
346
347   local $SIG{HUP} = 'IGNORE';
348   local $SIG{INT} = 'IGNORE';
349   local $SIG{QUIT} = 'IGNORE';
350   local $SIG{TERM} = 'IGNORE';
351   local $SIG{TSTP} = 'IGNORE';
352   local $SIG{PIPE} = 'IGNORE';
353
354   my $oldAutoCommit = $FS::UID::AutoCommit;
355   local $FS::UID::AutoCommit = 0;
356   my $dbh = dbh;
357
358   my $cust_bill_pkg_void = new FS::cust_bill_pkg_void ( {
359     map { $_ => $self->get($_) } $self->fields
360   } );
361   $cust_bill_pkg_void->reasonnum($reason->reasonnum) if $reason;
362   my $error = $cust_bill_pkg_void->insert;
363   if ( $error ) {
364     $dbh->rollback if $oldAutoCommit;
365     return $error;
366   }
367
368   #more efficiently than below, because there could be lots
369   $self->void_cust_bill_pkg_detail($reprocess_cdrs);
370
371   foreach my $table (qw(
372     cust_bill_pkg_display
373     cust_bill_pkg_discount
374     cust_bill_pkg_tax_location
375     cust_bill_pkg_tax_rate_location
376     cust_tax_exempt_pkg
377     cust_bill_pkg_fee
378   )) {
379     foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
380
381       my $vclass = 'FS::'.$table.'_void';
382       my $void = $vclass->new( {
383         map { $_ => $linked->get($_) } $linked->fields
384       });
385       my $error = $void->insert || $linked->delete;
386       if ( $error ) {
387         $dbh->rollback if $oldAutoCommit;
388         return $error;
389       }
390
391     }
392
393   }
394
395   $error = $self->delete;
396   if ( $error ) {
397     $dbh->rollback if $oldAutoCommit;
398     return $error;
399   }
400
401   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
402
403   '';
404
405 }
406
407 sub void_cust_bill_pkg_detail {
408   my( $self, $reprocess_cdrs ) = @_;
409
410   my $from_cust_bill_pkg_detail =
411     'FROM cust_bill_pkg_detail WHERE billpkgnum = ?';
412   my $where_detailnum =
413     "WHERE detailnum IN ( SELECT detailnum $from_cust_bill_pkg_detail )";
414
415   if ( $reprocess_cdrs ) {
416     #well, technically this could have been on other invoices / termination
417     # partners... separate flag?
418     $self->scalar_sql(
419       "DELETE FROM cdr_termination
420          WHERE acctid IN ( SELECT acctid FROM cdr $where_detailnum )
421       ",
422       $self->billpkgnum
423     );
424   }
425
426   my $setstatus = $reprocess_cdrs ? ', freesidestatus = NULL' : '';
427   $self->scalar_sql(
428     "UPDATE cdr SET detailnum = NULL $setstatus $where_detailnum",
429     $self->billpkgnum
430   );
431
432   $self->scalar_sql("INSERT INTO cust_bill_pkg_detail_void
433                        SELECT * $from_cust_bill_pkg_detail",
434                     $self->billpkgnum
435                    );
436
437   $self->scalar_sql("DELETE $from_cust_bill_pkg_detail", $self->billpkgnum);
438
439 }
440
441 =item delete
442
443 Not recommended.
444
445 =cut
446
447 sub delete {
448   my $self = shift;
449
450   local $SIG{HUP} = 'IGNORE';
451   local $SIG{INT} = 'IGNORE';
452   local $SIG{QUIT} = 'IGNORE';
453   local $SIG{TERM} = 'IGNORE';
454   local $SIG{TSTP} = 'IGNORE';
455   local $SIG{PIPE} = 'IGNORE';
456
457   my $oldAutoCommit = $FS::UID::AutoCommit;
458   local $FS::UID::AutoCommit = 0;
459   my $dbh = dbh;
460
461   foreach my $table (qw(
462     cust_bill_pkg_detail
463     cust_bill_pkg_display
464     cust_bill_pkg_discount
465     cust_bill_pkg_tax_location
466     cust_bill_pkg_tax_rate_location
467     cust_tax_exempt_pkg
468     cust_bill_pay_pkg
469     cust_credit_bill_pkg
470     cust_bill_pkg_fee
471   )) {
472
473     foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
474       my $error = $linked->delete;
475       if ( $error ) {
476         $dbh->rollback if $oldAutoCommit;
477         return $error;
478       }
479     }
480
481   }
482
483   foreach my $cust_tax_adjustment (
484     qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
485   ) {
486     $cust_tax_adjustment->billpkgnum(''); #NULL
487     my $error = $cust_tax_adjustment->replace;
488     if ( $error ) {
489       $dbh->rollback if $oldAutoCommit;
490       return $error;
491     }
492   }
493
494   #fix the invoice amount
495
496   my $cust_bill = $self->cust_bill;
497   $cust_bill->charged( $cust_bill->charged - $self->setup - $self->recur );
498
499   #not adding a cc surcharge, but this override lets us modify charged
500   $cust_bill->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
501
502   my $error = $cust_bill->replace;
503   if ( $error ) {
504     $dbh->rollback if $oldAutoCommit;
505     return $error;
506   }
507
508   my $error = $self->SUPER::delete(@_);
509   if ( $error ) {
510     $dbh->rollback if $oldAutoCommit;
511     return $error;
512   }
513
514   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
515
516   '';
517
518 }
519
520 #alas, bin/follow-tax-rename
521 #
522 #=item replace OLD_RECORD
523 #
524 #Currently unimplemented.  This would be even more of an accounting nightmare
525 #than deleteing the items.  Just don't do it.
526 #
527 #=cut
528 #
529 #sub replace {
530 #  return "Can't modify cust_bill_pkg records!";
531 #}
532
533 =item check
534
535 Checks all fields to make sure this is a valid line item.  If there is an
536 error, returns the error, otherwise returns false.  Called by the insert
537 method.
538
539 =cut
540
541 sub check {
542   my $self = shift;
543
544   my $error =
545          $self->ut_numbern('billpkgnum')
546       || $self->ut_snumber('pkgnum')
547       || $self->ut_number('invnum')
548       || $self->ut_money('setup')
549       || $self->ut_moneyn('unitsetup')
550       || $self->ut_currencyn('setup_billed_currency')
551       || $self->ut_moneyn('setup_billed_amount')
552       || $self->ut_money('recur')
553       || $self->ut_moneyn('unitrecur')
554       || $self->ut_currencyn('recur_billed_currency')
555       || $self->ut_moneyn('recur_billed_amount')
556       || $self->ut_numbern('sdate')
557       || $self->ut_numbern('edate')
558       || $self->ut_textn('itemdesc')
559       || $self->ut_textn('itemcomment')
560       || $self->ut_enum('hidden', [ '', 'Y' ])
561   ;
562   return $error if $error;
563
564   $self->regularize_details;
565
566   #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
567   if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
568     return "Unknown pkgnum ". $self->pkgnum
569       unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
570   }
571
572   return "Unknown invnum"
573     unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
574
575   $self->SUPER::check;
576 }
577
578 =item regularize_details
579
580 Converts the contents of the 'details' pseudo-field to 
581 L<FS::cust_bill_pkg_detail> objects, if they aren't already.
582
583 =cut
584
585 sub regularize_details {
586   my $self = shift;
587   if ( $self->get('details') ) {
588     foreach my $detail ( @{$self->get('details')} ) {
589       if ( ref($detail) ne 'FS::cust_bill_pkg_detail' ) {
590         # then turn it into one
591         my %hash = ();
592         if ( ! ref($detail) ) {
593           $hash{'detail'} = $detail;
594         }
595         elsif ( ref($detail) eq 'HASH' ) {
596           %hash = %$detail;
597         }
598         elsif ( ref($detail) eq 'ARRAY' ) {
599           carp "passing invoice details as arrays is deprecated";
600           #carp "this way sucks, use a hash"; #but more useful/friendly
601           $hash{'format'}      = $detail->[0];
602           $hash{'detail'}      = $detail->[1];
603           $hash{'amount'}      = $detail->[2];
604           $hash{'classnum'}    = $detail->[3];
605           $hash{'phonenum'}    = $detail->[4];
606           $hash{'accountcode'} = $detail->[5];
607           $hash{'startdate'}   = $detail->[6];
608           $hash{'duration'}    = $detail->[7];
609           $hash{'regionname'}  = $detail->[8];
610         }
611         else {
612           die "unknown detail type ". ref($detail);
613         }
614         $detail = new FS::cust_bill_pkg_detail \%hash;
615       }
616       $detail->billpkgnum($self->billpkgnum) if $self->billpkgnum;
617     }
618   }
619   return;
620 }
621
622 =item set_exemptions TAXOBJECT, OPTIONS
623
624 Sets up tax exemptions.  TAXOBJECT is the L<FS::cust_main_county> or 
625 L<FS::tax_rate> record for the tax.
626
627 This will deal with the following cases:
628
629 =over 4
630
631 =item Fully exempt customers (cust_main.tax flag) or customer classes 
632 (cust_class.tax).
633
634 =item Customers exempt from specific named taxes (cust_main_exemption 
635 records).
636
637 =item Taxes that don't apply to setup or recurring fees 
638 (cust_main_county.setuptax and recurtax, tax_rate.setuptax and recurtax).
639
640 =item Packages that are marked as tax-exempt (part_pkg.setuptax,
641 part_pkg.recurtax).
642
643 =item Fees that aren't marked as taxable (part_fee.taxable).
644
645 =back
646
647 It does NOT deal with monthly tax exemptions, which need more context 
648 than this humble little method cares to deal with.
649
650 OPTIONS should include "custnum" => the customer number if this tax line
651 hasn't been inserted (which it probably hasn't).
652
653 Returns a list of exemption objects, which will also be attached to the 
654 line item as the 'cust_tax_exempt_pkg' pseudo-field.  Inserting the line
655 item will insert these records as well.
656
657 =cut
658
659 sub set_exemptions {
660   my $self = shift;
661   my $tax = shift;
662   my %opt = @_;
663
664   my $part_pkg  = $self->part_pkg;
665   my $part_fee  = $self->part_fee;
666
667   my $cust_main;
668   my $custnum = $opt{custnum};
669   $custnum ||= $self->cust_bill->custnum if $self->cust_bill;
670
671   $cust_main = FS::cust_main->by_key( $custnum )
672     or die "set_exemptions can't identify customer (pass custnum option)\n";
673
674   my @new_exemptions;
675   my $taxable_charged = $self->setup + $self->recur;
676   return unless $taxable_charged > 0;
677
678   ### Fully exempt customer ###
679   my $exempt_cust;
680   my $conf = FS::Conf->new;
681   if ( $conf->exists('cust_class-tax_exempt') ) {
682     my $cust_class = $cust_main->cust_class;
683     $exempt_cust = $cust_class->tax if $cust_class;
684   } else {
685     $exempt_cust = $cust_main->tax;
686   }
687
688   ### Exemption from named tax ###
689   my $exempt_cust_taxname;
690   if ( !$exempt_cust and $tax->taxname ) {
691     $exempt_cust_taxname = $cust_main->tax_exemption($tax->taxname);
692   }
693
694   if ( $exempt_cust ) {
695
696     push @new_exemptions, FS::cust_tax_exempt_pkg->new({
697         amount => $taxable_charged,
698         exempt_cust => 'Y',
699       });
700     $taxable_charged = 0;
701
702   } elsif ( $exempt_cust_taxname ) {
703
704     push @new_exemptions, FS::cust_tax_exempt_pkg->new({
705         amount => $taxable_charged,
706         exempt_cust_taxname => 'Y',
707       });
708     $taxable_charged = 0;
709
710   }
711
712   my $exempt_setup = ( ($part_fee and not $part_fee->taxable)
713       or ($part_pkg and $part_pkg->setuptax)
714       or $tax->setuptax );
715
716   if ( $exempt_setup
717       and $self->setup > 0
718       and $taxable_charged > 0 ) {
719
720     push @new_exemptions, FS::cust_tax_exempt_pkg->new({
721         amount => $self->setup,
722         exempt_setup => 'Y'
723       });
724     $taxable_charged -= $self->setup;
725
726   }
727
728   my $exempt_recur = ( ($part_fee and not $part_fee->taxable)
729       or ($part_pkg and $part_pkg->recurtax)
730       or $tax->recurtax );
731
732   if ( $exempt_recur
733       and $self->recur > 0
734       and $taxable_charged > 0 ) {
735
736     push @new_exemptions, FS::cust_tax_exempt_pkg->new({
737         amount => $self->recur,
738         exempt_recur => 'Y'
739       });
740     $taxable_charged -= $self->recur;
741
742   }
743
744   foreach (@new_exemptions) {
745     $_->set('taxnum', $tax->taxnum);
746     $_->set('taxtype', ref($tax));
747   }
748
749   push @{ $self->cust_tax_exempt_pkg }, @new_exemptions;
750   return @new_exemptions;
751
752 }
753
754 =item cust_bill
755
756 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
757
758 =item cust_main
759
760 Returns the customer (L<FS::cust_main> object) for this line item.
761
762 =cut
763
764 sub cust_main {
765   carp "->cust_main called" if $DEBUG;
766   # required for cust_main_Mixin equivalence
767   # and use cust_bill instead of cust_pkg because this might not have a 
768   # cust_pkg
769   my $self = shift;
770   my $cust_bill = $self->cust_bill or return '';
771   $cust_bill->cust_main;
772 }
773
774 =item previous_cust_bill_pkg
775
776 Returns the previous cust_bill_pkg for this package, if any.
777
778 =cut
779
780 sub previous_cust_bill_pkg {
781   my $self = shift;
782   return unless $self->sdate;
783   qsearchs({
784     'table'    => 'cust_bill_pkg',
785     'hashref'  => { 'pkgnum' => $self->pkgnum,
786                     'sdate'  => { op=>'<', value=>$self->sdate },
787                   },
788     'order_by' => 'ORDER BY sdate DESC LIMIT 1',
789   });
790 }
791
792 =item owed_setup
793
794 Returns the amount owed (still outstanding) on this line item's setup fee,
795 which is the amount of the line item minus all payment applications (see
796 L<FS::cust_bill_pay_pkg> and credit applications (see
797 L<FS::cust_credit_bill_pkg>).
798
799 =cut
800
801 sub owed_setup {
802   my $self = shift;
803   $self->owed('setup', @_);
804 }
805
806 =item owed_recur
807
808 Returns the amount owed (still outstanding) on this line item's recurring fee,
809 which is the amount of the line item minus all payment applications (see
810 L<FS::cust_bill_pay_pkg> and credit applications (see
811 L<FS::cust_credit_bill_pkg>).
812
813 =cut
814
815 sub owed_recur {
816   my $self = shift;
817   $self->owed('recur', @_);
818 }
819
820 # modeled after cust_bill::owed...
821 sub owed {
822   my( $self, $field ) = @_;
823   my $balance = $self->$field();
824   $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
825   $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
826   $balance = sprintf( '%.2f', $balance );
827   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
828   $balance;
829 }
830
831 #modeled after owed
832 sub payable {
833   my( $self, $field ) = @_;
834   my $balance = $self->$field();
835   $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
836   $balance = sprintf( '%.2f', $balance );
837   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
838   $balance;
839 }
840
841 sub cust_bill_pay_pkg {
842   my( $self, $field ) = @_;
843   qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
844                                   'setuprecur' => $field,
845                                 }
846          );
847 }
848
849 sub cust_credit_bill_pkg {
850   my( $self, $field ) = @_;
851   qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
852                                      'setuprecur' => $field,
853                                    }
854          );
855 }
856
857 =item units
858
859 Returns the number of billing units (for tax purposes) represented by this,
860 line item.
861
862 =cut
863
864 sub units {
865   my $self = shift;
866   $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
867 }
868
869 =item _item_discount
870
871 If this item has any discounts, returns a hashref in the format used
872 by L<FS::Template_Mixin/_items_cust_bill_pkg> to describe the discount(s)
873 on an invoice. This will contain the keys 'description', 'amount', 
874 'ext_description' (an arrayref of text lines describing the discounts),
875 and '_is_discount' (a flag).
876
877 The value for 'amount' will be negative, and will be scaled for the package
878 quantity.
879
880 =cut
881
882 sub _item_discount {
883   my $self = shift;
884   my %options = @_;
885
886   my $d; # this will be returned.
887
888   my @pkg_discounts = $self->pkg_discount;
889   if (@pkg_discounts) {
890     # special case: if there are old "discount details" on this line item,
891     # don't show discount line items
892     if ( FS::cust_bill_pkg_detail->count("detail LIKE 'Includes discount%' AND billpkgnum = ?", $self->billpkgnum || 0) > 0 ) {
893       return;
894     } 
895     
896     my @ext;
897     $d = {
898       _is_discount    => 1,
899       description     => $self->mt('Discount'),
900       setup_amount    => 0,
901       recur_amount    => 0,
902       ext_description => \@ext,
903       pkgpart         => $self->pkgpart,
904       feepart         => $self->feepart,
905       # maybe should show quantity/unit discount?
906     };
907     foreach my $pkg_discount (@pkg_discounts) {
908       push @ext, $pkg_discount->description;
909       my $setuprecur = $pkg_discount->cust_pkg_discount->setuprecur;
910       $d->{$setuprecur.'_amount'} -= $pkg_discount->amount;
911     }
912   }
913
914   # show introductory rate as a pseudo-discount
915   if (!$d) { # this will conflict with showing real discounts
916     my $part_pkg = $self->part_pkg;
917     if ( $part_pkg and $part_pkg->option('show_as_discount',1) ) {
918       my $cust_pkg = $self->cust_pkg;
919       my $intro_end = $part_pkg->intro_end($cust_pkg);
920       my $_date = $self->cust_bill->_date;
921       if ( $intro_end > $_date ) {
922         $d = $part_pkg->item_discount($cust_pkg);
923       }
924     }
925   }
926
927   if ( $d ) {
928     $d->{setup_amount} *= $self->quantity || 1; # ??
929     $d->{recur_amount} *= $self->quantity || 1; # ??
930   }
931     
932   $d;
933 }
934
935 =item set_display OPTION => VALUE ...
936
937 A helper method for I<insert>, populates the pseudo-field B<display> with
938 appropriate FS::cust_bill_pkg_display objects.
939
940 Options are passed as a list of name/value pairs.  Options are:
941
942 part_pkg: FS::part_pkg object from this line item's package.
943
944 real_pkgpart: if this line item comes from a bundled package, the pkgpart 
945 of the owning package.  Otherwise the same as the part_pkg's pkgpart above.
946
947 =cut
948
949 sub set_display {
950   my( $self, %opt ) = @_;
951   my $part_pkg = $opt{'part_pkg'};
952   my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
953
954   my $conf = new FS::Conf;
955
956   # whether to break this down into setup/recur/usage
957   my $separate = $conf->exists('separate_usage');
958
959   my $usage_mandate =            $part_pkg->option('usage_mandate', 'Hush!')
960                     || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
961
962   # or use the category from $opt{'part_pkg'} if its not bundled?
963   my $categoryname = $cust_pkg->part_pkg->categoryname;
964
965   # if we don't have to separate setup/recur/usage, or put this in a 
966   # package-specific section, or display a usage summary, then don't 
967   # even create one of these.  The item will just display in the unnamed
968   # section as a single line plus details.
969   return $self->set('display', [])
970     unless $separate || $categoryname || $usage_mandate;
971   
972   my @display = ();
973
974   my %hash = ( 'section' => $categoryname );
975
976   # whether to put usage details in a separate section, and if so, which one
977   my $usage_section =            $part_pkg->option('usage_section', 'Hush!')
978                     || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
979
980   # whether to show a usage summary line (total usage charges, no details)
981   my $summary =            $part_pkg->option('summarize_usage', 'Hush!')
982               || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
983
984   if ( $separate ) {
985     # create lines for setup and (non-usage) recur, in the main section
986     push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
987     push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
988   } else {
989     # display everything in a single line
990     push @display, new FS::cust_bill_pkg_display
991                      { type => '',
992                        %hash,
993                        # and if usage_mandate is enabled, hide details
994                        # (this only works on multisection invoices...)
995                        ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
996                      };
997   }
998
999   if ($separate && $usage_section && $summary) {
1000     # create a line for the usage summary in the main section
1001     push @display, new FS::cust_bill_pkg_display { type    => 'U',
1002                                                    summary => 'Y',
1003                                                    %hash,
1004                                                  };
1005   }
1006
1007   if ($usage_mandate || ($usage_section && $summary) ) {
1008     $hash{post_total} = 'Y';
1009   }
1010
1011   if ($separate || $usage_mandate) {
1012     # show call details for this line item in the usage section.
1013     # if usage_mandate is on, this will display below the section subtotal.
1014     # this also happens if usage is in a separate section and there's a 
1015     # summary in the main section, though I'm not sure why.
1016     $hash{section} = $usage_section if $usage_section;
1017     push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
1018   }
1019
1020   $self->set('display', \@display);
1021
1022 }
1023
1024 =item disintegrate
1025
1026 Returns a hash: keys are "setup", "recur" or usage classnum, values are
1027 FS::cust_bill_pkg objects, each with no more than a single class (setup or
1028 recur) of charge.
1029
1030 =cut
1031
1032 sub disintegrate {
1033   my $self = shift;
1034   # XXX this goes away with cust_bill_pkg refactor
1035   # or at least I wish it would, but it turns out to be harder than
1036   # that.
1037
1038   #my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash }; # wha huh?
1039   my %cust_bill_pkg = ();
1040
1041   my $usage_total;
1042   foreach my $classnum ($self->usage_classes) {
1043     my $amount = $self->usage($classnum);
1044     next if $amount == 0; # though if so we shouldn't be here
1045     my $usage_item = FS::cust_bill_pkg->new({
1046         $self->hash,
1047         'setup'     => 0,
1048         'recur'     => $amount,
1049         'taxclass'  => $classnum,
1050         'inherit'   => $self
1051     });
1052     $cust_bill_pkg{$classnum} = $usage_item;
1053     $usage_total += $amount;
1054   }
1055
1056   foreach (qw(setup recur)) {
1057     next if ($self->get($_) == 0);
1058     my $item = FS::cust_bill_pkg->new({
1059         $self->hash,
1060         'setup'     => 0,
1061         'recur'     => 0,
1062         'taxclass'  => $_,
1063         'inherit'   => $self,
1064     });
1065     $item->set($_, $self->get($_));
1066     $cust_bill_pkg{$_} = $item;
1067   }
1068
1069   if ($usage_total) {
1070     $cust_bill_pkg{recur}->set('recur',
1071       sprintf('%.2f', $cust_bill_pkg{recur}->get('recur') - $usage_total)
1072     );
1073   }
1074
1075   %cust_bill_pkg;
1076 }
1077
1078 =item usage CLASSNUM
1079
1080 Returns the amount of the charge associated with usage class CLASSNUM if
1081 CLASSNUM is defined.  Otherwise returns the total charge associated with
1082 usage.
1083   
1084 =cut
1085
1086 sub usage {
1087   my( $self, $classnum ) = @_;
1088   $self->regularize_details;
1089
1090   if ( $self->get('details') ) {
1091
1092     return sum( 0, 
1093       map { $_->amount || 0 }
1094       grep { !defined($classnum) or $classnum eq $_->classnum }
1095       @{ $self->get('details') }
1096     );
1097
1098   } else {
1099
1100     my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
1101               ' WHERE billpkgnum = '. $self->billpkgnum;
1102     if (defined $classnum) {
1103       if ($classnum =~ /^(\d+)$/) {
1104         $sql .= " AND classnum = $1";
1105       } elsif ($classnum eq '') {
1106         $sql .= " AND classnum IS NULL";
1107       }
1108     }
1109
1110     my $sth = dbh->prepare($sql) or die dbh->errstr;
1111     $sth->execute or die $sth->errstr;
1112
1113     return $sth->fetchrow_arrayref->[0] || 0;
1114
1115   }
1116
1117 }
1118
1119 =item usage_classes
1120
1121 Returns a list of usage classnums associated with this invoice line's
1122 details.
1123   
1124 =cut
1125
1126 sub usage_classes {
1127   my( $self ) = @_;
1128   $self->regularize_details;
1129
1130   if ( $self->get('details') ) {
1131
1132     my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
1133     keys %seen;
1134
1135   } else {
1136
1137     map { $_->classnum }
1138         qsearch({ table   => 'cust_bill_pkg_detail',
1139                   hashref => { billpkgnum => $self->billpkgnum },
1140                   select  => 'DISTINCT classnum',
1141                });
1142
1143   }
1144
1145 }
1146
1147 sub cust_tax_exempt_pkg {
1148   my ( $self ) = @_;
1149
1150   my $array = $self->{Hash}->{cust_tax_exempt_pkg} ||= [];
1151 }
1152
1153 =item cust_bill_pkg_tax_Xlocation
1154
1155 Returns the list of associated cust_bill_pkg_tax_location and/or
1156 cust_bill_pkg_tax_rate_location objects
1157
1158 =cut
1159
1160 sub cust_bill_pkg_tax_Xlocation {
1161   my $self = shift;
1162
1163   my %hash = ( 'billpkgnum' => $self->billpkgnum );
1164
1165   (
1166     qsearch ( 'cust_bill_pkg_tax_location', { %hash  } ),
1167     qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
1168   );
1169
1170 }
1171
1172 =item recur_show_zero
1173
1174 Whether to show a zero recurring amount. This is true if the package or its
1175 definition has the recur_show_zero flag, and the recurring fee is actually
1176 zero for this period.
1177
1178 =cut
1179
1180 sub recur_show_zero {
1181   my( $self, $what ) = @_;
1182
1183   return 0 unless $self->get('recur') == 0 && $self->pkgnum;
1184
1185   $self->cust_pkg->_X_show_zero('recur');
1186 }
1187
1188 =item setup_show_zero
1189
1190 Whether to show a zero setup charge. This requires the package or its
1191 definition to have the setup_show_zero flag, but it also returns false if
1192 the package's setup date is before this line item's start date.
1193
1194 =cut
1195
1196 sub setup_show_zero {
1197   my $self = shift;
1198   return 0 unless $self->get('setup') == 0 && $self->pkgnum;
1199   my $cust_pkg = $self->cust_pkg;
1200   return 0 if ( $self->sdate || 0 ) > ( $cust_pkg->setup || 0 );
1201   return $cust_pkg->_X_show_zero('setup');
1202 }
1203
1204 =item credited [ BEFORE, AFTER, OPTIONS ]
1205
1206 Returns the sum of credits applied to this item.  Arguments are the same as
1207 owed_sql/paid_sql/credited_sql.
1208
1209 =cut
1210
1211 sub credited {
1212   my $self = shift;
1213   $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
1214 }
1215
1216 =item tax_locationnum
1217
1218 Returns the L<FS::cust_location> number that this line item is in for tax
1219 purposes.  For package sales, it's the package tax location; for fees, 
1220 it's the customer's default service location.
1221
1222 =cut
1223
1224 sub tax_locationnum {
1225   my $self = shift;
1226   if ( $self->pkgnum ) { # normal sales
1227     return $self->cust_pkg->tax_locationnum;
1228   } elsif ( $self->feepart ) { # fees
1229     my $custnum = $self->fee_origin->custnum;
1230     if ( $custnum ) {
1231       return FS::cust_main->by_key($custnum)->ship_locationnum;
1232     }
1233   } else { # taxes
1234     return '';
1235   }
1236 }
1237
1238 sub tax_location {
1239   my $self = shift;
1240   if ( $self->pkgnum ) { # normal sales
1241     return $self->cust_pkg->tax_location;
1242   } elsif ( $self->feepart ) { # fees
1243     my $fee_origin = $self->fee_origin;
1244     if ( $fee_origin ) {
1245       my $custnum = $fee_origin->custnum;
1246       if ( $custnum ) {
1247         return FS::cust_main->by_key($custnum)->ship_location;
1248       }
1249     }
1250   } else { # taxes
1251     return;
1252   }
1253 }
1254
1255 =back
1256
1257 =head1 CLASS METHODS
1258
1259 =over 4
1260
1261 =item usage_sql
1262
1263 Returns an SQL expression for the total usage charges in details on
1264 an item.
1265
1266 =cut
1267
1268 my $usage_sql =
1269   '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0) 
1270     FROM cust_bill_pkg_detail 
1271     WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
1272
1273 sub usage_sql { $usage_sql }
1274
1275 # this makes owed_sql, etc. much more concise
1276 sub charged_sql {
1277   my ($class, $start, $end, %opt) = @_;
1278   my $setuprecur = $opt{setuprecur} || '';
1279   my $charged = 
1280     $setuprecur =~ /^s/ ? 'cust_bill_pkg.setup' :
1281     $setuprecur =~ /^r/ ? 'cust_bill_pkg.recur' :
1282     'cust_bill_pkg.setup + cust_bill_pkg.recur';
1283
1284   if ($opt{no_usage} and $charged =~ /recur/) { 
1285     $charged = "$charged - $usage_sql"
1286   }
1287
1288   $charged;
1289 }
1290
1291
1292 =item owed_sql [ BEFORE, AFTER, OPTIONS ]
1293
1294 Returns an SQL expression for the amount owed.  BEFORE and AFTER specify
1295 a date window.  OPTIONS may include 'no_usage' (excludes usage charges)
1296 and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
1297
1298 =cut
1299
1300 sub owed_sql {
1301   my $class = shift;
1302   '(' . $class->charged_sql(@_) . 
1303   ' - ' . $class->paid_sql(@_) .
1304   ' - ' . $class->credited_sql(@_) . ')'
1305 }
1306
1307 =item paid_sql [ BEFORE, AFTER, OPTIONS ]
1308
1309 Returns an SQL expression for the sum of payments applied to this item.
1310
1311 =cut
1312
1313 sub paid_sql {
1314   my ($class, $start, $end, %opt) = @_;
1315   my $s = $start ? "AND cust_pay._date <= $start" : '';
1316   my $e = $end   ? "AND cust_pay._date >  $end"   : '';
1317   my $setuprecur = $opt{setuprecur} || '';
1318   $setuprecur = 'setup' if $setuprecur =~ /^s/;
1319   $setuprecur = 'recur' if $setuprecur =~ /^r/;
1320   $setuprecur &&= "AND setuprecur = '$setuprecur'";
1321
1322   my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
1323      FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
1324                             JOIN cust_pay      USING (paynum)
1325      WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1326            $s $e $setuprecur )";
1327
1328   if ( $opt{no_usage} ) {
1329     # cap the amount paid at the sum of non-usage charges, 
1330     # minus the amount credited against non-usage charges
1331     "LEAST($paid, ". 
1332       $class->charged_sql($start, $end, %opt) . ' - ' .
1333       $class->credited_sql($start, $end, %opt).')';
1334   }
1335   else {
1336     $paid;
1337   }
1338
1339 }
1340
1341 sub credited_sql {
1342   my ($class, $start, $end, %opt) = @_;
1343   my $s = $start ? "AND cust_credit._date <= $start" : '';
1344   my $e = $end   ? "AND cust_credit._date >  $end"   : '';
1345   my $setuprecur = $opt{setuprecur} || '';
1346   $setuprecur = 'setup' if $setuprecur =~ /^s/;
1347   $setuprecur = 'recur' if $setuprecur =~ /^r/;
1348   $setuprecur &&= "AND setuprecur = '$setuprecur'";
1349
1350   my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
1351      FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
1352                                JOIN cust_credit      USING (crednum)
1353      WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1354            $s $e $setuprecur )";
1355
1356   if ( $opt{no_usage} ) {
1357     # cap the amount credited at the sum of non-usage charges
1358     "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
1359   }
1360   else {
1361     $credited;
1362   }
1363
1364 }
1365
1366 sub upgrade_tax_location {
1367   # For taxes that were calculated/invoiced before cust_location refactoring
1368   # (May-June 2012), there are no cust_bill_pkg_tax_location records unless
1369   # they were calculated on a package-location basis.  Create them here, 
1370   # along with any necessary cust_location records and any tax exemption 
1371   # records.
1372
1373   my ($class, %opt) = @_;
1374   # %opt may include 's' and 'e': start and end date ranges
1375   # and 'X': abort on any error, instead of just rolling back changes to 
1376   # that invoice
1377   my $dbh = dbh;
1378   my $oldAutoCommit = $FS::UID::AutoCommit;
1379   local $FS::UID::AutoCommit = 0;
1380
1381   eval {
1382     use FS::h_cust_main;
1383     use FS::h_cust_bill;
1384     use FS::h_part_pkg;
1385     use FS::h_cust_main_exemption;
1386   };
1387
1388   local $FS::cust_location::import = 1;
1389
1390   my $conf = FS::Conf->new; # h_conf?
1391   return if $conf->config('tax_data_vendor'); #don't touch this case
1392   my $use_ship = $conf->exists('tax-ship_address');
1393   my $use_pkgloc = $conf->exists('tax-pkg_address');
1394
1395   my $date_where = '';
1396   if ($opt{s}) {
1397     $date_where .= " AND cust_bill._date >= $opt{s}";
1398   }
1399   if ($opt{e}) {
1400     $date_where .= " AND cust_bill._date < $opt{e}";
1401   }
1402
1403   my $commit_each_invoice = 1 unless $opt{X};
1404
1405   # if an invoice has either of these kinds of objects, then it doesn't
1406   # need to be upgraded...probably
1407   my $sub_has_tax_link = 'SELECT 1 FROM cust_bill_pkg_tax_location'.
1408   ' JOIN cust_bill_pkg USING (billpkgnum)'.
1409   ' WHERE cust_bill_pkg.invnum = cust_bill.invnum';
1410   my $sub_has_exempt = 'SELECT 1 FROM cust_tax_exempt_pkg'.
1411   ' JOIN cust_bill_pkg USING (billpkgnum)'.
1412   ' WHERE cust_bill_pkg.invnum = cust_bill.invnum'.
1413   ' AND exempt_monthly IS NULL';
1414
1415   my %all_tax_names = (
1416     '' => 1,
1417     'Tax' => 1,
1418     map { $_->taxname => 1 }
1419       qsearch('h_cust_main_county', { taxname => { op => '!=', value => '' }})
1420   );
1421
1422   my $search = FS::Cursor->new({
1423       table => 'cust_bill',
1424       hashref => {},
1425       extra_sql => "WHERE NOT EXISTS($sub_has_tax_link) ".
1426                    "AND NOT EXISTS($sub_has_exempt) ".
1427                     $date_where,
1428   });
1429
1430 #print "Processing ".scalar(@invnums)." invoices...\n";
1431
1432   my $committed;
1433   INVOICE:
1434   while (my $cust_bill = $search->fetch) {
1435     my $invnum = $cust_bill->invnum;
1436     $committed = 0;
1437     print STDERR "Invoice #$invnum\n";
1438     my $pre = '';
1439     my %pkgpart_taxclass; # pkgpart => taxclass
1440     my %pkgpart_exempt_setup;
1441     my %pkgpart_exempt_recur;
1442     my $h_cust_bill = qsearchs('h_cust_bill',
1443       { invnum => $invnum,
1444         history_action => 'insert' });
1445     if (!$h_cust_bill) {
1446       warn "no insert record for invoice $invnum; skipped\n";
1447       #$date = $cust_bill->_date as a fallback?
1448       # We're trying to avoid using non-real dates (-d/-y invoice dates)
1449       # when looking up history records in other tables.
1450       next INVOICE;
1451     }
1452     my $custnum = $h_cust_bill->custnum;
1453
1454     # Determine the address corresponding to this tax region.
1455     # It's either the bill or ship address of the customer as of the
1456     # invoice date-of-insertion.  (Not necessarily the invoice date.)
1457     my $date = $h_cust_bill->history_date;
1458     local($FS::Record::qsearch_qualify_columns) = 0;
1459     my $h_cust_main = qsearchs('h_cust_main',
1460         { custnum   => $custnum },
1461         FS::h_cust_main->sql_h_searchs($date)
1462       );
1463     if (!$h_cust_main ) {
1464       warn "no historical address for cust#".$h_cust_bill->custnum."; skipped\n";
1465       next INVOICE;
1466       # fallback to current $cust_main?  sounds dangerous.
1467     }
1468
1469     # This is a historical customer record, so it has a historical address.
1470     # If there's no cust_location matching this custnum and address (there 
1471     # probably isn't), create one.
1472     my %tax_loc; # keys are pkgnums, values are cust_location objects
1473     my $default_tax_loc;
1474     if ( $h_cust_main->bill_locationnum ) {
1475       # the location has already been upgraded
1476       if ($use_ship) {
1477         $default_tax_loc = $h_cust_main->ship_location;
1478       } else {
1479         $default_tax_loc = $h_cust_main->bill_location;
1480       }
1481     } else {
1482       $pre = 'ship_' if $use_ship and length($h_cust_main->get('ship_last'));
1483       my %hash = map { $_ => $h_cust_main->get($pre.$_) }
1484                     FS::cust_main->location_fields;
1485       # not really needed for this, and often result in duplicate locations
1486       delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
1487
1488       $hash{custnum} = $h_cust_main->custnum;
1489       $default_tax_loc = FS::cust_location->new(\%hash);
1490       my $error = $default_tax_loc->find_or_insert || $default_tax_loc->disable_if_unused;
1491       if ( $error ) {
1492         warn "couldn't create historical location record for cust#".
1493         $h_cust_main->custnum.": $error\n";
1494         next INVOICE;
1495       }
1496     }
1497     my $exempt_cust;
1498     $exempt_cust = 1 if $h_cust_main->tax;
1499
1500     # classify line items
1501     my @tax_items;
1502     my %nontax_items; # taxclass => array of cust_bill_pkg
1503     foreach my $item ($h_cust_bill->cust_bill_pkg) {
1504       my $pkgnum = $item->pkgnum;
1505
1506       if ( $pkgnum == 0 ) {
1507
1508         push @tax_items, $item;
1509
1510       } else {
1511         # (pkgparts really shouldn't change, right?)
1512         local($FS::Record::qsearch_qualify_columns) = 0;
1513         my $h_cust_pkg = qsearchs('h_cust_pkg', { pkgnum => $pkgnum },
1514           FS::h_cust_pkg->sql_h_searchs($date)
1515         );
1516         if ( !$h_cust_pkg ) {
1517           warn "no historical package #".$item->pkgpart."; skipped\n";
1518           next INVOICE;
1519         }
1520         my $pkgpart = $h_cust_pkg->pkgpart;
1521
1522         if ( $use_pkgloc and $h_cust_pkg->locationnum ) {
1523           # then this package already had a locationnum assigned, and that's 
1524           # the one to use for tax calculation
1525           $tax_loc{$pkgnum} = FS::cust_location->by_key($h_cust_pkg->locationnum);
1526         } else {
1527           # use the customer's bill or ship loc, which was inserted earlier
1528           $tax_loc{$pkgnum} = $default_tax_loc;
1529         }
1530
1531         if (!exists $pkgpart_taxclass{$pkgpart}) {
1532           local($FS::Record::qsearch_qualify_columns) = 0;
1533           my $h_part_pkg = qsearchs('h_part_pkg', { pkgpart => $pkgpart },
1534             FS::h_part_pkg->sql_h_searchs($date)
1535           );
1536           if ( !$h_part_pkg ) {
1537             warn "no historical package def #$pkgpart; skipped\n";
1538             next INVOICE;
1539           }
1540           $pkgpart_taxclass{$pkgpart} = $h_part_pkg->taxclass || '';
1541           $pkgpart_exempt_setup{$pkgpart} = 1 if $h_part_pkg->setuptax;
1542           $pkgpart_exempt_recur{$pkgpart} = 1 if $h_part_pkg->recurtax;
1543         }
1544         
1545         # mark any exemptions that apply
1546         if ( $pkgpart_exempt_setup{$pkgpart} ) {
1547           $item->set('exempt_setup' => 1);
1548         }
1549
1550         if ( $pkgpart_exempt_recur{$pkgpart} ) {
1551           $item->set('exempt_recur' => 1);
1552         }
1553
1554         my $taxclass = $pkgpart_taxclass{ $pkgpart };
1555
1556         $nontax_items{$taxclass} ||= [];
1557         push @{ $nontax_items{$taxclass} }, $item;
1558       }
1559     }
1560
1561     printf("%d tax items: \$%.2f\n", scalar(@tax_items), map {$_->setup} @tax_items)
1562       if @tax_items;
1563
1564     # Get any per-customer taxname exemptions that were in effect.
1565     my %exempt_cust_taxname;
1566     foreach (keys %all_tax_names) {
1567      local($FS::Record::qsearch_qualify_columns) = 0;
1568       my $h_exemption = qsearchs('h_cust_main_exemption', {
1569           'custnum' => $custnum,
1570           'taxname' => $_,
1571         },
1572         FS::h_cust_main_exemption->sql_h_searchs($date, $date)
1573       );
1574       if ($h_exemption) {
1575         $exempt_cust_taxname{ $_ } = 1;
1576       }
1577     }
1578
1579     # Use a variation on the procedure in 
1580     # FS::cust_main::Billing::_handle_taxes to identify taxes that apply 
1581     # to this bill.
1582     my @loc_keys = qw( district city county state country );
1583     my %taxdef_by_name; # by name, and then by taxclass
1584     my %est_tax; # by name, and then by taxclass
1585     my %taxable_items; # by taxnum, and then an array
1586
1587     foreach my $taxclass (keys %nontax_items) {
1588       foreach my $orig_item (@{ $nontax_items{$taxclass} }) {
1589         my $my_tax_loc = $tax_loc{ $orig_item->pkgnum };
1590         my %myhash = map { $_ => $my_tax_loc->get($pre.$_) } @loc_keys;
1591         my @elim = qw( district city county state );
1592         my @taxdefs; # because there may be several with different taxnames
1593         do {
1594           $myhash{taxclass} = $taxclass;
1595           @taxdefs = qsearch('cust_main_county', \%myhash);
1596           if ( !@taxdefs ) {
1597             $myhash{taxclass} = '';
1598             @taxdefs = qsearch('cust_main_county', \%myhash);
1599           }
1600           $myhash{ shift @elim } = '';
1601         } while scalar(@elim) and !@taxdefs;
1602
1603         foreach my $taxdef (@taxdefs) {
1604           next if $taxdef->tax == 0;
1605           $taxdef_by_name{$taxdef->taxname}{$taxdef->taxclass} = $taxdef;
1606
1607           $taxable_items{$taxdef->taxnum} ||= [];
1608           # clone the item so that taxdef-dependent changes don't
1609           # change it for other taxdefs
1610           my $item = FS::cust_bill_pkg->new({ $orig_item->hash });
1611
1612           # these flags are already set if the part_pkg declares itself exempt
1613           $item->set('exempt_setup' => 1) if $taxdef->setuptax;
1614           $item->set('exempt_recur' => 1) if $taxdef->recurtax;
1615
1616           my @new_exempt;
1617           my $taxable = $item->setup + $item->recur;
1618           # credits
1619           # h_cust_credit_bill_pkg?
1620           # NO.  Because if these exemptions HAD been created at the time of 
1621           # billing, and then a credit applied later, the exemption would 
1622           # have been adjusted by the amount of the credit.  So we adjust
1623           # the taxable amount before creating the exemption.
1624           # But don't deduct the credit from taxable, because the tax was 
1625           # calculated before the credit was applied.
1626           foreach my $f (qw(setup recur)) {
1627             my $credited = FS::Record->scalar_sql(
1628               "SELECT SUM(amount) FROM cust_credit_bill_pkg ".
1629               "WHERE billpkgnum = ? AND setuprecur = ?",
1630               $item->billpkgnum,
1631               $f
1632             );
1633             $item->set($f, $item->get($f) - $credited) if $credited;
1634           }
1635           my $existing_exempt = FS::Record->scalar_sql(
1636             "SELECT SUM(amount) FROM cust_tax_exempt_pkg WHERE ".
1637             "billpkgnum = ? AND taxnum = ?",
1638             $item->billpkgnum, $taxdef->taxnum
1639           ) || 0;
1640           $taxable -= $existing_exempt;
1641
1642           if ( $taxable and $exempt_cust ) {
1643             push @new_exempt, { exempt_cust => 'Y',  amount => $taxable };
1644             $taxable = 0;
1645           }
1646           if ( $taxable and $exempt_cust_taxname{$taxdef->taxname} ){
1647             push @new_exempt, { exempt_cust_taxname => 'Y', amount => $taxable };
1648             $taxable = 0;
1649           }
1650           if ( $taxable and $item->exempt_setup ) {
1651             push @new_exempt, { exempt_setup => 'Y', amount => $item->setup };
1652             $taxable -= $item->setup;
1653           }
1654           if ( $taxable and $item->exempt_recur ) {
1655             push @new_exempt, { exempt_recur => 'Y', amount => $item->recur };
1656             $taxable -= $item->recur;
1657           }
1658
1659           $item->set('taxable' => $taxable);
1660           push @{ $taxable_items{$taxdef->taxnum} }, $item
1661             if $taxable > 0;
1662
1663           # estimate the amount of tax (this is necessary because different
1664           # taxdefs with the same taxname may have different tax rates) 
1665           # and sum that for each taxname/taxclass combination
1666           # (in cents)
1667           $est_tax{$taxdef->taxname} ||= {};
1668           $est_tax{$taxdef->taxname}{$taxdef->taxclass} ||= 0;
1669           $est_tax{$taxdef->taxname}{$taxdef->taxclass} += 
1670             $taxable * $taxdef->tax;
1671
1672           foreach (@new_exempt) {
1673             next if $_->{amount} == 0;
1674             my $cust_tax_exempt_pkg = FS::cust_tax_exempt_pkg->new({
1675                 %$_,
1676                 billpkgnum  => $item->billpkgnum,
1677                 taxnum      => $taxdef->taxnum,
1678               });
1679             my $error = $cust_tax_exempt_pkg->insert;
1680             if ($error) {
1681               my $pkgnum = $item->pkgnum;
1682               warn "error creating tax exemption for inv$invnum pkg$pkgnum:".
1683                 "\n$error\n\n";
1684               next INVOICE;
1685             }
1686           } #foreach @new_exempt
1687         } #foreach $taxdef
1688       } #foreach $item
1689     } #foreach $taxclass
1690
1691     # Now go through the billed taxes and match them up with the line items.
1692     TAX_ITEM: foreach my $tax_item ( @tax_items )
1693     {
1694       my $taxname = $tax_item->itemdesc;
1695       $taxname = '' if $taxname eq 'Tax';
1696
1697       if ( !exists( $taxdef_by_name{$taxname} ) ) {
1698         # then we didn't find any applicable taxes with this name
1699         warn "no definition found for tax item '$taxname', custnum $custnum\n";
1700         # possibly all of these should be "next TAX_ITEM", but whole invoices
1701         # are transaction protected and we can go back and retry them.
1702         next INVOICE;
1703       }
1704       # classname => cust_main_county
1705       my %taxdef_by_class = %{ $taxdef_by_name{$taxname} };
1706
1707       # Divide the tax item among taxclasses, if necessary
1708       # classname => estimated tax amount
1709       my $this_est_tax = $est_tax{$taxname};
1710       if (!defined $this_est_tax) {
1711         warn "no taxable sales found for inv#$invnum, tax item '$taxname'.\n";
1712         next INVOICE;
1713       }
1714       my $est_total = sum(values %$this_est_tax);
1715       if ( $est_total == 0 ) {
1716         # shouldn't happen
1717         warn "estimated tax on invoice #$invnum is zero.\n";
1718         next INVOICE;
1719       }
1720
1721       my $real_tax = $tax_item->setup;
1722       printf ("Distributing \$%.2f tax:\n", $real_tax);
1723       my $cents_remaining = $real_tax * 100; # for rounding error
1724       my @tax_links; # partial CBPTL hashrefs
1725       foreach my $taxclass (keys %taxdef_by_class) {
1726         my $taxdef = $taxdef_by_class{$taxclass};
1727         # these items already have "taxable" set to their charge amount
1728         # after applying any credits or exemptions
1729         my @items = @{ $taxable_items{$taxdef->taxnum} };
1730         my $subtotal = sum(map {$_->get('taxable')} @items);
1731         printf("\t$taxclass: %.2f\n", $this_est_tax->{$taxclass}/$est_total);
1732
1733         foreach my $nontax (@items) {
1734           my $my_tax_loc = $tax_loc{ $nontax->pkgnum };
1735           my $part = int($real_tax
1736                             # class allocation
1737                          * ($this_est_tax->{$taxclass}/$est_total) 
1738                             # item allocation
1739                          * ($nontax->get('taxable'))/$subtotal
1740                             # convert to cents
1741                          * 100
1742                        );
1743           $cents_remaining -= $part;
1744           push @tax_links, {
1745             taxnum      => $taxdef->taxnum,
1746             pkgnum      => $nontax->pkgnum,
1747             locationnum => $my_tax_loc->locationnum,
1748             billpkgnum  => $nontax->billpkgnum,
1749             cents       => $part,
1750           };
1751         } #foreach $nontax
1752       } #foreach $taxclass
1753       # Distribute any leftover tax round-robin style, one cent at a time.
1754       my $i = 0;
1755       my $nlinks = scalar(@tax_links);
1756       if ( $nlinks ) {
1757         # ensure that it really is an integer
1758         $cents_remaining = sprintf('%.0f', $cents_remaining);
1759         while ($cents_remaining > 0) {
1760           $tax_links[$i % $nlinks]->{cents} += 1;
1761           $cents_remaining--;
1762           $i++;
1763         }
1764       } else {
1765         warn "Can't create tax links--no taxable items found.\n";
1766         next INVOICE;
1767       }
1768
1769       # Gather credit/payment applications so that we can link them
1770       # appropriately.
1771       my @unlinked = (
1772         qsearch( 'cust_credit_bill_pkg',
1773           { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1774         ),
1775         qsearch( 'cust_bill_pay_pkg',
1776           { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1777         )
1778       );
1779
1780       # grab the first one
1781       my $this_unlinked = shift @unlinked;
1782       my $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1783
1784       # Create tax links (yay!)
1785       printf("Creating %d tax links.\n",scalar(@tax_links));
1786       foreach (@tax_links) {
1787         my $link = FS::cust_bill_pkg_tax_location->new({
1788             billpkgnum  => $tax_item->billpkgnum,
1789             taxtype     => 'FS::cust_main_county',
1790             locationnum => $_->{locationnum},
1791             taxnum      => $_->{taxnum},
1792             pkgnum      => $_->{pkgnum},
1793             amount      => sprintf('%.2f', $_->{cents} / 100),
1794             taxable_billpkgnum => $_->{billpkgnum},
1795         });
1796         my $error = $link->insert;
1797         if ( $error ) {
1798           warn "Can't create tax link for inv#$invnum: $error\n";
1799           next INVOICE;
1800         }
1801
1802         my $link_cents = $_->{cents};
1803         # update/create subitem links
1804         #
1805         # If $this_unlinked is undef, then we've allocated all of the
1806         # credit/payment applications to the tax item.  If $link_cents is 0,
1807         # then we've applied credits/payments to all of this package fraction,
1808         # so go on to the next.
1809         while ($this_unlinked and $link_cents) {
1810           # apply as much as possible of $link_amount to this credit/payment
1811           # link
1812           my $apply_cents = min($link_cents, $unlinked_cents);
1813           $link_cents -= $apply_cents;
1814           $unlinked_cents -= $apply_cents;
1815           # $link_cents or $unlinked_cents or both are now zero
1816           $this_unlinked->set('amount' => sprintf('%.2f',$apply_cents/100));
1817           $this_unlinked->set('billpkgtaxlocationnum' => $link->billpkgtaxlocationnum);
1818           my $pkey = $this_unlinked->primary_key; #creditbillpkgnum or billpaypkgnum
1819           if ( $this_unlinked->$pkey ) {
1820             # then it's an existing link--replace it
1821             $error = $this_unlinked->replace;
1822           } else {
1823             $this_unlinked->insert;
1824           }
1825           # what do we do with errors at this stage?
1826           if ( $error ) {
1827             warn "Error creating tax application link: $error\n";
1828             next INVOICE; # for lack of a better idea
1829           }
1830           
1831           if ( $unlinked_cents == 0 ) {
1832             # then we've allocated all of this payment/credit application, 
1833             # so grab the next one
1834             $this_unlinked = shift @unlinked;
1835             $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1836           } elsif ( $link_cents == 0 ) {
1837             # then we've covered all of this package tax fraction, so split
1838             # off a new application from this one
1839             $this_unlinked = $this_unlinked->new({
1840                 $this_unlinked->hash,
1841                 $pkey     => '',
1842             });
1843             # $unlinked_cents is still what it is
1844           }
1845
1846         } #while $this_unlinked and $link_cents
1847       } #foreach (@tax_links)
1848     } #foreach $tax_item
1849
1850     $dbh->commit if $commit_each_invoice and $oldAutoCommit;
1851     $committed = 1;
1852
1853   } #foreach $invnum
1854   continue {
1855     if (!$committed) {
1856       $dbh->rollback if $oldAutoCommit;
1857       die "Upgrade halted.\n" unless $commit_each_invoice;
1858     }
1859   }
1860
1861   $dbh->commit if $oldAutoCommit and !$commit_each_invoice;
1862   '';
1863 }
1864
1865 sub _pkg_tax_list {
1866   # Return an array of hashrefs for each cust_bill_pkg_tax_location
1867   # applied to this bill for this cust_bill_pkg.pkgnum.
1868   #
1869   # ! Important Note:
1870   #   In some situations, this list will contain more tax records than the
1871   #   ones directly related to $self->billpkgnum.  The returned list contains
1872   #   all records, for this bill, charged against this billpkgnum's pkgnum.
1873   #
1874   #   One must keep this in mind when using data returned by this method.
1875   #
1876   #   An unaddressed deficiency in the cust_bill_pkg_tax_location model makes
1877   #   this necessary:  When a linked-hidden package generates a tax/fee as a row
1878   #   in cust_bill_pkg_tax_location, there is not enough information to surmise
1879   #   with specificity which billpkgnum row represents the direct parent of the
1880   #   the linked-hidden package's tax row.  The closest we can get to this
1881   #   backwards reassociation is to use the pkgnum.  Therefore, when multiple
1882   #   billpkgnum's appear with the same pkgnum, this method is going to return
1883   #   the tax records for ALL of those billpkgnum's, not just $self->billpkgnum.
1884   #
1885   #   This could be addressed with an update to the model, and to the billing
1886   #   routine that generates rows into cust_bill_pkg_tax_location.  Perhaps a
1887   #   column, link_billpkgnum or parent_billpkgnum, recording the link. I'm not
1888   #   doing that now, because there would be no possible repair of data stored
1889   #   historically prior to such a fix.  I need _pkg_tax_list() to not be
1890   #   broken for already-generated bills.
1891   #
1892   #   Any code you write relying on _pkg_tax_list() MUST be aware of, and
1893   #   account for, the possible return of duplicated tax records returned
1894   #   when method is called on multiple cust_bill_pkg_tax_location rows.
1895   #   Duplicates can be identified by billpkgtaxlocationnum column.
1896
1897   my $self = shift;
1898
1899   my $search_selector;
1900   if ( $self->pkgnum ) {
1901
1902     # For taxes applied to normal billing items
1903     $search_selector =
1904       ' cust_bill_pkg_tax_location.pkgnum = '
1905       . dbh->quote( $self->pkgnum );
1906
1907   } elsif ( $self->feepart ) {
1908
1909     # For taxes applied to fees, when the fee is not attached to a package
1910     # i.e. late fees, billing events fees
1911     $search_selector =
1912       ' cust_bill_pkg_tax_location.taxable_billpkgnum = '
1913       . dbh->quote( $self->billpkgnum );
1914
1915   } else {
1916     warn "_pkg_tax_list() unhandled case breaking taxes into sections";
1917     warn "_pkg_tax_list() $_: ".$self->$_
1918       for qw(pkgnum billpkgnum feepart);
1919     return;
1920   }
1921
1922   map +{
1923       billpkgtaxlocationnum => $_->billpkgtaxlocationnum,
1924       billpkgnum            => $_->billpkgnum,
1925       taxnum                => $_->taxnum,
1926       amount                => $_->amount,
1927       taxname               => $_->taxname,
1928   },
1929   qsearch({
1930     table  => 'cust_bill_pkg_tax_location',
1931     addl_from => '
1932       LEFT JOIN cust_bill_pkg
1933               ON cust_bill_pkg.billpkgnum
1934          = cust_bill_pkg_tax_location.taxable_billpkgnum
1935     ',
1936     select => join( ', ', (qw|
1937       cust_bill_pkg.billpkgnum
1938       cust_bill_pkg_tax_location.billpkgtaxlocationnum
1939       cust_bill_pkg_tax_location.taxnum
1940       cust_bill_pkg_tax_location.amount
1941     |)),
1942     extra_sql =>
1943       ' WHERE '.
1944       ' cust_bill_pkg.invnum = ' . dbh->quote( $self->invnum ) .
1945       ' AND '.
1946       $search_selector
1947   });
1948
1949 }
1950
1951 sub _upgrade_data {
1952   # Create a queue job to run upgrade_tax_location from January 1, 2012 to 
1953   # the present date.
1954   eval {
1955     use FS::queue;
1956     use Date::Parse 'str2time';
1957   };
1958   my $class = shift;
1959   my $upgrade = 'tax_location_2012';
1960   return if FS::upgrade_journal->is_done($upgrade);
1961   my $job = FS::queue->new({
1962       'job' => 'FS::cust_bill_pkg::upgrade_tax_location'
1963   });
1964   # call it kind of like a class method, not that it matters much
1965   $job->insert($class, 's' => str2time('2012-01-01'));
1966   # if there's a customer location upgrade queued also, wait for it to 
1967   # finish
1968   my $location_job = qsearchs('queue', {
1969       job => 'FS::cust_main::Location::process_upgrade_location'
1970     });
1971   if ( $location_job ) {
1972     $job->depend_insert($location_job->jobnum);
1973   }
1974   # Then mark the upgrade as done, so that we don't queue the job twice
1975   # and somehow run two of them concurrently.
1976   FS::upgrade_journal->set_done($upgrade);
1977   # This upgrade now does the job of assigning taxable_billpkgnums to 
1978   # cust_bill_pkg_tax_location, so set that task done also.
1979   FS::upgrade_journal->set_done('tax_location_taxable_billpkgnum');
1980 }
1981
1982 =back
1983
1984 =head1 BUGS
1985
1986 setup and recur shouldn't be separate fields.  There should be one "amount"
1987 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1988
1989 A line item with both should really be two separate records (preserving
1990 sdate and edate for setup fees for recurring packages - that information may
1991 be valuable later).  Invoice generation (cust_main::bill), invoice printing
1992 (cust_bill), tax reports (report_tax.cgi) and line item reports 
1993 (cust_bill_pkg.cgi) would need to be updated.
1994
1995 owed_setup and owed_recur could then be repaced by just owed, and
1996 cust_bill::open_cust_bill_pkg and
1997 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1998
1999 The upgrade procedure is pretty sketchy.
2000
2001 =head1 SEE ALSO
2002
2003 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
2004 from the base documentation.
2005
2006 =cut
2007
2008 1;