305ad63a6a92c339637032cb565685319d612d4b
[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             || $self->SUPER::delete(@_);
504   if ( $error ) {
505     $dbh->rollback if $oldAutoCommit;
506     return $error;
507   }
508
509   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
510
511   '';
512
513 }
514
515 #alas, bin/follow-tax-rename
516 #
517 #=item replace OLD_RECORD
518 #
519 #Currently unimplemented.  This would be even more of an accounting nightmare
520 #than deleteing the items.  Just don't do it.
521 #
522 #=cut
523 #
524 #sub replace {
525 #  return "Can't modify cust_bill_pkg records!";
526 #}
527
528 =item check
529
530 Checks all fields to make sure this is a valid line item.  If there is an
531 error, returns the error, otherwise returns false.  Called by the insert
532 method.
533
534 =cut
535
536 sub check {
537   my $self = shift;
538
539   my $error =
540          $self->ut_numbern('billpkgnum')
541       || $self->ut_snumber('pkgnum')
542       || $self->ut_number('invnum')
543       || $self->ut_money('setup')
544       || $self->ut_moneyn('unitsetup')
545       || $self->ut_currencyn('setup_billed_currency')
546       || $self->ut_moneyn('setup_billed_amount')
547       || $self->ut_money('recur')
548       || $self->ut_moneyn('unitrecur')
549       || $self->ut_currencyn('recur_billed_currency')
550       || $self->ut_moneyn('recur_billed_amount')
551       || $self->ut_numbern('sdate')
552       || $self->ut_numbern('edate')
553       || $self->ut_textn('itemdesc')
554       || $self->ut_textn('itemcomment')
555       || $self->ut_enum('hidden', [ '', 'Y' ])
556   ;
557   return $error if $error;
558
559   $self->regularize_details;
560
561   #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
562   if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
563     return "Unknown pkgnum ". $self->pkgnum
564       unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
565   }
566
567   return "Unknown invnum"
568     unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
569
570   $self->SUPER::check;
571 }
572
573 =item regularize_details
574
575 Converts the contents of the 'details' pseudo-field to 
576 L<FS::cust_bill_pkg_detail> objects, if they aren't already.
577
578 =cut
579
580 sub regularize_details {
581   my $self = shift;
582   if ( $self->get('details') ) {
583     foreach my $detail ( @{$self->get('details')} ) {
584       if ( ref($detail) ne 'FS::cust_bill_pkg_detail' ) {
585         # then turn it into one
586         my %hash = ();
587         if ( ! ref($detail) ) {
588           $hash{'detail'} = $detail;
589         }
590         elsif ( ref($detail) eq 'HASH' ) {
591           %hash = %$detail;
592         }
593         elsif ( ref($detail) eq 'ARRAY' ) {
594           carp "passing invoice details as arrays is deprecated";
595           #carp "this way sucks, use a hash"; #but more useful/friendly
596           $hash{'format'}      = $detail->[0];
597           $hash{'detail'}      = $detail->[1];
598           $hash{'amount'}      = $detail->[2];
599           $hash{'classnum'}    = $detail->[3];
600           $hash{'phonenum'}    = $detail->[4];
601           $hash{'accountcode'} = $detail->[5];
602           $hash{'startdate'}   = $detail->[6];
603           $hash{'duration'}    = $detail->[7];
604           $hash{'regionname'}  = $detail->[8];
605         }
606         else {
607           die "unknown detail type ". ref($detail);
608         }
609         $detail = new FS::cust_bill_pkg_detail \%hash;
610       }
611       $detail->billpkgnum($self->billpkgnum) if $self->billpkgnum;
612     }
613   }
614   return;
615 }
616
617 =item set_exemptions TAXOBJECT, OPTIONS
618
619 Sets up tax exemptions.  TAXOBJECT is the L<FS::cust_main_county> or 
620 L<FS::tax_rate> record for the tax.
621
622 This will deal with the following cases:
623
624 =over 4
625
626 =item Fully exempt customers (cust_main.tax flag) or customer classes 
627 (cust_class.tax).
628
629 =item Customers exempt from specific named taxes (cust_main_exemption 
630 records).
631
632 =item Taxes that don't apply to setup or recurring fees 
633 (cust_main_county.setuptax and recurtax, tax_rate.setuptax and recurtax).
634
635 =item Packages that are marked as tax-exempt (part_pkg.setuptax,
636 part_pkg.recurtax).
637
638 =item Fees that aren't marked as taxable (part_fee.taxable).
639
640 =back
641
642 It does NOT deal with monthly tax exemptions, which need more context 
643 than this humble little method cares to deal with.
644
645 OPTIONS should include "custnum" => the customer number if this tax line
646 hasn't been inserted (which it probably hasn't).
647
648 Returns a list of exemption objects, which will also be attached to the 
649 line item as the 'cust_tax_exempt_pkg' pseudo-field.  Inserting the line
650 item will insert these records as well.
651
652 =cut
653
654 sub set_exemptions {
655   my $self = shift;
656   my $tax = shift;
657   my %opt = @_;
658
659   my $part_pkg  = $self->part_pkg;
660   my $part_fee  = $self->part_fee;
661
662   my $cust_main;
663   my $custnum = $opt{custnum};
664   $custnum ||= $self->cust_bill->custnum if $self->cust_bill;
665
666   $cust_main = FS::cust_main->by_key( $custnum )
667     or die "set_exemptions can't identify customer (pass custnum option)\n";
668
669   my @new_exemptions;
670   my $taxable_charged = $self->setup + $self->recur;
671   return unless $taxable_charged > 0;
672
673   ### Fully exempt customer ###
674   my $exempt_cust;
675   my $conf = FS::Conf->new;
676   if ( $conf->exists('cust_class-tax_exempt') ) {
677     my $cust_class = $cust_main->cust_class;
678     $exempt_cust = $cust_class->tax if $cust_class;
679   } else {
680     $exempt_cust = $cust_main->tax;
681   }
682
683   ### Exemption from named tax ###
684   my $exempt_cust_taxname;
685   if ( !$exempt_cust and $tax->taxname ) {
686     $exempt_cust_taxname = $cust_main->tax_exemption($tax->taxname);
687   }
688
689   if ( $exempt_cust ) {
690
691     push @new_exemptions, FS::cust_tax_exempt_pkg->new({
692         amount => $taxable_charged,
693         exempt_cust => 'Y',
694       });
695     $taxable_charged = 0;
696
697   } elsif ( $exempt_cust_taxname ) {
698
699     push @new_exemptions, FS::cust_tax_exempt_pkg->new({
700         amount => $taxable_charged,
701         exempt_cust_taxname => 'Y',
702       });
703     $taxable_charged = 0;
704
705   }
706
707   my $exempt_setup = ( ($part_fee and not $part_fee->taxable)
708       or ($part_pkg and $part_pkg->setuptax)
709       or $tax->setuptax );
710
711   if ( $exempt_setup
712       and $self->setup > 0
713       and $taxable_charged > 0 ) {
714
715     push @new_exemptions, FS::cust_tax_exempt_pkg->new({
716         amount => $self->setup,
717         exempt_setup => 'Y'
718       });
719     $taxable_charged -= $self->setup;
720
721   }
722
723   my $exempt_recur = ( ($part_fee and not $part_fee->taxable)
724       or ($part_pkg and $part_pkg->recurtax)
725       or $tax->recurtax );
726
727   if ( $exempt_recur
728       and $self->recur > 0
729       and $taxable_charged > 0 ) {
730
731     push @new_exemptions, FS::cust_tax_exempt_pkg->new({
732         amount => $self->recur,
733         exempt_recur => 'Y'
734       });
735     $taxable_charged -= $self->recur;
736
737   }
738
739   foreach (@new_exemptions) {
740     $_->set('taxnum', $tax->taxnum);
741     $_->set('taxtype', ref($tax));
742   }
743
744   push @{ $self->cust_tax_exempt_pkg }, @new_exemptions;
745   return @new_exemptions;
746
747 }
748
749 =item cust_bill
750
751 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
752
753 =item cust_main
754
755 Returns the customer (L<FS::cust_main> object) for this line item.
756
757 =cut
758
759 sub cust_main {
760   carp "->cust_main called" if $DEBUG;
761   # required for cust_main_Mixin equivalence
762   # and use cust_bill instead of cust_pkg because this might not have a 
763   # cust_pkg
764   my $self = shift;
765   my $cust_bill = $self->cust_bill or return '';
766   $cust_bill->cust_main;
767 }
768
769 =item previous_cust_bill_pkg
770
771 Returns the previous cust_bill_pkg for this package, if any.
772
773 =cut
774
775 sub previous_cust_bill_pkg {
776   my $self = shift;
777   return unless $self->sdate;
778   qsearchs({
779     'table'    => 'cust_bill_pkg',
780     'hashref'  => { 'pkgnum' => $self->pkgnum,
781                     'sdate'  => { op=>'<', value=>$self->sdate },
782                   },
783     'order_by' => 'ORDER BY sdate DESC LIMIT 1',
784   });
785 }
786
787 =item owed_setup
788
789 Returns the amount owed (still outstanding) on this line item's setup fee,
790 which is the amount of the line item minus all payment applications (see
791 L<FS::cust_bill_pay_pkg> and credit applications (see
792 L<FS::cust_credit_bill_pkg>).
793
794 =cut
795
796 sub owed_setup {
797   my $self = shift;
798   $self->owed('setup', @_);
799 }
800
801 =item owed_recur
802
803 Returns the amount owed (still outstanding) on this line item's recurring fee,
804 which is the amount of the line item minus all payment applications (see
805 L<FS::cust_bill_pay_pkg> and credit applications (see
806 L<FS::cust_credit_bill_pkg>).
807
808 =cut
809
810 sub owed_recur {
811   my $self = shift;
812   $self->owed('recur', @_);
813 }
814
815 # modeled after cust_bill::owed...
816 sub owed {
817   my( $self, $field ) = @_;
818   my $balance = $self->$field();
819   $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
820   $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
821   $balance = sprintf( '%.2f', $balance );
822   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
823   $balance;
824 }
825
826 #modeled after owed
827 sub payable {
828   my( $self, $field ) = @_;
829   my $balance = $self->$field();
830   $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
831   $balance = sprintf( '%.2f', $balance );
832   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
833   $balance;
834 }
835
836 sub cust_bill_pay_pkg {
837   my( $self, $field ) = @_;
838   qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
839                                   'setuprecur' => $field,
840                                 }
841          );
842 }
843
844 sub cust_credit_bill_pkg {
845   my( $self, $field ) = @_;
846   qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
847                                      'setuprecur' => $field,
848                                    }
849          );
850 }
851
852 =item units
853
854 Returns the number of billing units (for tax purposes) represented by this,
855 line item.
856
857 =cut
858
859 sub units {
860   my $self = shift;
861   $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
862 }
863
864 =item _item_discount
865
866 If this item has any discounts, returns a hashref in the format used
867 by L<FS::Template_Mixin/_items_cust_bill_pkg> to describe the discount(s)
868 on an invoice. This will contain the keys 'description', 'amount', 
869 'ext_description' (an arrayref of text lines describing the discounts),
870 and '_is_discount' (a flag).
871
872 The value for 'amount' will be negative, and will be scaled for the package
873 quantity.
874
875 =cut
876
877 sub _item_discount {
878   my $self = shift;
879   my %options = @_;
880
881   my $d; # this will be returned.
882
883   my @pkg_discounts = $self->pkg_discount;
884   if (@pkg_discounts) {
885     # special case: if there are old "discount details" on this line item,
886     # don't show discount line items
887     if ( FS::cust_bill_pkg_detail->count("detail LIKE 'Includes discount%' AND billpkgnum = ?", $self->billpkgnum || 0) > 0 ) {
888       return;
889     } 
890     
891     my @ext;
892     $d = {
893       _is_discount    => 1,
894       description     => $self->mt('Discount'),
895       setup_amount    => 0,
896       recur_amount    => 0,
897       ext_description => \@ext,
898       pkgpart         => $self->pkgpart,
899       feepart         => $self->feepart,
900       # maybe should show quantity/unit discount?
901     };
902     foreach my $pkg_discount (@pkg_discounts) {
903       push @ext, $pkg_discount->description;
904       my $setuprecur = $pkg_discount->cust_pkg_discount->setuprecur;
905       $d->{$setuprecur.'_amount'} -= $pkg_discount->amount;
906     }
907   }
908
909   # show introductory rate as a pseudo-discount
910   if (!$d) { # this will conflict with showing real discounts
911     my $part_pkg = $self->part_pkg;
912     if ( $part_pkg and $part_pkg->option('show_as_discount',1) ) {
913       my $cust_pkg = $self->cust_pkg;
914       my $intro_end = $part_pkg->intro_end($cust_pkg);
915       my $_date = $self->cust_bill->_date;
916       if ( $intro_end > $_date ) {
917         $d = $part_pkg->item_discount($cust_pkg);
918       }
919     }
920   }
921
922   if ( $d ) {
923     $d->{setup_amount} *= $self->quantity || 1; # ??
924     $d->{recur_amount} *= $self->quantity || 1; # ??
925   }
926     
927   $d;
928 }
929
930 =item set_display OPTION => VALUE ...
931
932 A helper method for I<insert>, populates the pseudo-field B<display> with
933 appropriate FS::cust_bill_pkg_display objects.
934
935 Options are passed as a list of name/value pairs.  Options are:
936
937 part_pkg: FS::part_pkg object from this line item's package.
938
939 real_pkgpart: if this line item comes from a bundled package, the pkgpart 
940 of the owning package.  Otherwise the same as the part_pkg's pkgpart above.
941
942 =cut
943
944 sub set_display {
945   my( $self, %opt ) = @_;
946   my $part_pkg = $opt{'part_pkg'};
947   my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
948
949   my $conf = new FS::Conf;
950
951   # whether to break this down into setup/recur/usage
952   my $separate = $conf->exists('separate_usage');
953
954   my $usage_mandate =            $part_pkg->option('usage_mandate', 'Hush!')
955                     || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
956
957   # or use the category from $opt{'part_pkg'} if its not bundled?
958   my $categoryname = $cust_pkg->part_pkg->categoryname;
959
960   # if we don't have to separate setup/recur/usage, or put this in a 
961   # package-specific section, or display a usage summary, then don't 
962   # even create one of these.  The item will just display in the unnamed
963   # section as a single line plus details.
964   return $self->set('display', [])
965     unless $separate || $categoryname || $usage_mandate;
966   
967   my @display = ();
968
969   my %hash = ( 'section' => $categoryname );
970
971   # whether to put usage details in a separate section, and if so, which one
972   my $usage_section =            $part_pkg->option('usage_section', 'Hush!')
973                     || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
974
975   # whether to show a usage summary line (total usage charges, no details)
976   my $summary =            $part_pkg->option('summarize_usage', 'Hush!')
977               || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
978
979   if ( $separate ) {
980     # create lines for setup and (non-usage) recur, in the main section
981     push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
982     push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
983   } else {
984     # display everything in a single line
985     push @display, new FS::cust_bill_pkg_display
986                      { type => '',
987                        %hash,
988                        # and if usage_mandate is enabled, hide details
989                        # (this only works on multisection invoices...)
990                        ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
991                      };
992   }
993
994   if ($separate && $usage_section && $summary) {
995     # create a line for the usage summary in the main section
996     push @display, new FS::cust_bill_pkg_display { type    => 'U',
997                                                    summary => 'Y',
998                                                    %hash,
999                                                  };
1000   }
1001
1002   if ($usage_mandate || ($usage_section && $summary) ) {
1003     $hash{post_total} = 'Y';
1004   }
1005
1006   if ($separate || $usage_mandate) {
1007     # show call details for this line item in the usage section.
1008     # if usage_mandate is on, this will display below the section subtotal.
1009     # this also happens if usage is in a separate section and there's a 
1010     # summary in the main section, though I'm not sure why.
1011     $hash{section} = $usage_section if $usage_section;
1012     push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
1013   }
1014
1015   $self->set('display', \@display);
1016
1017 }
1018
1019 =item disintegrate
1020
1021 Returns a hash: keys are "setup", "recur" or usage classnum, values are
1022 FS::cust_bill_pkg objects, each with no more than a single class (setup or
1023 recur) of charge.
1024
1025 =cut
1026
1027 sub disintegrate {
1028   my $self = shift;
1029   # XXX this goes away with cust_bill_pkg refactor
1030   # or at least I wish it would, but it turns out to be harder than
1031   # that.
1032
1033   #my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash }; # wha huh?
1034   my %cust_bill_pkg = ();
1035
1036   my $usage_total;
1037   foreach my $classnum ($self->usage_classes) {
1038     my $amount = $self->usage($classnum);
1039     next if $amount == 0; # though if so we shouldn't be here
1040     my $usage_item = FS::cust_bill_pkg->new({
1041         $self->hash,
1042         'setup'     => 0,
1043         'recur'     => $amount,
1044         'taxclass'  => $classnum,
1045         'inherit'   => $self
1046     });
1047     $cust_bill_pkg{$classnum} = $usage_item;
1048     $usage_total += $amount;
1049   }
1050
1051   foreach (qw(setup recur)) {
1052     next if ($self->get($_) == 0);
1053     my $item = FS::cust_bill_pkg->new({
1054         $self->hash,
1055         'setup'     => 0,
1056         'recur'     => 0,
1057         'taxclass'  => $_,
1058         'inherit'   => $self,
1059     });
1060     $item->set($_, $self->get($_));
1061     $cust_bill_pkg{$_} = $item;
1062   }
1063
1064   if ($usage_total) {
1065     $cust_bill_pkg{recur}->set('recur',
1066       sprintf('%.2f', $cust_bill_pkg{recur}->get('recur') - $usage_total)
1067     );
1068   }
1069
1070   %cust_bill_pkg;
1071 }
1072
1073 =item usage CLASSNUM
1074
1075 Returns the amount of the charge associated with usage class CLASSNUM if
1076 CLASSNUM is defined.  Otherwise returns the total charge associated with
1077 usage.
1078   
1079 =cut
1080
1081 sub usage {
1082   my( $self, $classnum ) = @_;
1083   $self->regularize_details;
1084
1085   if ( $self->get('details') ) {
1086
1087     return sum( 0, 
1088       map { $_->amount || 0 }
1089       grep { !defined($classnum) or $classnum eq $_->classnum }
1090       @{ $self->get('details') }
1091     );
1092
1093   } else {
1094
1095     my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
1096               ' WHERE billpkgnum = '. $self->billpkgnum;
1097     if (defined $classnum) {
1098       if ($classnum =~ /^(\d+)$/) {
1099         $sql .= " AND classnum = $1";
1100       } elsif ($classnum eq '') {
1101         $sql .= " AND classnum IS NULL";
1102       }
1103     }
1104
1105     my $sth = dbh->prepare($sql) or die dbh->errstr;
1106     $sth->execute or die $sth->errstr;
1107
1108     return $sth->fetchrow_arrayref->[0] || 0;
1109
1110   }
1111
1112 }
1113
1114 =item usage_classes
1115
1116 Returns a list of usage classnums associated with this invoice line's
1117 details.
1118   
1119 =cut
1120
1121 sub usage_classes {
1122   my( $self ) = @_;
1123   $self->regularize_details;
1124
1125   if ( $self->get('details') ) {
1126
1127     my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
1128     keys %seen;
1129
1130   } else {
1131
1132     map { $_->classnum }
1133         qsearch({ table   => 'cust_bill_pkg_detail',
1134                   hashref => { billpkgnum => $self->billpkgnum },
1135                   select  => 'DISTINCT classnum',
1136                });
1137
1138   }
1139
1140 }
1141
1142 sub cust_tax_exempt_pkg {
1143   my ( $self ) = @_;
1144
1145   my $array = $self->{Hash}->{cust_tax_exempt_pkg} ||= [];
1146 }
1147
1148 =item cust_bill_pkg_tax_Xlocation
1149
1150 Returns the list of associated cust_bill_pkg_tax_location and/or
1151 cust_bill_pkg_tax_rate_location objects
1152
1153 =cut
1154
1155 sub cust_bill_pkg_tax_Xlocation {
1156   my $self = shift;
1157
1158   my %hash = ( 'billpkgnum' => $self->billpkgnum );
1159
1160   (
1161     qsearch ( 'cust_bill_pkg_tax_location', { %hash  } ),
1162     qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
1163   );
1164
1165 }
1166
1167 =item recur_show_zero
1168
1169 Whether to show a zero recurring amount. This is true if the package or its
1170 definition has the recur_show_zero flag, and the recurring fee is actually
1171 zero for this period.
1172
1173 =cut
1174
1175 sub recur_show_zero {
1176   my( $self, $what ) = @_;
1177
1178   return 0 unless $self->get('recur') == 0 && $self->pkgnum;
1179
1180   $self->cust_pkg->_X_show_zero('recur');
1181 }
1182
1183 =item setup_show_zero
1184
1185 Whether to show a zero setup charge. This requires the package or its
1186 definition to have the setup_show_zero flag, but it also returns false if
1187 the package's setup date is before this line item's start date.
1188
1189 =cut
1190
1191 sub setup_show_zero {
1192   my $self = shift;
1193   return 0 unless $self->get('setup') == 0 && $self->pkgnum;
1194   my $cust_pkg = $self->cust_pkg;
1195   return 0 if ( $self->sdate || 0 ) > ( $cust_pkg->setup || 0 );
1196   return $cust_pkg->_X_show_zero('setup');
1197 }
1198
1199 =item credited [ BEFORE, AFTER, OPTIONS ]
1200
1201 Returns the sum of credits applied to this item.  Arguments are the same as
1202 owed_sql/paid_sql/credited_sql.
1203
1204 =cut
1205
1206 sub credited {
1207   my $self = shift;
1208   $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
1209 }
1210
1211 =item tax_locationnum
1212
1213 Returns the L<FS::cust_location> number that this line item is in for tax
1214 purposes.  For package sales, it's the package tax location; for fees, 
1215 it's the customer's default service location.
1216
1217 =cut
1218
1219 sub tax_locationnum {
1220   my $self = shift;
1221   if ( $self->pkgnum ) { # normal sales
1222     return $self->cust_pkg->tax_locationnum;
1223   } elsif ( $self->feepart ) { # fees
1224     my $custnum = $self->fee_origin->custnum;
1225     if ( $custnum ) {
1226       return FS::cust_main->by_key($custnum)->ship_locationnum;
1227     }
1228   } else { # taxes
1229     return '';
1230   }
1231 }
1232
1233 sub tax_location {
1234   my $self = shift;
1235   if ( $self->pkgnum ) { # normal sales
1236     return $self->cust_pkg->tax_location;
1237   } elsif ( $self->feepart ) { # fees
1238     my $fee_origin = $self->fee_origin;
1239     if ( $fee_origin ) {
1240       my $custnum = $fee_origin->custnum;
1241       if ( $custnum ) {
1242         return FS::cust_main->by_key($custnum)->ship_location;
1243       }
1244     }
1245   } else { # taxes
1246     return;
1247   }
1248 }
1249
1250 =back
1251
1252 =head1 CLASS METHODS
1253
1254 =over 4
1255
1256 =item usage_sql
1257
1258 Returns an SQL expression for the total usage charges in details on
1259 an item.
1260
1261 =cut
1262
1263 my $usage_sql =
1264   '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0) 
1265     FROM cust_bill_pkg_detail 
1266     WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
1267
1268 sub usage_sql { $usage_sql }
1269
1270 # this makes owed_sql, etc. much more concise
1271 sub charged_sql {
1272   my ($class, $start, $end, %opt) = @_;
1273   my $setuprecur = $opt{setuprecur} || '';
1274   my $charged = 
1275     $setuprecur =~ /^s/ ? 'cust_bill_pkg.setup' :
1276     $setuprecur =~ /^r/ ? 'cust_bill_pkg.recur' :
1277     'cust_bill_pkg.setup + cust_bill_pkg.recur';
1278
1279   if ($opt{no_usage} and $charged =~ /recur/) { 
1280     $charged = "$charged - $usage_sql"
1281   }
1282
1283   $charged;
1284 }
1285
1286
1287 =item owed_sql [ BEFORE, AFTER, OPTIONS ]
1288
1289 Returns an SQL expression for the amount owed.  BEFORE and AFTER specify
1290 a date window.  OPTIONS may include 'no_usage' (excludes usage charges)
1291 and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
1292
1293 =cut
1294
1295 sub owed_sql {
1296   my $class = shift;
1297   '(' . $class->charged_sql(@_) . 
1298   ' - ' . $class->paid_sql(@_) .
1299   ' - ' . $class->credited_sql(@_) . ')'
1300 }
1301
1302 =item paid_sql [ BEFORE, AFTER, OPTIONS ]
1303
1304 Returns an SQL expression for the sum of payments applied to this item.
1305
1306 =cut
1307
1308 sub paid_sql {
1309   my ($class, $start, $end, %opt) = @_;
1310   my $s = $start ? "AND cust_pay._date <= $start" : '';
1311   my $e = $end   ? "AND cust_pay._date >  $end"   : '';
1312   my $setuprecur = $opt{setuprecur} || '';
1313   $setuprecur = 'setup' if $setuprecur =~ /^s/;
1314   $setuprecur = 'recur' if $setuprecur =~ /^r/;
1315   $setuprecur &&= "AND setuprecur = '$setuprecur'";
1316
1317   my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
1318      FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
1319                             JOIN cust_pay      USING (paynum)
1320      WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1321            $s $e $setuprecur )";
1322
1323   if ( $opt{no_usage} ) {
1324     # cap the amount paid at the sum of non-usage charges, 
1325     # minus the amount credited against non-usage charges
1326     "LEAST($paid, ". 
1327       $class->charged_sql($start, $end, %opt) . ' - ' .
1328       $class->credited_sql($start, $end, %opt).')';
1329   }
1330   else {
1331     $paid;
1332   }
1333
1334 }
1335
1336 sub credited_sql {
1337   my ($class, $start, $end, %opt) = @_;
1338   my $s = $start ? "AND cust_credit._date <= $start" : '';
1339   my $e = $end   ? "AND cust_credit._date >  $end"   : '';
1340   my $setuprecur = $opt{setuprecur} || '';
1341   $setuprecur = 'setup' if $setuprecur =~ /^s/;
1342   $setuprecur = 'recur' if $setuprecur =~ /^r/;
1343   $setuprecur &&= "AND setuprecur = '$setuprecur'";
1344
1345   my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
1346      FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
1347                                JOIN cust_credit      USING (crednum)
1348      WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1349            $s $e $setuprecur )";
1350
1351   if ( $opt{no_usage} ) {
1352     # cap the amount credited at the sum of non-usage charges
1353     "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
1354   }
1355   else {
1356     $credited;
1357   }
1358
1359 }
1360
1361 sub upgrade_tax_location {
1362   # For taxes that were calculated/invoiced before cust_location refactoring
1363   # (May-June 2012), there are no cust_bill_pkg_tax_location records unless
1364   # they were calculated on a package-location basis.  Create them here, 
1365   # along with any necessary cust_location records and any tax exemption 
1366   # records.
1367
1368   my ($class, %opt) = @_;
1369   # %opt may include 's' and 'e': start and end date ranges
1370   # and 'X': abort on any error, instead of just rolling back changes to 
1371   # that invoice
1372   my $dbh = dbh;
1373   my $oldAutoCommit = $FS::UID::AutoCommit;
1374   local $FS::UID::AutoCommit = 0;
1375
1376   eval {
1377     use FS::h_cust_main;
1378     use FS::h_cust_bill;
1379     use FS::h_part_pkg;
1380     use FS::h_cust_main_exemption;
1381   };
1382
1383   local $FS::cust_location::import = 1;
1384
1385   my $conf = FS::Conf->new; # h_conf?
1386   return if $conf->config('tax_data_vendor'); #don't touch this case
1387   my $use_ship = $conf->exists('tax-ship_address');
1388   my $use_pkgloc = $conf->exists('tax-pkg_address');
1389
1390   my $date_where = '';
1391   if ($opt{s}) {
1392     $date_where .= " AND cust_bill._date >= $opt{s}";
1393   }
1394   if ($opt{e}) {
1395     $date_where .= " AND cust_bill._date < $opt{e}";
1396   }
1397
1398   my $commit_each_invoice = 1 unless $opt{X};
1399
1400   # if an invoice has either of these kinds of objects, then it doesn't
1401   # need to be upgraded...probably
1402   my $sub_has_tax_link = 'SELECT 1 FROM cust_bill_pkg_tax_location'.
1403   ' JOIN cust_bill_pkg USING (billpkgnum)'.
1404   ' WHERE cust_bill_pkg.invnum = cust_bill.invnum';
1405   my $sub_has_exempt = 'SELECT 1 FROM cust_tax_exempt_pkg'.
1406   ' JOIN cust_bill_pkg USING (billpkgnum)'.
1407   ' WHERE cust_bill_pkg.invnum = cust_bill.invnum'.
1408   ' AND exempt_monthly IS NULL';
1409
1410   my %all_tax_names = (
1411     '' => 1,
1412     'Tax' => 1,
1413     map { $_->taxname => 1 }
1414       qsearch('h_cust_main_county', { taxname => { op => '!=', value => '' }})
1415   );
1416
1417   my $search = FS::Cursor->new({
1418       table => 'cust_bill',
1419       hashref => {},
1420       extra_sql => "WHERE NOT EXISTS($sub_has_tax_link) ".
1421                    "AND NOT EXISTS($sub_has_exempt) ".
1422                     $date_where,
1423   });
1424
1425 #print "Processing ".scalar(@invnums)." invoices...\n";
1426
1427   my $committed;
1428   INVOICE:
1429   while (my $cust_bill = $search->fetch) {
1430     my $invnum = $cust_bill->invnum;
1431     $committed = 0;
1432     print STDERR "Invoice #$invnum\n";
1433     my $pre = '';
1434     my %pkgpart_taxclass; # pkgpart => taxclass
1435     my %pkgpart_exempt_setup;
1436     my %pkgpart_exempt_recur;
1437     my $h_cust_bill = qsearchs('h_cust_bill',
1438       { invnum => $invnum,
1439         history_action => 'insert' });
1440     if (!$h_cust_bill) {
1441       warn "no insert record for invoice $invnum; skipped\n";
1442       #$date = $cust_bill->_date as a fallback?
1443       # We're trying to avoid using non-real dates (-d/-y invoice dates)
1444       # when looking up history records in other tables.
1445       next INVOICE;
1446     }
1447     my $custnum = $h_cust_bill->custnum;
1448
1449     # Determine the address corresponding to this tax region.
1450     # It's either the bill or ship address of the customer as of the
1451     # invoice date-of-insertion.  (Not necessarily the invoice date.)
1452     my $date = $h_cust_bill->history_date;
1453     local($FS::Record::qsearch_qualify_columns) = 0;
1454     my $h_cust_main = qsearchs('h_cust_main',
1455         { custnum   => $custnum },
1456         FS::h_cust_main->sql_h_searchs($date)
1457       );
1458     if (!$h_cust_main ) {
1459       warn "no historical address for cust#".$h_cust_bill->custnum."; skipped\n";
1460       next INVOICE;
1461       # fallback to current $cust_main?  sounds dangerous.
1462     }
1463
1464     # This is a historical customer record, so it has a historical address.
1465     # If there's no cust_location matching this custnum and address (there 
1466     # probably isn't), create one.
1467     my %tax_loc; # keys are pkgnums, values are cust_location objects
1468     my $default_tax_loc;
1469     if ( $h_cust_main->bill_locationnum ) {
1470       # the location has already been upgraded
1471       if ($use_ship) {
1472         $default_tax_loc = $h_cust_main->ship_location;
1473       } else {
1474         $default_tax_loc = $h_cust_main->bill_location;
1475       }
1476     } else {
1477       $pre = 'ship_' if $use_ship and length($h_cust_main->get('ship_last'));
1478       my %hash = map { $_ => $h_cust_main->get($pre.$_) }
1479                     FS::cust_main->location_fields;
1480       # not really needed for this, and often result in duplicate locations
1481       delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
1482
1483       $hash{custnum} = $h_cust_main->custnum;
1484       $default_tax_loc = FS::cust_location->new(\%hash);
1485       my $error = $default_tax_loc->find_or_insert || $default_tax_loc->disable_if_unused;
1486       if ( $error ) {
1487         warn "couldn't create historical location record for cust#".
1488         $h_cust_main->custnum.": $error\n";
1489         next INVOICE;
1490       }
1491     }
1492     my $exempt_cust;
1493     $exempt_cust = 1 if $h_cust_main->tax;
1494
1495     # classify line items
1496     my @tax_items;
1497     my %nontax_items; # taxclass => array of cust_bill_pkg
1498     foreach my $item ($h_cust_bill->cust_bill_pkg) {
1499       my $pkgnum = $item->pkgnum;
1500
1501       if ( $pkgnum == 0 ) {
1502
1503         push @tax_items, $item;
1504
1505       } else {
1506         # (pkgparts really shouldn't change, right?)
1507         local($FS::Record::qsearch_qualify_columns) = 0;
1508         my $h_cust_pkg = qsearchs('h_cust_pkg', { pkgnum => $pkgnum },
1509           FS::h_cust_pkg->sql_h_searchs($date)
1510         );
1511         if ( !$h_cust_pkg ) {
1512           warn "no historical package #".$item->pkgpart."; skipped\n";
1513           next INVOICE;
1514         }
1515         my $pkgpart = $h_cust_pkg->pkgpart;
1516
1517         if ( $use_pkgloc and $h_cust_pkg->locationnum ) {
1518           # then this package already had a locationnum assigned, and that's 
1519           # the one to use for tax calculation
1520           $tax_loc{$pkgnum} = FS::cust_location->by_key($h_cust_pkg->locationnum);
1521         } else {
1522           # use the customer's bill or ship loc, which was inserted earlier
1523           $tax_loc{$pkgnum} = $default_tax_loc;
1524         }
1525
1526         if (!exists $pkgpart_taxclass{$pkgpart}) {
1527           local($FS::Record::qsearch_qualify_columns) = 0;
1528           my $h_part_pkg = qsearchs('h_part_pkg', { pkgpart => $pkgpart },
1529             FS::h_part_pkg->sql_h_searchs($date)
1530           );
1531           if ( !$h_part_pkg ) {
1532             warn "no historical package def #$pkgpart; skipped\n";
1533             next INVOICE;
1534           }
1535           $pkgpart_taxclass{$pkgpart} = $h_part_pkg->taxclass || '';
1536           $pkgpart_exempt_setup{$pkgpart} = 1 if $h_part_pkg->setuptax;
1537           $pkgpart_exempt_recur{$pkgpart} = 1 if $h_part_pkg->recurtax;
1538         }
1539         
1540         # mark any exemptions that apply
1541         if ( $pkgpart_exempt_setup{$pkgpart} ) {
1542           $item->set('exempt_setup' => 1);
1543         }
1544
1545         if ( $pkgpart_exempt_recur{$pkgpart} ) {
1546           $item->set('exempt_recur' => 1);
1547         }
1548
1549         my $taxclass = $pkgpart_taxclass{ $pkgpart };
1550
1551         $nontax_items{$taxclass} ||= [];
1552         push @{ $nontax_items{$taxclass} }, $item;
1553       }
1554     }
1555
1556     printf("%d tax items: \$%.2f\n", scalar(@tax_items), map {$_->setup} @tax_items)
1557       if @tax_items;
1558
1559     # Get any per-customer taxname exemptions that were in effect.
1560     my %exempt_cust_taxname;
1561     foreach (keys %all_tax_names) {
1562      local($FS::Record::qsearch_qualify_columns) = 0;
1563       my $h_exemption = qsearchs('h_cust_main_exemption', {
1564           'custnum' => $custnum,
1565           'taxname' => $_,
1566         },
1567         FS::h_cust_main_exemption->sql_h_searchs($date, $date)
1568       );
1569       if ($h_exemption) {
1570         $exempt_cust_taxname{ $_ } = 1;
1571       }
1572     }
1573
1574     # Use a variation on the procedure in 
1575     # FS::cust_main::Billing::_handle_taxes to identify taxes that apply 
1576     # to this bill.
1577     my @loc_keys = qw( district city county state country );
1578     my %taxdef_by_name; # by name, and then by taxclass
1579     my %est_tax; # by name, and then by taxclass
1580     my %taxable_items; # by taxnum, and then an array
1581
1582     foreach my $taxclass (keys %nontax_items) {
1583       foreach my $orig_item (@{ $nontax_items{$taxclass} }) {
1584         my $my_tax_loc = $tax_loc{ $orig_item->pkgnum };
1585         my %myhash = map { $_ => $my_tax_loc->get($pre.$_) } @loc_keys;
1586         my @elim = qw( district city county state );
1587         my @taxdefs; # because there may be several with different taxnames
1588         do {
1589           $myhash{taxclass} = $taxclass;
1590           @taxdefs = qsearch('cust_main_county', \%myhash);
1591           if ( !@taxdefs ) {
1592             $myhash{taxclass} = '';
1593             @taxdefs = qsearch('cust_main_county', \%myhash);
1594           }
1595           $myhash{ shift @elim } = '';
1596         } while scalar(@elim) and !@taxdefs;
1597
1598         foreach my $taxdef (@taxdefs) {
1599           next if $taxdef->tax == 0;
1600           $taxdef_by_name{$taxdef->taxname}{$taxdef->taxclass} = $taxdef;
1601
1602           $taxable_items{$taxdef->taxnum} ||= [];
1603           # clone the item so that taxdef-dependent changes don't
1604           # change it for other taxdefs
1605           my $item = FS::cust_bill_pkg->new({ $orig_item->hash });
1606
1607           # these flags are already set if the part_pkg declares itself exempt
1608           $item->set('exempt_setup' => 1) if $taxdef->setuptax;
1609           $item->set('exempt_recur' => 1) if $taxdef->recurtax;
1610
1611           my @new_exempt;
1612           my $taxable = $item->setup + $item->recur;
1613           # credits
1614           # h_cust_credit_bill_pkg?
1615           # NO.  Because if these exemptions HAD been created at the time of 
1616           # billing, and then a credit applied later, the exemption would 
1617           # have been adjusted by the amount of the credit.  So we adjust
1618           # the taxable amount before creating the exemption.
1619           # But don't deduct the credit from taxable, because the tax was 
1620           # calculated before the credit was applied.
1621           foreach my $f (qw(setup recur)) {
1622             my $credited = FS::Record->scalar_sql(
1623               "SELECT SUM(amount) FROM cust_credit_bill_pkg ".
1624               "WHERE billpkgnum = ? AND setuprecur = ?",
1625               $item->billpkgnum,
1626               $f
1627             );
1628             $item->set($f, $item->get($f) - $credited) if $credited;
1629           }
1630           my $existing_exempt = FS::Record->scalar_sql(
1631             "SELECT SUM(amount) FROM cust_tax_exempt_pkg WHERE ".
1632             "billpkgnum = ? AND taxnum = ?",
1633             $item->billpkgnum, $taxdef->taxnum
1634           ) || 0;
1635           $taxable -= $existing_exempt;
1636
1637           if ( $taxable and $exempt_cust ) {
1638             push @new_exempt, { exempt_cust => 'Y',  amount => $taxable };
1639             $taxable = 0;
1640           }
1641           if ( $taxable and $exempt_cust_taxname{$taxdef->taxname} ){
1642             push @new_exempt, { exempt_cust_taxname => 'Y', amount => $taxable };
1643             $taxable = 0;
1644           }
1645           if ( $taxable and $item->exempt_setup ) {
1646             push @new_exempt, { exempt_setup => 'Y', amount => $item->setup };
1647             $taxable -= $item->setup;
1648           }
1649           if ( $taxable and $item->exempt_recur ) {
1650             push @new_exempt, { exempt_recur => 'Y', amount => $item->recur };
1651             $taxable -= $item->recur;
1652           }
1653
1654           $item->set('taxable' => $taxable);
1655           push @{ $taxable_items{$taxdef->taxnum} }, $item
1656             if $taxable > 0;
1657
1658           # estimate the amount of tax (this is necessary because different
1659           # taxdefs with the same taxname may have different tax rates) 
1660           # and sum that for each taxname/taxclass combination
1661           # (in cents)
1662           $est_tax{$taxdef->taxname} ||= {};
1663           $est_tax{$taxdef->taxname}{$taxdef->taxclass} ||= 0;
1664           $est_tax{$taxdef->taxname}{$taxdef->taxclass} += 
1665             $taxable * $taxdef->tax;
1666
1667           foreach (@new_exempt) {
1668             next if $_->{amount} == 0;
1669             my $cust_tax_exempt_pkg = FS::cust_tax_exempt_pkg->new({
1670                 %$_,
1671                 billpkgnum  => $item->billpkgnum,
1672                 taxnum      => $taxdef->taxnum,
1673               });
1674             my $error = $cust_tax_exempt_pkg->insert;
1675             if ($error) {
1676               my $pkgnum = $item->pkgnum;
1677               warn "error creating tax exemption for inv$invnum pkg$pkgnum:".
1678                 "\n$error\n\n";
1679               next INVOICE;
1680             }
1681           } #foreach @new_exempt
1682         } #foreach $taxdef
1683       } #foreach $item
1684     } #foreach $taxclass
1685
1686     # Now go through the billed taxes and match them up with the line items.
1687     TAX_ITEM: foreach my $tax_item ( @tax_items )
1688     {
1689       my $taxname = $tax_item->itemdesc;
1690       $taxname = '' if $taxname eq 'Tax';
1691
1692       if ( !exists( $taxdef_by_name{$taxname} ) ) {
1693         # then we didn't find any applicable taxes with this name
1694         warn "no definition found for tax item '$taxname', custnum $custnum\n";
1695         # possibly all of these should be "next TAX_ITEM", but whole invoices
1696         # are transaction protected and we can go back and retry them.
1697         next INVOICE;
1698       }
1699       # classname => cust_main_county
1700       my %taxdef_by_class = %{ $taxdef_by_name{$taxname} };
1701
1702       # Divide the tax item among taxclasses, if necessary
1703       # classname => estimated tax amount
1704       my $this_est_tax = $est_tax{$taxname};
1705       if (!defined $this_est_tax) {
1706         warn "no taxable sales found for inv#$invnum, tax item '$taxname'.\n";
1707         next INVOICE;
1708       }
1709       my $est_total = sum(values %$this_est_tax);
1710       if ( $est_total == 0 ) {
1711         # shouldn't happen
1712         warn "estimated tax on invoice #$invnum is zero.\n";
1713         next INVOICE;
1714       }
1715
1716       my $real_tax = $tax_item->setup;
1717       printf ("Distributing \$%.2f tax:\n", $real_tax);
1718       my $cents_remaining = $real_tax * 100; # for rounding error
1719       my @tax_links; # partial CBPTL hashrefs
1720       foreach my $taxclass (keys %taxdef_by_class) {
1721         my $taxdef = $taxdef_by_class{$taxclass};
1722         # these items already have "taxable" set to their charge amount
1723         # after applying any credits or exemptions
1724         my @items = @{ $taxable_items{$taxdef->taxnum} };
1725         my $subtotal = sum(map {$_->get('taxable')} @items);
1726         printf("\t$taxclass: %.2f\n", $this_est_tax->{$taxclass}/$est_total);
1727
1728         foreach my $nontax (@items) {
1729           my $my_tax_loc = $tax_loc{ $nontax->pkgnum };
1730           my $part = int($real_tax
1731                             # class allocation
1732                          * ($this_est_tax->{$taxclass}/$est_total) 
1733                             # item allocation
1734                          * ($nontax->get('taxable'))/$subtotal
1735                             # convert to cents
1736                          * 100
1737                        );
1738           $cents_remaining -= $part;
1739           push @tax_links, {
1740             taxnum      => $taxdef->taxnum,
1741             pkgnum      => $nontax->pkgnum,
1742             locationnum => $my_tax_loc->locationnum,
1743             billpkgnum  => $nontax->billpkgnum,
1744             cents       => $part,
1745           };
1746         } #foreach $nontax
1747       } #foreach $taxclass
1748       # Distribute any leftover tax round-robin style, one cent at a time.
1749       my $i = 0;
1750       my $nlinks = scalar(@tax_links);
1751       if ( $nlinks ) {
1752         # ensure that it really is an integer
1753         $cents_remaining = sprintf('%.0f', $cents_remaining);
1754         while ($cents_remaining > 0) {
1755           $tax_links[$i % $nlinks]->{cents} += 1;
1756           $cents_remaining--;
1757           $i++;
1758         }
1759       } else {
1760         warn "Can't create tax links--no taxable items found.\n";
1761         next INVOICE;
1762       }
1763
1764       # Gather credit/payment applications so that we can link them
1765       # appropriately.
1766       my @unlinked = (
1767         qsearch( 'cust_credit_bill_pkg',
1768           { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1769         ),
1770         qsearch( 'cust_bill_pay_pkg',
1771           { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1772         )
1773       );
1774
1775       # grab the first one
1776       my $this_unlinked = shift @unlinked;
1777       my $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1778
1779       # Create tax links (yay!)
1780       printf("Creating %d tax links.\n",scalar(@tax_links));
1781       foreach (@tax_links) {
1782         my $link = FS::cust_bill_pkg_tax_location->new({
1783             billpkgnum  => $tax_item->billpkgnum,
1784             taxtype     => 'FS::cust_main_county',
1785             locationnum => $_->{locationnum},
1786             taxnum      => $_->{taxnum},
1787             pkgnum      => $_->{pkgnum},
1788             amount      => sprintf('%.2f', $_->{cents} / 100),
1789             taxable_billpkgnum => $_->{billpkgnum},
1790         });
1791         my $error = $link->insert;
1792         if ( $error ) {
1793           warn "Can't create tax link for inv#$invnum: $error\n";
1794           next INVOICE;
1795         }
1796
1797         my $link_cents = $_->{cents};
1798         # update/create subitem links
1799         #
1800         # If $this_unlinked is undef, then we've allocated all of the
1801         # credit/payment applications to the tax item.  If $link_cents is 0,
1802         # then we've applied credits/payments to all of this package fraction,
1803         # so go on to the next.
1804         while ($this_unlinked and $link_cents) {
1805           # apply as much as possible of $link_amount to this credit/payment
1806           # link
1807           my $apply_cents = min($link_cents, $unlinked_cents);
1808           $link_cents -= $apply_cents;
1809           $unlinked_cents -= $apply_cents;
1810           # $link_cents or $unlinked_cents or both are now zero
1811           $this_unlinked->set('amount' => sprintf('%.2f',$apply_cents/100));
1812           $this_unlinked->set('billpkgtaxlocationnum' => $link->billpkgtaxlocationnum);
1813           my $pkey = $this_unlinked->primary_key; #creditbillpkgnum or billpaypkgnum
1814           if ( $this_unlinked->$pkey ) {
1815             # then it's an existing link--replace it
1816             $error = $this_unlinked->replace;
1817           } else {
1818             $this_unlinked->insert;
1819           }
1820           # what do we do with errors at this stage?
1821           if ( $error ) {
1822             warn "Error creating tax application link: $error\n";
1823             next INVOICE; # for lack of a better idea
1824           }
1825           
1826           if ( $unlinked_cents == 0 ) {
1827             # then we've allocated all of this payment/credit application, 
1828             # so grab the next one
1829             $this_unlinked = shift @unlinked;
1830             $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1831           } elsif ( $link_cents == 0 ) {
1832             # then we've covered all of this package tax fraction, so split
1833             # off a new application from this one
1834             $this_unlinked = $this_unlinked->new({
1835                 $this_unlinked->hash,
1836                 $pkey     => '',
1837             });
1838             # $unlinked_cents is still what it is
1839           }
1840
1841         } #while $this_unlinked and $link_cents
1842       } #foreach (@tax_links)
1843     } #foreach $tax_item
1844
1845     $dbh->commit if $commit_each_invoice and $oldAutoCommit;
1846     $committed = 1;
1847
1848   } #foreach $invnum
1849   continue {
1850     if (!$committed) {
1851       $dbh->rollback if $oldAutoCommit;
1852       die "Upgrade halted.\n" unless $commit_each_invoice;
1853     }
1854   }
1855
1856   $dbh->commit if $oldAutoCommit and !$commit_each_invoice;
1857   '';
1858 }
1859
1860 sub _pkg_tax_list {
1861   # Return an array of hashrefs for each cust_bill_pkg_tax_location
1862   # applied to this bill for this cust_bill_pkg.pkgnum.
1863   #
1864   # ! Important Note:
1865   #   In some situations, this list will contain more tax records than the
1866   #   ones directly related to $self->billpkgnum.  The returned list contains
1867   #   all records, for this bill, charged against this billpkgnum's pkgnum.
1868   #
1869   #   One must keep this in mind when using data returned by this method.
1870   #
1871   #   An unaddressed deficiency in the cust_bill_pkg_tax_location model makes
1872   #   this necessary:  When a linked-hidden package generates a tax/fee as a row
1873   #   in cust_bill_pkg_tax_location, there is not enough information to surmise
1874   #   with specificity which billpkgnum row represents the direct parent of the
1875   #   the linked-hidden package's tax row.  The closest we can get to this
1876   #   backwards reassociation is to use the pkgnum.  Therefore, when multiple
1877   #   billpkgnum's appear with the same pkgnum, this method is going to return
1878   #   the tax records for ALL of those billpkgnum's, not just $self->billpkgnum.
1879   #
1880   #   This could be addressed with an update to the model, and to the billing
1881   #   routine that generates rows into cust_bill_pkg_tax_location.  Perhaps a
1882   #   column, link_billpkgnum or parent_billpkgnum, recording the link. I'm not
1883   #   doing that now, because there would be no possible repair of data stored
1884   #   historically prior to such a fix.  I need _pkg_tax_list() to not be
1885   #   broken for already-generated bills.
1886   #
1887   #   Any code you write relying on _pkg_tax_list() MUST be aware of, and
1888   #   account for, the possible return of duplicated tax records returned
1889   #   when method is called on multiple cust_bill_pkg_tax_location rows.
1890   #   Duplicates can be identified by billpkgtaxlocationnum column.
1891
1892   my $self = shift;
1893
1894   my $search_selector;
1895   if ( $self->pkgnum ) {
1896
1897     # For taxes applied to normal billing items
1898     $search_selector =
1899       ' cust_bill_pkg_tax_location.pkgnum = '
1900       . dbh->quote( $self->pkgnum );
1901
1902   } elsif ( $self->feepart ) {
1903
1904     # For taxes applied to fees, when the fee is not attached to a package
1905     # i.e. late fees, billing events fees
1906     $search_selector =
1907       ' cust_bill_pkg_tax_location.taxable_billpkgnum = '
1908       . dbh->quote( $self->billpkgnum );
1909
1910   } else {
1911     warn "_pkg_tax_list() unhandled case breaking taxes into sections";
1912     warn "_pkg_tax_list() $_: ".$self->$_
1913       for qw(pkgnum billpkgnum feepart);
1914     return;
1915   }
1916
1917   map +{
1918       billpkgtaxlocationnum => $_->billpkgtaxlocationnum,
1919       billpkgnum            => $_->billpkgnum,
1920       taxnum                => $_->taxnum,
1921       amount                => $_->amount,
1922       taxname               => $_->taxname,
1923   },
1924   qsearch({
1925     table  => 'cust_bill_pkg_tax_location',
1926     addl_from => '
1927       LEFT JOIN cust_bill_pkg
1928               ON cust_bill_pkg.billpkgnum
1929          = cust_bill_pkg_tax_location.taxable_billpkgnum
1930     ',
1931     select => join( ', ', (qw|
1932       cust_bill_pkg.billpkgnum
1933       cust_bill_pkg_tax_location.billpkgtaxlocationnum
1934       cust_bill_pkg_tax_location.taxnum
1935       cust_bill_pkg_tax_location.amount
1936     |)),
1937     extra_sql =>
1938       ' WHERE '.
1939       ' cust_bill_pkg.invnum = ' . dbh->quote( $self->invnum ) .
1940       ' AND '.
1941       $search_selector
1942   });
1943
1944 }
1945
1946 sub _upgrade_data {
1947   # Create a queue job to run upgrade_tax_location from January 1, 2012 to 
1948   # the present date.
1949   eval {
1950     use FS::queue;
1951     use Date::Parse 'str2time';
1952   };
1953   my $class = shift;
1954   my $upgrade = 'tax_location_2012';
1955   return if FS::upgrade_journal->is_done($upgrade);
1956   my $job = FS::queue->new({
1957       'job' => 'FS::cust_bill_pkg::upgrade_tax_location'
1958   });
1959   # call it kind of like a class method, not that it matters much
1960   $job->insert($class, 's' => str2time('2012-01-01'));
1961   # if there's a customer location upgrade queued also, wait for it to 
1962   # finish
1963   my $location_job = qsearchs('queue', {
1964       job => 'FS::cust_main::Location::process_upgrade_location'
1965     });
1966   if ( $location_job ) {
1967     $job->depend_insert($location_job->jobnum);
1968   }
1969   # Then mark the upgrade as done, so that we don't queue the job twice
1970   # and somehow run two of them concurrently.
1971   FS::upgrade_journal->set_done($upgrade);
1972   # This upgrade now does the job of assigning taxable_billpkgnums to 
1973   # cust_bill_pkg_tax_location, so set that task done also.
1974   FS::upgrade_journal->set_done('tax_location_taxable_billpkgnum');
1975 }
1976
1977 =back
1978
1979 =head1 BUGS
1980
1981 setup and recur shouldn't be separate fields.  There should be one "amount"
1982 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1983
1984 A line item with both should really be two separate records (preserving
1985 sdate and edate for setup fees for recurring packages - that information may
1986 be valuable later).  Invoice generation (cust_main::bill), invoice printing
1987 (cust_bill), tax reports (report_tax.cgi) and line item reports 
1988 (cust_bill_pkg.cgi) would need to be updated.
1989
1990 owed_setup and owed_recur could then be repaced by just owed, and
1991 cust_bill::open_cust_bill_pkg and
1992 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1993
1994 The upgrade procedure is pretty sketchy.
1995
1996 =head1 SEE ALSO
1997
1998 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1999 from the base documentation.
2000
2001 =cut
2002
2003 1;