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