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