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