RT# 79636 Taxes per section when using invoice_sections
[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   if ( my $fee_origin = $self->get('fee_origin') ) {
304     $fee_origin->set('billpkgnum' => $self->billpkgnum);
305     $error = $fee_origin->replace;
306     if ( $error ) {
307       $dbh->rollback if $oldAutoCommit;
308       return "error updating fee origin record: $error";
309     }
310   }
311
312   my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
313   if ( $cust_tax_adjustment ) {
314     $cust_tax_adjustment->billpkgnum($self->billpkgnum);
315     $error = $cust_tax_adjustment->replace;
316     if ( $error ) {
317       $dbh->rollback if $oldAutoCommit;
318       return "error replacing cust_tax_adjustment: $error";
319     }
320   }
321
322   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
323   '';
324
325 }
326
327 =item void
328
329 Voids this line item: deletes the line item and adds a record of the voided
330 line item to the FS::cust_bill_pkg_void table (and related tables).
331
332 =cut
333
334 sub void {
335   my $self = shift;
336   my $reason = scalar(@_) ? shift : '';
337   my $reprocess_cdrs = 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     my %delete_args = ();
370     $delete_args{'reprocess_cdrs'} = $reprocess_cdrs
371       if $table eq 'cust_bill_pkg_detail';
372
373     foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
374
375       my $vclass = 'FS::'.$table.'_void';
376       my $void = $vclass->new( {
377         map { $_ => $linked->get($_) } $linked->fields
378       });
379       my $error = $void->insert || $linked->delete(%delete_args);
380       if ( $error ) {
381         $dbh->rollback if $oldAutoCommit;
382         return $error;
383       }
384
385     }
386
387   }
388
389   $error = $self->delete;
390   if ( $error ) {
391     $dbh->rollback if $oldAutoCommit;
392     return $error;
393   }
394
395   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
396
397   '';
398
399 }
400
401 =item delete
402
403 Not recommended.
404
405 =cut
406
407 sub delete {
408   my $self = shift;
409
410   local $SIG{HUP} = 'IGNORE';
411   local $SIG{INT} = 'IGNORE';
412   local $SIG{QUIT} = 'IGNORE';
413   local $SIG{TERM} = 'IGNORE';
414   local $SIG{TSTP} = 'IGNORE';
415   local $SIG{PIPE} = 'IGNORE';
416
417   my $oldAutoCommit = $FS::UID::AutoCommit;
418   local $FS::UID::AutoCommit = 0;
419   my $dbh = dbh;
420
421   foreach my $table (qw(
422     cust_bill_pkg_detail
423     cust_bill_pkg_display
424     cust_bill_pkg_discount
425     cust_bill_pkg_tax_location
426     cust_bill_pkg_tax_rate_location
427     cust_tax_exempt_pkg
428     cust_bill_pay_pkg
429     cust_credit_bill_pkg
430     cust_bill_pkg_fee
431   )) {
432
433     foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
434       my $error = $linked->delete;
435       if ( $error ) {
436         $dbh->rollback if $oldAutoCommit;
437         return $error;
438       }
439     }
440
441   }
442
443   foreach my $cust_tax_adjustment (
444     qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
445   ) {
446     $cust_tax_adjustment->billpkgnum(''); #NULL
447     my $error = $cust_tax_adjustment->replace;
448     if ( $error ) {
449       $dbh->rollback if $oldAutoCommit;
450       return $error;
451     }
452   }
453
454   my $error = $self->SUPER::delete(@_);
455   if ( $error ) {
456     $dbh->rollback if $oldAutoCommit;
457     return $error;
458   }
459
460   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
461
462   '';
463
464 }
465
466 #alas, bin/follow-tax-rename
467 #
468 #=item replace OLD_RECORD
469 #
470 #Currently unimplemented.  This would be even more of an accounting nightmare
471 #than deleteing the items.  Just don't do it.
472 #
473 #=cut
474 #
475 #sub replace {
476 #  return "Can't modify cust_bill_pkg records!";
477 #}
478
479 =item check
480
481 Checks all fields to make sure this is a valid line item.  If there is an
482 error, returns the error, otherwise returns false.  Called by the insert
483 method.
484
485 =cut
486
487 sub check {
488   my $self = shift;
489
490   my $error =
491          $self->ut_numbern('billpkgnum')
492       || $self->ut_snumber('pkgnum')
493       || $self->ut_number('invnum')
494       || $self->ut_money('setup')
495       || $self->ut_money('recur')
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 =cut
699
700 sub cust_bill {
701   my $self = shift;
702   qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
703 }
704
705 =item cust_main
706
707 Returns the customer (L<FS::cust_main> object) for this line item.
708
709 =cut
710
711 sub cust_main {
712   # required for cust_main_Mixin equivalence
713   # and use cust_bill instead of cust_pkg because this might not have a 
714   # cust_pkg
715   my $self = shift;
716   my $cust_bill = $self->cust_bill or return '';
717   $cust_bill->cust_main;
718 }
719
720 =item previous_cust_bill_pkg
721
722 Returns the previous cust_bill_pkg for this package, if any.
723
724 =cut
725
726 sub previous_cust_bill_pkg {
727   my $self = shift;
728   return unless $self->sdate;
729   qsearchs({
730     'table'    => 'cust_bill_pkg',
731     'hashref'  => { 'pkgnum' => $self->pkgnum,
732                     'sdate'  => { op=>'<', value=>$self->sdate },
733                   },
734     'order_by' => 'ORDER BY sdate DESC LIMIT 1',
735   });
736 }
737
738 =item owed_setup
739
740 Returns the amount owed (still outstanding) on this line item's setup fee,
741 which is the amount of the line item minus all payment applications (see
742 L<FS::cust_bill_pay_pkg> and credit applications (see
743 L<FS::cust_credit_bill_pkg>).
744
745 =cut
746
747 sub owed_setup {
748   my $self = shift;
749   $self->owed('setup', @_);
750 }
751
752 =item owed_recur
753
754 Returns the amount owed (still outstanding) on this line item's recurring fee,
755 which is the amount of the line item minus all payment applications (see
756 L<FS::cust_bill_pay_pkg> and credit applications (see
757 L<FS::cust_credit_bill_pkg>).
758
759 =cut
760
761 sub owed_recur {
762   my $self = shift;
763   $self->owed('recur', @_);
764 }
765
766 # modeled after cust_bill::owed...
767 sub owed {
768   my( $self, $field ) = @_;
769   my $balance = $self->$field();
770   $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
771   $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
772   $balance = sprintf( '%.2f', $balance );
773   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
774   $balance;
775 }
776
777 #modeled after owed
778 sub payable {
779   my( $self, $field ) = @_;
780   my $balance = $self->$field();
781   $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
782   $balance = sprintf( '%.2f', $balance );
783   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
784   $balance;
785 }
786
787 sub cust_bill_pay_pkg {
788   my( $self, $field ) = @_;
789   qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
790                                   'setuprecur' => $field,
791                                 }
792          );
793 }
794
795 sub cust_credit_bill_pkg {
796   my( $self, $field ) = @_;
797   qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
798                                      'setuprecur' => $field,
799                                    }
800          );
801 }
802
803 =item units
804
805 Returns the number of billing units (for tax purposes) represented by this,
806 line item.
807
808 =cut
809
810 sub units {
811   my $self = shift;
812   $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
813 }
814
815 =item _item_discount
816
817 If this item has any discounts, returns a hashref in the format used
818 by L<FS::Template_Mixin/_items_cust_bill_pkg> to describe the discount(s)
819 on an invoice. This will contain the keys 'description', 'amount', 
820 'ext_description' (an arrayref of text lines describing the discounts),
821 and '_is_discount' (a flag).
822
823 The value for 'amount' will be negative, and will be scaled for the package
824 quantity.
825
826 =cut
827
828 sub _item_discount {
829   my $self = shift;
830
831   my $d; # this will be returned.
832
833   my @pkg_discounts = $self->pkg_discount;
834   if (@pkg_discounts) {
835     # special case: if there are old "discount details" on this line item,
836     # don't show discount line items
837     if ( FS::cust_bill_pkg_detail->count("detail LIKE 'Includes discount%' AND billpkgnum = ?", $self->billpkgnum || 0) > 0 ) {
838       return;
839     } 
840     
841     my @ext;
842     $d = {
843       _is_discount    => 1,
844       description     => $self->mt('Discount'),
845       amount          => 0,
846       ext_description => \@ext,
847       pkgpart         => $self->pkgpart,
848       feepart         => $self->feepart,
849       # maybe should show quantity/unit discount?
850     };
851     foreach my $pkg_discount (@pkg_discounts) {
852       push @ext, $pkg_discount->description;
853       $d->{'amount'} -= $pkg_discount->amount;
854     }
855   }
856
857   # show introductory rate as a pseudo-discount
858   if (!$d) { # this will conflict with showing real discounts
859     my $part_pkg = $self->part_pkg;
860     if ( $part_pkg and $part_pkg->option('show_as_discount',1) ) {
861       my $cust_pkg = $self->cust_pkg;
862       my $intro_end = $part_pkg->intro_end($cust_pkg);
863       my $_date = $self->cust_bill->_date;
864       if ( $intro_end > $_date ) {
865         $d = $part_pkg->item_discount($cust_pkg);
866       }
867     }
868   }
869
870   if ( $d ) {
871     $d->{amount} *= $self->quantity || 1;
872   }
873     
874   $d;
875 }
876
877 =item set_display OPTION => VALUE ...
878
879 A helper method for I<insert>, populates the pseudo-field B<display> with
880 appropriate FS::cust_bill_pkg_display objects.
881
882 Options are passed as a list of name/value pairs.  Options are:
883
884 part_pkg: FS::part_pkg object from this line item's package.
885
886 real_pkgpart: if this line item comes from a bundled package, the pkgpart 
887 of the owning package.  Otherwise the same as the part_pkg's pkgpart above.
888
889 =cut
890
891 sub set_display {
892   my( $self, %opt ) = @_;
893   my $part_pkg = $opt{'part_pkg'};
894   my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
895
896   my $conf = new FS::Conf;
897
898   # whether to break this down into setup/recur/usage
899   my $separate = $conf->exists('separate_usage');
900
901   my $usage_mandate =            $part_pkg->option('usage_mandate', 'Hush!')
902                     || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
903
904   # or use the category from $opt{'part_pkg'} if its not bundled?
905   my $categoryname = $cust_pkg->part_pkg->categoryname;
906
907   # if we don't have to separate setup/recur/usage, or put this in a 
908   # package-specific section, or display a usage summary, then don't 
909   # even create one of these.  The item will just display in the unnamed
910   # section as a single line plus details.
911   return $self->set('display', [])
912     unless $separate || $categoryname || $usage_mandate;
913   
914   my @display = ();
915
916   my %hash = ( 'section' => $categoryname );
917
918   # whether to put usage details in a separate section, and if so, which one
919   my $usage_section =            $part_pkg->option('usage_section', 'Hush!')
920                     || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
921
922   # whether to show a usage summary line (total usage charges, no details)
923   my $summary =            $part_pkg->option('summarize_usage', 'Hush!')
924               || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
925
926   if ( $separate ) {
927     # create lines for setup and (non-usage) recur, in the main section
928     push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
929     push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
930   } else {
931     # display everything in a single line
932     push @display, new FS::cust_bill_pkg_display
933                      { type => '',
934                        %hash,
935                        # and if usage_mandate is enabled, hide details
936                        # (this only works on multisection invoices...)
937                        ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
938                      };
939   }
940
941   if ($separate && $usage_section && $summary) {
942     # create a line for the usage summary in the main section
943     push @display, new FS::cust_bill_pkg_display { type    => 'U',
944                                                    summary => 'Y',
945                                                    %hash,
946                                                  };
947   }
948
949   if ($usage_mandate || ($usage_section && $summary) ) {
950     $hash{post_total} = 'Y';
951   }
952
953   if ($separate || $usage_mandate) {
954     # show call details for this line item in the usage section.
955     # if usage_mandate is on, this will display below the section subtotal.
956     # this also happens if usage is in a separate section and there's a 
957     # summary in the main section, though I'm not sure why.
958     $hash{section} = $usage_section if $usage_section;
959     push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
960   }
961
962   $self->set('display', \@display);
963
964 }
965
966 =item disintegrate
967
968 Returns a hash: keys are "setup", "recur" or usage classnum, values are
969 FS::cust_bill_pkg objects, each with no more than a single class (setup or
970 recur) of charge.
971
972 =cut
973
974 sub disintegrate {
975   my $self = shift;
976   # XXX this goes away with cust_bill_pkg refactor
977   # or at least I wish it would, but it turns out to be harder than
978   # that.
979
980   #my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash }; # wha huh?
981   my %cust_bill_pkg = ();
982
983   my $usage_total;
984   foreach my $classnum ($self->usage_classes) {
985     next if $classnum eq ''; # null-class usage is included in 'recur'
986     my $amount = $self->usage($classnum);
987     next if $amount == 0; # though if so we shouldn't be here
988     my $usage_item = FS::cust_bill_pkg->new({
989         $self->hash,
990         'setup'     => 0,
991         'recur'     => $amount,
992         'taxclass'  => $classnum,
993         'inherit'   => $self
994     });
995     $cust_bill_pkg{$classnum} = $usage_item;
996     $usage_total += $amount;
997   }
998
999   foreach (qw(setup recur)) {
1000     next if ($self->get($_) == 0);
1001     my $item = FS::cust_bill_pkg->new({
1002         $self->hash,
1003         'setup'     => 0,
1004         'recur'     => 0,
1005         'taxclass'  => $_,
1006         'inherit'   => $self,
1007     });
1008     $item->set($_, $self->get($_));
1009     $cust_bill_pkg{$_} = $item;
1010   }
1011
1012   if ($usage_total) {
1013     $cust_bill_pkg{recur}->set('recur',
1014       sprintf('%.2f', $cust_bill_pkg{recur}->get('recur') - $usage_total)
1015     );
1016   }
1017
1018   %cust_bill_pkg;
1019 }
1020
1021 =item usage CLASSNUM
1022
1023 Returns the amount of the charge associated with usage class CLASSNUM if
1024 CLASSNUM is defined.  Otherwise returns the total charge associated with
1025 usage.
1026   
1027 =cut
1028
1029 sub usage {
1030   my( $self, $classnum ) = @_;
1031   $self->regularize_details;
1032
1033   if ( $self->get('details') ) {
1034
1035     return sum( 0, 
1036       map { $_->amount || 0 }
1037       grep { !defined($classnum) or $classnum eq $_->classnum }
1038       @{ $self->get('details') }
1039     );
1040
1041   } else {
1042
1043     my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
1044               ' WHERE billpkgnum = '. $self->billpkgnum;
1045     if (defined $classnum) {
1046       if ($classnum =~ /^(\d+)$/) {
1047         $sql .= " AND classnum = $1";
1048       } elsif (defined($classnum) and $classnum eq '') {
1049         $sql .= " AND classnum IS NULL";
1050       }
1051     }
1052
1053     my $sth = dbh->prepare($sql) or die dbh->errstr;
1054     $sth->execute or die $sth->errstr;
1055
1056     return $sth->fetchrow_arrayref->[0] || 0;
1057
1058   }
1059
1060 }
1061
1062 =item usage_classes
1063
1064 Returns a list of usage classnums associated with this invoice line's
1065 details.
1066   
1067 =cut
1068
1069 sub usage_classes {
1070   my( $self ) = @_;
1071   $self->regularize_details;
1072
1073   if ( $self->get('details') ) {
1074
1075     my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
1076     keys %seen;
1077
1078   } else {
1079
1080     map { $_->classnum }
1081         qsearch({ table   => 'cust_bill_pkg_detail',
1082                   hashref => { billpkgnum => $self->billpkgnum },
1083                   select  => 'DISTINCT classnum',
1084                });
1085
1086   }
1087
1088 }
1089
1090 sub cust_tax_exempt_pkg {
1091   my ( $self ) = @_;
1092
1093   my $array = $self->{Hash}->{cust_tax_exempt_pkg} ||= [];
1094 }
1095
1096 =item cust_bill_pkg_fee
1097
1098 Returns the list of associated cust_bill_pkg_fee objects, if this is 
1099 a fee-type item.
1100
1101 =cut
1102
1103 sub cust_bill_pkg_fee {
1104   my $self = shift;
1105   qsearch('cust_bill_pkg_fee', { billpkgnum => $self->billpkgnum });
1106 }
1107
1108 =item cust_bill_pkg_tax_Xlocation
1109
1110 Returns the list of associated cust_bill_pkg_tax_location and/or
1111 cust_bill_pkg_tax_rate_location objects
1112
1113 =cut
1114
1115 sub cust_bill_pkg_tax_Xlocation {
1116   my $self = shift;
1117
1118   my %hash = ( 'billpkgnum' => $self->billpkgnum );
1119
1120   (
1121     qsearch ( 'cust_bill_pkg_tax_location', { %hash  } ),
1122     qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
1123   );
1124
1125 }
1126
1127 =item recur_show_zero
1128
1129 Whether to show a zero recurring amount. This is true if the package or its
1130 definition has the recur_show_zero flag, and the recurring fee is actually
1131 zero for this period.
1132
1133 =cut
1134
1135 sub recur_show_zero {
1136   my( $self, $what ) = @_;
1137
1138   return 0 unless $self->get('recur') == 0 && $self->pkgnum;
1139
1140   $self->cust_pkg->_X_show_zero('recur');
1141 }
1142
1143 =item setup_show_zero
1144
1145 Whether to show a zero setup charge. This requires the package or its
1146 definition to have the setup_show_zero flag, but it also returns false if
1147 the package's setup date is before this line item's start date.
1148
1149 =cut
1150
1151 sub setup_show_zero {
1152   my $self = shift;
1153   return 0 unless $self->get('setup') == 0 && $self->pkgnum;
1154   my $cust_pkg = $self->cust_pkg;
1155   return 0 if ( $self->sdate || 0 ) > ( $cust_pkg->setup || 0 );
1156   return $cust_pkg->_X_show_zero('setup');
1157 }
1158
1159 =item credited [ BEFORE, AFTER, OPTIONS ]
1160
1161 Returns the sum of credits applied to this item.  Arguments are the same as
1162 owed_sql/paid_sql/credited_sql.
1163
1164 =cut
1165
1166 sub credited {
1167   my $self = shift;
1168   $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
1169 }
1170
1171 =item tax_locationnum
1172
1173 Returns the L<FS::cust_location> number that this line item is in for tax
1174 purposes.  For package sales, it's the package tax location; for fees, 
1175 it's the customer's default service location.
1176
1177 =cut
1178
1179 sub tax_locationnum {
1180   my $self = shift;
1181   if ( $self->pkgnum ) { # normal sales
1182     return $self->cust_pkg->tax_locationnum;
1183   } elsif ( $self->feepart and $self->invnum ) { # fees
1184     return $self->cust_bill->cust_main->ship_locationnum;
1185   } else { # taxes
1186     return '';
1187   }
1188 }
1189
1190 sub tax_location {
1191   my $self = shift;
1192   if ( $self->pkgnum ) { # normal sales
1193     return $self->cust_pkg->tax_location;
1194   } elsif ( $self->feepart and $self->invnum ) { # fees
1195     return $self->cust_bill->cust_main->ship_location;
1196   } else { # taxes
1197     return;
1198   }
1199 }
1200
1201 =back
1202
1203 =head1 CLASS METHODS
1204
1205 =over 4
1206
1207 =item usage_sql
1208
1209 Returns an SQL expression for the total usage charges in details on
1210 an item.
1211
1212 =cut
1213
1214 my $usage_sql =
1215   '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0) 
1216     FROM cust_bill_pkg_detail 
1217     WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
1218
1219 sub usage_sql { $usage_sql }
1220
1221 # this makes owed_sql, etc. much more concise
1222 sub charged_sql {
1223   my ($class, $start, $end, %opt) = @_;
1224   my $setuprecur = $opt{setuprecur} || '';
1225   my $charged = 
1226     $setuprecur =~ /^s/ ? 'cust_bill_pkg.setup' :
1227     $setuprecur =~ /^r/ ? 'cust_bill_pkg.recur' :
1228     'cust_bill_pkg.setup + cust_bill_pkg.recur';
1229
1230   if ($opt{no_usage} and $charged =~ /recur/) { 
1231     $charged = "$charged - $usage_sql"
1232   }
1233
1234   $charged;
1235 }
1236
1237
1238 =item owed_sql [ BEFORE, AFTER, OPTIONS ]
1239
1240 Returns an SQL expression for the amount owed.  BEFORE and AFTER specify
1241 a date window.  OPTIONS may include 'no_usage' (excludes usage charges)
1242 and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
1243
1244 =cut
1245
1246 sub owed_sql {
1247   my $class = shift;
1248   '(' . $class->charged_sql(@_) . 
1249   ' - ' . $class->paid_sql(@_) .
1250   ' - ' . $class->credited_sql(@_) . ')'
1251 }
1252
1253 =item paid_sql [ BEFORE, AFTER, OPTIONS ]
1254
1255 Returns an SQL expression for the sum of payments applied to this item.
1256
1257 =cut
1258
1259 sub paid_sql {
1260   my ($class, $start, $end, %opt) = @_;
1261   my $s = $start ? "AND cust_pay._date <= $start" : '';
1262   my $e = $end   ? "AND cust_pay._date >  $end"   : '';
1263   my $setuprecur = $opt{setuprecur} || '';
1264   $setuprecur = 'setup' if $setuprecur =~ /^s/;
1265   $setuprecur = 'recur' if $setuprecur =~ /^r/;
1266   $setuprecur &&= "AND setuprecur = '$setuprecur'";
1267
1268   my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
1269      FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
1270                             JOIN cust_pay      USING (paynum)
1271      WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1272            $s $e $setuprecur )";
1273
1274   if ( $opt{no_usage} ) {
1275     # cap the amount paid at the sum of non-usage charges, 
1276     # minus the amount credited against non-usage charges
1277     "LEAST($paid, ". 
1278       $class->charged_sql($start, $end, %opt) . ' - ' .
1279       $class->credited_sql($start, $end, %opt).')';
1280   }
1281   else {
1282     $paid;
1283   }
1284
1285 }
1286
1287 sub credited_sql {
1288   my ($class, $start, $end, %opt) = @_;
1289   my $s = $start ? "AND cust_credit._date <= $start" : '';
1290   my $e = $end   ? "AND cust_credit._date >  $end"   : '';
1291   my $setuprecur = $opt{setuprecur} || '';
1292   $setuprecur = 'setup' if $setuprecur =~ /^s/;
1293   $setuprecur = 'recur' if $setuprecur =~ /^r/;
1294   $setuprecur &&= "AND setuprecur = '$setuprecur'";
1295
1296   my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
1297      FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
1298                                JOIN cust_credit      USING (crednum)
1299      WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1300            $s $e $setuprecur )";
1301
1302   if ( $opt{no_usage} ) {
1303     # cap the amount credited at the sum of non-usage charges
1304     "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
1305   }
1306   else {
1307     $credited;
1308   }
1309
1310 }
1311
1312 sub upgrade_tax_location {
1313   # For taxes that were calculated/invoiced before cust_location refactoring
1314   # (May-June 2012), there are no cust_bill_pkg_tax_location records unless
1315   # they were calculated on a package-location basis.  Create them here, 
1316   # along with any necessary cust_location records and any tax exemption 
1317   # records.
1318
1319   my ($class, %opt) = @_;
1320   # %opt may include 's' and 'e': start and end date ranges
1321   # and 'X': abort on any error, instead of just rolling back changes to 
1322   # that invoice
1323   my $dbh = dbh;
1324   my $oldAutoCommit = $FS::UID::AutoCommit;
1325   local $FS::UID::AutoCommit = 0;
1326
1327   eval {
1328     use FS::h_cust_main;
1329     use FS::h_cust_bill;
1330     use FS::h_part_pkg;
1331     use FS::h_cust_main_exemption;
1332   };
1333
1334   local $FS::cust_location::import = 1;
1335
1336   my $conf = FS::Conf->new; # h_conf?
1337   return if $conf->exists('enable_taxproducts'); #don't touch this case
1338   my $use_ship = $conf->exists('tax-ship_address');
1339   my $use_pkgloc = $conf->exists('tax-pkg_address');
1340
1341   my $date_where = '';
1342   if ($opt{s}) {
1343     $date_where .= " AND cust_bill._date >= $opt{s}";
1344   }
1345   if ($opt{e}) {
1346     $date_where .= " AND cust_bill._date < $opt{e}";
1347   }
1348
1349   my $commit_each_invoice = 1 unless $opt{X};
1350
1351   # if an invoice has either of these kinds of objects, then it doesn't
1352   # need to be upgraded...probably
1353   my $sub_has_tax_link = 'SELECT 1 FROM cust_bill_pkg_tax_location'.
1354   ' JOIN cust_bill_pkg USING (billpkgnum)'.
1355   ' WHERE cust_bill_pkg.invnum = cust_bill.invnum';
1356   my $sub_has_exempt = 'SELECT 1 FROM cust_tax_exempt_pkg'.
1357   ' JOIN cust_bill_pkg USING (billpkgnum)'.
1358   ' WHERE cust_bill_pkg.invnum = cust_bill.invnum'.
1359   ' AND exempt_monthly IS NULL';
1360
1361   my %all_tax_names = (
1362     '' => 1,
1363     'Tax' => 1,
1364     map { $_->taxname => 1 }
1365       qsearch('h_cust_main_county', { taxname => { op => '!=', value => '' }})
1366   );
1367
1368   my $search = FS::Cursor->new({
1369       table => 'cust_bill',
1370       hashref => {},
1371       extra_sql => "WHERE NOT EXISTS($sub_has_tax_link) ".
1372                    "AND NOT EXISTS($sub_has_exempt) ".
1373                     $date_where,
1374   });
1375
1376 #print "Processing ".scalar(@invnums)." invoices...\n";
1377
1378   my $committed;
1379   INVOICE:
1380   while (my $cust_bill = $search->fetch) {
1381     my $invnum = $cust_bill->invnum;
1382     $committed = 0;
1383     print STDERR "Invoice #$invnum\n";
1384     my $pre = '';
1385     my %pkgpart_taxclass; # pkgpart => taxclass
1386     my %pkgpart_exempt_setup;
1387     my %pkgpart_exempt_recur;
1388     my $h_cust_bill = qsearchs('h_cust_bill',
1389       { invnum => $invnum,
1390         history_action => 'insert' });
1391     if (!$h_cust_bill) {
1392       warn "no insert record for invoice $invnum; skipped\n";
1393       #$date = $cust_bill->_date as a fallback?
1394       # We're trying to avoid using non-real dates (-d/-y invoice dates)
1395       # when looking up history records in other tables.
1396       next INVOICE;
1397     }
1398     my $custnum = $h_cust_bill->custnum;
1399
1400     # Determine the address corresponding to this tax region.
1401     # It's either the bill or ship address of the customer as of the
1402     # invoice date-of-insertion.  (Not necessarily the invoice date.)
1403     my $date = $h_cust_bill->history_date;
1404     my $h_cust_main = qsearchs('h_cust_main',
1405         { custnum   => $custnum },
1406         FS::h_cust_main->sql_h_searchs($date)
1407       );
1408     if (!$h_cust_main ) {
1409       warn "no historical address for cust#".$h_cust_bill->custnum."; skipped\n";
1410       next INVOICE;
1411       # fallback to current $cust_main?  sounds dangerous.
1412     }
1413
1414     # This is a historical customer record, so it has a historical address.
1415     # If there's no cust_location matching this custnum and address (there 
1416     # probably isn't), create one.
1417     my %tax_loc; # keys are pkgnums, values are cust_location objects
1418     my $default_tax_loc;
1419     if ( $h_cust_main->bill_locationnum ) {
1420       # the location has already been upgraded
1421       if ($use_ship) {
1422         $default_tax_loc = $h_cust_main->ship_location;
1423       } else {
1424         $default_tax_loc = $h_cust_main->bill_location;
1425       }
1426     } else {
1427       $pre = 'ship_' if $use_ship and length($h_cust_main->get('ship_last'));
1428       my %hash = map { $_ => $h_cust_main->get($pre.$_) }
1429                     FS::cust_main->location_fields;
1430       # not really needed for this, and often result in duplicate locations
1431       delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
1432
1433       $hash{custnum} = $h_cust_main->custnum;
1434       $default_tax_loc = FS::cust_location->new(\%hash);
1435       my $error = $default_tax_loc->find_or_insert || $default_tax_loc->disable_if_unused;
1436       if ( $error ) {
1437         warn "couldn't create historical location record for cust#".
1438         $h_cust_main->custnum.": $error\n";
1439         next INVOICE;
1440       }
1441     }
1442     my $exempt_cust;
1443     $exempt_cust = 1 if $h_cust_main->tax;
1444
1445     # classify line items
1446     my @tax_items;
1447     my %nontax_items; # taxclass => array of cust_bill_pkg
1448     foreach my $item ($h_cust_bill->cust_bill_pkg) {
1449       my $pkgnum = $item->pkgnum;
1450
1451       if ( $pkgnum == 0 ) {
1452
1453         push @tax_items, $item;
1454
1455       } else {
1456         # (pkgparts really shouldn't change, right?)
1457         my $h_cust_pkg = qsearchs('h_cust_pkg', { pkgnum => $pkgnum },
1458           FS::h_cust_pkg->sql_h_searchs($date)
1459         );
1460         if ( !$h_cust_pkg ) {
1461           warn "no historical package #".$item->pkgpart."; skipped\n";
1462           next INVOICE;
1463         }
1464         my $pkgpart = $h_cust_pkg->pkgpart;
1465
1466         if ( $use_pkgloc and $h_cust_pkg->locationnum ) {
1467           # then this package already had a locationnum assigned, and that's 
1468           # the one to use for tax calculation
1469           $tax_loc{$pkgnum} = FS::cust_location->by_key($h_cust_pkg->locationnum);
1470         } else {
1471           # use the customer's bill or ship loc, which was inserted earlier
1472           $tax_loc{$pkgnum} = $default_tax_loc;
1473         }
1474
1475         if (!exists $pkgpart_taxclass{$pkgpart}) {
1476           my $h_part_pkg = qsearchs('h_part_pkg', { pkgpart => $pkgpart },
1477             FS::h_part_pkg->sql_h_searchs($date)
1478           );
1479           if ( !$h_part_pkg ) {
1480             warn "no historical package def #$pkgpart; skipped\n";
1481             next INVOICE;
1482           }
1483           $pkgpart_taxclass{$pkgpart} = $h_part_pkg->taxclass || '';
1484           $pkgpart_exempt_setup{$pkgpart} = 1 if $h_part_pkg->setuptax;
1485           $pkgpart_exempt_recur{$pkgpart} = 1 if $h_part_pkg->recurtax;
1486         }
1487         
1488         # mark any exemptions that apply
1489         if ( $pkgpart_exempt_setup{$pkgpart} ) {
1490           $item->set('exempt_setup' => 1);
1491         }
1492
1493         if ( $pkgpart_exempt_recur{$pkgpart} ) {
1494           $item->set('exempt_recur' => 1);
1495         }
1496
1497         my $taxclass = $pkgpart_taxclass{ $pkgpart };
1498
1499         $nontax_items{$taxclass} ||= [];
1500         push @{ $nontax_items{$taxclass} }, $item;
1501       }
1502     }
1503
1504     printf("%d tax items: \$%.2f\n", scalar(@tax_items), map {$_->setup} @tax_items)
1505       if @tax_items;
1506
1507     # Get any per-customer taxname exemptions that were in effect.
1508     my %exempt_cust_taxname;
1509     foreach (keys %all_tax_names) {
1510       my $h_exemption = qsearchs('h_cust_main_exemption', {
1511           'custnum' => $custnum,
1512           'taxname' => $_,
1513         },
1514         FS::h_cust_main_exemption->sql_h_searchs($date, $date)
1515       );
1516       if ($h_exemption) {
1517         $exempt_cust_taxname{ $_ } = 1;
1518       }
1519     }
1520
1521     # Use a variation on the procedure in 
1522     # FS::cust_main::Billing::_handle_taxes to identify taxes that apply 
1523     # to this bill.
1524     my @loc_keys = qw( district city county state country );
1525     my %taxdef_by_name; # by name, and then by taxclass
1526     my %est_tax; # by name, and then by taxclass
1527     my %taxable_items; # by taxnum, and then an array
1528
1529     foreach my $taxclass (keys %nontax_items) {
1530       foreach my $orig_item (@{ $nontax_items{$taxclass} }) {
1531         my $my_tax_loc = $tax_loc{ $orig_item->pkgnum };
1532         my %myhash = map { $_ => $my_tax_loc->get($pre.$_) } @loc_keys;
1533         my @elim = qw( district city county state );
1534         my @taxdefs; # because there may be several with different taxnames
1535         do {
1536           $myhash{taxclass} = $taxclass;
1537           @taxdefs = qsearch('cust_main_county', \%myhash);
1538           if ( !@taxdefs ) {
1539             $myhash{taxclass} = '';
1540             @taxdefs = qsearch('cust_main_county', \%myhash);
1541           }
1542           $myhash{ shift @elim } = '';
1543         } while scalar(@elim) and !@taxdefs;
1544
1545         foreach my $taxdef (@taxdefs) {
1546           next if $taxdef->tax == 0;
1547           $taxdef_by_name{$taxdef->taxname}{$taxdef->taxclass} = $taxdef;
1548
1549           $taxable_items{$taxdef->taxnum} ||= [];
1550           # clone the item so that taxdef-dependent changes don't
1551           # change it for other taxdefs
1552           my $item = FS::cust_bill_pkg->new({ $orig_item->hash });
1553
1554           # these flags are already set if the part_pkg declares itself exempt
1555           $item->set('exempt_setup' => 1) if $taxdef->setuptax;
1556           $item->set('exempt_recur' => 1) if $taxdef->recurtax;
1557
1558           my @new_exempt;
1559           my $taxable = $item->setup + $item->recur;
1560           # credits
1561           # h_cust_credit_bill_pkg?
1562           # NO.  Because if these exemptions HAD been created at the time of 
1563           # billing, and then a credit applied later, the exemption would 
1564           # have been adjusted by the amount of the credit.  So we adjust
1565           # the taxable amount before creating the exemption.
1566           # But don't deduct the credit from taxable, because the tax was 
1567           # calculated before the credit was applied.
1568           foreach my $f (qw(setup recur)) {
1569             my $credited = FS::Record->scalar_sql(
1570               "SELECT SUM(amount) FROM cust_credit_bill_pkg ".
1571               "WHERE billpkgnum = ? AND setuprecur = ?",
1572               $item->billpkgnum,
1573               $f
1574             );
1575             $item->set($f, $item->get($f) - $credited) if $credited;
1576           }
1577           my $existing_exempt = FS::Record->scalar_sql(
1578             "SELECT SUM(amount) FROM cust_tax_exempt_pkg WHERE ".
1579             "billpkgnum = ? AND taxnum = ?",
1580             $item->billpkgnum, $taxdef->taxnum
1581           ) || 0;
1582           $taxable -= $existing_exempt;
1583
1584           if ( $taxable and $exempt_cust ) {
1585             push @new_exempt, { exempt_cust => 'Y',  amount => $taxable };
1586             $taxable = 0;
1587           }
1588           if ( $taxable and $exempt_cust_taxname{$taxdef->taxname} ){
1589             push @new_exempt, { exempt_cust_taxname => 'Y', amount => $taxable };
1590             $taxable = 0;
1591           }
1592           if ( $taxable and $item->exempt_setup ) {
1593             push @new_exempt, { exempt_setup => 'Y', amount => $item->setup };
1594             $taxable -= $item->setup;
1595           }
1596           if ( $taxable and $item->exempt_recur ) {
1597             push @new_exempt, { exempt_recur => 'Y', amount => $item->recur };
1598             $taxable -= $item->recur;
1599           }
1600
1601           $item->set('taxable' => $taxable);
1602           push @{ $taxable_items{$taxdef->taxnum} }, $item
1603             if $taxable > 0;
1604
1605           # estimate the amount of tax (this is necessary because different
1606           # taxdefs with the same taxname may have different tax rates) 
1607           # and sum that for each taxname/taxclass combination
1608           # (in cents)
1609           $est_tax{$taxdef->taxname} ||= {};
1610           $est_tax{$taxdef->taxname}{$taxdef->taxclass} ||= 0;
1611           $est_tax{$taxdef->taxname}{$taxdef->taxclass} += 
1612             $taxable * $taxdef->tax;
1613
1614           foreach (@new_exempt) {
1615             next if $_->{amount} == 0;
1616             my $cust_tax_exempt_pkg = FS::cust_tax_exempt_pkg->new({
1617                 %$_,
1618                 billpkgnum  => $item->billpkgnum,
1619                 taxnum      => $taxdef->taxnum,
1620               });
1621             my $error = $cust_tax_exempt_pkg->insert;
1622             if ($error) {
1623               my $pkgnum = $item->pkgnum;
1624               warn "error creating tax exemption for inv$invnum pkg$pkgnum:".
1625                 "\n$error\n\n";
1626               next INVOICE;
1627             }
1628           } #foreach @new_exempt
1629         } #foreach $taxdef
1630       } #foreach $item
1631     } #foreach $taxclass
1632
1633     # Now go through the billed taxes and match them up with the line items.
1634     TAX_ITEM: foreach my $tax_item ( @tax_items )
1635     {
1636       my $taxname = $tax_item->itemdesc;
1637       $taxname = '' if $taxname eq 'Tax';
1638
1639       if ( !exists( $taxdef_by_name{$taxname} ) ) {
1640         # then we didn't find any applicable taxes with this name
1641         warn "no definition found for tax item '$taxname', custnum $custnum\n";
1642         # possibly all of these should be "next TAX_ITEM", but whole invoices
1643         # are transaction protected and we can go back and retry them.
1644         next INVOICE;
1645       }
1646       # classname => cust_main_county
1647       my %taxdef_by_class = %{ $taxdef_by_name{$taxname} };
1648
1649       # Divide the tax item among taxclasses, if necessary
1650       # classname => estimated tax amount
1651       my $this_est_tax = $est_tax{$taxname};
1652       if (!defined $this_est_tax) {
1653         warn "no taxable sales found for inv#$invnum, tax item '$taxname'.\n";
1654         next INVOICE;
1655       }
1656       my $est_total = sum(values %$this_est_tax);
1657       if ( $est_total == 0 ) {
1658         # shouldn't happen
1659         warn "estimated tax on invoice #$invnum is zero.\n";
1660         next INVOICE;
1661       }
1662
1663       my $real_tax = $tax_item->setup;
1664       printf ("Distributing \$%.2f tax:\n", $real_tax);
1665       my $cents_remaining = $real_tax * 100; # for rounding error
1666       my @tax_links; # partial CBPTL hashrefs
1667       foreach my $taxclass (keys %taxdef_by_class) {
1668         my $taxdef = $taxdef_by_class{$taxclass};
1669         # these items already have "taxable" set to their charge amount
1670         # after applying any credits or exemptions
1671         my @items = @{ $taxable_items{$taxdef->taxnum} };
1672         my $subtotal = sum(map {$_->get('taxable')} @items);
1673         printf("\t$taxclass: %.2f\n", $this_est_tax->{$taxclass}/$est_total);
1674
1675         foreach my $nontax (@items) {
1676           my $my_tax_loc = $tax_loc{ $nontax->pkgnum };
1677           my $part = int($real_tax
1678                             # class allocation
1679                          * ($this_est_tax->{$taxclass}/$est_total) 
1680                             # item allocation
1681                          * ($nontax->get('taxable'))/$subtotal
1682                             # convert to cents
1683                          * 100
1684                        );
1685           $cents_remaining -= $part;
1686           push @tax_links, {
1687             taxnum      => $taxdef->taxnum,
1688             pkgnum      => $nontax->pkgnum,
1689             locationnum => $my_tax_loc->locationnum,
1690             billpkgnum  => $nontax->billpkgnum,
1691             cents       => $part,
1692           };
1693         } #foreach $nontax
1694       } #foreach $taxclass
1695       # Distribute any leftover tax round-robin style, one cent at a time.
1696       my $i = 0;
1697       my $nlinks = scalar(@tax_links);
1698       if ( $nlinks ) {
1699         # ensure that it really is an integer
1700         $cents_remaining = sprintf('%.0f', $cents_remaining);
1701         while ($cents_remaining > 0) {
1702           $tax_links[$i % $nlinks]->{cents} += 1;
1703           $cents_remaining--;
1704           $i++;
1705         }
1706       } else {
1707         warn "Can't create tax links--no taxable items found.\n";
1708         next INVOICE;
1709       }
1710
1711       # Gather credit/payment applications so that we can link them
1712       # appropriately.
1713       my @unlinked = (
1714         qsearch( 'cust_credit_bill_pkg',
1715           { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1716         ),
1717         qsearch( 'cust_bill_pay_pkg',
1718           { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1719         )
1720       );
1721
1722       # grab the first one
1723       my $this_unlinked = shift @unlinked;
1724       my $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1725
1726       # Create tax links (yay!)
1727       printf("Creating %d tax links.\n",scalar(@tax_links));
1728       foreach (@tax_links) {
1729         my $link = FS::cust_bill_pkg_tax_location->new({
1730             billpkgnum  => $tax_item->billpkgnum,
1731             taxtype     => 'FS::cust_main_county',
1732             locationnum => $_->{locationnum},
1733             taxnum      => $_->{taxnum},
1734             pkgnum      => $_->{pkgnum},
1735             amount      => sprintf('%.2f', $_->{cents} / 100),
1736             taxable_billpkgnum => $_->{billpkgnum},
1737         });
1738         my $error = $link->insert;
1739         if ( $error ) {
1740           warn "Can't create tax link for inv#$invnum: $error\n";
1741           next INVOICE;
1742         }
1743
1744         my $link_cents = $_->{cents};
1745         # update/create subitem links
1746         #
1747         # If $this_unlinked is undef, then we've allocated all of the
1748         # credit/payment applications to the tax item.  If $link_cents is 0,
1749         # then we've applied credits/payments to all of this package fraction,
1750         # so go on to the next.
1751         while ($this_unlinked and $link_cents) {
1752           # apply as much as possible of $link_amount to this credit/payment
1753           # link
1754           my $apply_cents = min($link_cents, $unlinked_cents);
1755           $link_cents -= $apply_cents;
1756           $unlinked_cents -= $apply_cents;
1757           # $link_cents or $unlinked_cents or both are now zero
1758           $this_unlinked->set('amount' => sprintf('%.2f',$apply_cents/100));
1759           $this_unlinked->set('billpkgtaxlocationnum' => $link->billpkgtaxlocationnum);
1760           my $pkey = $this_unlinked->primary_key; #creditbillpkgnum or billpaypkgnum
1761           if ( $this_unlinked->$pkey ) {
1762             # then it's an existing link--replace it
1763             $error = $this_unlinked->replace;
1764           } else {
1765             $this_unlinked->insert;
1766           }
1767           # what do we do with errors at this stage?
1768           if ( $error ) {
1769             warn "Error creating tax application link: $error\n";
1770             next INVOICE; # for lack of a better idea
1771           }
1772           
1773           if ( $unlinked_cents == 0 ) {
1774             # then we've allocated all of this payment/credit application, 
1775             # so grab the next one
1776             $this_unlinked = shift @unlinked;
1777             $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1778           } elsif ( $link_cents == 0 ) {
1779             # then we've covered all of this package tax fraction, so split
1780             # off a new application from this one
1781             $this_unlinked = $this_unlinked->new({
1782                 $this_unlinked->hash,
1783                 $pkey     => '',
1784             });
1785             # $unlinked_cents is still what it is
1786           }
1787
1788         } #while $this_unlinked and $link_cents
1789       } #foreach (@tax_links)
1790     } #foreach $tax_item
1791
1792     $dbh->commit if $commit_each_invoice and $oldAutoCommit;
1793     $committed = 1;
1794
1795   } #foreach $invnum
1796   continue {
1797     if (!$committed) {
1798       $dbh->rollback if $oldAutoCommit;
1799       die "Upgrade halted.\n" unless $commit_each_invoice;
1800     }
1801   }
1802
1803   $dbh->commit if $oldAutoCommit and !$commit_each_invoice;
1804   '';
1805 }
1806
1807 sub _pkg_tax_list {
1808   # Return an array of hashrefs for each cust_bill_pkg_tax_location
1809   # applied to this bill for this cust_bill_pkg.pkgnum.
1810   #
1811   # ! Important Note:
1812   #   In some situations, this list will contain more tax records than the
1813   #   ones directly related to $self->billpkgnum.  The returned list contains
1814   #   all records, for this bill, charged against this billpkgnum's pkgnum.
1815   #
1816   #   One must keep this in mind when using data returned by this method.
1817   #
1818   #   An unaddressed deficiency in the cust_bill_pkg_tax_location model makes
1819   #   this necessary:  When a linked-hidden package generates a tax/fee as a row
1820   #   in cust_bill_pkg_tax_location, there is not enough information to surmise
1821   #   with specificity which billpkgnum row represents the direct parent of the
1822   #   the linked-hidden package's tax row.  The closest we can get to this
1823   #   backwards reassociation is to use the pkgnum.  Therefore, when multiple
1824   #   billpkgnum's appear with the same pkgnum, this method is going to return
1825   #   the tax records for ALL of those billpkgnum's, not just $self->billpkgnum.
1826   #
1827   #   This could be addressed with an update to the model, and to the billing
1828   #   routine that generates rows into cust_bill_pkg_tax_location.  Perhaps a
1829   #   column, link_billpkgnum or parent_billpkgnum, recording the link. I'm not
1830   #   doing that now, because there would be no possible repair of data stored
1831   #   historically prior to such a fix.  I need _pkg_tax_list() to not be
1832   #   broken for already-generated bills.
1833   #
1834   #   Any code you write relying on _pkg_tax_list() MUST be aware of, and
1835   #   account for, the possible return of duplicated tax records returned
1836   #   when method is called on multiple cust_bill_pkg_tax_location rows.
1837   #   Duplicates can be identified by billpkgtaxlocationnum column.
1838
1839   my $self = shift;
1840   return unless $self->pkgnum;
1841
1842   map +{
1843       billpkgtaxlocationnum => $_->billpkgtaxlocationnum,
1844       billpkgnum            => $_->billpkgnum,
1845       taxnum                => $_->taxnum,
1846       amount                => $_->amount,
1847       taxname               => $_->taxname,
1848   },
1849   qsearch({
1850     table  => 'cust_bill_pkg_tax_location',
1851     addl_from => '
1852       LEFT JOIN cust_bill_pkg
1853               ON cust_bill_pkg.billpkgnum
1854          = cust_bill_pkg_tax_location.taxable_billpkgnum
1855     ',
1856     select => join( ', ', (qw|
1857       cust_bill_pkg.billpkgnum
1858       cust_bill_pkg_tax_location.billpkgtaxlocationnum
1859       cust_bill_pkg_tax_location.taxnum
1860       cust_bill_pkg_tax_location.amount
1861     |)),
1862     extra_sql =>
1863       ' WHERE '.
1864       ' cust_bill_pkg.invnum = ' . dbh->quote( $self->invnum ) .
1865       ' AND '.
1866       ' cust_bill_pkg_tax_location.pkgnum = ' . dbh->quote( $self->pkgnum ),
1867   });
1868
1869 }
1870
1871 sub _upgrade_data {
1872   # Create a queue job to run upgrade_tax_location from January 1, 2012 to 
1873   # the present date.
1874   eval {
1875     use FS::queue;
1876     use Date::Parse 'str2time';
1877   };
1878   my $class = shift;
1879   my $upgrade = 'tax_location_2012';
1880   return if FS::upgrade_journal->is_done($upgrade);
1881   my $job = FS::queue->new({
1882       'job' => 'FS::cust_bill_pkg::upgrade_tax_location'
1883   });
1884   # call it kind of like a class method, not that it matters much
1885   $job->insert($class, 's' => str2time('2012-01-01'));
1886   # if there's a customer location upgrade queued also, wait for it to 
1887   # finish
1888   my $location_job = qsearchs('queue', {
1889       job => 'FS::cust_main::Location::process_upgrade_location'
1890     });
1891   if ( $location_job ) {
1892     $job->depend_insert($location_job->jobnum);
1893   }
1894   # Then mark the upgrade as done, so that we don't queue the job twice
1895   # and somehow run two of them concurrently.
1896   FS::upgrade_journal->set_done($upgrade);
1897   # This upgrade now does the job of assigning taxable_billpkgnums to 
1898   # cust_bill_pkg_tax_location, so set that task done also.
1899   FS::upgrade_journal->set_done('tax_location_taxable_billpkgnum');
1900 }
1901
1902 =back
1903
1904 =head1 BUGS
1905
1906 setup and recur shouldn't be separate fields.  There should be one "amount"
1907 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1908
1909 A line item with both should really be two separate records (preserving
1910 sdate and edate for setup fees for recurring packages - that information may
1911 be valuable later).  Invoice generation (cust_main::bill), invoice printing
1912 (cust_bill), tax reports (report_tax.cgi) and line item reports 
1913 (cust_bill_pkg.cgi) would need to be updated.
1914
1915 owed_setup and owed_recur could then be repaced by just owed, and
1916 cust_bill::open_cust_bill_pkg and
1917 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1918
1919 The upgrade procedure is pretty sketchy.
1920
1921 =head1 SEE ALSO
1922
1923 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1924 from the base documentation.
1925
1926 =cut
1927
1928 1;