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 use List::Util qw(sum);
24 @ISA = qw( FS::cust_main_Mixin FS::Record );
27 $me = '[FS::cust_bill_pkg]';
31 FS::cust_bill_pkg - Object methods for cust_bill_pkg records
35 use FS::cust_bill_pkg;
37 $record = new FS::cust_bill_pkg \%hash;
38 $record = new FS::cust_bill_pkg { 'column' => 'value' };
40 $error = $record->insert;
42 $error = $record->check;
46 An FS::cust_bill_pkg object represents an invoice line item.
47 FS::cust_bill_pkg inherits from FS::Record. The following fields are currently
58 invoice (see L<FS::cust_bill>)
62 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)
64 =item pkgpart_override
66 optional package definition (see L<FS::part_pkg>) override
78 starting date of recurring fee
82 ending date of recurring fee
86 Line item description (overrides normal package description)
90 If not set, defaults to 1
94 If not set, defaults to setup
98 If not set, defaults to recur
102 If set to Y, indicates data should not appear as separate line item on invoice
106 sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">. Also
107 see L<Time::Local> and L<Date::Parse> for conversion functions.
115 Creates a new line item. To add the line item to the database, see
116 L<"insert">. Line items are normally created by calling the bill method of a
117 customer object (see L<FS::cust_main>).
121 sub table { 'cust_bill_pkg'; }
125 Adds this line item to the database. If there is an error, returns the error,
126 otherwise returns false.
133 local $SIG{HUP} = 'IGNORE';
134 local $SIG{INT} = 'IGNORE';
135 local $SIG{QUIT} = 'IGNORE';
136 local $SIG{TERM} = 'IGNORE';
137 local $SIG{TSTP} = 'IGNORE';
138 local $SIG{PIPE} = 'IGNORE';
140 my $oldAutoCommit = $FS::UID::AutoCommit;
141 local $FS::UID::AutoCommit = 0;
144 my $error = $self->SUPER::insert;
146 $dbh->rollback if $oldAutoCommit;
150 if ( $self->get('details') ) {
151 foreach my $detail ( @{$self->get('details')} ) {
152 $detail->billpkgnum($self->billpkgnum);
153 $error = $detail->insert;
155 $dbh->rollback if $oldAutoCommit;
156 return "error inserting cust_bill_pkg_detail: $error";
161 if ( $self->get('display') ) {
162 foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
163 $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
164 $error = $cust_bill_pkg_display->insert;
166 $dbh->rollback if $oldAutoCommit;
167 return "error inserting cust_bill_pkg_display: $error";
172 if ( $self->get('discounts') ) {
173 foreach my $cust_bill_pkg_discount ( @{$self->get('discounts')} ) {
174 $cust_bill_pkg_discount->billpkgnum($self->billpkgnum);
175 $error = $cust_bill_pkg_discount->insert;
177 $dbh->rollback if $oldAutoCommit;
178 return "error inserting cust_bill_pkg_discount: $error";
183 if ( $self->_cust_tax_exempt_pkg ) {
184 foreach my $cust_tax_exempt_pkg ( @{$self->_cust_tax_exempt_pkg} ) {
185 $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
186 $error = $cust_tax_exempt_pkg->insert;
188 $dbh->rollback if $oldAutoCommit;
189 return "error inserting cust_tax_exempt_pkg: $error";
194 my $tax_location = $self->get('cust_bill_pkg_tax_location');
195 if ( $tax_location ) {
196 foreach my $cust_bill_pkg_tax_location ( @$tax_location ) {
197 $cust_bill_pkg_tax_location->billpkgnum($self->billpkgnum);
198 $error = $cust_bill_pkg_tax_location->insert;
200 $dbh->rollback if $oldAutoCommit;
201 return "error inserting cust_bill_pkg_tax_location: $error";
206 my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
207 if ( $tax_rate_location ) {
208 foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
209 $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
210 $error = $cust_bill_pkg_tax_rate_location->insert;
212 $dbh->rollback if $oldAutoCommit;
213 return "error inserting cust_bill_pkg_tax_rate_location: $error";
218 my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
219 if ( $cust_tax_adjustment ) {
220 $cust_tax_adjustment->billpkgnum($self->billpkgnum);
221 $error = $cust_tax_adjustment->replace;
223 $dbh->rollback if $oldAutoCommit;
224 return "error replacing cust_tax_adjustment: $error";
228 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
242 local $SIG{HUP} = 'IGNORE';
243 local $SIG{INT} = 'IGNORE';
244 local $SIG{QUIT} = 'IGNORE';
245 local $SIG{TERM} = 'IGNORE';
246 local $SIG{TSTP} = 'IGNORE';
247 local $SIG{PIPE} = 'IGNORE';
249 my $oldAutoCommit = $FS::UID::AutoCommit;
250 local $FS::UID::AutoCommit = 0;
253 foreach my $table (qw(
255 cust_bill_pkg_display
256 cust_bill_pkg_tax_location
257 cust_bill_pkg_tax_rate_location
263 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
264 my $error = $linked->delete;
266 $dbh->rollback if $oldAutoCommit;
273 foreach my $cust_tax_adjustment (
274 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
276 $cust_tax_adjustment->billpkgnum(''); #NULL
277 my $error = $cust_tax_adjustment->replace;
279 $dbh->rollback if $oldAutoCommit;
284 my $error = $self->SUPER::delete(@_);
286 $dbh->rollback if $oldAutoCommit;
290 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
296 #alas, bin/follow-tax-rename
298 #=item replace OLD_RECORD
300 #Currently unimplemented. This would be even more of an accounting nightmare
301 #than deleteing the items. Just don't do it.
306 # return "Can't modify cust_bill_pkg records!";
311 Checks all fields to make sure this is a valid line item. If there is an
312 error, returns the error, otherwise returns false. Called by the insert
321 $self->ut_numbern('billpkgnum')
322 || $self->ut_snumber('pkgnum')
323 || $self->ut_number('invnum')
324 || $self->ut_money('setup')
325 || $self->ut_money('recur')
326 || $self->ut_numbern('sdate')
327 || $self->ut_numbern('edate')
328 || $self->ut_textn('itemdesc')
329 || $self->ut_textn('itemcomment')
330 || $self->ut_enum('hidden', [ '', 'Y' ])
332 return $error if $error;
334 $self->regularize_details;
336 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
337 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
338 return "Unknown pkgnum ". $self->pkgnum
339 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
342 return "Unknown invnum"
343 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
348 =item regularize_details
350 Converts the contents of the 'details' pseudo-field to
351 L<FS::cust_bill_pkg_detail> objects, if they aren't already.
355 sub regularize_details {
357 if ( $self->get('details') ) {
358 foreach my $detail ( @{$self->get('details')} ) {
359 if ( ref($detail) ne 'FS::cust_bill_pkg_detail' ) {
360 # then turn it into one
362 if ( ! ref($detail) ) {
363 $hash{'detail'} = 'detail';
365 elsif ( ref($detail) eq 'HASH' ) {
368 elsif ( ref($detail) eq 'ARRAY' ) {
369 carp "passing invoice details as arrays is deprecated";
370 #carp "this way sucks, use a hash"; #but more useful/friendly
371 $hash{'format'} = $detail->[0];
372 $hash{'detail'} = $detail->[1];
373 $hash{'amount'} = $detail->[2];
374 $hash{'classnum'} = $detail->[3];
375 $hash{'phonenum'} = $detail->[4];
376 $hash{'accountcode'} = $detail->[5];
377 $hash{'startdate'} = $detail->[6];
378 $hash{'duration'} = $detail->[7];
379 $hash{'regionname'} = $detail->[8];
382 die "unknown detail type ". ref($detail);
384 $detail = new FS::cust_bill_pkg_detail \%hash;
386 $detail->billpkgnum($self->billpkgnum) if $self->billpkgnum;
394 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
400 carp "$me $self -> cust_pkg" if $DEBUG;
401 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
406 Returns the package definition for this invoice line item.
412 if ( $self->pkgpart_override ) {
413 qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
416 my $cust_pkg = $self->cust_pkg;
417 $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
424 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
430 qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
433 =item previous_cust_bill_pkg
435 Returns the previous cust_bill_pkg for this package, if any.
439 sub previous_cust_bill_pkg {
441 return unless $self->sdate;
443 'table' => 'cust_bill_pkg',
444 'hashref' => { 'pkgnum' => $self->pkgnum,
445 'sdate' => { op=>'<', value=>$self->sdate },
447 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
451 =item details [ OPTION => VALUE ... ]
453 Returns an array of detail information for the invoice line item.
455 Currently available options are: I<format>, I<escape_function> and
458 If I<format> is set to html or latex then the array members are improved
459 for tabular appearance in those environments if possible.
461 If I<escape_function> is set then the array members are processed by this
462 function before being returned.
464 I<format_function> overrides the normal HTML or LaTeX function for returning
465 formatted CDRs. It can be set to a subroutine which returns an empty list
466 to skip usage detail:
468 'format_function' => sub { () },
473 my ( $self, %opt ) = @_;
474 my $escape_function = $opt{escape_function} || sub { shift };
476 my $csv = new Text::CSV_XS;
478 if ( $opt{format_function} ) {
480 #this still expects to be passed a cust_bill_pkg_detail object as the
481 #second argument, which is expensive
482 carp "deprecated format_function passed to cust_bill_pkg->details";
483 my $format_sub = $opt{format_function} if $opt{format_function};
485 map { ( $_->format eq 'C'
486 ? &{$format_sub}( $_->detail, $_ )
487 : &{$escape_function}( $_->detail )
490 qsearch ({ 'table' => 'cust_bill_pkg_detail',
491 'hashref' => { 'billpkgnum' => $self->billpkgnum },
492 'order_by' => 'ORDER BY detailnum',
495 } elsif ( $opt{'no_usage'} ) {
497 my $sql = "SELECT detail FROM cust_bill_pkg_detail ".
498 " WHERE billpkgnum = ". $self->billpkgnum.
499 " AND ( format IS NULL OR format != 'C' ) ".
500 " ORDER BY detailnum";
501 my $sth = dbh->prepare($sql) or die dbh->errstr;
502 $sth->execute or die $sth->errstr;
504 map &{$escape_function}( $_->[0] ), @{ $sth->fetchall_arrayref };
509 my $format = $opt{format} || '';
510 if ( $format eq 'html' ) {
512 $format_sub = sub { my $detail = shift;
513 $csv->parse($detail) or return "can't parse $detail";
514 join('</TD><TD>', map { &$escape_function($_) }
519 } elsif ( $format eq 'latex' ) {
523 $csv->parse($detail) or return "can't parse $detail";
524 #join(' & ', map { '\small{'. &$escape_function($_). '}' }
528 foreach ($csv->fields) {
529 $result .= ' & ' if $column > 1;
530 if ($column > 6) { # KLUDGE ALERT!
531 $result .= '\multicolumn{1}{l}{\scriptsize{'.
532 &$escape_function($_). '}}';
534 $result .= '\scriptsize{'. &$escape_function($_). '}';
543 $format_sub = sub { my $detail = shift;
544 $csv->parse($detail) or return "can't parse $detail";
545 join(' - ', map { &$escape_function($_) }
552 my $sql = "SELECT format, detail FROM cust_bill_pkg_detail ".
553 " WHERE billpkgnum = ". $self->billpkgnum.
554 " ORDER BY detailnum";
555 my $sth = dbh->prepare($sql) or die dbh->errstr;
556 $sth->execute or die $sth->errstr;
558 #avoid the fetchall_arrayref and loop for less memory usage?
560 map { (defined($_->[0]) && $_->[0] eq 'C')
561 ? &{$format_sub}( $_->[1] )
562 : &{$escape_function}( $_->[1] );
564 @{ $sth->fetchall_arrayref };
570 =item details_header [ OPTION => VALUE ... ]
572 Returns a list representing an invoice line item detail header, if any.
573 This relies on the behavior of voip_cdr in that it expects the header
574 to be the first CSV formatted detail (as is expected by invoice generation
575 routines). Returns the empty list otherwise.
581 return '' unless defined dbdef->table('cust_bill_pkg_detail');
583 my $csv = new Text::CSV_XS;
586 qsearch ({ 'table' => 'cust_bill_pkg_detail',
587 'hashref' => { 'billpkgnum' => $self->billpkgnum,
590 'order_by' => 'ORDER BY detailnum LIMIT 1',
592 return() unless scalar(@detail);
593 $csv->parse($detail[0]->detail) or return ();
599 Returns a description for this line item. For typical line items, this is the
600 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
601 For one-shot line items and named taxes, it is the I<itemdesc> field of this
602 line item, and for generic taxes, simply returns "Tax".
609 if ( $self->pkgnum > 0 ) {
610 $self->itemdesc || $self->part_pkg->pkg;
612 my $desc = $self->itemdesc || 'Tax';
613 $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
620 Returns the amount owed (still outstanding) on this line item's setup fee,
621 which is the amount of the line item minus all payment applications (see
622 L<FS::cust_bill_pay_pkg> and credit applications (see
623 L<FS::cust_credit_bill_pkg>).
629 $self->owed('setup', @_);
634 Returns the amount owed (still outstanding) on this line item's recurring fee,
635 which is the amount of the line item minus all payment applications (see
636 L<FS::cust_bill_pay_pkg> and credit applications (see
637 L<FS::cust_credit_bill_pkg>).
643 $self->owed('recur', @_);
646 # modeled after cust_bill::owed...
648 my( $self, $field ) = @_;
649 my $balance = $self->$field();
650 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
651 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
652 $balance = sprintf( '%.2f', $balance );
653 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
659 my( $self, $field ) = @_;
660 my $balance = $self->$field();
661 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
662 $balance = sprintf( '%.2f', $balance );
663 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
667 sub cust_bill_pay_pkg {
668 my( $self, $field ) = @_;
669 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
670 'setuprecur' => $field,
675 sub cust_credit_bill_pkg {
676 my( $self, $field ) = @_;
677 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
678 'setuprecur' => $field,
685 Returns the number of billing units (for tax purposes) represented by this,
692 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
700 my( $self, $value ) = @_;
701 if ( defined($value) ) {
702 $self->setfield('quantity', $value);
704 $self->getfield('quantity') || 1;
712 my( $self, $value ) = @_;
713 if ( defined($value) ) {
714 $self->setfield('unitsetup', $value);
716 $self->getfield('unitsetup') eq ''
717 ? $self->getfield('setup')
718 : $self->getfield('unitsetup');
726 my( $self, $value ) = @_;
727 if ( defined($value) ) {
728 $self->setfield('unitrecur', $value);
730 $self->getfield('unitrecur') eq ''
731 ? $self->getfield('recur')
732 : $self->getfield('unitrecur');
735 =item set_display OPTION => VALUE ...
737 A helper method for I<insert>, populates the pseudo-field B<display> with
738 appropriate FS::cust_bill_pkg_display objects.
740 Options are passed as a list of name/value pairs. Options are:
742 part_pkg: FS::part_pkg object from the
744 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.
749 my( $self, %opt ) = @_;
750 my $part_pkg = $opt{'part_pkg'};
751 my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
753 my $conf = new FS::Conf;
755 my $separate = $conf->exists('separate_usage');
756 my $usage_mandate = $part_pkg->option('usage_mandate', 'Hush!')
757 || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
759 # or use the category from $opt{'part_pkg'} if its not bundled?
760 my $categoryname = $cust_pkg->part_pkg->categoryname;
762 return $self->set('display', [])
763 unless $separate || $categoryname || $usage_mandate;
767 my %hash = ( 'section' => $categoryname );
769 my $usage_section = $part_pkg->option('usage_section', 'Hush!')
770 || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
772 my $summary = $part_pkg->option('summarize_usage', 'Hush!')
773 || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
776 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
777 push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
779 push @display, new FS::cust_bill_pkg_display
782 ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
786 if ($separate && $usage_section && $summary) {
787 push @display, new FS::cust_bill_pkg_display { type => 'U',
792 if ($usage_mandate || ($usage_section && $summary) ) {
793 $hash{post_total} = 'Y';
796 if ($separate || $usage_mandate) {
797 $hash{section} = $usage_section if $usage_section;
798 push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
801 $self->set('display', \@display);
807 Returns a list of cust_bill_pkg objects each with no more than a single class
808 (including setup or recur) of charge.
814 # XXX this goes away with cust_bill_pkg refactor
816 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
817 my %cust_bill_pkg = ();
819 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
820 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
823 #split setup and recur
824 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
825 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
826 $cust_bill_pkg->set('details', []);
827 $cust_bill_pkg->recur(0);
828 $cust_bill_pkg->unitrecur(0);
829 $cust_bill_pkg->type('');
830 $cust_bill_pkg_recur->setup(0);
831 $cust_bill_pkg_recur->unitsetup(0);
832 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
836 #split usage from recur
837 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
838 if exists($cust_bill_pkg{recur});
839 warn "usage is $usage\n" if $DEBUG > 1;
841 my $cust_bill_pkg_usage =
842 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
843 $cust_bill_pkg_usage->recur( $usage );
844 $cust_bill_pkg_usage->type( 'U' );
845 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
846 $cust_bill_pkg{recur}->recur( $recur );
847 $cust_bill_pkg{recur}->type( '' );
848 $cust_bill_pkg{recur}->set('details', []);
849 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
852 #subdivide usage by usage_class
853 if (exists($cust_bill_pkg{''})) {
854 foreach my $class (grep { $_ } $self->usage_classes) {
855 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
856 my $cust_bill_pkg_usage =
857 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
858 $cust_bill_pkg_usage->recur( $usage );
859 $cust_bill_pkg_usage->set('details', []);
860 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
861 $cust_bill_pkg{''}->recur( $classless );
862 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
864 warn "Unexpected classless usage value: ". $cust_bill_pkg{''}->recur
865 if ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur < 0);
866 delete $cust_bill_pkg{''}
867 unless ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur > 0);
870 # # sort setup,recur,'', and the rest numeric && return
871 # my @result = map { $cust_bill_pkg{$_} }
872 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
873 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
875 # keys %cust_bill_pkg;
884 Returns the amount of the charge associated with usage class CLASSNUM if
885 CLASSNUM is defined. Otherwise returns the total charge associated with
891 my( $self, $classnum ) = @_;
892 $self->regularize_details;
894 if ( $self->get('details') ) {
897 map { $_->amount || 0 }
898 grep { !defined($classnum) or $classnum eq $_->classnum }
899 @{ $self->get('details') }
904 my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
905 ' WHERE billpkgnum = '. $self->billpkgnum;
906 $sql .= " AND classnum = $classnum" if defined($classnum);
908 my $sth = dbh->prepare($sql) or die dbh->errstr;
909 $sth->execute or die $sth->errstr;
911 return $sth->fetchrow_arrayref->[0];
919 Returns a list of usage classnums associated with this invoice line's
926 $self->regularize_details;
928 if ( $self->get('details') ) {
930 my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
936 qsearch({ table => 'cust_bill_pkg_detail',
937 hashref => { billpkgnum => $self->billpkgnum },
938 select => 'DISTINCT classnum',
945 =item cust_bill_pkg_display [ type => TYPE ]
947 Returns an array of display information for the invoice line item optionally
952 sub cust_bill_pkg_display {
953 my ( $self, %opt ) = @_;
956 new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
958 return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
960 my $type = $opt{type} if exists $opt{type};
963 if ( $self->get('display') ) {
964 @result = grep { defined($type) ? ($type eq $_->type) : 1 }
965 @{ $self->get('display') };
967 my $hashref = { 'billpkgnum' => $self->billpkgnum };
968 $hashref->{type} = $type if defined($type);
970 @result = qsearch ({ 'table' => 'cust_bill_pkg_display',
971 'hashref' => { 'billpkgnum' => $self->billpkgnum },
972 'order_by' => 'ORDER BY billpkgdisplaynum',
976 push @result, $default unless ( scalar(@result) || $type );
982 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
983 # and FS::cust_main::bill
985 sub _cust_tax_exempt_pkg {
988 $self->{Hash}->{_cust_tax_exempt_pkg} or
989 $self->{Hash}->{_cust_tax_exempt_pkg} = [];
993 =item cust_bill_pkg_tax_Xlocation
995 Returns the list of associated cust_bill_pkg_tax_location and/or
996 cust_bill_pkg_tax_rate_location objects
1000 sub cust_bill_pkg_tax_Xlocation {
1003 my %hash = ( 'billpkgnum' => $self->billpkgnum );
1006 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
1007 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
1012 =item cust_bill_pkg_detail [ CLASSNUM ]
1014 Returns the list of associated cust_bill_pkg_detail objects
1015 The optional CLASSNUM argument will limit the details to the specified usage
1020 sub cust_bill_pkg_detail {
1022 my $classnum = shift || '';
1024 my %hash = ( 'billpkgnum' => $self->billpkgnum );
1025 $hash{classnum} = $classnum if $classnum;
1027 qsearch( 'cust_bill_pkg_detail', \%hash ),
1031 =item cust_bill_pkg_discount
1033 Returns the list of associated cust_bill_pkg_discount objects.
1037 sub cust_bill_pkg_discount {
1039 qsearch( 'cust_bill_pkg_discount', { 'billpkgnum' => $self->billpkgnum } );
1042 =item recur_show_zero
1046 sub recur_show_zero {
1050 #&& $self->cust_pkg->part_pkg->recur_show_zero;
1052 shift->_X_show_zero('recur');
1056 sub setup_show_zero {
1057 shift->_X_show_zero('setup');
1061 my( $self, $what ) = @_;
1063 return 0 unless $self->$what() == 0 && $self->pkgnum;
1065 $self->cust_pkg->_X_show_zero($what);
1072 setup and recur shouldn't be separate fields. There should be one "amount"
1073 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1075 A line item with both should really be two separate records (preserving
1076 sdate and edate for setup fees for recurring packages - that information may
1077 be valuable later). Invoice generation (cust_main::bill), invoice printing
1078 (cust_bill), tax reports (report_tax.cgi) and line item reports
1079 (cust_bill_pkg.cgi) would need to be updated.
1081 owed_setup and owed_recur could then be repaced by just owed, and
1082 cust_bill::open_cust_bill_pkg and
1083 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1087 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1088 from the base documentation.