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