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