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