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