8e4fa26890a15d5ee236b743f0c55f08055f0b6c
[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
827   my $d; # this will be returned.
828
829   my @pkg_discounts = $self->pkg_discount;
830   if (@pkg_discounts) {
831     # special case: if there are old "discount details" on this line item,
832     # don't show discount line items
833     if ( FS::cust_bill_pkg_detail->count("detail LIKE 'Includes discount%' AND billpkgnum = ?", $self->billpkgnum || 0) > 0 ) {
834       return;
835     } 
836     
837     my @ext;
838     $d = {
839       _is_discount    => 1,
840       description     => $self->mt('Discount'),
841       amount          => 0,
842       ext_description => \@ext,
843       pkgpart         => $self->pkgpart,
844       feepart         => $self->feepart,
845       # maybe should show quantity/unit discount?
846     };
847     foreach my $pkg_discount (@pkg_discounts) {
848       push @ext, $pkg_discount->description;
849       $d->{'amount'} -= $pkg_discount->amount;
850     }
851   }
852
853   # show introductory rate as a pseudo-discount
854   if (!$d) { # this will conflict with showing real discounts
855     my $part_pkg = $self->part_pkg;
856     if ( $part_pkg and $part_pkg->option('show_as_discount') ) {
857       my $cust_pkg = $self->cust_pkg;
858       my $intro_end = $part_pkg->intro_end($cust_pkg);
859       my $_date = $self->cust_bill->_date;
860       if ( $intro_end > $_date ) {
861         $d = $part_pkg->item_discount($cust_pkg);
862       }
863     }
864   }
865
866   if ( $d ) {
867     $d->{amount} *= $self->quantity || 1;
868   }
869     
870   $d;
871 }
872
873 =item set_display OPTION => VALUE ...
874
875 A helper method for I<insert>, populates the pseudo-field B<display> with
876 appropriate FS::cust_bill_pkg_display objects.
877
878 Options are passed as a list of name/value pairs.  Options are:
879
880 part_pkg: FS::part_pkg object from this line item's package.
881
882 real_pkgpart: if this line item comes from a bundled package, the pkgpart 
883 of the owning package.  Otherwise the same as the part_pkg's pkgpart above.
884
885 =cut
886
887 sub set_display {
888   my( $self, %opt ) = @_;
889   my $part_pkg = $opt{'part_pkg'};
890   my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
891
892   my $conf = new FS::Conf;
893
894   # whether to break this down into setup/recur/usage
895   my $separate = $conf->exists('separate_usage');
896
897   my $usage_mandate =            $part_pkg->option('usage_mandate', 'Hush!')
898                     || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
899
900   # or use the category from $opt{'part_pkg'} if its not bundled?
901   my $categoryname = $cust_pkg->part_pkg->categoryname;
902
903   # if we don't have to separate setup/recur/usage, or put this in a 
904   # package-specific section, or display a usage summary, then don't 
905   # even create one of these.  The item will just display in the unnamed
906   # section as a single line plus details.
907   return $self->set('display', [])
908     unless $separate || $categoryname || $usage_mandate;
909   
910   my @display = ();
911
912   my %hash = ( 'section' => $categoryname );
913
914   # whether to put usage details in a separate section, and if so, which one
915   my $usage_section =            $part_pkg->option('usage_section', 'Hush!')
916                     || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
917
918   # whether to show a usage summary line (total usage charges, no details)
919   my $summary =            $part_pkg->option('summarize_usage', 'Hush!')
920               || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
921
922   if ( $separate ) {
923     # create lines for setup and (non-usage) recur, in the main section
924     push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
925     push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
926   } else {
927     # display everything in a single line
928     push @display, new FS::cust_bill_pkg_display
929                      { type => '',
930                        %hash,
931                        # and if usage_mandate is enabled, hide details
932                        # (this only works on multisection invoices...)
933                        ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
934                      };
935   }
936
937   if ($separate && $usage_section && $summary) {
938     # create a line for the usage summary in the main section
939     push @display, new FS::cust_bill_pkg_display { type    => 'U',
940                                                    summary => 'Y',
941                                                    %hash,
942                                                  };
943   }
944
945   if ($usage_mandate || ($usage_section && $summary) ) {
946     $hash{post_total} = 'Y';
947   }
948
949   if ($separate || $usage_mandate) {
950     # show call details for this line item in the usage section.
951     # if usage_mandate is on, this will display below the section subtotal.
952     # this also happens if usage is in a separate section and there's a 
953     # summary in the main section, though I'm not sure why.
954     $hash{section} = $usage_section if $usage_section;
955     push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
956   }
957
958   $self->set('display', \@display);
959
960 }
961
962 =item disintegrate
963
964 Returns a hash: keys are "setup", "recur" or usage classnum, values are
965 FS::cust_bill_pkg objects, each with no more than a single class (setup or
966 recur) of charge.
967
968 =cut
969
970 sub disintegrate {
971   my $self = shift;
972   # XXX this goes away with cust_bill_pkg refactor
973   # or at least I wish it would, but it turns out to be harder than
974   # that.
975
976   #my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash }; # wha huh?
977   my %cust_bill_pkg = ();
978
979   my $usage_total;
980   foreach my $classnum ($self->usage_classes) {
981     next if $classnum eq ''; # null-class usage is included in 'recur'
982     my $amount = $self->usage($classnum);
983     next if $amount == 0; # though if so we shouldn't be here
984     my $usage_item = FS::cust_bill_pkg->new({
985         $self->hash,
986         'setup'     => 0,
987         'recur'     => $amount,
988         'taxclass'  => $classnum,
989         'inherit'   => $self
990     });
991     $cust_bill_pkg{$classnum} = $usage_item;
992     $usage_total += $amount;
993   }
994
995   foreach (qw(setup recur)) {
996     next if ($self->get($_) == 0);
997     my $item = FS::cust_bill_pkg->new({
998         $self->hash,
999         'setup'     => 0,
1000         'recur'     => 0,
1001         'taxclass'  => $_,
1002         'inherit'   => $self,
1003     });
1004     $item->set($_, $self->get($_));
1005     $cust_bill_pkg{$_} = $item;
1006   }
1007
1008   if ($usage_total) {
1009     $cust_bill_pkg{recur}->set('recur',
1010       sprintf('%.2f', $cust_bill_pkg{recur}->get('recur') - $usage_total)
1011     );
1012   }
1013
1014   %cust_bill_pkg;
1015 }
1016
1017 =item usage CLASSNUM
1018
1019 Returns the amount of the charge associated with usage class CLASSNUM if
1020 CLASSNUM is defined.  Otherwise returns the total charge associated with
1021 usage.
1022   
1023 =cut
1024
1025 sub usage {
1026   my( $self, $classnum ) = @_;
1027   $self->regularize_details;
1028
1029   if ( $self->get('details') ) {
1030
1031     return sum( 0, 
1032       map { $_->amount || 0 }
1033       grep { !defined($classnum) or $classnum eq $_->classnum }
1034       @{ $self->get('details') }
1035     );
1036
1037   } else {
1038
1039     my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
1040               ' WHERE billpkgnum = '. $self->billpkgnum;
1041     if (defined $classnum) {
1042       if ($classnum =~ /^(\d+)$/) {
1043         $sql .= " AND classnum = $1";
1044       } elsif (defined($classnum) and $classnum eq '') {
1045         $sql .= " AND classnum IS NULL";
1046       }
1047     }
1048
1049     my $sth = dbh->prepare($sql) or die dbh->errstr;
1050     $sth->execute or die $sth->errstr;
1051
1052     return $sth->fetchrow_arrayref->[0] || 0;
1053
1054   }
1055
1056 }
1057
1058 =item usage_classes
1059
1060 Returns a list of usage classnums associated with this invoice line's
1061 details.
1062   
1063 =cut
1064
1065 sub usage_classes {
1066   my( $self ) = @_;
1067   $self->regularize_details;
1068
1069   if ( $self->get('details') ) {
1070
1071     my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
1072     keys %seen;
1073
1074   } else {
1075
1076     map { $_->classnum }
1077         qsearch({ table   => 'cust_bill_pkg_detail',
1078                   hashref => { billpkgnum => $self->billpkgnum },
1079                   select  => 'DISTINCT classnum',
1080                });
1081
1082   }
1083
1084 }
1085
1086 sub cust_tax_exempt_pkg {
1087   my ( $self ) = @_;
1088
1089   my $array = $self->{Hash}->{cust_tax_exempt_pkg} ||= [];
1090 }
1091
1092 =item cust_bill_pkg_fee
1093
1094 Returns the list of associated cust_bill_pkg_fee objects, if this is 
1095 a fee-type item.
1096
1097 =cut
1098
1099 sub cust_bill_pkg_fee {
1100   my $self = shift;
1101   qsearch('cust_bill_pkg_fee', { billpkgnum => $self->billpkgnum });
1102 }
1103
1104 =item cust_bill_pkg_tax_Xlocation
1105
1106 Returns the list of associated cust_bill_pkg_tax_location and/or
1107 cust_bill_pkg_tax_rate_location objects
1108
1109 =cut
1110
1111 sub cust_bill_pkg_tax_Xlocation {
1112   my $self = shift;
1113
1114   my %hash = ( 'billpkgnum' => $self->billpkgnum );
1115
1116   (
1117     qsearch ( 'cust_bill_pkg_tax_location', { %hash  } ),
1118     qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
1119   );
1120
1121 }
1122
1123 =item recur_show_zero
1124
1125 Whether to show a zero recurring amount. This is true if the package or its
1126 definition has the recur_show_zero flag, and the recurring fee is actually
1127 zero for this period.
1128
1129 =cut
1130
1131 sub recur_show_zero {
1132   my( $self, $what ) = @_;
1133
1134   return 0 unless $self->get('recur') == 0 && $self->pkgnum;
1135
1136   $self->cust_pkg->_X_show_zero('recur');
1137 }
1138
1139 =item setup_show_zero
1140
1141 Whether to show a zero setup charge. This requires the package or its
1142 definition to have the setup_show_zero flag, but it also returns false if
1143 the package's setup date is before this line item's start date.
1144
1145 =cut
1146
1147 sub setup_show_zero {
1148   my $self = shift;
1149   return 0 unless $self->get('setup') == 0 && $self->pkgnum;
1150   my $cust_pkg = $self->cust_pkg;
1151   return 0 if ( $self->sdate || 0 ) > ( $cust_pkg->setup || 0 );
1152   return $cust_pkg->_X_show_zero('setup');
1153 }
1154
1155 =item credited [ BEFORE, AFTER, OPTIONS ]
1156
1157 Returns the sum of credits applied to this item.  Arguments are the same as
1158 owed_sql/paid_sql/credited_sql.
1159
1160 =cut
1161
1162 sub credited {
1163   my $self = shift;
1164   $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
1165 }
1166
1167 =item tax_locationnum
1168
1169 Returns the L<FS::cust_location> number that this line item is in for tax
1170 purposes.  For package sales, it's the package tax location; for fees, 
1171 it's the customer's default service location.
1172
1173 =cut
1174
1175 sub tax_locationnum {
1176   my $self = shift;
1177   if ( $self->pkgnum ) { # normal sales
1178     return $self->cust_pkg->tax_locationnum;
1179   } elsif ( $self->feepart and $self->invnum ) { # fees
1180     return $self->cust_bill->cust_main->ship_locationnum;
1181   } else { # taxes
1182     return '';
1183   }
1184 }
1185
1186 sub tax_location {
1187   my $self = shift;
1188   if ( $self->pkgnum ) { # normal sales
1189     return $self->cust_pkg->tax_location;
1190   } elsif ( $self->feepart and $self->invnum ) { # fees
1191     return $self->cust_bill->cust_main->ship_location;
1192   } else { # taxes
1193     return;
1194   }
1195 }
1196
1197 =back
1198
1199 =head1 CLASS METHODS
1200
1201 =over 4
1202
1203 =item usage_sql
1204
1205 Returns an SQL expression for the total usage charges in details on
1206 an item.
1207
1208 =cut
1209
1210 my $usage_sql =
1211   '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0) 
1212     FROM cust_bill_pkg_detail 
1213     WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
1214
1215 sub usage_sql { $usage_sql }
1216
1217 # this makes owed_sql, etc. much more concise
1218 sub charged_sql {
1219   my ($class, $start, $end, %opt) = @_;
1220   my $setuprecur = $opt{setuprecur} || '';
1221   my $charged = 
1222     $setuprecur =~ /^s/ ? 'cust_bill_pkg.setup' :
1223     $setuprecur =~ /^r/ ? 'cust_bill_pkg.recur' :
1224     'cust_bill_pkg.setup + cust_bill_pkg.recur';
1225
1226   if ($opt{no_usage} and $charged =~ /recur/) { 
1227     $charged = "$charged - $usage_sql"
1228   }
1229
1230   $charged;
1231 }
1232
1233
1234 =item owed_sql [ BEFORE, AFTER, OPTIONS ]
1235
1236 Returns an SQL expression for the amount owed.  BEFORE and AFTER specify
1237 a date window.  OPTIONS may include 'no_usage' (excludes usage charges)
1238 and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
1239
1240 =cut
1241
1242 sub owed_sql {
1243   my $class = shift;
1244   '(' . $class->charged_sql(@_) . 
1245   ' - ' . $class->paid_sql(@_) .
1246   ' - ' . $class->credited_sql(@_) . ')'
1247 }
1248
1249 =item paid_sql [ BEFORE, AFTER, OPTIONS ]
1250
1251 Returns an SQL expression for the sum of payments applied to this item.
1252
1253 =cut
1254
1255 sub paid_sql {
1256   my ($class, $start, $end, %opt) = @_;
1257   my $s = $start ? "AND cust_pay._date <= $start" : '';
1258   my $e = $end   ? "AND cust_pay._date >  $end"   : '';
1259   my $setuprecur = $opt{setuprecur} || '';
1260   $setuprecur = 'setup' if $setuprecur =~ /^s/;
1261   $setuprecur = 'recur' if $setuprecur =~ /^r/;
1262   $setuprecur &&= "AND setuprecur = '$setuprecur'";
1263
1264   my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
1265      FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
1266                             JOIN cust_pay      USING (paynum)
1267      WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1268            $s $e $setuprecur )";
1269
1270   if ( $opt{no_usage} ) {
1271     # cap the amount paid at the sum of non-usage charges, 
1272     # minus the amount credited against non-usage charges
1273     "LEAST($paid, ". 
1274       $class->charged_sql($start, $end, %opt) . ' - ' .
1275       $class->credited_sql($start, $end, %opt).')';
1276   }
1277   else {
1278     $paid;
1279   }
1280
1281 }
1282
1283 sub credited_sql {
1284   my ($class, $start, $end, %opt) = @_;
1285   my $s = $start ? "AND cust_credit._date <= $start" : '';
1286   my $e = $end   ? "AND cust_credit._date >  $end"   : '';
1287   my $setuprecur = $opt{setuprecur} || '';
1288   $setuprecur = 'setup' if $setuprecur =~ /^s/;
1289   $setuprecur = 'recur' if $setuprecur =~ /^r/;
1290   $setuprecur &&= "AND setuprecur = '$setuprecur'";
1291
1292   my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
1293      FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
1294                                JOIN cust_credit      USING (crednum)
1295      WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1296            $s $e $setuprecur )";
1297
1298   if ( $opt{no_usage} ) {
1299     # cap the amount credited at the sum of non-usage charges
1300     "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
1301   }
1302   else {
1303     $credited;
1304   }
1305
1306 }
1307
1308 sub upgrade_tax_location {
1309   # For taxes that were calculated/invoiced before cust_location refactoring
1310   # (May-June 2012), there are no cust_bill_pkg_tax_location records unless
1311   # they were calculated on a package-location basis.  Create them here, 
1312   # along with any necessary cust_location records and any tax exemption 
1313   # records.
1314
1315   my ($class, %opt) = @_;
1316   # %opt may include 's' and 'e': start and end date ranges
1317   # and 'X': abort on any error, instead of just rolling back changes to 
1318   # that invoice
1319   my $dbh = dbh;
1320   my $oldAutoCommit = $FS::UID::AutoCommit;
1321   local $FS::UID::AutoCommit = 0;
1322
1323   eval {
1324     use FS::h_cust_main;
1325     use FS::h_cust_bill;
1326     use FS::h_part_pkg;
1327     use FS::h_cust_main_exemption;
1328   };
1329
1330   local $FS::cust_location::import = 1;
1331
1332   my $conf = FS::Conf->new; # h_conf?
1333   return if $conf->exists('enable_taxproducts'); #don't touch this case
1334   my $use_ship = $conf->exists('tax-ship_address');
1335   my $use_pkgloc = $conf->exists('tax-pkg_address');
1336
1337   my $date_where = '';
1338   if ($opt{s}) {
1339     $date_where .= " AND cust_bill._date >= $opt{s}";
1340   }
1341   if ($opt{e}) {
1342     $date_where .= " AND cust_bill._date < $opt{e}";
1343   }
1344
1345   my $commit_each_invoice = 1 unless $opt{X};
1346
1347   # if an invoice has either of these kinds of objects, then it doesn't
1348   # need to be upgraded...probably
1349   my $sub_has_tax_link = 'SELECT 1 FROM cust_bill_pkg_tax_location'.
1350   ' JOIN cust_bill_pkg USING (billpkgnum)'.
1351   ' WHERE cust_bill_pkg.invnum = cust_bill.invnum';
1352   my $sub_has_exempt = 'SELECT 1 FROM cust_tax_exempt_pkg'.
1353   ' JOIN cust_bill_pkg USING (billpkgnum)'.
1354   ' WHERE cust_bill_pkg.invnum = cust_bill.invnum'.
1355   ' AND exempt_monthly IS NULL';
1356
1357   my %all_tax_names = (
1358     '' => 1,
1359     'Tax' => 1,
1360     map { $_->taxname => 1 }
1361       qsearch('h_cust_main_county', { taxname => { op => '!=', value => '' }})
1362   );
1363
1364   my $search = FS::Cursor->new({
1365       table => 'cust_bill',
1366       hashref => {},
1367       extra_sql => "WHERE NOT EXISTS($sub_has_tax_link) ".
1368                    "AND NOT EXISTS($sub_has_exempt) ".
1369                     $date_where,
1370   });
1371
1372 #print "Processing ".scalar(@invnums)." invoices...\n";
1373
1374   my $committed;
1375   INVOICE:
1376   while (my $cust_bill = $search->fetch) {
1377     my $invnum = $cust_bill->invnum;
1378     $committed = 0;
1379     print STDERR "Invoice #$invnum\n";
1380     my $pre = '';
1381     my %pkgpart_taxclass; # pkgpart => taxclass
1382     my %pkgpart_exempt_setup;
1383     my %pkgpart_exempt_recur;
1384     my $h_cust_bill = qsearchs('h_cust_bill',
1385       { invnum => $invnum,
1386         history_action => 'insert' });
1387     if (!$h_cust_bill) {
1388       warn "no insert record for invoice $invnum; skipped\n";
1389       #$date = $cust_bill->_date as a fallback?
1390       # We're trying to avoid using non-real dates (-d/-y invoice dates)
1391       # when looking up history records in other tables.
1392       next INVOICE;
1393     }
1394     my $custnum = $h_cust_bill->custnum;
1395
1396     # Determine the address corresponding to this tax region.
1397     # It's either the bill or ship address of the customer as of the
1398     # invoice date-of-insertion.  (Not necessarily the invoice date.)
1399     my $date = $h_cust_bill->history_date;
1400     my $h_cust_main = qsearchs('h_cust_main',
1401         { custnum   => $custnum },
1402         FS::h_cust_main->sql_h_searchs($date)
1403       );
1404     if (!$h_cust_main ) {
1405       warn "no historical address for cust#".$h_cust_bill->custnum."; skipped\n";
1406       next INVOICE;
1407       # fallback to current $cust_main?  sounds dangerous.
1408     }
1409
1410     # This is a historical customer record, so it has a historical address.
1411     # If there's no cust_location matching this custnum and address (there 
1412     # probably isn't), create one.
1413     my %tax_loc; # keys are pkgnums, values are cust_location objects
1414     my $default_tax_loc;
1415     if ( $h_cust_main->bill_locationnum ) {
1416       # the location has already been upgraded
1417       if ($use_ship) {
1418         $default_tax_loc = $h_cust_main->ship_location;
1419       } else {
1420         $default_tax_loc = $h_cust_main->bill_location;
1421       }
1422     } else {
1423       $pre = 'ship_' if $use_ship and length($h_cust_main->get('ship_last'));
1424       my %hash = map { $_ => $h_cust_main->get($pre.$_) }
1425                     FS::cust_main->location_fields;
1426       # not really needed for this, and often result in duplicate locations
1427       delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
1428
1429       $hash{custnum} = $h_cust_main->custnum;
1430       $default_tax_loc = FS::cust_location->new(\%hash);
1431       my $error = $default_tax_loc->find_or_insert || $default_tax_loc->disable_if_unused;
1432       if ( $error ) {
1433         warn "couldn't create historical location record for cust#".
1434         $h_cust_main->custnum.": $error\n";
1435         next INVOICE;
1436       }
1437     }
1438     my $exempt_cust;
1439     $exempt_cust = 1 if $h_cust_main->tax;
1440
1441     # classify line items
1442     my @tax_items;
1443     my %nontax_items; # taxclass => array of cust_bill_pkg
1444     foreach my $item ($h_cust_bill->cust_bill_pkg) {
1445       my $pkgnum = $item->pkgnum;
1446
1447       if ( $pkgnum == 0 ) {
1448
1449         push @tax_items, $item;
1450
1451       } else {
1452         # (pkgparts really shouldn't change, right?)
1453         my $h_cust_pkg = qsearchs('h_cust_pkg', { pkgnum => $pkgnum },
1454           FS::h_cust_pkg->sql_h_searchs($date)
1455         );
1456         if ( !$h_cust_pkg ) {
1457           warn "no historical package #".$item->pkgpart."; skipped\n";
1458           next INVOICE;
1459         }
1460         my $pkgpart = $h_cust_pkg->pkgpart;
1461
1462         if ( $use_pkgloc and $h_cust_pkg->locationnum ) {
1463           # then this package already had a locationnum assigned, and that's 
1464           # the one to use for tax calculation
1465           $tax_loc{$pkgnum} = FS::cust_location->by_key($h_cust_pkg->locationnum);
1466         } else {
1467           # use the customer's bill or ship loc, which was inserted earlier
1468           $tax_loc{$pkgnum} = $default_tax_loc;
1469         }
1470
1471         if (!exists $pkgpart_taxclass{$pkgpart}) {
1472           my $h_part_pkg = qsearchs('h_part_pkg', { pkgpart => $pkgpart },
1473             FS::h_part_pkg->sql_h_searchs($date)
1474           );
1475           if ( !$h_part_pkg ) {
1476             warn "no historical package def #$pkgpart; skipped\n";
1477             next INVOICE;
1478           }
1479           $pkgpart_taxclass{$pkgpart} = $h_part_pkg->taxclass || '';
1480           $pkgpart_exempt_setup{$pkgpart} = 1 if $h_part_pkg->setuptax;
1481           $pkgpart_exempt_recur{$pkgpart} = 1 if $h_part_pkg->recurtax;
1482         }
1483         
1484         # mark any exemptions that apply
1485         if ( $pkgpart_exempt_setup{$pkgpart} ) {
1486           $item->set('exempt_setup' => 1);
1487         }
1488
1489         if ( $pkgpart_exempt_recur{$pkgpart} ) {
1490           $item->set('exempt_recur' => 1);
1491         }
1492
1493         my $taxclass = $pkgpart_taxclass{ $pkgpart };
1494
1495         $nontax_items{$taxclass} ||= [];
1496         push @{ $nontax_items{$taxclass} }, $item;
1497       }
1498     }
1499
1500     printf("%d tax items: \$%.2f\n", scalar(@tax_items), map {$_->setup} @tax_items)
1501       if @tax_items;
1502
1503     # Get any per-customer taxname exemptions that were in effect.
1504     my %exempt_cust_taxname;
1505     foreach (keys %all_tax_names) {
1506       my $h_exemption = qsearchs('h_cust_main_exemption', {
1507           'custnum' => $custnum,
1508           'taxname' => $_,
1509         },
1510         FS::h_cust_main_exemption->sql_h_searchs($date, $date)
1511       );
1512       if ($h_exemption) {
1513         $exempt_cust_taxname{ $_ } = 1;
1514       }
1515     }
1516
1517     # Use a variation on the procedure in 
1518     # FS::cust_main::Billing::_handle_taxes to identify taxes that apply 
1519     # to this bill.
1520     my @loc_keys = qw( district city county state country );
1521     my %taxdef_by_name; # by name, and then by taxclass
1522     my %est_tax; # by name, and then by taxclass
1523     my %taxable_items; # by taxnum, and then an array
1524
1525     foreach my $taxclass (keys %nontax_items) {
1526       foreach my $orig_item (@{ $nontax_items{$taxclass} }) {
1527         my $my_tax_loc = $tax_loc{ $orig_item->pkgnum };
1528         my %myhash = map { $_ => $my_tax_loc->get($pre.$_) } @loc_keys;
1529         my @elim = qw( district city county state );
1530         my @taxdefs; # because there may be several with different taxnames
1531         do {
1532           $myhash{taxclass} = $taxclass;
1533           @taxdefs = qsearch('cust_main_county', \%myhash);
1534           if ( !@taxdefs ) {
1535             $myhash{taxclass} = '';
1536             @taxdefs = qsearch('cust_main_county', \%myhash);
1537           }
1538           $myhash{ shift @elim } = '';
1539         } while scalar(@elim) and !@taxdefs;
1540
1541         foreach my $taxdef (@taxdefs) {
1542           next if $taxdef->tax == 0;
1543           $taxdef_by_name{$taxdef->taxname}{$taxdef->taxclass} = $taxdef;
1544
1545           $taxable_items{$taxdef->taxnum} ||= [];
1546           # clone the item so that taxdef-dependent changes don't
1547           # change it for other taxdefs
1548           my $item = FS::cust_bill_pkg->new({ $orig_item->hash });
1549
1550           # these flags are already set if the part_pkg declares itself exempt
1551           $item->set('exempt_setup' => 1) if $taxdef->setuptax;
1552           $item->set('exempt_recur' => 1) if $taxdef->recurtax;
1553
1554           my @new_exempt;
1555           my $taxable = $item->setup + $item->recur;
1556           # credits
1557           # h_cust_credit_bill_pkg?
1558           # NO.  Because if these exemptions HAD been created at the time of 
1559           # billing, and then a credit applied later, the exemption would 
1560           # have been adjusted by the amount of the credit.  So we adjust
1561           # the taxable amount before creating the exemption.
1562           # But don't deduct the credit from taxable, because the tax was 
1563           # calculated before the credit was applied.
1564           foreach my $f (qw(setup recur)) {
1565             my $credited = FS::Record->scalar_sql(
1566               "SELECT SUM(amount) FROM cust_credit_bill_pkg ".
1567               "WHERE billpkgnum = ? AND setuprecur = ?",
1568               $item->billpkgnum,
1569               $f
1570             );
1571             $item->set($f, $item->get($f) - $credited) if $credited;
1572           }
1573           my $existing_exempt = FS::Record->scalar_sql(
1574             "SELECT SUM(amount) FROM cust_tax_exempt_pkg WHERE ".
1575             "billpkgnum = ? AND taxnum = ?",
1576             $item->billpkgnum, $taxdef->taxnum
1577           ) || 0;
1578           $taxable -= $existing_exempt;
1579
1580           if ( $taxable and $exempt_cust ) {
1581             push @new_exempt, { exempt_cust => 'Y',  amount => $taxable };
1582             $taxable = 0;
1583           }
1584           if ( $taxable and $exempt_cust_taxname{$taxdef->taxname} ){
1585             push @new_exempt, { exempt_cust_taxname => 'Y', amount => $taxable };
1586             $taxable = 0;
1587           }
1588           if ( $taxable and $item->exempt_setup ) {
1589             push @new_exempt, { exempt_setup => 'Y', amount => $item->setup };
1590             $taxable -= $item->setup;
1591           }
1592           if ( $taxable and $item->exempt_recur ) {
1593             push @new_exempt, { exempt_recur => 'Y', amount => $item->recur };
1594             $taxable -= $item->recur;
1595           }
1596
1597           $item->set('taxable' => $taxable);
1598           push @{ $taxable_items{$taxdef->taxnum} }, $item
1599             if $taxable > 0;
1600
1601           # estimate the amount of tax (this is necessary because different
1602           # taxdefs with the same taxname may have different tax rates) 
1603           # and sum that for each taxname/taxclass combination
1604           # (in cents)
1605           $est_tax{$taxdef->taxname} ||= {};
1606           $est_tax{$taxdef->taxname}{$taxdef->taxclass} ||= 0;
1607           $est_tax{$taxdef->taxname}{$taxdef->taxclass} += 
1608             $taxable * $taxdef->tax;
1609
1610           foreach (@new_exempt) {
1611             next if $_->{amount} == 0;
1612             my $cust_tax_exempt_pkg = FS::cust_tax_exempt_pkg->new({
1613                 %$_,
1614                 billpkgnum  => $item->billpkgnum,
1615                 taxnum      => $taxdef->taxnum,
1616               });
1617             my $error = $cust_tax_exempt_pkg->insert;
1618             if ($error) {
1619               my $pkgnum = $item->pkgnum;
1620               warn "error creating tax exemption for inv$invnum pkg$pkgnum:".
1621                 "\n$error\n\n";
1622               next INVOICE;
1623             }
1624           } #foreach @new_exempt
1625         } #foreach $taxdef
1626       } #foreach $item
1627     } #foreach $taxclass
1628
1629     # Now go through the billed taxes and match them up with the line items.
1630     TAX_ITEM: foreach my $tax_item ( @tax_items )
1631     {
1632       my $taxname = $tax_item->itemdesc;
1633       $taxname = '' if $taxname eq 'Tax';
1634
1635       if ( !exists( $taxdef_by_name{$taxname} ) ) {
1636         # then we didn't find any applicable taxes with this name
1637         warn "no definition found for tax item '$taxname', custnum $custnum\n";
1638         # possibly all of these should be "next TAX_ITEM", but whole invoices
1639         # are transaction protected and we can go back and retry them.
1640         next INVOICE;
1641       }
1642       # classname => cust_main_county
1643       my %taxdef_by_class = %{ $taxdef_by_name{$taxname} };
1644
1645       # Divide the tax item among taxclasses, if necessary
1646       # classname => estimated tax amount
1647       my $this_est_tax = $est_tax{$taxname};
1648       if (!defined $this_est_tax) {
1649         warn "no taxable sales found for inv#$invnum, tax item '$taxname'.\n";
1650         next INVOICE;
1651       }
1652       my $est_total = sum(values %$this_est_tax);
1653       if ( $est_total == 0 ) {
1654         # shouldn't happen
1655         warn "estimated tax on invoice #$invnum is zero.\n";
1656         next INVOICE;
1657       }
1658
1659       my $real_tax = $tax_item->setup;
1660       printf ("Distributing \$%.2f tax:\n", $real_tax);
1661       my $cents_remaining = $real_tax * 100; # for rounding error
1662       my @tax_links; # partial CBPTL hashrefs
1663       foreach my $taxclass (keys %taxdef_by_class) {
1664         my $taxdef = $taxdef_by_class{$taxclass};
1665         # these items already have "taxable" set to their charge amount
1666         # after applying any credits or exemptions
1667         my @items = @{ $taxable_items{$taxdef->taxnum} };
1668         my $subtotal = sum(map {$_->get('taxable')} @items);
1669         printf("\t$taxclass: %.2f\n", $this_est_tax->{$taxclass}/$est_total);
1670
1671         foreach my $nontax (@items) {
1672           my $my_tax_loc = $tax_loc{ $nontax->pkgnum };
1673           my $part = int($real_tax
1674                             # class allocation
1675                          * ($this_est_tax->{$taxclass}/$est_total) 
1676                             # item allocation
1677                          * ($nontax->get('taxable'))/$subtotal
1678                             # convert to cents
1679                          * 100
1680                        );
1681           $cents_remaining -= $part;
1682           push @tax_links, {
1683             taxnum      => $taxdef->taxnum,
1684             pkgnum      => $nontax->pkgnum,
1685             locationnum => $my_tax_loc->locationnum,
1686             billpkgnum  => $nontax->billpkgnum,
1687             cents       => $part,
1688           };
1689         } #foreach $nontax
1690       } #foreach $taxclass
1691       # Distribute any leftover tax round-robin style, one cent at a time.
1692       my $i = 0;
1693       my $nlinks = scalar(@tax_links);
1694       if ( $nlinks ) {
1695         # ensure that it really is an integer
1696         $cents_remaining = sprintf('%.0f', $cents_remaining);
1697         while ($cents_remaining > 0) {
1698           $tax_links[$i % $nlinks]->{cents} += 1;
1699           $cents_remaining--;
1700           $i++;
1701         }
1702       } else {
1703         warn "Can't create tax links--no taxable items found.\n";
1704         next INVOICE;
1705       }
1706
1707       # Gather credit/payment applications so that we can link them
1708       # appropriately.
1709       my @unlinked = (
1710         qsearch( 'cust_credit_bill_pkg',
1711           { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1712         ),
1713         qsearch( 'cust_bill_pay_pkg',
1714           { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1715         )
1716       );
1717
1718       # grab the first one
1719       my $this_unlinked = shift @unlinked;
1720       my $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1721
1722       # Create tax links (yay!)
1723       printf("Creating %d tax links.\n",scalar(@tax_links));
1724       foreach (@tax_links) {
1725         my $link = FS::cust_bill_pkg_tax_location->new({
1726             billpkgnum  => $tax_item->billpkgnum,
1727             taxtype     => 'FS::cust_main_county',
1728             locationnum => $_->{locationnum},
1729             taxnum      => $_->{taxnum},
1730             pkgnum      => $_->{pkgnum},
1731             amount      => sprintf('%.2f', $_->{cents} / 100),
1732             taxable_billpkgnum => $_->{billpkgnum},
1733         });
1734         my $error = $link->insert;
1735         if ( $error ) {
1736           warn "Can't create tax link for inv#$invnum: $error\n";
1737           next INVOICE;
1738         }
1739
1740         my $link_cents = $_->{cents};
1741         # update/create subitem links
1742         #
1743         # If $this_unlinked is undef, then we've allocated all of the
1744         # credit/payment applications to the tax item.  If $link_cents is 0,
1745         # then we've applied credits/payments to all of this package fraction,
1746         # so go on to the next.
1747         while ($this_unlinked and $link_cents) {
1748           # apply as much as possible of $link_amount to this credit/payment
1749           # link
1750           my $apply_cents = min($link_cents, $unlinked_cents);
1751           $link_cents -= $apply_cents;
1752           $unlinked_cents -= $apply_cents;
1753           # $link_cents or $unlinked_cents or both are now zero
1754           $this_unlinked->set('amount' => sprintf('%.2f',$apply_cents/100));
1755           $this_unlinked->set('billpkgtaxlocationnum' => $link->billpkgtaxlocationnum);
1756           my $pkey = $this_unlinked->primary_key; #creditbillpkgnum or billpaypkgnum
1757           if ( $this_unlinked->$pkey ) {
1758             # then it's an existing link--replace it
1759             $error = $this_unlinked->replace;
1760           } else {
1761             $this_unlinked->insert;
1762           }
1763           # what do we do with errors at this stage?
1764           if ( $error ) {
1765             warn "Error creating tax application link: $error\n";
1766             next INVOICE; # for lack of a better idea
1767           }
1768           
1769           if ( $unlinked_cents == 0 ) {
1770             # then we've allocated all of this payment/credit application, 
1771             # so grab the next one
1772             $this_unlinked = shift @unlinked;
1773             $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1774           } elsif ( $link_cents == 0 ) {
1775             # then we've covered all of this package tax fraction, so split
1776             # off a new application from this one
1777             $this_unlinked = $this_unlinked->new({
1778                 $this_unlinked->hash,
1779                 $pkey     => '',
1780             });
1781             # $unlinked_cents is still what it is
1782           }
1783
1784         } #while $this_unlinked and $link_cents
1785       } #foreach (@tax_links)
1786     } #foreach $tax_item
1787
1788     $dbh->commit if $commit_each_invoice and $oldAutoCommit;
1789     $committed = 1;
1790
1791   } #foreach $invnum
1792   continue {
1793     if (!$committed) {
1794       $dbh->rollback if $oldAutoCommit;
1795       die "Upgrade halted.\n" unless $commit_each_invoice;
1796     }
1797   }
1798
1799   $dbh->commit if $oldAutoCommit and !$commit_each_invoice;
1800   '';
1801 }
1802
1803 sub _upgrade_data {
1804   # Create a queue job to run upgrade_tax_location from January 1, 2012 to 
1805   # the present date.
1806   eval {
1807     use FS::queue;
1808     use Date::Parse 'str2time';
1809   };
1810   my $class = shift;
1811   my $upgrade = 'tax_location_2012';
1812   return if FS::upgrade_journal->is_done($upgrade);
1813   my $job = FS::queue->new({
1814       'job' => 'FS::cust_bill_pkg::upgrade_tax_location'
1815   });
1816   # call it kind of like a class method, not that it matters much
1817   $job->insert($class, 's' => str2time('2012-01-01'));
1818   # if there's a customer location upgrade queued also, wait for it to 
1819   # finish
1820   my $location_job = qsearchs('queue', {
1821       job => 'FS::cust_main::Location::process_upgrade_location'
1822     });
1823   if ( $location_job ) {
1824     $job->depend_insert($location_job->jobnum);
1825   }
1826   # Then mark the upgrade as done, so that we don't queue the job twice
1827   # and somehow run two of them concurrently.
1828   FS::upgrade_journal->set_done($upgrade);
1829   # This upgrade now does the job of assigning taxable_billpkgnums to 
1830   # cust_bill_pkg_tax_location, so set that task done also.
1831   FS::upgrade_journal->set_done('tax_location_taxable_billpkgnum');
1832 }
1833
1834 =back
1835
1836 =head1 BUGS
1837
1838 setup and recur shouldn't be separate fields.  There should be one "amount"
1839 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1840
1841 A line item with both should really be two separate records (preserving
1842 sdate and edate for setup fees for recurring packages - that information may
1843 be valuable later).  Invoice generation (cust_main::bill), invoice printing
1844 (cust_bill), tax reports (report_tax.cgi) and line item reports 
1845 (cust_bill_pkg.cgi) would need to be updated.
1846
1847 owed_setup and owed_recur could then be repaced by just owed, and
1848 cust_bill::open_cust_bill_pkg and
1849 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1850
1851 The upgrade procedure is pretty sketchy.
1852
1853 =head1 SEE ALSO
1854
1855 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1856 from the base documentation.
1857
1858 =cut
1859
1860 1;
1861