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