5861ee47f0b24c2678cb4e670dafb99e4eb63b0e
[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 %options = @_;
824
825   my @pkg_discounts = $self->pkg_discount;
826   return if @pkg_discounts == 0;
827   # special case: if there are old "discount details" on this line item, don't
828   # show discount line items
829   if ( FS::cust_bill_pkg_detail->count("detail LIKE 'Includes discount%' AND billpkgnum = ?", $self->billpkgnum || 0) > 0 ) {
830     return;
831   } 
832   
833   my @ext;
834   my $d = {
835     _is_discount    => 1,
836     description     => $self->mt('Discount'),
837     setup_amount    => 0,
838     recur_amount    => 0,
839     ext_description => \@ext,
840     pkgpart         => $self->pkgpart,
841     feepart         => $self->feepart,
842     # maybe should show quantity/unit discount?
843   };
844   foreach my $pkg_discount (@pkg_discounts) {
845     push @ext, $pkg_discount->description;
846     my $setuprecur = $pkg_discount->cust_pkg_discount->setuprecur;
847     $d->{$setuprecur.'_amount'} -= $pkg_discount->amount;
848   } 
849   $d->{setup_amount} *= $self->quantity || 1; # ??
850   $d->{recur_amount} *= $self->quantity || 1; # ??
851   
852   return $d;
853 }
854
855 =item set_display OPTION => VALUE ...
856
857 A helper method for I<insert>, populates the pseudo-field B<display> with
858 appropriate FS::cust_bill_pkg_display objects.
859
860 Options are passed as a list of name/value pairs.  Options are:
861
862 part_pkg: FS::part_pkg object from this line item's package.
863
864 real_pkgpart: if this line item comes from a bundled package, the pkgpart 
865 of the owning package.  Otherwise the same as the part_pkg's pkgpart above.
866
867 =cut
868
869 sub set_display {
870   my( $self, %opt ) = @_;
871   my $part_pkg = $opt{'part_pkg'};
872   my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
873
874   my $conf = new FS::Conf;
875
876   # whether to break this down into setup/recur/usage
877   my $separate = $conf->exists('separate_usage');
878
879   my $usage_mandate =            $part_pkg->option('usage_mandate', 'Hush!')
880                     || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
881
882   # or use the category from $opt{'part_pkg'} if its not bundled?
883   my $categoryname = $cust_pkg->part_pkg->categoryname;
884
885   # if we don't have to separate setup/recur/usage, or put this in a 
886   # package-specific section, or display a usage summary, then don't 
887   # even create one of these.  The item will just display in the unnamed
888   # section as a single line plus details.
889   return $self->set('display', [])
890     unless $separate || $categoryname || $usage_mandate;
891   
892   my @display = ();
893
894   my %hash = ( 'section' => $categoryname );
895
896   # whether to put usage details in a separate section, and if so, which one
897   my $usage_section =            $part_pkg->option('usage_section', 'Hush!')
898                     || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
899
900   # whether to show a usage summary line (total usage charges, no details)
901   my $summary =            $part_pkg->option('summarize_usage', 'Hush!')
902               || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
903
904   if ( $separate ) {
905     # create lines for setup and (non-usage) recur, in the main section
906     push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
907     push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
908   } else {
909     # display everything in a single line
910     push @display, new FS::cust_bill_pkg_display
911                      { type => '',
912                        %hash,
913                        # and if usage_mandate is enabled, hide details
914                        # (this only works on multisection invoices...)
915                        ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
916                      };
917   }
918
919   if ($separate && $usage_section && $summary) {
920     # create a line for the usage summary in the main section
921     push @display, new FS::cust_bill_pkg_display { type    => 'U',
922                                                    summary => 'Y',
923                                                    %hash,
924                                                  };
925   }
926
927   if ($usage_mandate || ($usage_section && $summary) ) {
928     $hash{post_total} = 'Y';
929   }
930
931   if ($separate || $usage_mandate) {
932     # show call details for this line item in the usage section.
933     # if usage_mandate is on, this will display below the section subtotal.
934     # this also happens if usage is in a separate section and there's a 
935     # summary in the main section, though I'm not sure why.
936     $hash{section} = $usage_section if $usage_section;
937     push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
938   }
939
940   $self->set('display', \@display);
941
942 }
943
944 =item disintegrate
945
946 Returns a hash: keys are "setup", "recur" or usage classnum, values are
947 FS::cust_bill_pkg objects, each with no more than a single class (setup or
948 recur) of charge.
949
950 =cut
951
952 sub disintegrate {
953   my $self = shift;
954   # XXX this goes away with cust_bill_pkg refactor
955   # or at least I wish it would, but it turns out to be harder than
956   # that.
957
958   #my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash }; # wha huh?
959   my %cust_bill_pkg = ();
960
961   my $usage_total;
962   foreach my $classnum ($self->usage_classes) {
963     my $amount = $self->usage($classnum);
964     next if $amount == 0; # though if so we shouldn't be here
965     my $usage_item = FS::cust_bill_pkg->new({
966         $self->hash,
967         'setup'     => 0,
968         'recur'     => $amount,
969         'taxclass'  => $classnum,
970         'inherit'   => $self
971     });
972     $cust_bill_pkg{$classnum} = $usage_item;
973     $usage_total += $amount;
974   }
975
976   foreach (qw(setup recur)) {
977     next if ($self->get($_) == 0);
978     my $item = FS::cust_bill_pkg->new({
979         $self->hash,
980         'setup'     => 0,
981         'recur'     => 0,
982         'taxclass'  => $_,
983         'inherit'   => $self,
984     });
985     $item->set($_, $self->get($_));
986     $cust_bill_pkg{$_} = $item;
987   }
988
989   if ($usage_total) {
990     $cust_bill_pkg{recur}->set('recur',
991       sprintf('%.2f', $cust_bill_pkg{recur}->get('recur') - $usage_total)
992     );
993   }
994
995   %cust_bill_pkg;
996 }
997
998 =item usage CLASSNUM
999
1000 Returns the amount of the charge associated with usage class CLASSNUM if
1001 CLASSNUM is defined.  Otherwise returns the total charge associated with
1002 usage.
1003   
1004 =cut
1005
1006 sub usage {
1007   my( $self, $classnum ) = @_;
1008   $self->regularize_details;
1009
1010   if ( $self->get('details') ) {
1011
1012     return sum( 0, 
1013       map { $_->amount || 0 }
1014       grep { !defined($classnum) or $classnum eq $_->classnum }
1015       @{ $self->get('details') }
1016     );
1017
1018   } else {
1019
1020     my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
1021               ' WHERE billpkgnum = '. $self->billpkgnum;
1022     if (defined $classnum) {
1023       if ($classnum =~ /^(\d+)$/) {
1024         $sql .= " AND classnum = $1";
1025       } elsif ($classnum eq '') {
1026         $sql .= " AND classnum IS NULL";
1027       }
1028     }
1029
1030     my $sth = dbh->prepare($sql) or die dbh->errstr;
1031     $sth->execute or die $sth->errstr;
1032
1033     return $sth->fetchrow_arrayref->[0] || 0;
1034
1035   }
1036
1037 }
1038
1039 =item usage_classes
1040
1041 Returns a list of usage classnums associated with this invoice line's
1042 details.
1043   
1044 =cut
1045
1046 sub usage_classes {
1047   my( $self ) = @_;
1048   $self->regularize_details;
1049
1050   if ( $self->get('details') ) {
1051
1052     my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
1053     keys %seen;
1054
1055   } else {
1056
1057     map { $_->classnum }
1058         qsearch({ table   => 'cust_bill_pkg_detail',
1059                   hashref => { billpkgnum => $self->billpkgnum },
1060                   select  => 'DISTINCT classnum',
1061                });
1062
1063   }
1064
1065 }
1066
1067 sub cust_tax_exempt_pkg {
1068   my ( $self ) = @_;
1069
1070   my $array = $self->{Hash}->{cust_tax_exempt_pkg} ||= [];
1071 }
1072
1073 =item cust_bill_pkg_tax_Xlocation
1074
1075 Returns the list of associated cust_bill_pkg_tax_location and/or
1076 cust_bill_pkg_tax_rate_location objects
1077
1078 =cut
1079
1080 sub cust_bill_pkg_tax_Xlocation {
1081   my $self = shift;
1082
1083   my %hash = ( 'billpkgnum' => $self->billpkgnum );
1084
1085   (
1086     qsearch ( 'cust_bill_pkg_tax_location', { %hash  } ),
1087     qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
1088   );
1089
1090 }
1091
1092 =item recur_show_zero
1093
1094 =cut
1095
1096 sub recur_show_zero { shift->_X_show_zero('recur'); }
1097 sub setup_show_zero { shift->_X_show_zero('setup'); }
1098
1099 sub _X_show_zero {
1100   my( $self, $what ) = @_;
1101
1102   return 0 unless $self->$what() == 0 && $self->pkgnum;
1103
1104   $self->cust_pkg->_X_show_zero($what);
1105 }
1106
1107 =item credited [ BEFORE, AFTER, OPTIONS ]
1108
1109 Returns the sum of credits applied to this item.  Arguments are the same as
1110 owed_sql/paid_sql/credited_sql.
1111
1112 =cut
1113
1114 sub credited {
1115   my $self = shift;
1116   $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
1117 }
1118
1119 =item tax_locationnum
1120
1121 Returns the L<FS::cust_location> number that this line item is in for tax
1122 purposes.  For package sales, it's the package tax location; for fees, 
1123 it's the customer's default service location.
1124
1125 =cut
1126
1127 sub tax_locationnum {
1128   my $self = shift;
1129   if ( $self->pkgnum ) { # normal sales
1130     return $self->cust_pkg->tax_locationnum;
1131   } elsif ( $self->feepart ) { # fees
1132     my $custnum = $self->fee_origin->custnum;
1133     if ( $custnum ) {
1134       return FS::cust_main->by_key($custnum)->ship_locationnum;
1135     }
1136   } else { # taxes
1137     return '';
1138   }
1139 }
1140
1141 sub tax_location {
1142   my $self = shift;
1143   if ( $self->pkgnum ) { # normal sales
1144     return $self->cust_pkg->tax_location;
1145   } elsif ( $self->feepart ) { # fees
1146     my $custnum = $self->fee_origin->custnum;
1147     if ( $custnum ) {
1148       return FS::cust_main->by_key($custnum)->ship_location;
1149     }
1150   } else { # taxes
1151     return;
1152   }
1153 }
1154
1155 =back
1156
1157 =head1 CLASS METHODS
1158
1159 =over 4
1160
1161 =item usage_sql
1162
1163 Returns an SQL expression for the total usage charges in details on
1164 an item.
1165
1166 =cut
1167
1168 my $usage_sql =
1169   '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0) 
1170     FROM cust_bill_pkg_detail 
1171     WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
1172
1173 sub usage_sql { $usage_sql }
1174
1175 # this makes owed_sql, etc. much more concise
1176 sub charged_sql {
1177   my ($class, $start, $end, %opt) = @_;
1178   my $setuprecur = $opt{setuprecur} || '';
1179   my $charged = 
1180     $setuprecur =~ /^s/ ? 'cust_bill_pkg.setup' :
1181     $setuprecur =~ /^r/ ? 'cust_bill_pkg.recur' :
1182     'cust_bill_pkg.setup + cust_bill_pkg.recur';
1183
1184   if ($opt{no_usage} and $charged =~ /recur/) { 
1185     $charged = "$charged - $usage_sql"
1186   }
1187
1188   $charged;
1189 }
1190
1191
1192 =item owed_sql [ BEFORE, AFTER, OPTIONS ]
1193
1194 Returns an SQL expression for the amount owed.  BEFORE and AFTER specify
1195 a date window.  OPTIONS may include 'no_usage' (excludes usage charges)
1196 and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
1197
1198 =cut
1199
1200 sub owed_sql {
1201   my $class = shift;
1202   '(' . $class->charged_sql(@_) . 
1203   ' - ' . $class->paid_sql(@_) .
1204   ' - ' . $class->credited_sql(@_) . ')'
1205 }
1206
1207 =item paid_sql [ BEFORE, AFTER, OPTIONS ]
1208
1209 Returns an SQL expression for the sum of payments applied to this item.
1210
1211 =cut
1212
1213 sub paid_sql {
1214   my ($class, $start, $end, %opt) = @_;
1215   my $s = $start ? "AND cust_pay._date <= $start" : '';
1216   my $e = $end   ? "AND cust_pay._date >  $end"   : '';
1217   my $setuprecur = $opt{setuprecur} || '';
1218   $setuprecur = 'setup' if $setuprecur =~ /^s/;
1219   $setuprecur = 'recur' if $setuprecur =~ /^r/;
1220   $setuprecur &&= "AND setuprecur = '$setuprecur'";
1221
1222   my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
1223      FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
1224                             JOIN cust_pay      USING (paynum)
1225      WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1226            $s $e $setuprecur )";
1227
1228   if ( $opt{no_usage} ) {
1229     # cap the amount paid at the sum of non-usage charges, 
1230     # minus the amount credited against non-usage charges
1231     "LEAST($paid, ". 
1232       $class->charged_sql($start, $end, %opt) . ' - ' .
1233       $class->credited_sql($start, $end, %opt).')';
1234   }
1235   else {
1236     $paid;
1237   }
1238
1239 }
1240
1241 sub credited_sql {
1242   my ($class, $start, $end, %opt) = @_;
1243   my $s = $start ? "AND cust_credit._date <= $start" : '';
1244   my $e = $end   ? "AND cust_credit._date >  $end"   : '';
1245   my $setuprecur = $opt{setuprecur} || '';
1246   $setuprecur = 'setup' if $setuprecur =~ /^s/;
1247   $setuprecur = 'recur' if $setuprecur =~ /^r/;
1248   $setuprecur &&= "AND setuprecur = '$setuprecur'";
1249
1250   my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
1251      FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
1252                                JOIN cust_credit      USING (crednum)
1253      WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1254            $s $e $setuprecur )";
1255
1256   if ( $opt{no_usage} ) {
1257     # cap the amount credited at the sum of non-usage charges
1258     "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
1259   }
1260   else {
1261     $credited;
1262   }
1263
1264 }
1265
1266 sub upgrade_tax_location {
1267   # For taxes that were calculated/invoiced before cust_location refactoring
1268   # (May-June 2012), there are no cust_bill_pkg_tax_location records unless
1269   # they were calculated on a package-location basis.  Create them here, 
1270   # along with any necessary cust_location records and any tax exemption 
1271   # records.
1272
1273   my ($class, %opt) = @_;
1274   # %opt may include 's' and 'e': start and end date ranges
1275   # and 'X': abort on any error, instead of just rolling back changes to 
1276   # that invoice
1277   my $dbh = dbh;
1278   my $oldAutoCommit = $FS::UID::AutoCommit;
1279   local $FS::UID::AutoCommit = 0;
1280
1281   eval {
1282     use FS::h_cust_main;
1283     use FS::h_cust_bill;
1284     use FS::h_part_pkg;
1285     use FS::h_cust_main_exemption;
1286   };
1287
1288   local $FS::cust_location::import = 1;
1289
1290   my $conf = FS::Conf->new; # h_conf?
1291   return if $conf->config('tax_data_vendor'); #don't touch this case
1292   my $use_ship = $conf->exists('tax-ship_address');
1293   my $use_pkgloc = $conf->exists('tax-pkg_address');
1294
1295   my $date_where = '';
1296   if ($opt{s}) {
1297     $date_where .= " AND cust_bill._date >= $opt{s}";
1298   }
1299   if ($opt{e}) {
1300     $date_where .= " AND cust_bill._date < $opt{e}";
1301   }
1302
1303   my $commit_each_invoice = 1 unless $opt{X};
1304
1305   # if an invoice has either of these kinds of objects, then it doesn't
1306   # need to be upgraded...probably
1307   my $sub_has_tax_link = 'SELECT 1 FROM cust_bill_pkg_tax_location'.
1308   ' JOIN cust_bill_pkg USING (billpkgnum)'.
1309   ' WHERE cust_bill_pkg.invnum = cust_bill.invnum';
1310   my $sub_has_exempt = 'SELECT 1 FROM cust_tax_exempt_pkg'.
1311   ' JOIN cust_bill_pkg USING (billpkgnum)'.
1312   ' WHERE cust_bill_pkg.invnum = cust_bill.invnum'.
1313   ' AND exempt_monthly IS NULL';
1314
1315   my %all_tax_names = (
1316     '' => 1,
1317     'Tax' => 1,
1318     map { $_->taxname => 1 }
1319       qsearch('h_cust_main_county', { taxname => { op => '!=', value => '' }})
1320   );
1321
1322   my $search = FS::Cursor->new({
1323       table => 'cust_bill',
1324       hashref => {},
1325       extra_sql => "WHERE NOT EXISTS($sub_has_tax_link) ".
1326                    "AND NOT EXISTS($sub_has_exempt) ".
1327                     $date_where,
1328   });
1329
1330 #print "Processing ".scalar(@invnums)." invoices...\n";
1331
1332   my $committed;
1333   INVOICE:
1334   while (my $cust_bill = $search->fetch) {
1335     my $invnum = $cust_bill->invnum;
1336     $committed = 0;
1337     print STDERR "Invoice #$invnum\n";
1338     my $pre = '';
1339     my %pkgpart_taxclass; # pkgpart => taxclass
1340     my %pkgpart_exempt_setup;
1341     my %pkgpart_exempt_recur;
1342     my $h_cust_bill = qsearchs('h_cust_bill',
1343       { invnum => $invnum,
1344         history_action => 'insert' });
1345     if (!$h_cust_bill) {
1346       warn "no insert record for invoice $invnum; skipped\n";
1347       #$date = $cust_bill->_date as a fallback?
1348       # We're trying to avoid using non-real dates (-d/-y invoice dates)
1349       # when looking up history records in other tables.
1350       next INVOICE;
1351     }
1352     my $custnum = $h_cust_bill->custnum;
1353
1354     # Determine the address corresponding to this tax region.
1355     # It's either the bill or ship address of the customer as of the
1356     # invoice date-of-insertion.  (Not necessarily the invoice date.)
1357     my $date = $h_cust_bill->history_date;
1358     local($FS::Record::qsearch_qualify_columns) = 0;
1359     my $h_cust_main = qsearchs('h_cust_main',
1360         { custnum   => $custnum },
1361         FS::h_cust_main->sql_h_searchs($date)
1362       );
1363     if (!$h_cust_main ) {
1364       warn "no historical address for cust#".$h_cust_bill->custnum."; skipped\n";
1365       next INVOICE;
1366       # fallback to current $cust_main?  sounds dangerous.
1367     }
1368
1369     # This is a historical customer record, so it has a historical address.
1370     # If there's no cust_location matching this custnum and address (there 
1371     # probably isn't), create one.
1372     my %tax_loc; # keys are pkgnums, values are cust_location objects
1373     my $default_tax_loc;
1374     if ( $h_cust_main->bill_locationnum ) {
1375       # the location has already been upgraded
1376       if ($use_ship) {
1377         $default_tax_loc = $h_cust_main->ship_location;
1378       } else {
1379         $default_tax_loc = $h_cust_main->bill_location;
1380       }
1381     } else {
1382       $pre = 'ship_' if $use_ship and length($h_cust_main->get('ship_last'));
1383       my %hash = map { $_ => $h_cust_main->get($pre.$_) }
1384                     FS::cust_main->location_fields;
1385       # not really needed for this, and often result in duplicate locations
1386       delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
1387
1388       $hash{custnum} = $h_cust_main->custnum;
1389       $default_tax_loc = FS::cust_location->new(\%hash);
1390       my $error = $default_tax_loc->find_or_insert || $default_tax_loc->disable_if_unused;
1391       if ( $error ) {
1392         warn "couldn't create historical location record for cust#".
1393         $h_cust_main->custnum.": $error\n";
1394         next INVOICE;
1395       }
1396     }
1397     my $exempt_cust;
1398     $exempt_cust = 1 if $h_cust_main->tax;
1399
1400     # classify line items
1401     my @tax_items;
1402     my %nontax_items; # taxclass => array of cust_bill_pkg
1403     foreach my $item ($h_cust_bill->cust_bill_pkg) {
1404       my $pkgnum = $item->pkgnum;
1405
1406       if ( $pkgnum == 0 ) {
1407
1408         push @tax_items, $item;
1409
1410       } else {
1411         # (pkgparts really shouldn't change, right?)
1412         local($FS::Record::qsearch_qualify_columns) = 0;
1413         my $h_cust_pkg = qsearchs('h_cust_pkg', { pkgnum => $pkgnum },
1414           FS::h_cust_pkg->sql_h_searchs($date)
1415         );
1416         if ( !$h_cust_pkg ) {
1417           warn "no historical package #".$item->pkgpart."; skipped\n";
1418           next INVOICE;
1419         }
1420         my $pkgpart = $h_cust_pkg->pkgpart;
1421
1422         if ( $use_pkgloc and $h_cust_pkg->locationnum ) {
1423           # then this package already had a locationnum assigned, and that's 
1424           # the one to use for tax calculation
1425           $tax_loc{$pkgnum} = FS::cust_location->by_key($h_cust_pkg->locationnum);
1426         } else {
1427           # use the customer's bill or ship loc, which was inserted earlier
1428           $tax_loc{$pkgnum} = $default_tax_loc;
1429         }
1430
1431         if (!exists $pkgpart_taxclass{$pkgpart}) {
1432           local($FS::Record::qsearch_qualify_columns) = 0;
1433           my $h_part_pkg = qsearchs('h_part_pkg', { pkgpart => $pkgpart },
1434             FS::h_part_pkg->sql_h_searchs($date)
1435           );
1436           if ( !$h_part_pkg ) {
1437             warn "no historical package def #$pkgpart; skipped\n";
1438             next INVOICE;
1439           }
1440           $pkgpart_taxclass{$pkgpart} = $h_part_pkg->taxclass || '';
1441           $pkgpart_exempt_setup{$pkgpart} = 1 if $h_part_pkg->setuptax;
1442           $pkgpart_exempt_recur{$pkgpart} = 1 if $h_part_pkg->recurtax;
1443         }
1444         
1445         # mark any exemptions that apply
1446         if ( $pkgpart_exempt_setup{$pkgpart} ) {
1447           $item->set('exempt_setup' => 1);
1448         }
1449
1450         if ( $pkgpart_exempt_recur{$pkgpart} ) {
1451           $item->set('exempt_recur' => 1);
1452         }
1453
1454         my $taxclass = $pkgpart_taxclass{ $pkgpart };
1455
1456         $nontax_items{$taxclass} ||= [];
1457         push @{ $nontax_items{$taxclass} }, $item;
1458       }
1459     }
1460
1461     printf("%d tax items: \$%.2f\n", scalar(@tax_items), map {$_->setup} @tax_items)
1462       if @tax_items;
1463
1464     # Get any per-customer taxname exemptions that were in effect.
1465     my %exempt_cust_taxname;
1466     foreach (keys %all_tax_names) {
1467      local($FS::Record::qsearch_qualify_columns) = 0;
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