1 package FS::cust_bill_pkg;
4 use vars qw( @ISA $DEBUG $me );
7 use FS::Record qw( qsearch qsearchs dbdef dbh );
8 use FS::cust_main_Mixin;
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;
22 @ISA = qw( FS::cust_main_Mixin FS::Record );
25 $me = '[FS::cust_bill_pkg]';
29 FS::cust_bill_pkg - Object methods for cust_bill_pkg records
33 use FS::cust_bill_pkg;
35 $record = new FS::cust_bill_pkg \%hash;
36 $record = new FS::cust_bill_pkg { 'column' => 'value' };
38 $error = $record->insert;
40 $error = $record->check;
44 An FS::cust_bill_pkg object represents an invoice line item.
45 FS::cust_bill_pkg inherits from FS::Record. The following fields are currently
56 invoice (see L<FS::cust_bill>)
60 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)
62 =item pkgpart_override
64 optional package definition (see L<FS::part_pkg>) override
76 starting date of recurring fee
80 ending date of recurring fee
84 Line item description (overrides normal package description)
88 If not set, defaults to 1
92 If not set, defaults to setup
96 If not set, defaults to recur
100 If set to Y, indicates data should not appear as separate line item on invoice
104 sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">. Also
105 see L<Time::Local> and L<Date::Parse> for conversion functions.
113 Creates a new line item. To add the line item to the database, see
114 L<"insert">. Line items are normally created by calling the bill method of a
115 customer object (see L<FS::cust_main>).
119 sub table { 'cust_bill_pkg'; }
123 Adds this line item to the database. If there is an error, returns the error,
124 otherwise returns false.
131 local $SIG{HUP} = 'IGNORE';
132 local $SIG{INT} = 'IGNORE';
133 local $SIG{QUIT} = 'IGNORE';
134 local $SIG{TERM} = 'IGNORE';
135 local $SIG{TSTP} = 'IGNORE';
136 local $SIG{PIPE} = 'IGNORE';
138 my $oldAutoCommit = $FS::UID::AutoCommit;
139 local $FS::UID::AutoCommit = 0;
142 my $error = $self->SUPER::insert;
144 $dbh->rollback if $oldAutoCommit;
148 if ( $self->get('details') ) {
149 foreach my $detail ( @{$self->get('details')} ) {
151 if ( ref($detail) ) {
152 if ( ref($detail) eq 'ARRAY' ) {
153 #carp "this way sucks, use a hash"; #but more useful/friendly
154 $hash{'format'} = $detail->[0];
155 $hash{'detail'} = $detail->[1];
156 $hash{'amount'} = $detail->[2];
157 $hash{'classnum'} = $detail->[3];
158 $hash{'phonenum'} = $detail->[4];
159 $hash{'accountcode'} = $detail->[5];
160 $hash{'startdate'} = $detail->[6];
161 $hash{'duration'} = $detail->[7];
162 $hash{'regionname'} = $detail->[8];
163 } elsif ( ref($detail) eq 'HASH' ) {
166 die "unknow detail type ". ref($detail);
169 $hash{'detail'} = $detail;
171 $hash{'billpkgnum'} = $self->billpkgnum;
172 my $cust_bill_pkg_detail = new FS::cust_bill_pkg_detail \%hash;
173 $error = $cust_bill_pkg_detail->insert;
175 $dbh->rollback if $oldAutoCommit;
176 return "error inserting cust_bill_pkg_detail: $error";
181 if ( $self->get('display') ) {
182 foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
183 $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
184 $error = $cust_bill_pkg_display->insert;
186 $dbh->rollback if $oldAutoCommit;
187 return "error inserting cust_bill_pkg_display: $error";
192 if ( $self->get('discounts') ) {
193 foreach my $cust_bill_pkg_discount ( @{$self->get('discounts')} ) {
194 $cust_bill_pkg_discount->billpkgnum($self->billpkgnum);
195 $error = $cust_bill_pkg_discount->insert;
197 $dbh->rollback if $oldAutoCommit;
198 return "error inserting cust_bill_pkg_discount: $error";
203 if ( $self->_cust_tax_exempt_pkg ) {
204 foreach my $cust_tax_exempt_pkg ( @{$self->_cust_tax_exempt_pkg} ) {
205 $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
206 $error = $cust_tax_exempt_pkg->insert;
208 $dbh->rollback if $oldAutoCommit;
209 return "error inserting cust_tax_exempt_pkg: $error";
214 my $tax_location = $self->get('cust_bill_pkg_tax_location');
215 if ( $tax_location ) {
216 foreach my $cust_bill_pkg_tax_location ( @$tax_location ) {
217 $cust_bill_pkg_tax_location->billpkgnum($self->billpkgnum);
218 $error = $cust_bill_pkg_tax_location->insert;
220 $dbh->rollback if $oldAutoCommit;
221 return "error inserting cust_bill_pkg_tax_location: $error";
226 my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
227 if ( $tax_rate_location ) {
228 foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
229 $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
230 $error = $cust_bill_pkg_tax_rate_location->insert;
232 $dbh->rollback if $oldAutoCommit;
233 return "error inserting cust_bill_pkg_tax_rate_location: $error";
238 my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
239 if ( $cust_tax_adjustment ) {
240 $cust_tax_adjustment->billpkgnum($self->billpkgnum);
241 $error = $cust_tax_adjustment->replace;
243 $dbh->rollback if $oldAutoCommit;
244 return "error replacing cust_tax_adjustment: $error";
248 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
262 local $SIG{HUP} = 'IGNORE';
263 local $SIG{INT} = 'IGNORE';
264 local $SIG{QUIT} = 'IGNORE';
265 local $SIG{TERM} = 'IGNORE';
266 local $SIG{TSTP} = 'IGNORE';
267 local $SIG{PIPE} = 'IGNORE';
269 my $oldAutoCommit = $FS::UID::AutoCommit;
270 local $FS::UID::AutoCommit = 0;
273 foreach my $table (qw(
275 cust_bill_pkg_display
276 cust_bill_pkg_tax_location
277 cust_bill_pkg_tax_rate_location
283 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
284 my $error = $linked->delete;
286 $dbh->rollback if $oldAutoCommit;
293 foreach my $cust_tax_adjustment (
294 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
296 $cust_tax_adjustment->billpkgnum(''); #NULL
297 my $error = $cust_tax_adjustment->replace;
299 $dbh->rollback if $oldAutoCommit;
304 my $error = $self->SUPER::delete(@_);
306 $dbh->rollback if $oldAutoCommit;
310 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
316 #alas, bin/follow-tax-rename
318 #=item replace OLD_RECORD
320 #Currently unimplemented. This would be even more of an accounting nightmare
321 #than deleteing the items. Just don't do it.
326 # return "Can't modify cust_bill_pkg records!";
331 Checks all fields to make sure this is a valid line item. If there is an
332 error, returns the error, otherwise returns false. Called by the insert
341 $self->ut_numbern('billpkgnum')
342 || $self->ut_snumber('pkgnum')
343 || $self->ut_number('invnum')
344 || $self->ut_money('setup')
345 || $self->ut_money('recur')
346 || $self->ut_numbern('sdate')
347 || $self->ut_numbern('edate')
348 || $self->ut_textn('itemdesc')
349 || $self->ut_textn('itemcomment')
350 || $self->ut_enum('hidden', [ '', 'Y' ])
352 return $error if $error;
354 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
355 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
356 return "Unknown pkgnum ". $self->pkgnum
357 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
360 return "Unknown invnum"
361 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
368 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
374 carp "$me $self -> cust_pkg" if $DEBUG;
375 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
380 Returns the package definition for this invoice line item.
386 if ( $self->pkgpart_override ) {
387 qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
390 my $cust_pkg = $self->cust_pkg;
391 $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
398 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
404 qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
407 =item previous_cust_bill_pkg
409 Returns the previous cust_bill_pkg for this package, if any.
413 sub previous_cust_bill_pkg {
415 return unless $self->sdate;
417 'table' => 'cust_bill_pkg',
418 'hashref' => { 'pkgnum' => $self->pkgnum,
419 'sdate' => { op=>'<', value=>$self->sdate },
421 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
425 =item details [ OPTION => VALUE ... ]
427 Returns an array of detail information for the invoice line item.
429 Currently available options are: I<format>, I<escape_function> and
432 If I<format> is set to html or latex then the array members are improved
433 for tabular appearance in those environments if possible.
435 If I<escape_function> is set then the array members are processed by this
436 function before being returned.
438 I<format_function> overrides the normal HTML or LaTeX function for returning
439 formatted CDRs. It can be set to a subroutine which returns an empty list
440 to skip usage detail:
442 'format_function' => sub { () },
447 my ( $self, %opt ) = @_;
448 my $escape_function = $opt{escape_function} || sub { shift };
450 my $csv = new Text::CSV_XS;
452 if ( $opt{format_function} ) {
454 #this still expects to be passed a cust_bill_pkg_detail object as the
455 #second argument, which is expensive
456 carp "deprecated format_function passed to cust_bill_pkg->details";
457 my $format_sub = $opt{format_function} if $opt{format_function};
459 map { ( $_->format eq 'C'
460 ? &{$format_sub}( $_->detail, $_ )
461 : &{$escape_function}( $_->detail )
464 qsearch ({ 'table' => 'cust_bill_pkg_detail',
465 'hashref' => { 'billpkgnum' => $self->billpkgnum },
466 'order_by' => 'ORDER BY detailnum',
469 } elsif ( $opt{'no_usage'} ) {
471 my $sql = "SELECT detail FROM cust_bill_pkg_detail ".
472 " WHERE billpkgnum = ". $self->billpkgnum.
473 " AND ( format IS NULL OR format != 'C' ) ".
474 " ORDER BY detailnum";
475 my $sth = dbh->prepare($sql) or die dbh->errstr;
476 $sth->execute or die $sth->errstr;
478 map &{$escape_function}( $_->[0] ), @{ $sth->fetchall_arrayref };
483 my $format = $opt{format} || '';
484 if ( $format eq 'html' ) {
486 $format_sub = sub { my $detail = shift;
487 $csv->parse($detail) or return "can't parse $detail";
488 join('</TD><TD>', map { &$escape_function($_) }
493 } elsif ( $format eq 'latex' ) {
497 $csv->parse($detail) or return "can't parse $detail";
498 #join(' & ', map { '\small{'. &$escape_function($_). '}' }
502 foreach ($csv->fields) {
503 $result .= ' & ' if $column > 1;
504 if ($column > 6) { # KLUDGE ALERT!
505 $result .= '\multicolumn{1}{l}{\scriptsize{'.
506 &$escape_function($_). '}}';
508 $result .= '\scriptsize{'. &$escape_function($_). '}';
517 $format_sub = sub { my $detail = shift;
518 $csv->parse($detail) or return "can't parse $detail";
519 join(' - ', map { &$escape_function($_) }
526 my $sql = "SELECT format, detail FROM cust_bill_pkg_detail ".
527 " WHERE billpkgnum = ". $self->billpkgnum.
528 " ORDER BY detailnum";
529 my $sth = dbh->prepare($sql) or die dbh->errstr;
530 $sth->execute or die $sth->errstr;
532 #avoid the fetchall_arrayref and loop for less memory usage?
534 map { (defined($_->[0]) && $_->[0] eq 'C')
535 ? &{$format_sub}( $_->[1] )
536 : &{$escape_function}( $_->[1] );
538 @{ $sth->fetchall_arrayref };
544 =item details_header [ OPTION => VALUE ... ]
546 Returns a list representing an invoice line item detail header, if any.
547 This relies on the behavior of voip_cdr in that it expects the header
548 to be the first CSV formatted detail (as is expected by invoice generation
549 routines). Returns the empty list otherwise.
555 return '' unless defined dbdef->table('cust_bill_pkg_detail');
557 my $csv = new Text::CSV_XS;
560 qsearch ({ 'table' => 'cust_bill_pkg_detail',
561 'hashref' => { 'billpkgnum' => $self->billpkgnum,
564 'order_by' => 'ORDER BY detailnum LIMIT 1',
566 return() unless scalar(@detail);
567 $csv->parse($detail[0]->detail) or return ();
573 Returns a description for this line item. For typical line items, this is the
574 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
575 For one-shot line items and named taxes, it is the I<itemdesc> field of this
576 line item, and for generic taxes, simply returns "Tax".
583 if ( $self->pkgnum > 0 ) {
584 $self->itemdesc || $self->part_pkg->pkg;
586 my $desc = $self->itemdesc || 'Tax';
587 $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
594 Returns the amount owed (still outstanding) on this line item's setup fee,
595 which is the amount of the line item minus all payment applications (see
596 L<FS::cust_bill_pay_pkg> and credit applications (see
597 L<FS::cust_credit_bill_pkg>).
603 $self->owed('setup', @_);
608 Returns the amount owed (still outstanding) on this line item's recurring fee,
609 which is the amount of the line item minus all payment applications (see
610 L<FS::cust_bill_pay_pkg> and credit applications (see
611 L<FS::cust_credit_bill_pkg>).
617 $self->owed('recur', @_);
620 # modeled after cust_bill::owed...
622 my( $self, $field ) = @_;
623 my $balance = $self->$field();
624 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
625 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
626 $balance = sprintf( '%.2f', $balance );
627 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
633 my( $self, $field ) = @_;
634 my $balance = $self->$field();
635 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
636 $balance = sprintf( '%.2f', $balance );
637 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
641 sub cust_bill_pay_pkg {
642 my( $self, $field ) = @_;
643 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
644 'setuprecur' => $field,
649 sub cust_credit_bill_pkg {
650 my( $self, $field ) = @_;
651 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
652 'setuprecur' => $field,
659 Returns the number of billing units (for tax purposes) represented by this,
666 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
674 my( $self, $value ) = @_;
675 if ( defined($value) ) {
676 $self->setfield('quantity', $value);
678 $self->getfield('quantity') || 1;
686 my( $self, $value ) = @_;
687 if ( defined($value) ) {
688 $self->setfield('unitsetup', $value);
690 $self->getfield('unitsetup') eq ''
691 ? $self->getfield('setup')
692 : $self->getfield('unitsetup');
700 my( $self, $value ) = @_;
701 if ( defined($value) ) {
702 $self->setfield('unitrecur', $value);
704 $self->getfield('unitrecur') eq ''
705 ? $self->getfield('recur')
706 : $self->getfield('unitrecur');
709 =item set_display OPTION => VALUE ...
711 A helper method for I<insert>, populates the pseudo-field B<display> with
712 appropriate FS::cust_bill_pkg_display objects.
714 Options are passed as a list of name/value pairs. Options are:
716 part_pkg: FS::part_pkg object from the
718 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.
723 my( $self, %opt ) = @_;
724 my $part_pkg = $opt{'part_pkg'};
725 my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
727 my $conf = new FS::Conf;
729 my $separate = $conf->exists('separate_usage');
730 my $usage_mandate = $part_pkg->option('usage_mandate', 'Hush!')
731 || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
733 # or use the category from $opt{'part_pkg'} if its not bundled?
734 my $categoryname = $cust_pkg->part_pkg->categoryname;
736 return $self->set('display', [])
737 unless $separate || $categoryname || $usage_mandate;
741 my %hash = ( 'section' => $categoryname );
743 my $usage_section = $part_pkg->option('usage_section', 'Hush!')
744 || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
746 my $summary = $part_pkg->option('summarize_usage', 'Hush!')
747 || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
750 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
751 push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
753 push @display, new FS::cust_bill_pkg_display
756 ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
760 if ($separate && $usage_section && $summary) {
761 push @display, new FS::cust_bill_pkg_display { type => 'U',
766 if ($usage_mandate || ($usage_section && $summary) ) {
767 $hash{post_total} = 'Y';
770 if ($separate || $usage_mandate) {
771 $hash{section} = $usage_section if $usage_section;
772 push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
775 $self->set('display', \@display);
781 Returns a list of cust_bill_pkg objects each with no more than a single class
782 (including setup or recur) of charge.
788 # XXX this goes away with cust_bill_pkg refactor
790 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
791 my %cust_bill_pkg = ();
793 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
794 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
797 #split setup and recur
798 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
799 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
800 $cust_bill_pkg->set('details', []);
801 $cust_bill_pkg->recur(0);
802 $cust_bill_pkg->unitrecur(0);
803 $cust_bill_pkg->type('');
804 $cust_bill_pkg_recur->setup(0);
805 $cust_bill_pkg_recur->unitsetup(0);
806 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
810 #split usage from recur
811 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
812 if exists($cust_bill_pkg{recur});
813 warn "usage is $usage\n" if $DEBUG > 1;
815 my $cust_bill_pkg_usage =
816 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
817 $cust_bill_pkg_usage->recur( $usage );
818 $cust_bill_pkg_usage->type( 'U' );
819 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
820 $cust_bill_pkg{recur}->recur( $recur );
821 $cust_bill_pkg{recur}->type( '' );
822 $cust_bill_pkg{recur}->set('details', []);
823 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
826 #subdivide usage by usage_class
827 if (exists($cust_bill_pkg{''})) {
828 foreach my $class (grep { $_ } $self->usage_classes) {
829 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
830 my $cust_bill_pkg_usage =
831 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
832 $cust_bill_pkg_usage->recur( $usage );
833 $cust_bill_pkg_usage->set('details', []);
834 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
835 $cust_bill_pkg{''}->recur( $classless );
836 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
838 warn "Unexpected classless usage value: ". $cust_bill_pkg{''}->recur
839 if ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur < 0);
840 delete $cust_bill_pkg{''}
841 unless ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur > 0);
844 # # sort setup,recur,'', and the rest numeric && return
845 # my @result = map { $cust_bill_pkg{$_} }
846 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
847 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
849 # keys %cust_bill_pkg;
858 Returns the amount of the charge associated with usage class CLASSNUM if
859 CLASSNUM is defined. Otherwise returns the total charge associated with
865 my( $self, $classnum ) = @_;
867 if ( $self->get('details') ) {
871 map { ref($_) eq 'HASH'
875 grep { ref($_) && ( defined($classnum)
876 ? $classnum eq ( ref($_) eq 'HASH'
883 @{ $self->get('details') }
885 $sum += $value if $value;
892 my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
893 ' WHERE billpkgnum = '. $self->billpkgnum;
894 $sql .= " AND classnum = $classnum" if defined($classnum);
896 my $sth = dbh->prepare($sql) or die dbh->errstr;
897 $sth->execute or die $sth->errstr;
899 return $sth->fetchrow_arrayref->[0] || 0;
907 Returns a list of usage classnums associated with this invoice line's
915 if ( $self->get('details') ) {
918 foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
919 $seen{ (ref($detail) eq 'HASH'
920 ? $detail->{'classnum'}
921 : $detail->[3]) || ''
929 qsearch({ table => 'cust_bill_pkg_detail',
930 hashref => { billpkgnum => $self->billpkgnum },
931 select => 'DISTINCT classnum',
938 =item cust_bill_pkg_display [ type => TYPE ]
940 Returns an array of display information for the invoice line item optionally
945 sub cust_bill_pkg_display {
946 my ( $self, %opt ) = @_;
949 new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
951 return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
953 my $type = $opt{type} if exists $opt{type};
956 if ( $self->get('display') ) {
957 @result = grep { defined($type) ? ($type eq $_->type) : 1 }
958 @{ $self->get('display') };
960 my $hashref = { 'billpkgnum' => $self->billpkgnum };
961 $hashref->{type} = $type if defined($type);
963 @result = qsearch ({ 'table' => 'cust_bill_pkg_display',
964 'hashref' => { 'billpkgnum' => $self->billpkgnum },
965 'order_by' => 'ORDER BY billpkgdisplaynum',
969 push @result, $default unless ( scalar(@result) || $type );
975 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
976 # and FS::cust_main::bill
978 sub _cust_tax_exempt_pkg {
981 $self->{Hash}->{_cust_tax_exempt_pkg} or
982 $self->{Hash}->{_cust_tax_exempt_pkg} = [];
986 =item cust_bill_pkg_tax_Xlocation
988 Returns the list of associated cust_bill_pkg_tax_location and/or
989 cust_bill_pkg_tax_rate_location objects
993 sub cust_bill_pkg_tax_Xlocation {
996 my %hash = ( 'billpkgnum' => $self->billpkgnum );
999 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
1000 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
1005 =item cust_bill_pkg_detail [ CLASSNUM ]
1007 Returns the list of associated cust_bill_pkg_detail objects
1008 The optional CLASSNUM argument will limit the details to the specified usage
1013 sub cust_bill_pkg_detail {
1015 my $classnum = shift || '';
1017 my %hash = ( 'billpkgnum' => $self->billpkgnum );
1018 $hash{classnum} = $classnum if $classnum;
1020 qsearch( 'cust_bill_pkg_detail', \%hash ),
1024 =item cust_bill_pkg_discount
1026 Returns the list of associated cust_bill_pkg_discount objects.
1030 sub cust_bill_pkg_discount {
1032 qsearch( 'cust_bill_pkg_discount', { 'billpkgnum' => $self->billpkgnum } );
1035 =item recur_show_zero
1039 sub recur_show_zero {
1043 #&& $self->cust_pkg->part_pkg->recur_show_zero;
1045 shift->_X_show_zero('recur');
1049 sub setup_show_zero {
1050 shift->_X_show_zero('setup');
1054 my( $self, $what ) = @_;
1056 return 0 unless $self->$what() == 0 && $self->pkgnum;
1058 $self->cust_pkg->_X_show_zero($what);
1065 setup and recur shouldn't be separate fields. There should be one "amount"
1066 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1068 A line item with both should really be two separate records (preserving
1069 sdate and edate for setup fees for recurring packages - that information may
1070 be valuable later). Invoice generation (cust_main::bill), invoice printing
1071 (cust_bill), tax reports (report_tax.cgi) and line item reports
1072 (cust_bill_pkg.cgi) would need to be updated.
1074 owed_setup and owed_recur could then be repaced by just owed, and
1075 cust_bill::open_cust_bill_pkg and
1076 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1080 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1081 from the base documentation.