nvoice voiding, RT#18677
[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 );
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_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
29
30 $DEBUG = 0;
31 $me = '[FS::cust_bill_pkg]';
32
33 =head1 NAME
34
35 FS::cust_bill_pkg - Object methods for cust_bill_pkg records
36
37 =head1 SYNOPSIS
38
39   use FS::cust_bill_pkg;
40
41   $record = new FS::cust_bill_pkg \%hash;
42   $record = new FS::cust_bill_pkg { 'column' => 'value' };
43
44   $error = $record->insert;
45
46   $error = $record->check;
47
48 =head1 DESCRIPTION
49
50 An FS::cust_bill_pkg object represents an invoice line item.
51 FS::cust_bill_pkg inherits from FS::Record.  The following fields are currently
52 supported:
53
54 =over 4
55
56 =item billpkgnum
57
58 primary key
59
60 =item invnum
61
62 invoice (see L<FS::cust_bill>)
63
64 =item pkgnum
65
66 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)
67
68 =item pkgpart_override
69
70 optional package definition (see L<FS::part_pkg>) override
71
72 =item setup
73
74 setup fee
75
76 =item recur
77
78 recurring fee
79
80 =item sdate
81
82 starting date of recurring fee
83
84 =item edate
85
86 ending date of recurring fee
87
88 =item itemdesc
89
90 Line item description (overrides normal package description)
91
92 =item quantity
93
94 If not set, defaults to 1
95
96 =item unitsetup
97
98 If not set, defaults to setup
99
100 =item unitrecur
101
102 If not set, defaults to recur
103
104 =item hidden
105
106 If set to Y, indicates data should not appear as separate line item on invoice
107
108 =back
109
110 sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">.  Also
111 see L<Time::Local> and L<Date::Parse> for conversion functions.
112
113 =head1 METHODS
114
115 =over 4
116
117 =item new HASHREF
118
119 Creates a new line item.  To add the line item to the database, see
120 L<"insert">.  Line items are normally created by calling the bill method of a
121 customer object (see L<FS::cust_main>).
122
123 =cut
124
125 sub table { 'cust_bill_pkg'; }
126
127 sub detail_table            { 'cust_bill_pkg_detail'; }
128 sub display_table           { 'cust_bill_pkg_display'; }
129 sub discount_table          { 'cust_bill_pkg_discount'; }
130 #sub tax_location_table      { 'cust_bill_pkg_tax_location'; }
131 #sub tax_rate_location_table { 'cust_bill_pkg_tax_rate_location'; }
132 #sub tax_exempt_pkg_table    { 'cust_tax_exempt_pkg'; }
133
134 =item insert
135
136 Adds this line item to the database.  If there is an error, returns the error,
137 otherwise returns false.
138
139 =cut
140
141 sub insert {
142   my $self = shift;
143
144   local $SIG{HUP} = 'IGNORE';
145   local $SIG{INT} = 'IGNORE';
146   local $SIG{QUIT} = 'IGNORE';
147   local $SIG{TERM} = 'IGNORE';
148   local $SIG{TSTP} = 'IGNORE';
149   local $SIG{PIPE} = 'IGNORE';
150
151   my $oldAutoCommit = $FS::UID::AutoCommit;
152   local $FS::UID::AutoCommit = 0;
153   my $dbh = dbh;
154
155   my $error = $self->SUPER::insert;
156   if ( $error ) {
157     $dbh->rollback if $oldAutoCommit;
158     return $error;
159   }
160
161   if ( $self->get('details') ) {
162     foreach my $detail ( @{$self->get('details')} ) {
163       $detail->billpkgnum($self->billpkgnum);
164       $error = $detail->insert;
165       if ( $error ) {
166         $dbh->rollback if $oldAutoCommit;
167         return "error inserting cust_bill_pkg_detail: $error";
168       }
169     }
170   }
171
172   if ( $self->get('display') ) {
173     foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
174       $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
175       $error = $cust_bill_pkg_display->insert;
176       if ( $error ) {
177         $dbh->rollback if $oldAutoCommit;
178         return "error inserting cust_bill_pkg_display: $error";
179       }
180     }
181   }
182
183   if ( $self->get('discounts') ) {
184     foreach my $cust_bill_pkg_discount ( @{$self->get('discounts')} ) {
185       $cust_bill_pkg_discount->billpkgnum($self->billpkgnum);
186       $error = $cust_bill_pkg_discount->insert;
187       if ( $error ) {
188         $dbh->rollback if $oldAutoCommit;
189         return "error inserting cust_bill_pkg_discount: $error";
190       }
191     }
192   }
193
194   if ( $self->_cust_tax_exempt_pkg ) {
195     foreach my $cust_tax_exempt_pkg ( @{$self->_cust_tax_exempt_pkg} ) {
196       $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
197       $error = $cust_tax_exempt_pkg->insert;
198       if ( $error ) {
199         $dbh->rollback if $oldAutoCommit;
200         return "error inserting cust_tax_exempt_pkg: $error";
201       }
202     }
203   }
204
205   my $tax_location = $self->get('cust_bill_pkg_tax_location');
206   if ( $tax_location ) {
207     foreach my $cust_bill_pkg_tax_location ( @$tax_location ) {
208       $cust_bill_pkg_tax_location->billpkgnum($self->billpkgnum);
209       $error = $cust_bill_pkg_tax_location->insert;
210       if ( $error ) {
211         $dbh->rollback if $oldAutoCommit;
212         return "error inserting cust_bill_pkg_tax_location: $error";
213       }
214     }
215   }
216
217   my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
218   if ( $tax_rate_location ) {
219     foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
220       $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
221       $error = $cust_bill_pkg_tax_rate_location->insert;
222       if ( $error ) {
223         $dbh->rollback if $oldAutoCommit;
224         return "error inserting cust_bill_pkg_tax_rate_location: $error";
225       }
226     }
227   }
228
229   my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
230   if ( $cust_tax_adjustment ) {
231     $cust_tax_adjustment->billpkgnum($self->billpkgnum);
232     $error = $cust_tax_adjustment->replace;
233     if ( $error ) {
234       $dbh->rollback if $oldAutoCommit;
235       return "error replacing cust_tax_adjustment: $error";
236     }
237   }
238
239   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
240   '';
241
242 }
243
244 =item void
245
246 Voids this line item: deletes the line item and adds a record of the voided
247 line item to the FS::cust_bill_pkg_void table (and related tables).
248
249 =cut
250
251 sub void {
252   my $self = shift;
253   my $reason = scalar(@_) ? shift : '';
254
255   local $SIG{HUP} = 'IGNORE';
256   local $SIG{INT} = 'IGNORE';
257   local $SIG{QUIT} = 'IGNORE';
258   local $SIG{TERM} = 'IGNORE';
259   local $SIG{TSTP} = 'IGNORE';
260   local $SIG{PIPE} = 'IGNORE';
261
262   my $oldAutoCommit = $FS::UID::AutoCommit;
263   local $FS::UID::AutoCommit = 0;
264   my $dbh = dbh;
265
266   my $cust_bill_pkg_void = new FS::cust_bill_pkg_void ( {
267     map { $_ => $self->get($_) } $self->fields
268   } );
269   $cust_bill_pkg_void->reason($reason);
270   my $error = $cust_bill_pkg_void->insert;
271   if ( $error ) {
272     $dbh->rollback if $oldAutoCommit;
273     return $error;
274   }
275
276   foreach my $table (qw(
277     cust_bill_pkg_detail
278     cust_bill_pkg_display
279     cust_bill_pkg_discount
280     cust_bill_pkg_tax_location
281     cust_bill_pkg_tax_rate_location
282     cust_tax_exempt_pkg
283   )) {
284
285     foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
286
287       my $vclass = 'FS::'.$table.'_void';
288       my $void = $vclass->new( {
289         map { $_ => $linked->get($_) } $linked->fields
290       });
291       my $error = $void->insert || $linked->delete;
292       if ( $error ) {
293         $dbh->rollback if $oldAutoCommit;
294         return $error;
295       }
296
297     }
298
299   }
300
301   $error = $self->delete;
302   if ( $error ) {
303     $dbh->rollback if $oldAutoCommit;
304     return $error;
305   }
306
307   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
308
309   '';
310
311 }
312
313 =item delete
314
315 Not recommended.
316
317 =cut
318
319 sub delete {
320   my $self = shift;
321
322   local $SIG{HUP} = 'IGNORE';
323   local $SIG{INT} = 'IGNORE';
324   local $SIG{QUIT} = 'IGNORE';
325   local $SIG{TERM} = 'IGNORE';
326   local $SIG{TSTP} = 'IGNORE';
327   local $SIG{PIPE} = 'IGNORE';
328
329   my $oldAutoCommit = $FS::UID::AutoCommit;
330   local $FS::UID::AutoCommit = 0;
331   my $dbh = dbh;
332
333   foreach my $table (qw(
334     cust_bill_pkg_detail
335     cust_bill_pkg_display
336     cust_bill_pkg_discount
337     cust_bill_pkg_tax_location
338     cust_bill_pkg_tax_rate_location
339     cust_tax_exempt_pkg
340     cust_bill_pay_pkg
341     cust_credit_bill_pkg
342   )) {
343
344     foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
345       my $error = $linked->delete;
346       if ( $error ) {
347         $dbh->rollback if $oldAutoCommit;
348         return $error;
349       }
350     }
351
352   }
353
354   foreach my $cust_tax_adjustment (
355     qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
356   ) {
357     $cust_tax_adjustment->billpkgnum(''); #NULL
358     my $error = $cust_tax_adjustment->replace;
359     if ( $error ) {
360       $dbh->rollback if $oldAutoCommit;
361       return $error;
362     }
363   }
364
365   my $error = $self->SUPER::delete(@_);
366   if ( $error ) {
367     $dbh->rollback if $oldAutoCommit;
368     return $error;
369   }
370
371   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
372
373   '';
374
375 }
376
377 #alas, bin/follow-tax-rename
378 #
379 #=item replace OLD_RECORD
380 #
381 #Currently unimplemented.  This would be even more of an accounting nightmare
382 #than deleteing the items.  Just don't do it.
383 #
384 #=cut
385 #
386 #sub replace {
387 #  return "Can't modify cust_bill_pkg records!";
388 #}
389
390 =item check
391
392 Checks all fields to make sure this is a valid line item.  If there is an
393 error, returns the error, otherwise returns false.  Called by the insert
394 method.
395
396 =cut
397
398 sub check {
399   my $self = shift;
400
401   my $error =
402          $self->ut_numbern('billpkgnum')
403       || $self->ut_snumber('pkgnum')
404       || $self->ut_number('invnum')
405       || $self->ut_money('setup')
406       || $self->ut_money('recur')
407       || $self->ut_numbern('sdate')
408       || $self->ut_numbern('edate')
409       || $self->ut_textn('itemdesc')
410       || $self->ut_textn('itemcomment')
411       || $self->ut_enum('hidden', [ '', 'Y' ])
412   ;
413   return $error if $error;
414
415   $self->regularize_details;
416
417   #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
418   if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
419     return "Unknown pkgnum ". $self->pkgnum
420       unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
421   }
422
423   return "Unknown invnum"
424     unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
425
426   $self->SUPER::check;
427 }
428
429 =item regularize_details
430
431 Converts the contents of the 'details' pseudo-field to 
432 L<FS::cust_bill_pkg_detail> objects, if they aren't already.
433
434 =cut
435
436 sub regularize_details {
437   my $self = shift;
438   if ( $self->get('details') ) {
439     foreach my $detail ( @{$self->get('details')} ) {
440       if ( ref($detail) ne 'FS::cust_bill_pkg_detail' ) {
441         # then turn it into one
442         my %hash = ();
443         if ( ! ref($detail) ) {
444           $hash{'detail'} = $detail;
445         }
446         elsif ( ref($detail) eq 'HASH' ) {
447           %hash = %$detail;
448         }
449         elsif ( ref($detail) eq 'ARRAY' ) {
450           carp "passing invoice details as arrays is deprecated";
451           #carp "this way sucks, use a hash"; #but more useful/friendly
452           $hash{'format'}      = $detail->[0];
453           $hash{'detail'}      = $detail->[1];
454           $hash{'amount'}      = $detail->[2];
455           $hash{'classnum'}    = $detail->[3];
456           $hash{'phonenum'}    = $detail->[4];
457           $hash{'accountcode'} = $detail->[5];
458           $hash{'startdate'}   = $detail->[6];
459           $hash{'duration'}    = $detail->[7];
460           $hash{'regionname'}  = $detail->[8];
461         }
462         else {
463           die "unknown detail type ". ref($detail);
464         }
465         $detail = new FS::cust_bill_pkg_detail \%hash;
466       }
467       $detail->billpkgnum($self->billpkgnum) if $self->billpkgnum;
468     }
469   }
470   return;
471 }
472
473 =item cust_bill
474
475 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
476
477 =cut
478
479 sub cust_bill {
480   my $self = shift;
481   qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
482 }
483
484 =item previous_cust_bill_pkg
485
486 Returns the previous cust_bill_pkg for this package, if any.
487
488 =cut
489
490 sub previous_cust_bill_pkg {
491   my $self = shift;
492   return unless $self->sdate;
493   qsearchs({
494     'table'    => 'cust_bill_pkg',
495     'hashref'  => { 'pkgnum' => $self->pkgnum,
496                     'sdate'  => { op=>'<', value=>$self->sdate },
497                   },
498     'order_by' => 'ORDER BY sdate DESC LIMIT 1',
499   });
500 }
501
502 =item owed_setup
503
504 Returns the amount owed (still outstanding) on this line item's setup fee,
505 which is the amount of the line item minus all payment applications (see
506 L<FS::cust_bill_pay_pkg> and credit applications (see
507 L<FS::cust_credit_bill_pkg>).
508
509 =cut
510
511 sub owed_setup {
512   my $self = shift;
513   $self->owed('setup', @_);
514 }
515
516 =item owed_recur
517
518 Returns the amount owed (still outstanding) on this line item's recurring fee,
519 which is the amount of the line item minus all payment applications (see
520 L<FS::cust_bill_pay_pkg> and credit applications (see
521 L<FS::cust_credit_bill_pkg>).
522
523 =cut
524
525 sub owed_recur {
526   my $self = shift;
527   $self->owed('recur', @_);
528 }
529
530 # modeled after cust_bill::owed...
531 sub owed {
532   my( $self, $field ) = @_;
533   my $balance = $self->$field();
534   $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
535   $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
536   $balance = sprintf( '%.2f', $balance );
537   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
538   $balance;
539 }
540
541 #modeled after owed
542 sub payable {
543   my( $self, $field ) = @_;
544   my $balance = $self->$field();
545   $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
546   $balance = sprintf( '%.2f', $balance );
547   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
548   $balance;
549 }
550
551 sub cust_bill_pay_pkg {
552   my( $self, $field ) = @_;
553   qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
554                                   'setuprecur' => $field,
555                                 }
556          );
557 }
558
559 sub cust_credit_bill_pkg {
560   my( $self, $field ) = @_;
561   qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
562                                      'setuprecur' => $field,
563                                    }
564          );
565 }
566
567 =item units
568
569 Returns the number of billing units (for tax purposes) represented by this,
570 line item.
571
572 =cut
573
574 sub units {
575   my $self = shift;
576   $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
577 }
578
579
580 =item set_display OPTION => VALUE ...
581
582 A helper method for I<insert>, populates the pseudo-field B<display> with
583 appropriate FS::cust_bill_pkg_display objects.
584
585 Options are passed as a list of name/value pairs.  Options are:
586
587 part_pkg: FS::part_pkg object from the 
588
589 real_pkgpart: if this line item comes from a bundled package, the pkgpart of the owning package.  Otherwise the same as the part_pkg's pkgpart above.
590
591 =cut
592
593 sub set_display {
594   my( $self, %opt ) = @_;
595   my $part_pkg = $opt{'part_pkg'};
596   my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
597
598   my $conf = new FS::Conf;
599
600   my $separate = $conf->exists('separate_usage');
601   my $usage_mandate =            $part_pkg->option('usage_mandate', 'Hush!')
602                     || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
603
604   # or use the category from $opt{'part_pkg'} if its not bundled?
605   my $categoryname = $cust_pkg->part_pkg->categoryname;
606
607   return $self->set('display', [])
608     unless $separate || $categoryname || $usage_mandate;
609   
610   my @display = ();
611
612   my %hash = ( 'section' => $categoryname );
613
614   my $usage_section =            $part_pkg->option('usage_section', 'Hush!')
615                     || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
616
617   my $summary =            $part_pkg->option('summarize_usage', 'Hush!')
618               || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
619
620   if ( $separate ) {
621     push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
622     push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
623   } else {
624     push @display, new FS::cust_bill_pkg_display
625                      { type => '',
626                        %hash,
627                        ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
628                      };
629   }
630
631   if ($separate && $usage_section && $summary) {
632     push @display, new FS::cust_bill_pkg_display { type    => 'U',
633                                                    summary => 'Y',
634                                                    %hash,
635                                                  };
636   }
637   if ($usage_mandate || ($usage_section && $summary) ) {
638     $hash{post_total} = 'Y';
639   }
640
641   if ($separate || $usage_mandate) {
642     $hash{section} = $usage_section if $usage_section;
643     push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
644   }
645
646   $self->set('display', \@display);
647
648 }
649
650 =item disintegrate
651
652 Returns a list of cust_bill_pkg objects each with no more than a single class
653 (including setup or recur) of charge.
654
655 =cut
656
657 sub disintegrate {
658   my $self = shift;
659   # XXX this goes away with cust_bill_pkg refactor
660
661   my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
662   my %cust_bill_pkg = ();
663
664   $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
665   $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
666
667
668   #split setup and recur
669   if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
670     my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
671     $cust_bill_pkg->set('details', []);
672     $cust_bill_pkg->recur(0);
673     $cust_bill_pkg->unitrecur(0);
674     $cust_bill_pkg->type('');
675     $cust_bill_pkg_recur->setup(0);
676     $cust_bill_pkg_recur->unitsetup(0);
677     $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
678
679   }
680
681   #split usage from recur
682   my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
683     if exists($cust_bill_pkg{recur});
684   warn "usage is $usage\n" if $DEBUG > 1;
685   if ($usage) {
686     my $cust_bill_pkg_usage =
687         new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
688     $cust_bill_pkg_usage->recur( $usage );
689     $cust_bill_pkg_usage->type( 'U' );
690     my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
691     $cust_bill_pkg{recur}->recur( $recur );
692     $cust_bill_pkg{recur}->type( '' );
693     $cust_bill_pkg{recur}->set('details', []);
694     $cust_bill_pkg{''} = $cust_bill_pkg_usage;
695   }
696
697   #subdivide usage by usage_class
698   if (exists($cust_bill_pkg{''})) {
699     foreach my $class (grep { $_ } $self->usage_classes) {
700       my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
701       my $cust_bill_pkg_usage =
702           new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
703       $cust_bill_pkg_usage->recur( $usage );
704       $cust_bill_pkg_usage->set('details', []);
705       my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
706       $cust_bill_pkg{''}->recur( $classless );
707       $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
708     }
709     warn "Unexpected classless usage value: ". $cust_bill_pkg{''}->recur
710       if ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur < 0);
711     delete $cust_bill_pkg{''}
712       unless ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur > 0);
713   }
714
715 #  # sort setup,recur,'', and the rest numeric && return
716 #  my @result = map { $cust_bill_pkg{$_} }
717 #               sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
718 #                      ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
719 #                    }
720 #               keys %cust_bill_pkg;
721 #
722 #  return (@result);
723
724    %cust_bill_pkg;
725 }
726
727 =item usage CLASSNUM
728
729 Returns the amount of the charge associated with usage class CLASSNUM if
730 CLASSNUM is defined.  Otherwise returns the total charge associated with
731 usage.
732   
733 =cut
734
735 sub usage {
736   my( $self, $classnum ) = @_;
737   $self->regularize_details;
738
739   if ( $self->get('details') ) {
740
741     return sum( 0, 
742       map { $_->amount || 0 }
743       grep { !defined($classnum) or $classnum eq $_->classnum }
744       @{ $self->get('details') }
745     );
746
747   } else {
748
749     my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
750               ' WHERE billpkgnum = '. $self->billpkgnum;
751     $sql .= " AND classnum = $classnum" if defined($classnum);
752
753     my $sth = dbh->prepare($sql) or die dbh->errstr;
754     $sth->execute or die $sth->errstr;
755
756     return $sth->fetchrow_arrayref->[0] || 0;
757
758   }
759
760 }
761
762 =item usage_classes
763
764 Returns a list of usage classnums associated with this invoice line's
765 details.
766   
767 =cut
768
769 sub usage_classes {
770   my( $self ) = @_;
771   $self->regularize_details;
772
773   if ( $self->get('details') ) {
774
775     my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
776     keys %seen;
777
778   } else {
779
780     map { $_->classnum }
781         qsearch({ table   => 'cust_bill_pkg_detail',
782                   hashref => { billpkgnum => $self->billpkgnum },
783                   select  => 'DISTINCT classnum',
784                });
785
786   }
787
788 }
789
790 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
791 # and FS::cust_main::bill
792 sub _cust_tax_exempt_pkg {
793   my ( $self ) = @_;
794
795   $self->{Hash}->{_cust_tax_exempt_pkg} or
796   $self->{Hash}->{_cust_tax_exempt_pkg} = [];
797
798 }
799
800 =item cust_bill_pkg_tax_Xlocation
801
802 Returns the list of associated cust_bill_pkg_tax_location and/or
803 cust_bill_pkg_tax_rate_location objects
804
805 =cut
806
807 sub cust_bill_pkg_tax_Xlocation {
808   my $self = shift;
809
810   my %hash = ( 'billpkgnum' => $self->billpkgnum );
811
812   (
813     qsearch ( 'cust_bill_pkg_tax_location', { %hash  } ),
814     qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
815   );
816
817 }
818
819 =item recur_show_zero
820
821 =cut
822
823 sub recur_show_zero { shift->_X_show_zero('recur'); }
824 sub setup_show_zero { shift->_X_show_zero('setup'); }
825
826 sub _X_show_zero {
827   my( $self, $what ) = @_;
828
829   return 0 unless $self->$what() == 0 && $self->pkgnum;
830
831   $self->cust_pkg->_X_show_zero($what);
832 }
833
834 =back
835
836 =head1 CLASS METHODS
837
838 =over 4
839
840 =item usage_sql
841
842 Returns an SQL expression for the total usage charges in details on
843 an item.
844
845 =cut
846
847 my $usage_sql =
848   '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0) 
849     FROM cust_bill_pkg_detail 
850     WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
851
852 sub usage_sql { $usage_sql }
853
854 # this makes owed_sql, etc. much more concise
855 sub charged_sql {
856   my ($class, $start, $end, %opt) = @_;
857   my $charged = 
858     $opt{setuprecur} =~ /^s/ ? 'cust_bill_pkg.setup' :
859     $opt{setuprecur} =~ /^r/ ? 'cust_bill_pkg.recur' :
860     'cust_bill_pkg.setup + cust_bill_pkg.recur';
861
862   if ($opt{no_usage} and $charged =~ /recur/) { 
863     $charged = "$charged - $usage_sql"
864   }
865
866   $charged;
867 }
868
869
870 =item owed_sql [ BEFORE, AFTER, OPTIONS ]
871
872 Returns an SQL expression for the amount owed.  BEFORE and AFTER specify
873 a date window.  OPTIONS may include 'no_usage' (excludes usage charges)
874 and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
875
876 =cut
877
878 sub owed_sql {
879   my $class = shift;
880   '(' . $class->charged_sql(@_) . 
881   ' - ' . $class->paid_sql(@_) .
882   ' - ' . $class->credited_sql(@_) . ')'
883 }
884
885 =item paid_sql [ BEFORE, AFTER, OPTIONS ]
886
887 Returns an SQL expression for the sum of payments applied to this item.
888
889 =cut
890
891 sub paid_sql {
892   my ($class, $start, $end, %opt) = @_;
893   my $s = $start ? "AND cust_bill_pay._date <= $start" : '';
894   my $e = $end   ? "AND cust_bill_pay._date >  $end"   : '';
895   my $setuprecur = 
896     $opt{setuprecur} =~ /^s/ ? 'setup' :
897     $opt{setuprecur} =~ /^r/ ? 'recur' :
898     '';
899   $setuprecur &&= "AND setuprecur = '$setuprecur'";
900
901   my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
902      FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
903      WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
904            $s $e$setuprecur )";
905
906   if ( $opt{no_usage} ) {
907     # cap the amount paid at the sum of non-usage charges, 
908     # minus the amount credited against non-usage charges
909     "LEAST($paid, ". 
910       $class->charged_sql($start, $end, %opt) . ' - ' .
911       $class->credited_sql($start, $end, %opt).')';
912   }
913   else {
914     $paid;
915   }
916
917 }
918
919 sub credited_sql {
920   my ($class, $start, $end, %opt) = @_;
921   my $s = $start ? "AND cust_credit_bill._date <= $start" : '';
922   my $e = $end   ? "AND cust_credit_bill._date >  $end"   : '';
923   my $setuprecur = 
924     $opt{setuprecur} =~ /^s/ ? 'setup' :
925     $opt{setuprecur} =~ /^r/ ? 'recur' :
926     '';
927   $setuprecur &&= "AND setuprecur = '$setuprecur'";
928
929   my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
930      FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
931      WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
932            $s $e $setuprecur )";
933
934   if ( $opt{no_usage} ) {
935     # cap the amount credited at the sum of non-usage charges
936     "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
937   }
938   else {
939     $credited;
940   }
941
942 }
943
944 =back
945
946 =head1 BUGS
947
948 setup and recur shouldn't be separate fields.  There should be one "amount"
949 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
950
951 A line item with both should really be two separate records (preserving
952 sdate and edate for setup fees for recurring packages - that information may
953 be valuable later).  Invoice generation (cust_main::bill), invoice printing
954 (cust_bill), tax reports (report_tax.cgi) and line item reports 
955 (cust_bill_pkg.cgi) would need to be updated.
956
957 owed_setup and owed_recur could then be repaced by just owed, and
958 cust_bill::open_cust_bill_pkg and
959 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
960
961 =head1 SEE ALSO
962
963 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
964 from the base documentation.
965
966 =cut
967
968 1;
969