quotations + tax refactor, part 1
[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     # maybe should show quantity/unit discount?
838   };
839   foreach my $pkg_discount (@pkg_discounts) {
840     push @ext, $pkg_discount->description;
841     $d->{amount} -= $pkg_discount->amount;
842   } 
843   $d->{amount} *= $self->quantity || 1;
844   
845   return $d;
846 }
847
848 =item set_display OPTION => VALUE ...
849
850 A helper method for I<insert>, populates the pseudo-field B<display> with
851 appropriate FS::cust_bill_pkg_display objects.
852
853 Options are passed as a list of name/value pairs.  Options are:
854
855 part_pkg: FS::part_pkg object from this line item's package.
856
857 real_pkgpart: if this line item comes from a bundled package, the pkgpart 
858 of the owning package.  Otherwise the same as the part_pkg's pkgpart above.
859
860 =cut
861
862 sub set_display {
863   my( $self, %opt ) = @_;
864   my $part_pkg = $opt{'part_pkg'};
865   my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
866
867   my $conf = new FS::Conf;
868
869   # whether to break this down into setup/recur/usage
870   my $separate = $conf->exists('separate_usage');
871
872   my $usage_mandate =            $part_pkg->option('usage_mandate', 'Hush!')
873                     || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
874
875   # or use the category from $opt{'part_pkg'} if its not bundled?
876   my $categoryname = $cust_pkg->part_pkg->categoryname;
877
878   # if we don't have to separate setup/recur/usage, or put this in a 
879   # package-specific section, or display a usage summary, then don't 
880   # even create one of these.  The item will just display in the unnamed
881   # section as a single line plus details.
882   return $self->set('display', [])
883     unless $separate || $categoryname || $usage_mandate;
884   
885   my @display = ();
886
887   my %hash = ( 'section' => $categoryname );
888
889   # whether to put usage details in a separate section, and if so, which one
890   my $usage_section =            $part_pkg->option('usage_section', 'Hush!')
891                     || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
892
893   # whether to show a usage summary line (total usage charges, no details)
894   my $summary =            $part_pkg->option('summarize_usage', 'Hush!')
895               || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
896
897   if ( $separate ) {
898     # create lines for setup and (non-usage) recur, in the main section
899     push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
900     push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
901   } else {
902     # display everything in a single line
903     push @display, new FS::cust_bill_pkg_display
904                      { type => '',
905                        %hash,
906                        # and if usage_mandate is enabled, hide details
907                        # (this only works on multisection invoices...)
908                        ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
909                      };
910   }
911
912   if ($separate && $usage_section && $summary) {
913     # create a line for the usage summary in the main section
914     push @display, new FS::cust_bill_pkg_display { type    => 'U',
915                                                    summary => 'Y',
916                                                    %hash,
917                                                  };
918   }
919
920   if ($usage_mandate || ($usage_section && $summary) ) {
921     $hash{post_total} = 'Y';
922   }
923
924   if ($separate || $usage_mandate) {
925     # show call details for this line item in the usage section.
926     # if usage_mandate is on, this will display below the section subtotal.
927     # this also happens if usage is in a separate section and there's a 
928     # summary in the main section, though I'm not sure why.
929     $hash{section} = $usage_section if $usage_section;
930     push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
931   }
932
933   $self->set('display', \@display);
934
935 }
936
937 =item disintegrate
938
939 Returns a hash: keys are "setup", "recur" or usage classnum, values are
940 FS::cust_bill_pkg objects, each with no more than a single class (setup or
941 recur) of charge.
942
943 =cut
944
945 sub disintegrate {
946   my $self = shift;
947   # XXX this goes away with cust_bill_pkg refactor
948   # or at least I wish it would, but it turns out to be harder than
949   # that.
950
951   #my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash }; # wha huh?
952   my %cust_bill_pkg = ();
953
954   my $usage_total;
955   foreach my $classnum ($self->usage_classes) {
956     my $amount = $self->usage($classnum);
957     next if $amount == 0; # though if so we shouldn't be here
958     my $usage_item = FS::cust_bill_pkg->new({
959         $self->hash,
960         'setup'     => 0,
961         'recur'     => $amount,
962         'taxclass'  => $classnum,
963         'inherit'   => $self
964     });
965     $cust_bill_pkg{$classnum} = $usage_item;
966     $usage_total += $amount;
967   }
968
969   foreach (qw(setup recur)) {
970     next if ($self->get($_) == 0);
971     my $item = FS::cust_bill_pkg->new({
972         $self->hash,
973         'setup'     => 0,
974         'recur'     => 0,
975         'taxclass'  => $_,
976         'inherit'   => $self,
977     });
978     $item->set($_, $self->get($_));
979     $cust_bill_pkg{$_} = $item;
980   }
981
982   if ($usage_total) {
983     $cust_bill_pkg{recur}->set('recur',
984       sprintf('%.2f', $cust_bill_pkg{recur}->get('recur') - $usage_total)
985     );
986   }
987
988   %cust_bill_pkg;
989 }
990
991 =item usage CLASSNUM
992
993 Returns the amount of the charge associated with usage class CLASSNUM if
994 CLASSNUM is defined.  Otherwise returns the total charge associated with
995 usage.
996   
997 =cut
998
999 sub usage {
1000   my( $self, $classnum ) = @_;
1001   $self->regularize_details;
1002
1003   if ( $self->get('details') ) {
1004
1005     return sum( 0, 
1006       map { $_->amount || 0 }
1007       grep { !defined($classnum) or $classnum eq $_->classnum }
1008       @{ $self->get('details') }
1009     );
1010
1011   } else {
1012
1013     my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
1014               ' WHERE billpkgnum = '. $self->billpkgnum;
1015     if (defined $classnum) {
1016       if ($classnum =~ /^(\d+)$/) {
1017         $sql .= " AND classnum = $1";
1018       } elsif ($classnum eq '') {
1019         $sql .= " AND classnum IS NULL";
1020       }
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_tax_Xlocation
1067
1068 Returns the list of associated cust_bill_pkg_tax_location and/or
1069 cust_bill_pkg_tax_rate_location objects
1070
1071 =cut
1072
1073 sub cust_bill_pkg_tax_Xlocation {
1074   my $self = shift;
1075
1076   my %hash = ( 'billpkgnum' => $self->billpkgnum );
1077
1078   (
1079     qsearch ( 'cust_bill_pkg_tax_location', { %hash  } ),
1080     qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
1081   );
1082
1083 }
1084
1085 =item recur_show_zero
1086
1087 =cut
1088
1089 sub recur_show_zero { shift->_X_show_zero('recur'); }
1090 sub setup_show_zero { shift->_X_show_zero('setup'); }
1091
1092 sub _X_show_zero {
1093   my( $self, $what ) = @_;
1094
1095   return 0 unless $self->$what() == 0 && $self->pkgnum;
1096
1097   $self->cust_pkg->_X_show_zero($what);
1098 }
1099
1100 =item credited [ BEFORE, AFTER, OPTIONS ]
1101
1102 Returns the sum of credits applied to this item.  Arguments are the same as
1103 owed_sql/paid_sql/credited_sql.
1104
1105 =cut
1106
1107 sub credited {
1108   my $self = shift;
1109   $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
1110 }
1111
1112 =item tax_locationnum
1113
1114 Returns the L<FS::cust_location> number that this line item is in for tax
1115 purposes.  For package sales, it's the package tax location; for fees, 
1116 it's the customer's default service location.
1117
1118 =cut
1119
1120 sub tax_locationnum {
1121   my $self = shift;
1122   if ( $self->pkgnum ) { # normal sales
1123     return $self->cust_pkg->tax_locationnum;
1124   } elsif ( $self->feepart ) { # fees
1125     return $self->cust_bill->cust_main->ship_locationnum;
1126   } else { # taxes
1127     return '';
1128   }
1129 }
1130
1131 sub tax_location {
1132   my $self = shift;
1133   if ( $self->pkgnum ) { # normal sales
1134     return $self->cust_pkg->tax_location;
1135   } elsif ( $self->feepart ) { # fees
1136     return $self->cust_bill->cust_main->ship_location;
1137   } else { # taxes
1138     return;
1139   }
1140 }
1141
1142 =back
1143
1144 =head1 CLASS METHODS
1145
1146 =over 4
1147
1148 =item usage_sql
1149
1150 Returns an SQL expression for the total usage charges in details on
1151 an item.
1152
1153 =cut
1154
1155 my $usage_sql =
1156   '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0) 
1157     FROM cust_bill_pkg_detail 
1158     WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
1159
1160 sub usage_sql { $usage_sql }
1161
1162 # this makes owed_sql, etc. much more concise
1163 sub charged_sql {
1164   my ($class, $start, $end, %opt) = @_;
1165   my $setuprecur = $opt{setuprecur} || '';
1166   my $charged = 
1167     $setuprecur =~ /^s/ ? 'cust_bill_pkg.setup' :
1168     $setuprecur =~ /^r/ ? 'cust_bill_pkg.recur' :
1169     'cust_bill_pkg.setup + cust_bill_pkg.recur';
1170
1171   if ($opt{no_usage} and $charged =~ /recur/) { 
1172     $charged = "$charged - $usage_sql"
1173   }
1174
1175   $charged;
1176 }
1177
1178
1179 =item owed_sql [ BEFORE, AFTER, OPTIONS ]
1180
1181 Returns an SQL expression for the amount owed.  BEFORE and AFTER specify
1182 a date window.  OPTIONS may include 'no_usage' (excludes usage charges)
1183 and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
1184
1185 =cut
1186
1187 sub owed_sql {
1188   my $class = shift;
1189   '(' . $class->charged_sql(@_) . 
1190   ' - ' . $class->paid_sql(@_) .
1191   ' - ' . $class->credited_sql(@_) . ')'
1192 }
1193
1194 =item paid_sql [ BEFORE, AFTER, OPTIONS ]
1195
1196 Returns an SQL expression for the sum of payments applied to this item.
1197
1198 =cut
1199
1200 sub paid_sql {
1201   my ($class, $start, $end, %opt) = @_;
1202   my $s = $start ? "AND cust_pay._date <= $start" : '';
1203   my $e = $end   ? "AND cust_pay._date >  $end"   : '';
1204   my $setuprecur = $opt{setuprecur} || '';
1205   $setuprecur = 'setup' if $setuprecur =~ /^s/;
1206   $setuprecur = 'recur' if $setuprecur =~ /^r/;
1207   $setuprecur &&= "AND setuprecur = '$setuprecur'";
1208
1209   my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
1210      FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
1211                             JOIN cust_pay      USING (paynum)
1212      WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1213            $s $e $setuprecur )";
1214
1215   if ( $opt{no_usage} ) {
1216     # cap the amount paid at the sum of non-usage charges, 
1217     # minus the amount credited against non-usage charges
1218     "LEAST($paid, ". 
1219       $class->charged_sql($start, $end, %opt) . ' - ' .
1220       $class->credited_sql($start, $end, %opt).')';
1221   }
1222   else {
1223     $paid;
1224   }
1225
1226 }
1227
1228 sub credited_sql {
1229   my ($class, $start, $end, %opt) = @_;
1230   my $s = $start ? "AND cust_credit._date <= $start" : '';
1231   my $e = $end   ? "AND cust_credit._date >  $end"   : '';
1232   my $setuprecur = $opt{setuprecur} || '';
1233   $setuprecur = 'setup' if $setuprecur =~ /^s/;
1234   $setuprecur = 'recur' if $setuprecur =~ /^r/;
1235   $setuprecur &&= "AND setuprecur = '$setuprecur'";
1236
1237   my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
1238      FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
1239                                JOIN cust_credit      USING (crednum)
1240      WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1241            $s $e $setuprecur )";
1242
1243   if ( $opt{no_usage} ) {
1244     # cap the amount credited at the sum of non-usage charges
1245     "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
1246   }
1247   else {
1248     $credited;
1249   }
1250
1251 }
1252
1253 sub upgrade_tax_location {
1254   # For taxes that were calculated/invoiced before cust_location refactoring
1255   # (May-June 2012), there are no cust_bill_pkg_tax_location records unless
1256   # they were calculated on a package-location basis.  Create them here, 
1257   # along with any necessary cust_location records and any tax exemption 
1258   # records.
1259
1260   my ($class, %opt) = @_;
1261   # %opt may include 's' and 'e': start and end date ranges
1262   # and 'X': abort on any error, instead of just rolling back changes to 
1263   # that invoice
1264   my $dbh = dbh;
1265   my $oldAutoCommit = $FS::UID::AutoCommit;
1266   local $FS::UID::AutoCommit = 0;
1267
1268   eval {
1269     use FS::h_cust_main;
1270     use FS::h_cust_bill;
1271     use FS::h_part_pkg;
1272     use FS::h_cust_main_exemption;
1273   };
1274
1275   local $FS::cust_location::import = 1;
1276
1277   my $conf = FS::Conf->new; # h_conf?
1278   return if $conf->exists('enable_taxproducts'); #don't touch this case
1279   my $use_ship = $conf->exists('tax-ship_address');
1280   my $use_pkgloc = $conf->exists('tax-pkg_address');
1281
1282   my $date_where = '';
1283   if ($opt{s}) {
1284     $date_where .= " AND cust_bill._date >= $opt{s}";
1285   }
1286   if ($opt{e}) {
1287     $date_where .= " AND cust_bill._date < $opt{e}";
1288   }
1289
1290   my $commit_each_invoice = 1 unless $opt{X};
1291
1292   # if an invoice has either of these kinds of objects, then it doesn't
1293   # need to be upgraded...probably
1294   my $sub_has_tax_link = 'SELECT 1 FROM cust_bill_pkg_tax_location'.
1295   ' JOIN cust_bill_pkg USING (billpkgnum)'.
1296   ' WHERE cust_bill_pkg.invnum = cust_bill.invnum';
1297   my $sub_has_exempt = 'SELECT 1 FROM cust_tax_exempt_pkg'.
1298   ' JOIN cust_bill_pkg USING (billpkgnum)'.
1299   ' WHERE cust_bill_pkg.invnum = cust_bill.invnum'.
1300   ' AND exempt_monthly IS NULL';
1301
1302   my %all_tax_names = (
1303     '' => 1,
1304     'Tax' => 1,
1305     map { $_->taxname => 1 }
1306       qsearch('h_cust_main_county', { taxname => { op => '!=', value => '' }})
1307   );
1308
1309   my $search = FS::Cursor->new({
1310       table => 'cust_bill',
1311       hashref => {},
1312       extra_sql => "WHERE NOT EXISTS($sub_has_tax_link) ".
1313                    "AND NOT EXISTS($sub_has_exempt) ".
1314                     $date_where,
1315   });
1316
1317 #print "Processing ".scalar(@invnums)." invoices...\n";
1318
1319   my $committed;
1320   INVOICE:
1321   while (my $cust_bill = $search->fetch) {
1322     my $invnum = $cust_bill->invnum;
1323     $committed = 0;
1324     print STDERR "Invoice #$invnum\n";
1325     my $pre = '';
1326     my %pkgpart_taxclass; # pkgpart => taxclass
1327     my %pkgpart_exempt_setup;
1328     my %pkgpart_exempt_recur;
1329     my $h_cust_bill = qsearchs('h_cust_bill',
1330       { invnum => $invnum,
1331         history_action => 'insert' });
1332     if (!$h_cust_bill) {
1333       warn "no insert record for invoice $invnum; skipped\n";
1334       #$date = $cust_bill->_date as a fallback?
1335       # We're trying to avoid using non-real dates (-d/-y invoice dates)
1336       # when looking up history records in other tables.
1337       next INVOICE;
1338     }
1339     my $custnum = $h_cust_bill->custnum;
1340
1341     # Determine the address corresponding to this tax region.
1342     # It's either the bill or ship address of the customer as of the
1343     # invoice date-of-insertion.  (Not necessarily the invoice date.)
1344     my $date = $h_cust_bill->history_date;
1345     local($FS::Record::qsearch_qualify_columns) = 0;
1346     my $h_cust_main = qsearchs('h_cust_main',
1347         { custnum   => $custnum },
1348         FS::h_cust_main->sql_h_searchs($date)
1349       );
1350     if (!$h_cust_main ) {
1351       warn "no historical address for cust#".$h_cust_bill->custnum."; skipped\n";
1352       next INVOICE;
1353       # fallback to current $cust_main?  sounds dangerous.
1354     }
1355
1356     # This is a historical customer record, so it has a historical address.
1357     # If there's no cust_location matching this custnum and address (there 
1358     # probably isn't), create one.
1359     my %tax_loc; # keys are pkgnums, values are cust_location objects
1360     my $default_tax_loc;
1361     if ( $h_cust_main->bill_locationnum ) {
1362       # the location has already been upgraded
1363       if ($use_ship) {
1364         $default_tax_loc = $h_cust_main->ship_location;
1365       } else {
1366         $default_tax_loc = $h_cust_main->bill_location;
1367       }
1368     } else {
1369       $pre = 'ship_' if $use_ship and length($h_cust_main->get('ship_last'));
1370       my %hash = map { $_ => $h_cust_main->get($pre.$_) }
1371                     FS::cust_main->location_fields;
1372       # not really needed for this, and often result in duplicate locations
1373       delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
1374
1375       $hash{custnum} = $h_cust_main->custnum;
1376       $default_tax_loc = FS::cust_location->new(\%hash);
1377       my $error = $default_tax_loc->find_or_insert || $default_tax_loc->disable_if_unused;
1378       if ( $error ) {
1379         warn "couldn't create historical location record for cust#".
1380         $h_cust_main->custnum.": $error\n";
1381         next INVOICE;
1382       }
1383     }
1384     my $exempt_cust;
1385     $exempt_cust = 1 if $h_cust_main->tax;
1386
1387     # classify line items
1388     my @tax_items;
1389     my %nontax_items; # taxclass => array of cust_bill_pkg
1390     foreach my $item ($h_cust_bill->cust_bill_pkg) {
1391       my $pkgnum = $item->pkgnum;
1392
1393       if ( $pkgnum == 0 ) {
1394
1395         push @tax_items, $item;
1396
1397       } else {
1398         # (pkgparts really shouldn't change, right?)
1399         local($FS::Record::qsearch_qualify_columns) = 0;
1400         my $h_cust_pkg = qsearchs('h_cust_pkg', { pkgnum => $pkgnum },
1401           FS::h_cust_pkg->sql_h_searchs($date)
1402         );
1403         if ( !$h_cust_pkg ) {
1404           warn "no historical package #".$item->pkgpart."; skipped\n";
1405           next INVOICE;
1406         }
1407         my $pkgpart = $h_cust_pkg->pkgpart;
1408
1409         if ( $use_pkgloc and $h_cust_pkg->locationnum ) {
1410           # then this package already had a locationnum assigned, and that's 
1411           # the one to use for tax calculation
1412           $tax_loc{$pkgnum} = FS::cust_location->by_key($h_cust_pkg->locationnum);
1413         } else {
1414           # use the customer's bill or ship loc, which was inserted earlier
1415           $tax_loc{$pkgnum} = $default_tax_loc;
1416         }
1417
1418         if (!exists $pkgpart_taxclass{$pkgpart}) {
1419           local($FS::Record::qsearch_qualify_columns) = 0;
1420           my $h_part_pkg = qsearchs('h_part_pkg', { pkgpart => $pkgpart },
1421             FS::h_part_pkg->sql_h_searchs($date)
1422           );
1423           if ( !$h_part_pkg ) {
1424             warn "no historical package def #$pkgpart; skipped\n";
1425             next INVOICE;
1426           }
1427           $pkgpart_taxclass{$pkgpart} = $h_part_pkg->taxclass || '';
1428           $pkgpart_exempt_setup{$pkgpart} = 1 if $h_part_pkg->setuptax;
1429           $pkgpart_exempt_recur{$pkgpart} = 1 if $h_part_pkg->recurtax;
1430         }
1431         
1432         # mark any exemptions that apply
1433         if ( $pkgpart_exempt_setup{$pkgpart} ) {
1434           $item->set('exempt_setup' => 1);
1435         }
1436
1437         if ( $pkgpart_exempt_recur{$pkgpart} ) {
1438           $item->set('exempt_recur' => 1);
1439         }
1440
1441         my $taxclass = $pkgpart_taxclass{ $pkgpart };
1442
1443         $nontax_items{$taxclass} ||= [];
1444         push @{ $nontax_items{$taxclass} }, $item;
1445       }
1446     }
1447
1448     printf("%d tax items: \$%.2f\n", scalar(@tax_items), map {$_->setup} @tax_items)
1449       if @tax_items;
1450
1451     # Get any per-customer taxname exemptions that were in effect.
1452     my %exempt_cust_taxname;
1453     foreach (keys %all_tax_names) {
1454      local($FS::Record::qsearch_qualify_columns) = 0;
1455       my $h_exemption = qsearchs('h_cust_main_exemption', {
1456           'custnum' => $custnum,
1457           'taxname' => $_,
1458         },
1459         FS::h_cust_main_exemption->sql_h_searchs($date, $date)
1460       );
1461       if ($h_exemption) {
1462         $exempt_cust_taxname{ $_ } = 1;
1463       }
1464     }
1465
1466     # Use a variation on the procedure in 
1467     # FS::cust_main::Billing::_handle_taxes to identify taxes that apply 
1468     # to this bill.
1469     my @loc_keys = qw( district city county state country );
1470     my %taxdef_by_name; # by name, and then by taxclass
1471     my %est_tax; # by name, and then by taxclass
1472     my %taxable_items; # by taxnum, and then an array
1473
1474     foreach my $taxclass (keys %nontax_items) {
1475       foreach my $orig_item (@{ $nontax_items{$taxclass} }) {
1476         my $my_tax_loc = $tax_loc{ $orig_item->pkgnum };
1477         my %myhash = map { $_ => $my_tax_loc->get($pre.$_) } @loc_keys;
1478         my @elim = qw( district city county state );
1479         my @taxdefs; # because there may be several with different taxnames
1480         do {
1481           $myhash{taxclass} = $taxclass;
1482           @taxdefs = qsearch('cust_main_county', \%myhash);
1483           if ( !@taxdefs ) {
1484             $myhash{taxclass} = '';
1485             @taxdefs = qsearch('cust_main_county', \%myhash);
1486           }
1487           $myhash{ shift @elim } = '';
1488         } while scalar(@elim) and !@taxdefs;
1489
1490         foreach my $taxdef (@taxdefs) {
1491           next if $taxdef->tax == 0;
1492           $taxdef_by_name{$taxdef->taxname}{$taxdef->taxclass} = $taxdef;
1493
1494           $taxable_items{$taxdef->taxnum} ||= [];
1495           # clone the item so that taxdef-dependent changes don't
1496           # change it for other taxdefs
1497           my $item = FS::cust_bill_pkg->new({ $orig_item->hash });
1498
1499           # these flags are already set if the part_pkg declares itself exempt
1500           $item->set('exempt_setup' => 1) if $taxdef->setuptax;
1501           $item->set('exempt_recur' => 1) if $taxdef->recurtax;
1502
1503           my @new_exempt;
1504           my $taxable = $item->setup + $item->recur;
1505           # credits
1506           # h_cust_credit_bill_pkg?
1507           # NO.  Because if these exemptions HAD been created at the time of 
1508           # billing, and then a credit applied later, the exemption would 
1509           # have been adjusted by the amount of the credit.  So we adjust
1510           # the taxable amount before creating the exemption.
1511           # But don't deduct the credit from taxable, because the tax was 
1512           # calculated before the credit was applied.
1513           foreach my $f (qw(setup recur)) {
1514             my $credited = FS::Record->scalar_sql(
1515               "SELECT SUM(amount) FROM cust_credit_bill_pkg ".
1516               "WHERE billpkgnum = ? AND setuprecur = ?",
1517               $item->billpkgnum,
1518               $f
1519             );
1520             $item->set($f, $item->get($f) - $credited) if $credited;
1521           }
1522           my $existing_exempt = FS::Record->scalar_sql(
1523             "SELECT SUM(amount) FROM cust_tax_exempt_pkg WHERE ".
1524             "billpkgnum = ? AND taxnum = ?",
1525             $item->billpkgnum, $taxdef->taxnum
1526           ) || 0;
1527           $taxable -= $existing_exempt;
1528
1529           if ( $taxable and $exempt_cust ) {
1530             push @new_exempt, { exempt_cust => 'Y',  amount => $taxable };
1531             $taxable = 0;
1532           }
1533           if ( $taxable and $exempt_cust_taxname{$taxdef->taxname} ){
1534             push @new_exempt, { exempt_cust_taxname => 'Y', amount => $taxable };
1535             $taxable = 0;
1536           }
1537           if ( $taxable and $item->exempt_setup ) {
1538             push @new_exempt, { exempt_setup => 'Y', amount => $item->setup };
1539             $taxable -= $item->setup;
1540           }
1541           if ( $taxable and $item->exempt_recur ) {
1542             push @new_exempt, { exempt_recur => 'Y', amount => $item->recur };
1543             $taxable -= $item->recur;
1544           }
1545
1546           $item->set('taxable' => $taxable);
1547           push @{ $taxable_items{$taxdef->taxnum} }, $item
1548             if $taxable > 0;
1549
1550           # estimate the amount of tax (this is necessary because different
1551           # taxdefs with the same taxname may have different tax rates) 
1552           # and sum that for each taxname/taxclass combination
1553           # (in cents)
1554           $est_tax{$taxdef->taxname} ||= {};
1555           $est_tax{$taxdef->taxname}{$taxdef->taxclass} ||= 0;
1556           $est_tax{$taxdef->taxname}{$taxdef->taxclass} += 
1557             $taxable * $taxdef->tax;
1558
1559           foreach (@new_exempt) {
1560             next if $_->{amount} == 0;
1561             my $cust_tax_exempt_pkg = FS::cust_tax_exempt_pkg->new({
1562                 %$_,
1563                 billpkgnum  => $item->billpkgnum,
1564                 taxnum      => $taxdef->taxnum,
1565               });
1566             my $error = $cust_tax_exempt_pkg->insert;
1567             if ($error) {
1568               my $pkgnum = $item->pkgnum;
1569               warn "error creating tax exemption for inv$invnum pkg$pkgnum:".
1570                 "\n$error\n\n";
1571               next INVOICE;
1572             }
1573           } #foreach @new_exempt
1574         } #foreach $taxdef
1575       } #foreach $item
1576     } #foreach $taxclass
1577
1578     # Now go through the billed taxes and match them up with the line items.
1579     TAX_ITEM: foreach my $tax_item ( @tax_items )
1580     {
1581       my $taxname = $tax_item->itemdesc;
1582       $taxname = '' if $taxname eq 'Tax';
1583
1584       if ( !exists( $taxdef_by_name{$taxname} ) ) {
1585         # then we didn't find any applicable taxes with this name
1586         warn "no definition found for tax item '$taxname', custnum $custnum\n";
1587         # possibly all of these should be "next TAX_ITEM", but whole invoices
1588         # are transaction protected and we can go back and retry them.
1589         next INVOICE;
1590       }
1591       # classname => cust_main_county
1592       my %taxdef_by_class = %{ $taxdef_by_name{$taxname} };
1593
1594       # Divide the tax item among taxclasses, if necessary
1595       # classname => estimated tax amount
1596       my $this_est_tax = $est_tax{$taxname};
1597       if (!defined $this_est_tax) {
1598         warn "no taxable sales found for inv#$invnum, tax item '$taxname'.\n";
1599         next INVOICE;
1600       }
1601       my $est_total = sum(values %$this_est_tax);
1602       if ( $est_total == 0 ) {
1603         # shouldn't happen
1604         warn "estimated tax on invoice #$invnum is zero.\n";
1605         next INVOICE;
1606       }
1607
1608       my $real_tax = $tax_item->setup;
1609       printf ("Distributing \$%.2f tax:\n", $real_tax);
1610       my $cents_remaining = $real_tax * 100; # for rounding error
1611       my @tax_links; # partial CBPTL hashrefs
1612       foreach my $taxclass (keys %taxdef_by_class) {
1613         my $taxdef = $taxdef_by_class{$taxclass};
1614         # these items already have "taxable" set to their charge amount
1615         # after applying any credits or exemptions
1616         my @items = @{ $taxable_items{$taxdef->taxnum} };
1617         my $subtotal = sum(map {$_->get('taxable')} @items);
1618         printf("\t$taxclass: %.2f\n", $this_est_tax->{$taxclass}/$est_total);
1619
1620         foreach my $nontax (@items) {
1621           my $my_tax_loc = $tax_loc{ $nontax->pkgnum };
1622           my $part = int($real_tax
1623                             # class allocation
1624                          * ($this_est_tax->{$taxclass}/$est_total) 
1625                             # item allocation
1626                          * ($nontax->get('taxable'))/$subtotal
1627                             # convert to cents
1628                          * 100
1629                        );
1630           $cents_remaining -= $part;
1631           push @tax_links, {
1632             taxnum      => $taxdef->taxnum,
1633             pkgnum      => $nontax->pkgnum,
1634             locationnum => $my_tax_loc->locationnum,
1635             billpkgnum  => $nontax->billpkgnum,
1636             cents       => $part,
1637           };
1638         } #foreach $nontax
1639       } #foreach $taxclass
1640       # Distribute any leftover tax round-robin style, one cent at a time.
1641       my $i = 0;
1642       my $nlinks = scalar(@tax_links);
1643       if ( $nlinks ) {
1644         # ensure that it really is an integer
1645         $cents_remaining = sprintf('%.0f', $cents_remaining);
1646         while ($cents_remaining > 0) {
1647           $tax_links[$i % $nlinks]->{cents} += 1;
1648           $cents_remaining--;
1649           $i++;
1650         }
1651       } else {
1652         warn "Can't create tax links--no taxable items found.\n";
1653         next INVOICE;
1654       }
1655
1656       # Gather credit/payment applications so that we can link them
1657       # appropriately.
1658       my @unlinked = (
1659         qsearch( 'cust_credit_bill_pkg',
1660           { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1661         ),
1662         qsearch( 'cust_bill_pay_pkg',
1663           { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1664         )
1665       );
1666
1667       # grab the first one
1668       my $this_unlinked = shift @unlinked;
1669       my $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1670
1671       # Create tax links (yay!)
1672       printf("Creating %d tax links.\n",scalar(@tax_links));
1673       foreach (@tax_links) {
1674         my $link = FS::cust_bill_pkg_tax_location->new({
1675             billpkgnum  => $tax_item->billpkgnum,
1676             taxtype     => 'FS::cust_main_county',
1677             locationnum => $_->{locationnum},
1678             taxnum      => $_->{taxnum},
1679             pkgnum      => $_->{pkgnum},
1680             amount      => sprintf('%.2f', $_->{cents} / 100),
1681             taxable_billpkgnum => $_->{billpkgnum},
1682         });
1683         my $error = $link->insert;
1684         if ( $error ) {
1685           warn "Can't create tax link for inv#$invnum: $error\n";
1686           next INVOICE;
1687         }
1688
1689         my $link_cents = $_->{cents};
1690         # update/create subitem links
1691         #
1692         # If $this_unlinked is undef, then we've allocated all of the
1693         # credit/payment applications to the tax item.  If $link_cents is 0,
1694         # then we've applied credits/payments to all of this package fraction,
1695         # so go on to the next.
1696         while ($this_unlinked and $link_cents) {
1697           # apply as much as possible of $link_amount to this credit/payment
1698           # link
1699           my $apply_cents = min($link_cents, $unlinked_cents);
1700           $link_cents -= $apply_cents;
1701           $unlinked_cents -= $apply_cents;
1702           # $link_cents or $unlinked_cents or both are now zero
1703           $this_unlinked->set('amount' => sprintf('%.2f',$apply_cents/100));
1704           $this_unlinked->set('billpkgtaxlocationnum' => $link->billpkgtaxlocationnum);
1705           my $pkey = $this_unlinked->primary_key; #creditbillpkgnum or billpaypkgnum
1706           if ( $this_unlinked->$pkey ) {
1707             # then it's an existing link--replace it
1708             $error = $this_unlinked->replace;
1709           } else {
1710             $this_unlinked->insert;
1711           }
1712           # what do we do with errors at this stage?
1713           if ( $error ) {
1714             warn "Error creating tax application link: $error\n";
1715             next INVOICE; # for lack of a better idea
1716           }
1717           
1718           if ( $unlinked_cents == 0 ) {
1719             # then we've allocated all of this payment/credit application, 
1720             # so grab the next one
1721             $this_unlinked = shift @unlinked;
1722             $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1723           } elsif ( $link_cents == 0 ) {
1724             # then we've covered all of this package tax fraction, so split
1725             # off a new application from this one
1726             $this_unlinked = $this_unlinked->new({
1727                 $this_unlinked->hash,
1728                 $pkey     => '',
1729             });
1730             # $unlinked_cents is still what it is
1731           }
1732
1733         } #while $this_unlinked and $link_cents
1734       } #foreach (@tax_links)
1735     } #foreach $tax_item
1736
1737     $dbh->commit if $commit_each_invoice and $oldAutoCommit;
1738     $committed = 1;
1739
1740   } #foreach $invnum
1741   continue {
1742     if (!$committed) {
1743       $dbh->rollback if $oldAutoCommit;
1744       die "Upgrade halted.\n" unless $commit_each_invoice;
1745     }
1746   }
1747
1748   $dbh->commit if $oldAutoCommit and !$commit_each_invoice;
1749   '';
1750 }
1751
1752 sub _upgrade_data {
1753   # Create a queue job to run upgrade_tax_location from January 1, 2012 to 
1754   # the present date.
1755   eval {
1756     use FS::queue;
1757     use Date::Parse 'str2time';
1758   };
1759   my $class = shift;
1760   my $upgrade = 'tax_location_2012';
1761   return if FS::upgrade_journal->is_done($upgrade);
1762   my $job = FS::queue->new({
1763       'job' => 'FS::cust_bill_pkg::upgrade_tax_location'
1764   });
1765   # call it kind of like a class method, not that it matters much
1766   $job->insert($class, 's' => str2time('2012-01-01'));
1767   # if there's a customer location upgrade queued also, wait for it to 
1768   # finish
1769   my $location_job = qsearchs('queue', {
1770       job => 'FS::cust_main::Location::process_upgrade_location'
1771     });
1772   if ( $location_job ) {
1773     $job->depend_insert($location_job->jobnum);
1774   }
1775   # Then mark the upgrade as done, so that we don't queue the job twice
1776   # and somehow run two of them concurrently.
1777   FS::upgrade_journal->set_done($upgrade);
1778   # This upgrade now does the job of assigning taxable_billpkgnums to 
1779   # cust_bill_pkg_tax_location, so set that task done also.
1780   FS::upgrade_journal->set_done('tax_location_taxable_billpkgnum');
1781 }
1782
1783 =back
1784
1785 =head1 BUGS
1786
1787 setup and recur shouldn't be separate fields.  There should be one "amount"
1788 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1789
1790 A line item with both should really be two separate records (preserving
1791 sdate and edate for setup fees for recurring packages - that information may
1792 be valuable later).  Invoice generation (cust_main::bill), invoice printing
1793 (cust_bill), tax reports (report_tax.cgi) and line item reports 
1794 (cust_bill_pkg.cgi) would need to be updated.
1795
1796 owed_setup and owed_recur could then be repaced by just owed, and
1797 cust_bill::open_cust_bill_pkg and
1798 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1799
1800 The upgrade procedure is pretty sketchy.
1801
1802 =head1 SEE ALSO
1803
1804 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1805 from the base documentation.
1806
1807 =cut
1808
1809 1;
1810