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