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