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