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