1 package FS::cust_bill_pkg;
4 use vars qw( @ISA $DEBUG $me );
6 use FS::Record qw( qsearch qsearchs dbdef dbh );
7 use FS::cust_main_Mixin;
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;
21 @ISA = qw( FS::cust_main_Mixin FS::Record );
24 $me = '[FS::cust_bill_pkg]';
28 FS::cust_bill_pkg - Object methods for cust_bill_pkg records
32 use FS::cust_bill_pkg;
34 $record = new FS::cust_bill_pkg \%hash;
35 $record = new FS::cust_bill_pkg { 'column' => 'value' };
37 $error = $record->insert;
39 $error = $record->check;
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
55 invoice (see L<FS::cust_bill>)
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)
61 =item pkgpart_override
63 optional package definition (see L<FS::part_pkg>) override
75 starting date of recurring fee
79 ending date of recurring fee
83 Line item description (overrides normal package description)
87 If not set, defaults to 1
91 If not set, defaults to setup
95 If not set, defaults to recur
99 If set to Y, indicates data should not appear as separate line item on invoice
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.
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>).
118 sub table { 'cust_bill_pkg'; }
122 Adds this line item to the database. If there is an error, returns the error,
123 otherwise returns false.
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';
137 my $oldAutoCommit = $FS::UID::AutoCommit;
138 local $FS::UID::AutoCommit = 0;
141 my $error = $self->SUPER::insert;
143 $dbh->rollback if $oldAutoCommit;
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] : '' ),
161 $error = $cust_bill_pkg_detail->insert;
163 $dbh->rollback if $oldAutoCommit;
164 return "error inserting cust_bill_pkg_detail: $error";
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;
174 $dbh->rollback if $oldAutoCommit;
175 return "error inserting cust_bill_pkg_display: $error";
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;
185 $dbh->rollback if $oldAutoCommit;
186 return "error inserting cust_bill_pkg_discount: $error";
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;
196 $dbh->rollback if $oldAutoCommit;
197 return "error inserting cust_tax_exempt_pkg: $error";
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;
208 $dbh->rollback if $oldAutoCommit;
209 return "error inserting cust_bill_pkg_tax_location: $error";
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;
220 $dbh->rollback if $oldAutoCommit;
221 return "error inserting cust_bill_pkg_tax_rate_location: $error";
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;
231 $dbh->rollback if $oldAutoCommit;
232 return "error replacing cust_tax_adjustment: $error";
236 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
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';
257 my $oldAutoCommit = $FS::UID::AutoCommit;
258 local $FS::UID::AutoCommit = 0;
261 foreach my $table (qw(
263 cust_bill_pkg_display
264 cust_bill_pkg_tax_location
265 cust_bill_pkg_tax_rate_location
271 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
272 my $error = $linked->delete;
274 $dbh->rollback if $oldAutoCommit;
281 foreach my $cust_tax_adjustment (
282 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
284 $cust_tax_adjustment->billpkgnum(''); #NULL
285 my $error = $cust_tax_adjustment->replace;
287 $dbh->rollback if $oldAutoCommit;
292 my $error = $self->SUPER::delete(@_);
294 $dbh->rollback if $oldAutoCommit;
298 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
304 #alas, bin/follow-tax-rename
306 #=item replace OLD_RECORD
308 #Currently unimplemented. This would be even more of an accounting nightmare
309 #than deleteing the items. Just don't do it.
314 # return "Can't modify cust_bill_pkg records!";
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
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' ])
340 return $error if $error;
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 } );
348 return "Unknown invnum"
349 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
356 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
362 carp "$me $self -> cust_pkg" if $DEBUG;
363 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
368 Returns the package definition for this invoice line item.
374 if ( $self->pkgpart_override ) {
375 qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
378 my $cust_pkg = $self->cust_pkg;
379 $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
386 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
392 qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
395 =item previous_cust_bill_pkg
397 Returns the previous cust_bill_pkg for this package, if any.
401 sub previous_cust_bill_pkg {
403 return unless $self->sdate;
405 'table' => 'cust_bill_pkg',
406 'hashref' => { 'pkgnum' => $self->pkgnum,
407 'sdate' => { op=>'<', value=>$self->sdate },
409 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
413 =item details [ OPTION => VALUE ... ]
415 Returns an array of detail information for the invoice line item.
417 Currently available options are: I<format>, I<escape_function> and
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.
423 If I<escape_function> is set then the array members are processed by this
424 function before being returned.
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:
430 'format_function' => sub { () },
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');
440 eval "use Text::CSV_XS;";
442 my $csv = new Text::CSV_XS;
444 my $format_sub = sub { my $detail = shift;
445 $csv->parse($detail) or return "can't parse $detail";
446 join(' - ', map { &$escape_function($_) }
451 $format_sub = sub { my $detail = shift;
452 $csv->parse($detail) or return "can't parse $detail";
453 join('</TD><TD>', map { &$escape_function($_) }
457 if $format eq 'html';
459 $format_sub = sub { my $detail = shift;
460 $csv->parse($detail) or return "can't parse $detail";
461 #join(' & ', map { '\small{'. &$escape_function($_). '}' }
465 foreach ($csv->fields) {
466 $result .= ' & ' if $column > 1;
467 if ($column > 6) { # KLUDGE ALERT!
468 $result .= '\multicolumn{1}{l}{\scriptsize{'.
469 &$escape_function($_). '}}';
471 $result .= '\scriptsize{'. &$escape_function($_). '}';
477 if $format eq 'latex';
479 $format_sub = $opt{format_function} if $opt{format_function};
481 map { ( $_->format eq 'C'
482 ? &{$format_sub}( $_->detail, $_ )
483 : &{$escape_function}( $_->detail )
486 qsearch ({ 'table' => 'cust_bill_pkg_detail',
487 'hashref' => { 'billpkgnum' => $self->billpkgnum },
488 'order_by' => 'ORDER BY detailnum',
490 #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum });
493 =item details_header [ OPTION => VALUE ... ]
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.
504 return '' unless defined dbdef->table('cust_bill_pkg_detail');
506 eval "use Text::CSV_XS;";
508 my $csv = new Text::CSV_XS;
511 qsearch ({ 'table' => 'cust_bill_pkg_detail',
512 'hashref' => { 'billpkgnum' => $self->billpkgnum,
515 'order_by' => 'ORDER BY detailnum LIMIT 1',
517 return() unless scalar(@detail);
518 $csv->parse($detail[0]->detail) or return ();
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".
534 if ( $self->pkgnum > 0 ) {
535 $self->itemdesc || $self->part_pkg->pkg;
537 my $desc = $self->itemdesc || 'Tax';
538 $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
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>).
554 $self->owed('setup', @_);
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>).
568 $self->owed('recur', @_);
571 # modeled after cust_bill::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
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
592 sub cust_bill_pay_pkg {
593 my( $self, $field ) = @_;
594 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
595 'setuprecur' => $field,
600 sub cust_credit_bill_pkg {
601 my( $self, $field ) = @_;
602 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
603 'setuprecur' => $field,
610 Returns the number of billing units (for tax purposes) represented by this,
617 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
625 my( $self, $value ) = @_;
626 if ( defined($value) ) {
627 $self->setfield('quantity', $value);
629 $self->getfield('quantity') || 1;
637 my( $self, $value ) = @_;
638 if ( defined($value) ) {
639 $self->setfield('unitsetup', $value);
641 $self->getfield('unitsetup') eq ''
642 ? $self->getfield('setup')
643 : $self->getfield('unitsetup');
651 my( $self, $value ) = @_;
652 if ( defined($value) ) {
653 $self->setfield('unitrecur', $value);
655 $self->getfield('unitrecur') eq ''
656 ? $self->getfield('recur')
657 : $self->getfield('unitrecur');
660 =item set_display OPTION => VALUE ...
662 A helper method for I<insert>, populates the pseudo-field B<display> with
663 appropriate FS::cust_bill_pkg_display objects.
665 Options are passed as a list of name/value pairs. Options are:
667 part_pkg: FS::part_pkg object from the
669 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.
674 my( $self, %opt ) = @_;
675 my $part_pkg = $opt{'part_pkg'};
676 my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
678 my $conf = new FS::Conf;
680 my $separate = $conf->exists('separate_usage');
681 my $usage_mandate = $part_pkg->option('usage_mandate', 'Hush!')
682 || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
684 # or use the category from $opt{'part_pkg'} if its not bundled?
685 my $categoryname = $cust_pkg->part_pkg->categoryname;
687 return $self->set('display', [])
688 unless $separate || $categoryname || $usage_mandate;
692 my %hash = ( 'section' => $categoryname );
694 my $usage_section = $part_pkg->option('usage_section', 'Hush!')
695 || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
697 my $summary = $part_pkg->option('summarize_usage', 'Hush!')
698 || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
701 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
702 push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
704 push @display, new FS::cust_bill_pkg_display
707 ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
711 if ($separate && $usage_section && $summary) {
712 push @display, new FS::cust_bill_pkg_display { type => 'U',
717 if ($usage_mandate || ($usage_section && $summary) ) {
718 $hash{post_total} = 'Y';
721 if ($separate || $usage_mandate) {
722 $hash{section} = $usage_section if $usage_section;
723 push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
726 $self->set('display', \@display);
732 Returns a list of cust_bill_pkg objects each with no more than a single class
733 (including setup or recur) of charge.
739 # XXX this goes away with cust_bill_pkg refactor
741 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
742 my %cust_bill_pkg = ();
744 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
745 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
748 #split setup and recur
749 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
750 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
751 $cust_bill_pkg->set('details', []);
752 $cust_bill_pkg->recur(0);
753 $cust_bill_pkg->unitrecur(0);
754 $cust_bill_pkg->type('');
755 $cust_bill_pkg_recur->setup(0);
756 $cust_bill_pkg_recur->unitsetup(0);
757 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
761 #split usage from recur
762 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
763 if exists($cust_bill_pkg{recur});
764 warn "usage is $usage\n" if $DEBUG > 1;
766 my $cust_bill_pkg_usage =
767 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
768 $cust_bill_pkg_usage->recur( $usage );
769 $cust_bill_pkg_usage->type( 'U' );
770 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
771 $cust_bill_pkg{recur}->recur( $recur );
772 $cust_bill_pkg{recur}->type( '' );
773 $cust_bill_pkg{recur}->set('details', []);
774 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
777 #subdivide usage by usage_class
778 if (exists($cust_bill_pkg{''})) {
779 foreach my $class (grep { $_ } $self->usage_classes) {
780 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
781 my $cust_bill_pkg_usage =
782 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
783 $cust_bill_pkg_usage->recur( $usage );
784 $cust_bill_pkg_usage->set('details', []);
785 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
786 $cust_bill_pkg{''}->recur( $classless );
787 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
789 warn "Unexpected classless usage value: ". $cust_bill_pkg{''}->recur
790 if ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur < 0);
791 delete $cust_bill_pkg{''}
792 unless ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur > 0);
795 # # sort setup,recur,'', and the rest numeric && return
796 # my @result = map { $cust_bill_pkg{$_} }
797 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
798 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
800 # keys %cust_bill_pkg;
809 Returns the amount of the charge associated with usage class CLASSNUM if
810 CLASSNUM is defined. Otherwise returns the total charge associated with
816 my( $self, $classnum ) = @_;
820 if ( $self->get('details') ) {
824 grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) }
825 @{ $self->get('details') };
829 my $hashref = { 'billpkgnum' => $self->billpkgnum };
830 $hashref->{ 'classnum' } = $classnum if defined($classnum);
831 @values = map { $_->amount } qsearch('cust_bill_pkg_detail', $hashref);
835 foreach ( @values ) {
843 Returns a list of usage classnums associated with this invoice line's
851 if ( $self->get('details') ) {
854 foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
855 $seen{ $detail->[3] } = 1;
862 qsearch({ table => 'cust_bill_pkg_detail',
863 hashref => { billpkgnum => $self->billpkgnum },
864 select => 'DISTINCT classnum',
871 =item cust_bill_pkg_display [ type => TYPE ]
873 Returns an array of display information for the invoice line item optionally
878 sub cust_bill_pkg_display {
879 my ( $self, %opt ) = @_;
882 new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
884 return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
886 my $type = $opt{type} if exists $opt{type};
889 if ( $self->get('display') ) {
890 @result = grep { defined($type) ? ($type eq $_->type) : 1 }
891 @{ $self->get('display') };
893 my $hashref = { 'billpkgnum' => $self->billpkgnum };
894 $hashref->{type} = $type if defined($type);
896 @result = qsearch ({ 'table' => 'cust_bill_pkg_display',
897 'hashref' => { 'billpkgnum' => $self->billpkgnum },
898 'order_by' => 'ORDER BY billpkgdisplaynum',
902 push @result, $default unless ( scalar(@result) || $type );
908 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
909 # and FS::cust_main::bill
911 sub _cust_tax_exempt_pkg {
914 $self->{Hash}->{_cust_tax_exempt_pkg} or
915 $self->{Hash}->{_cust_tax_exempt_pkg} = [];
919 =item cust_bill_pkg_tax_Xlocation
921 Returns the list of associated cust_bill_pkg_tax_location and/or
922 cust_bill_pkg_tax_rate_location objects
926 sub cust_bill_pkg_tax_Xlocation {
929 my %hash = ( 'billpkgnum' => $self->billpkgnum );
932 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
933 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
938 =item cust_bill_pkg_detail [ CLASSNUM ]
940 Returns the list of associated cust_bill_pkg_detail objects
941 The optional CLASSNUM argument will limit the details to the specified usage
946 sub cust_bill_pkg_detail {
948 my $classnum = shift || '';
950 my %hash = ( 'billpkgnum' => $self->billpkgnum );
951 $hash{classnum} = $classnum if $classnum;
953 qsearch( 'cust_bill_pkg_detail', \%hash ),
957 =item cust_bill_pkg_discount
959 Returns the list of associated cust_bill_pkg_discount objects.
963 sub cust_bill_pkg_discount {
965 qsearch( 'cust_bill_pkg_discount', { 'billpkgnum' => $self->billpkgnum } );
968 =item recur_show_zero
972 sub recur_show_zero {
976 #&& $self->cust_pkg->part_pkg->recur_show_zero;
978 shift->_X_show_zero('recur');
982 sub setup_show_zero {
983 shift->_X_show_zero('setup');
987 my( $self, $what ) = @_;
989 return 0 unless $self->$what() == 0 && $self->pkgnum;
991 $self->cust_pkg->_X_show_zero($what);
998 setup and recur shouldn't be separate fields. There should be one "amount"
999 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1001 A line item with both should really be two separate records (preserving
1002 sdate and edate for setup fees for recurring packages - that information may
1003 be valuable later). Invoice generation (cust_main::bill), invoice printing
1004 (cust_bill), tax reports (report_tax.cgi) and line item reports
1005 (cust_bill_pkg.cgi) would need to be updated.
1007 owed_setup and owed_recur could then be repaced by just owed, and
1008 cust_bill::open_cust_bill_pkg and
1009 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1013 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1014 from the base documentation.