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