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