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