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')} ) {
150 if ( ref($detail) ) {
151 if ( ref($detail) eq 'ARRAY' ) {
152 #carp "this way sucks, use a hash"; #but more useful/friendly
153 $hash{'format'} = $detail->[0];
154 $hash{'detail'} = $detail->[1];
155 $hash{'amount'} = $detail->[2];
156 $hash{'classnum'} = $detail->[3];
157 $hash{'phonenum'} = $detail->[4];
158 $hash{'accountcode'} = $detail->[5];
159 $hash{'startdate'} = $detail->[6];
160 $hash{'duration'} = $detail->[7];
161 $hash{'regionname'} = $detail->[8];
162 } elsif ( ref($detail) eq 'HASH' ) {
165 die "unknow detail type ". ref($detail);
168 $hash{'detail'} = $detail;
170 $hash{'billpkgnum'} = $self->billpkgnum;
171 my $cust_bill_pkg_detail = new FS::cust_bill_pkg_detail \%hash;
172 $error = $cust_bill_pkg_detail->insert;
174 $dbh->rollback if $oldAutoCommit;
175 return "error inserting cust_bill_pkg_detail: $error";
180 if ( $self->get('display') ) {
181 foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
182 $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
183 $error = $cust_bill_pkg_display->insert;
185 $dbh->rollback if $oldAutoCommit;
186 return "error inserting cust_bill_pkg_display: $error";
191 if ( $self->get('discounts') ) {
192 foreach my $cust_bill_pkg_discount ( @{$self->get('discounts')} ) {
193 $cust_bill_pkg_discount->billpkgnum($self->billpkgnum);
194 $error = $cust_bill_pkg_discount->insert;
196 $dbh->rollback if $oldAutoCommit;
197 return "error inserting cust_bill_pkg_discount: $error";
202 if ( $self->_cust_tax_exempt_pkg ) {
203 foreach my $cust_tax_exempt_pkg ( @{$self->_cust_tax_exempt_pkg} ) {
204 $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
205 $error = $cust_tax_exempt_pkg->insert;
207 $dbh->rollback if $oldAutoCommit;
208 return "error inserting cust_tax_exempt_pkg: $error";
213 my $tax_location = $self->get('cust_bill_pkg_tax_location');
214 if ( $tax_location ) {
215 foreach my $cust_bill_pkg_tax_location ( @$tax_location ) {
216 $cust_bill_pkg_tax_location->billpkgnum($self->billpkgnum);
217 $error = $cust_bill_pkg_tax_location->insert;
219 $dbh->rollback if $oldAutoCommit;
220 return "error inserting cust_bill_pkg_tax_location: $error";
225 my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
226 if ( $tax_rate_location ) {
227 foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
228 $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
229 $error = $cust_bill_pkg_tax_rate_location->insert;
231 $dbh->rollback if $oldAutoCommit;
232 return "error inserting cust_bill_pkg_tax_rate_location: $error";
237 my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
238 if ( $cust_tax_adjustment ) {
239 $cust_tax_adjustment->billpkgnum($self->billpkgnum);
240 $error = $cust_tax_adjustment->replace;
242 $dbh->rollback if $oldAutoCommit;
243 return "error replacing cust_tax_adjustment: $error";
247 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
261 local $SIG{HUP} = 'IGNORE';
262 local $SIG{INT} = 'IGNORE';
263 local $SIG{QUIT} = 'IGNORE';
264 local $SIG{TERM} = 'IGNORE';
265 local $SIG{TSTP} = 'IGNORE';
266 local $SIG{PIPE} = 'IGNORE';
268 my $oldAutoCommit = $FS::UID::AutoCommit;
269 local $FS::UID::AutoCommit = 0;
272 foreach my $table (qw(
274 cust_bill_pkg_display
275 cust_bill_pkg_tax_location
276 cust_bill_pkg_tax_rate_location
282 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
283 my $error = $linked->delete;
285 $dbh->rollback if $oldAutoCommit;
292 foreach my $cust_tax_adjustment (
293 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
295 $cust_tax_adjustment->billpkgnum(''); #NULL
296 my $error = $cust_tax_adjustment->replace;
298 $dbh->rollback if $oldAutoCommit;
303 my $error = $self->SUPER::delete(@_);
305 $dbh->rollback if $oldAutoCommit;
309 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
315 #alas, bin/follow-tax-rename
317 #=item replace OLD_RECORD
319 #Currently unimplemented. This would be even more of an accounting nightmare
320 #than deleteing the items. Just don't do it.
325 # return "Can't modify cust_bill_pkg records!";
330 Checks all fields to make sure this is a valid line item. If there is an
331 error, returns the error, otherwise returns false. Called by the insert
340 $self->ut_numbern('billpkgnum')
341 || $self->ut_snumber('pkgnum')
342 || $self->ut_number('invnum')
343 || $self->ut_money('setup')
344 || $self->ut_money('recur')
345 || $self->ut_numbern('sdate')
346 || $self->ut_numbern('edate')
347 || $self->ut_textn('itemdesc')
348 || $self->ut_textn('itemcomment')
349 || $self->ut_enum('hidden', [ '', 'Y' ])
351 return $error if $error;
353 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
354 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
355 return "Unknown pkgnum ". $self->pkgnum
356 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
359 return "Unknown invnum"
360 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
367 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
373 carp "$me $self -> cust_pkg" if $DEBUG;
374 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
379 Returns the package definition for this invoice line item.
385 if ( $self->pkgpart_override ) {
386 qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
389 my $cust_pkg = $self->cust_pkg;
390 $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
397 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
403 qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
406 =item previous_cust_bill_pkg
408 Returns the previous cust_bill_pkg for this package, if any.
412 sub previous_cust_bill_pkg {
414 return unless $self->sdate;
416 'table' => 'cust_bill_pkg',
417 'hashref' => { 'pkgnum' => $self->pkgnum,
418 'sdate' => { op=>'<', value=>$self->sdate },
420 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
424 =item details [ OPTION => VALUE ... ]
426 Returns an array of detail information for the invoice line item.
428 Currently available options are: I<format>, I<escape_function> and
431 If I<format> is set to html or latex then the array members are improved
432 for tabular appearance in those environments if possible.
434 If I<escape_function> is set then the array members are processed by this
435 function before being returned.
437 I<format_function> overrides the normal HTML or LaTeX function for returning
438 formatted CDRs. It can be set to a subroutine which returns an empty list
439 to skip usage detail:
441 'format_function' => sub { () },
446 my ( $self, %opt ) = @_;
447 my $format = $opt{format} || '';
448 my $escape_function = $opt{escape_function} || sub { shift };
449 return () unless defined dbdef->table('cust_bill_pkg_detail');
451 eval "use Text::CSV_XS;";
453 my $csv = new Text::CSV_XS;
455 my $format_sub = sub { my $detail = shift;
456 $csv->parse($detail) or return "can't parse $detail";
457 join(' - ', map { &$escape_function($_) }
462 $format_sub = sub { my $detail = shift;
463 $csv->parse($detail) or return "can't parse $detail";
464 join('</TD><TD>', map { &$escape_function($_) }
468 if $format eq 'html';
470 $format_sub = sub { my $detail = shift;
471 $csv->parse($detail) or return "can't parse $detail";
472 #join(' & ', map { '\small{'. &$escape_function($_). '}' }
476 foreach ($csv->fields) {
477 $result .= ' & ' if $column > 1;
478 if ($column > 6) { # KLUDGE ALERT!
479 $result .= '\multicolumn{1}{l}{\scriptsize{'.
480 &$escape_function($_). '}}';
482 $result .= '\scriptsize{'. &$escape_function($_). '}';
488 if $format eq 'latex';
490 $format_sub = $opt{format_function} if $opt{format_function};
492 map { ( $_->format eq 'C'
493 ? &{$format_sub}( $_->detail, $_ )
494 : &{$escape_function}( $_->detail )
497 qsearch ({ 'table' => 'cust_bill_pkg_detail',
498 'hashref' => { 'billpkgnum' => $self->billpkgnum },
499 'order_by' => 'ORDER BY detailnum',
501 #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum });
504 =item details_header [ OPTION => VALUE ... ]
506 Returns a list representing an invoice line item detail header, if any.
507 This relies on the behavior of voip_cdr in that it expects the header
508 to be the first CSV formatted detail (as is expected by invoice generation
509 routines). Returns the empty list otherwise.
515 return '' unless defined dbdef->table('cust_bill_pkg_detail');
517 eval "use Text::CSV_XS;";
519 my $csv = new Text::CSV_XS;
522 qsearch ({ 'table' => 'cust_bill_pkg_detail',
523 'hashref' => { 'billpkgnum' => $self->billpkgnum,
526 'order_by' => 'ORDER BY detailnum LIMIT 1',
528 return() unless scalar(@detail);
529 $csv->parse($detail[0]->detail) or return ();
535 Returns a description for this line item. For typical line items, this is the
536 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
537 For one-shot line items and named taxes, it is the I<itemdesc> field of this
538 line item, and for generic taxes, simply returns "Tax".
545 if ( $self->pkgnum > 0 ) {
546 $self->itemdesc || $self->part_pkg->pkg;
548 my $desc = $self->itemdesc || 'Tax';
549 $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
556 Returns the amount owed (still outstanding) on this line item's setup fee,
557 which is the amount of the line item minus all payment applications (see
558 L<FS::cust_bill_pay_pkg> and credit applications (see
559 L<FS::cust_credit_bill_pkg>).
565 $self->owed('setup', @_);
570 Returns the amount owed (still outstanding) on this line item's recurring fee,
571 which is the amount of the line item minus all payment applications (see
572 L<FS::cust_bill_pay_pkg> and credit applications (see
573 L<FS::cust_credit_bill_pkg>).
579 $self->owed('recur', @_);
582 # modeled after cust_bill::owed...
584 my( $self, $field ) = @_;
585 my $balance = $self->$field();
586 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
587 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
588 $balance = sprintf( '%.2f', $balance );
589 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
595 my( $self, $field ) = @_;
596 my $balance = $self->$field();
597 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
598 $balance = sprintf( '%.2f', $balance );
599 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
603 sub cust_bill_pay_pkg {
604 my( $self, $field ) = @_;
605 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
606 'setuprecur' => $field,
611 sub cust_credit_bill_pkg {
612 my( $self, $field ) = @_;
613 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
614 'setuprecur' => $field,
621 Returns the number of billing units (for tax purposes) represented by this,
628 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
636 my( $self, $value ) = @_;
637 if ( defined($value) ) {
638 $self->setfield('quantity', $value);
640 $self->getfield('quantity') || 1;
648 my( $self, $value ) = @_;
649 if ( defined($value) ) {
650 $self->setfield('unitsetup', $value);
652 $self->getfield('unitsetup') eq ''
653 ? $self->getfield('setup')
654 : $self->getfield('unitsetup');
662 my( $self, $value ) = @_;
663 if ( defined($value) ) {
664 $self->setfield('unitrecur', $value);
666 $self->getfield('unitrecur') eq ''
667 ? $self->getfield('recur')
668 : $self->getfield('unitrecur');
671 =item set_display OPTION => VALUE ...
673 A helper method for I<insert>, populates the pseudo-field B<display> with
674 appropriate FS::cust_bill_pkg_display objects.
676 Options are passed as a list of name/value pairs. Options are:
678 part_pkg: FS::part_pkg object from the
680 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.
685 my( $self, %opt ) = @_;
686 my $part_pkg = $opt{'part_pkg'};
687 my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
689 my $conf = new FS::Conf;
691 my $separate = $conf->exists('separate_usage');
692 my $usage_mandate = $part_pkg->option('usage_mandate', 'Hush!')
693 || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
695 # or use the category from $opt{'part_pkg'} if its not bundled?
696 my $categoryname = $cust_pkg->part_pkg->categoryname;
698 return $self->set('display', [])
699 unless $separate || $categoryname || $usage_mandate;
703 my %hash = ( 'section' => $categoryname );
705 my $usage_section = $part_pkg->option('usage_section', 'Hush!')
706 || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
708 my $summary = $part_pkg->option('summarize_usage', 'Hush!')
709 || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
712 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
713 push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
715 push @display, new FS::cust_bill_pkg_display
718 ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
722 if ($separate && $usage_section && $summary) {
723 push @display, new FS::cust_bill_pkg_display { type => 'U',
728 if ($usage_mandate || ($usage_section && $summary) ) {
729 $hash{post_total} = 'Y';
732 if ($separate || $usage_mandate) {
733 $hash{section} = $usage_section if $usage_section;
734 push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
737 $self->set('display', \@display);
743 Returns a list of cust_bill_pkg objects each with no more than a single class
744 (including setup or recur) of charge.
750 # XXX this goes away with cust_bill_pkg refactor
752 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
753 my %cust_bill_pkg = ();
755 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
756 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
759 #split setup and recur
760 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
761 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
762 $cust_bill_pkg->set('details', []);
763 $cust_bill_pkg->recur(0);
764 $cust_bill_pkg->unitrecur(0);
765 $cust_bill_pkg->type('');
766 $cust_bill_pkg_recur->setup(0);
767 $cust_bill_pkg_recur->unitsetup(0);
768 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
772 #split usage from recur
773 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
774 if exists($cust_bill_pkg{recur});
775 warn "usage is $usage\n" if $DEBUG > 1;
777 my $cust_bill_pkg_usage =
778 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
779 $cust_bill_pkg_usage->recur( $usage );
780 $cust_bill_pkg_usage->type( 'U' );
781 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
782 $cust_bill_pkg{recur}->recur( $recur );
783 $cust_bill_pkg{recur}->type( '' );
784 $cust_bill_pkg{recur}->set('details', []);
785 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
788 #subdivide usage by usage_class
789 if (exists($cust_bill_pkg{''})) {
790 foreach my $class (grep { $_ } $self->usage_classes) {
791 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
792 my $cust_bill_pkg_usage =
793 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
794 $cust_bill_pkg_usage->recur( $usage );
795 $cust_bill_pkg_usage->set('details', []);
796 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
797 $cust_bill_pkg{''}->recur( $classless );
798 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
800 warn "Unexpected classless usage value: ". $cust_bill_pkg{''}->recur
801 if ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur < 0);
802 delete $cust_bill_pkg{''}
803 unless ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur > 0);
806 # # sort setup,recur,'', and the rest numeric && return
807 # my @result = map { $cust_bill_pkg{$_} }
808 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
809 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
811 # keys %cust_bill_pkg;
820 Returns the amount of the charge associated with usage class CLASSNUM if
821 CLASSNUM is defined. Otherwise returns the total charge associated with
827 my( $self, $classnum ) = @_;
830 if ( $self->get('details') ) {
833 map { ref($_) eq 'HASH'
837 grep { ref($_) && ( defined($classnum)
838 ? $classnum eq ( ref($_) eq 'HASH'
845 @{ $self->get('details') }
847 $sum += $value if $value;
854 my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
855 ' WHERE billpkgnum = '. $self->billpkgnum;
856 $sql .= " AND classnum = $classnum" if defined($classnum);
858 my $sth = dbh->prepare($sql) or die dbh->errstr;
859 $sth->execute or die $sth->errstr;
861 return $sth->fetchrow_arrayref->[0];
869 Returns a list of usage classnums associated with this invoice line's
877 if ( $self->get('details') ) {
880 foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
881 $seen{ ref($detail) eq 'HASH'
882 ? $detail->{'classnum'}
891 qsearch({ table => 'cust_bill_pkg_detail',
892 hashref => { billpkgnum => $self->billpkgnum },
893 select => 'DISTINCT classnum',
900 =item cust_bill_pkg_display [ type => TYPE ]
902 Returns an array of display information for the invoice line item optionally
907 sub cust_bill_pkg_display {
908 my ( $self, %opt ) = @_;
911 new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
913 return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
915 my $type = $opt{type} if exists $opt{type};
918 if ( $self->get('display') ) {
919 @result = grep { defined($type) ? ($type eq $_->type) : 1 }
920 @{ $self->get('display') };
922 my $hashref = { 'billpkgnum' => $self->billpkgnum };
923 $hashref->{type} = $type if defined($type);
925 @result = qsearch ({ 'table' => 'cust_bill_pkg_display',
926 'hashref' => { 'billpkgnum' => $self->billpkgnum },
927 'order_by' => 'ORDER BY billpkgdisplaynum',
931 push @result, $default unless ( scalar(@result) || $type );
937 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
938 # and FS::cust_main::bill
940 sub _cust_tax_exempt_pkg {
943 $self->{Hash}->{_cust_tax_exempt_pkg} or
944 $self->{Hash}->{_cust_tax_exempt_pkg} = [];
948 =item cust_bill_pkg_tax_Xlocation
950 Returns the list of associated cust_bill_pkg_tax_location and/or
951 cust_bill_pkg_tax_rate_location objects
955 sub cust_bill_pkg_tax_Xlocation {
958 my %hash = ( 'billpkgnum' => $self->billpkgnum );
961 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
962 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
967 =item cust_bill_pkg_detail [ CLASSNUM ]
969 Returns the list of associated cust_bill_pkg_detail objects
970 The optional CLASSNUM argument will limit the details to the specified usage
975 sub cust_bill_pkg_detail {
977 my $classnum = shift || '';
979 my %hash = ( 'billpkgnum' => $self->billpkgnum );
980 $hash{classnum} = $classnum if $classnum;
982 qsearch( 'cust_bill_pkg_detail', \%hash ),
986 =item cust_bill_pkg_discount
988 Returns the list of associated cust_bill_pkg_discount objects.
992 sub cust_bill_pkg_discount {
994 qsearch( 'cust_bill_pkg_discount', { 'billpkgnum' => $self->billpkgnum } );
997 =item recur_show_zero
1001 sub recur_show_zero {
1005 #&& $self->cust_pkg->part_pkg->recur_show_zero;
1007 shift->_X_show_zero('recur');
1011 sub setup_show_zero {
1012 shift->_X_show_zero('setup');
1016 my( $self, $what ) = @_;
1018 return 0 unless $self->$what() == 0 && $self->pkgnum;
1020 $self->cust_pkg->_X_show_zero($what);
1027 setup and recur shouldn't be separate fields. There should be one "amount"
1028 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1030 A line item with both should really be two separate records (preserving
1031 sdate and edate for setup fees for recurring packages - that information may
1032 be valuable later). Invoice generation (cust_main::bill), invoice printing
1033 (cust_bill), tax reports (report_tax.cgi) and line item reports
1034 (cust_bill_pkg.cgi) would need to be updated.
1036 owed_setup and owed_recur could then be repaced by just owed, and
1037 cust_bill::open_cust_bill_pkg and
1038 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1042 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1043 from the base documentation.