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