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