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') ) {
823 map { ref($_) eq 'HASH'
827 grep { ref($_) && ( defined($classnum)
828 ? $classnum eq ( ref($_) eq 'HASH'
835 @{ $self->get('details') };
839 my $hashref = { 'billpkgnum' => $self->billpkgnum };
840 $hashref->{ 'classnum' } = $classnum if defined($classnum);
841 @values = map { $_->amount } qsearch('cust_bill_pkg_detail', $hashref);
845 foreach ( @values ) {
853 Returns a list of usage classnums associated with this invoice line's
861 if ( $self->get('details') ) {
864 foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
865 $seen{ ref($detail) eq 'HASH'
866 ? $detail->{'classnum'}
875 qsearch({ table => 'cust_bill_pkg_detail',
876 hashref => { billpkgnum => $self->billpkgnum },
877 select => 'DISTINCT classnum',
884 =item cust_bill_pkg_display [ type => TYPE ]
886 Returns an array of display information for the invoice line item optionally
891 sub cust_bill_pkg_display {
892 my ( $self, %opt ) = @_;
895 new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
897 return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
899 my $type = $opt{type} if exists $opt{type};
902 if ( $self->get('display') ) {
903 @result = grep { defined($type) ? ($type eq $_->type) : 1 }
904 @{ $self->get('display') };
906 my $hashref = { 'billpkgnum' => $self->billpkgnum };
907 $hashref->{type} = $type if defined($type);
909 @result = qsearch ({ 'table' => 'cust_bill_pkg_display',
910 'hashref' => { 'billpkgnum' => $self->billpkgnum },
911 'order_by' => 'ORDER BY billpkgdisplaynum',
915 push @result, $default unless ( scalar(@result) || $type );
921 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
922 # and FS::cust_main::bill
924 sub _cust_tax_exempt_pkg {
927 $self->{Hash}->{_cust_tax_exempt_pkg} or
928 $self->{Hash}->{_cust_tax_exempt_pkg} = [];
932 =item cust_bill_pkg_tax_Xlocation
934 Returns the list of associated cust_bill_pkg_tax_location and/or
935 cust_bill_pkg_tax_rate_location objects
939 sub cust_bill_pkg_tax_Xlocation {
942 my %hash = ( 'billpkgnum' => $self->billpkgnum );
945 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
946 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
951 =item cust_bill_pkg_detail [ CLASSNUM ]
953 Returns the list of associated cust_bill_pkg_detail objects
954 The optional CLASSNUM argument will limit the details to the specified usage
959 sub cust_bill_pkg_detail {
961 my $classnum = shift || '';
963 my %hash = ( 'billpkgnum' => $self->billpkgnum );
964 $hash{classnum} = $classnum if $classnum;
966 qsearch( 'cust_bill_pkg_detail', \%hash ),
970 =item cust_bill_pkg_discount
972 Returns the list of associated cust_bill_pkg_discount objects.
976 sub cust_bill_pkg_discount {
978 qsearch( 'cust_bill_pkg_discount', { 'billpkgnum' => $self->billpkgnum } );
981 =item recur_show_zero
985 sub recur_show_zero {
989 #&& $self->cust_pkg->part_pkg->recur_show_zero;
991 shift->_X_show_zero('recur');
995 sub setup_show_zero {
996 shift->_X_show_zero('setup');
1000 my( $self, $what ) = @_;
1002 return 0 unless $self->$what() == 0 && $self->pkgnum;
1004 $self->cust_pkg->_X_show_zero($what);
1011 setup and recur shouldn't be separate fields. There should be one "amount"
1012 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1014 A line item with both should really be two separate records (preserving
1015 sdate and edate for setup fees for recurring packages - that information may
1016 be valuable later). Invoice generation (cust_main::bill), invoice printing
1017 (cust_bill), tax reports (report_tax.cgi) and line item reports
1018 (cust_bill_pkg.cgi) would need to be updated.
1020 owed_setup and owed_recur could then be repaced by just owed, and
1021 cust_bill::open_cust_bill_pkg and
1022 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1026 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1027 from the base documentation.