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