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