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 this line item's package.
718 real_pkgpart: if this line item comes from a bundled package, the pkgpart
719 of the owning package. Otherwise the same as the part_pkg's pkgpart above.
724 my( $self, %opt ) = @_;
725 my $part_pkg = $opt{'part_pkg'};
726 my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
728 my $conf = new FS::Conf;
730 # whether to break this down into setup/recur/usage
731 my $separate = $conf->exists('separate_usage');
733 my $usage_mandate = $part_pkg->option('usage_mandate', 'Hush!')
734 || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
736 # or use the category from $opt{'part_pkg'} if its not bundled?
737 my $categoryname = $cust_pkg->part_pkg->categoryname;
739 # if we don't have to separate setup/recur/usage, or put this in a
740 # package-specific section, or display a usage summary, then don't
741 # even create one of these. The item will just display in the unnamed
742 # section as a single line plus details.
743 return $self->set('display', [])
744 unless $separate || $categoryname || $usage_mandate;
748 my %hash = ( 'section' => $categoryname );
750 # whether to put usage details in a separate section, and if so, which one
751 my $usage_section = $part_pkg->option('usage_section', 'Hush!')
752 || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
754 # whether to show a usage summary line (total usage charges, no details)
755 my $summary = $part_pkg->option('summarize_usage', 'Hush!')
756 || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
759 # create lines for setup and (non-usage) recur, in the main section
760 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
761 push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
763 # display everything in a single line
764 push @display, new FS::cust_bill_pkg_display
767 # and if usage_mandate is enabled, hide details
768 # (this only works on multisection invoices...)
769 ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
773 if ($separate && $usage_section && $summary) {
774 # create a line for the usage summary in the main section
775 push @display, new FS::cust_bill_pkg_display { type => 'U',
781 if ($usage_mandate || ($usage_section && $summary) ) {
782 $hash{post_total} = 'Y';
785 if ($separate || $usage_mandate) {
786 # show call details for this line item in the usage section.
787 # if usage_mandate is on, this will display below the section subtotal.
788 # this also happens if usage is in a separate section and there's a
789 # summary in the main section, though I'm not sure why.
790 $hash{section} = $usage_section if $usage_section;
791 push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
794 $self->set('display', \@display);
800 Returns a hash: keys are "setup", "recur" or usage classnum, values are
801 FS::cust_bill_pkg objects, each with no more than a single class (setup or
808 # XXX this goes away with cust_bill_pkg refactor
810 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
811 my %cust_bill_pkg = ();
813 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
814 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
817 #split setup and recur
818 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
819 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
820 $cust_bill_pkg->set('details', []);
821 $cust_bill_pkg->recur(0);
822 $cust_bill_pkg->unitrecur(0);
823 $cust_bill_pkg->type('');
824 $cust_bill_pkg_recur->setup(0);
825 $cust_bill_pkg_recur->unitsetup(0);
826 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
830 #split usage from recur
831 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
832 if exists($cust_bill_pkg{recur});
833 warn "usage is $usage\n" if $DEBUG > 1;
835 my $cust_bill_pkg_usage =
836 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
837 $cust_bill_pkg_usage->recur( $usage );
838 $cust_bill_pkg_usage->type( 'U' );
839 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
840 $cust_bill_pkg{recur}->recur( $recur );
841 $cust_bill_pkg{recur}->type( '' );
842 $cust_bill_pkg{recur}->set('details', []);
843 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
846 #subdivide usage by usage_class
847 if (exists($cust_bill_pkg{''})) {
848 foreach my $class (grep { $_ } $self->usage_classes) {
849 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
850 my $cust_bill_pkg_usage =
851 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
852 $cust_bill_pkg_usage->recur( $usage );
853 $cust_bill_pkg_usage->set('details', []);
854 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
855 $cust_bill_pkg{''}->recur( $classless );
856 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
858 warn "Unexpected classless usage value: ". $cust_bill_pkg{''}->recur
859 if ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur < 0);
860 delete $cust_bill_pkg{''}
861 unless ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur > 0);
864 # # sort setup,recur,'', and the rest numeric && return
865 # my @result = map { $cust_bill_pkg{$_} }
866 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
867 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
869 # keys %cust_bill_pkg;
878 Returns the amount of the charge associated with usage class CLASSNUM if
879 CLASSNUM is defined. Otherwise returns the total charge associated with
885 my( $self, $classnum ) = @_;
887 if ( $self->get('details') ) {
891 map { ref($_) eq 'HASH'
895 grep { ref($_) && ( defined($classnum)
896 ? $classnum eq ( ref($_) eq 'HASH'
903 @{ $self->get('details') }
905 $sum += $value if $value;
912 my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
913 ' WHERE billpkgnum = '. $self->billpkgnum;
914 $sql .= " AND classnum = $classnum" if defined($classnum);
916 my $sth = dbh->prepare($sql) or die dbh->errstr;
917 $sth->execute or die $sth->errstr;
919 return $sth->fetchrow_arrayref->[0] || 0;
927 Returns a list of usage classnums associated with this invoice line's
935 if ( $self->get('details') ) {
938 foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
939 $seen{ (ref($detail) eq 'HASH'
940 ? $detail->{'classnum'}
941 : $detail->[3]) || ''
949 qsearch({ table => 'cust_bill_pkg_detail',
950 hashref => { billpkgnum => $self->billpkgnum },
951 select => 'DISTINCT classnum',
958 =item cust_bill_pkg_display [ type => TYPE ]
960 Returns an array of display information for the invoice line item optionally
965 sub cust_bill_pkg_display {
966 my ( $self, %opt ) = @_;
969 new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
971 return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
973 my $type = $opt{type} if exists $opt{type};
976 if ( $self->get('display') ) {
977 @result = grep { defined($type) ? ($type eq $_->type) : 1 }
978 @{ $self->get('display') };
980 my $hashref = { 'billpkgnum' => $self->billpkgnum };
981 $hashref->{type} = $type if defined($type);
983 @result = qsearch ({ 'table' => 'cust_bill_pkg_display',
984 'hashref' => { 'billpkgnum' => $self->billpkgnum },
985 'order_by' => 'ORDER BY billpkgdisplaynum',
989 push @result, $default unless ( scalar(@result) || $type );
995 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
996 # and FS::cust_main::bill
998 sub _cust_tax_exempt_pkg {
1001 $self->{Hash}->{_cust_tax_exempt_pkg} or
1002 $self->{Hash}->{_cust_tax_exempt_pkg} = [];
1006 =item cust_bill_pkg_tax_Xlocation
1008 Returns the list of associated cust_bill_pkg_tax_location and/or
1009 cust_bill_pkg_tax_rate_location objects
1013 sub cust_bill_pkg_tax_Xlocation {
1016 my %hash = ( 'billpkgnum' => $self->billpkgnum );
1019 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
1020 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
1025 =item cust_bill_pkg_detail [ CLASSNUM ]
1027 Returns the list of associated cust_bill_pkg_detail objects
1028 The optional CLASSNUM argument will limit the details to the specified usage
1033 sub cust_bill_pkg_detail {
1035 my $classnum = shift || '';
1037 my %hash = ( 'billpkgnum' => $self->billpkgnum );
1038 $hash{classnum} = $classnum if $classnum;
1040 qsearch( 'cust_bill_pkg_detail', \%hash ),
1044 =item cust_bill_pkg_discount
1046 Returns the list of associated cust_bill_pkg_discount objects.
1050 sub cust_bill_pkg_discount {
1052 qsearch( 'cust_bill_pkg_discount', { 'billpkgnum' => $self->billpkgnum } );
1055 =item recur_show_zero
1059 sub recur_show_zero {
1063 #&& $self->cust_pkg->part_pkg->recur_show_zero;
1065 shift->_X_show_zero('recur');
1069 sub setup_show_zero {
1070 shift->_X_show_zero('setup');
1074 my( $self, $what ) = @_;
1076 return 0 unless $self->$what() == 0 && $self->pkgnum;
1078 $self->cust_pkg->_X_show_zero($what);
1081 =item credited [ BEFORE, AFTER, OPTIONS ]
1083 Returns the sum of credits applied to this item. Arguments are the same as
1084 owed_sql/paid_sql/credited_sql.
1090 $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
1095 =head1 CLASS METHODS
1101 Returns an SQL expression for the total usage charges in details on
1107 '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0)
1108 FROM cust_bill_pkg_detail
1109 WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
1111 sub usage_sql { $usage_sql }
1113 # this makes owed_sql, etc. much more concise
1115 my ($class, $start, $end, %opt) = @_;
1117 $opt{setuprecur} =~ /^s/ ? 'cust_bill_pkg.setup' :
1118 $opt{setuprecur} =~ /^r/ ? 'cust_bill_pkg.recur' :
1119 'cust_bill_pkg.setup + cust_bill_pkg.recur';
1121 if ($opt{no_usage} and $charged =~ /recur/) {
1122 $charged = "$charged - $usage_sql"
1129 =item owed_sql [ BEFORE, AFTER, OPTIONS ]
1131 Returns an SQL expression for the amount owed. BEFORE and AFTER specify
1132 a date window. OPTIONS may include 'no_usage' (excludes usage charges)
1133 and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
1139 '(' . $class->charged_sql(@_) .
1140 ' - ' . $class->paid_sql(@_) .
1141 ' - ' . $class->credited_sql(@_) . ')'
1144 =item paid_sql [ BEFORE, AFTER, OPTIONS ]
1146 Returns an SQL expression for the sum of payments applied to this item.
1151 my ($class, $start, $end, %opt) = @_;
1152 my $s = $start ? "AND cust_bill_pay._date <= $start" : '';
1153 my $e = $end ? "AND cust_bill_pay._date > $end" : '';
1155 $opt{setuprecur} =~ /^s/ ? 'setup' :
1156 $opt{setuprecur} =~ /^r/ ? 'recur' :
1158 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1160 my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
1161 FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
1162 WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1163 $s $e $setuprecur )";
1165 if ( $opt{no_usage} ) {
1166 # cap the amount paid at the sum of non-usage charges,
1167 # minus the amount credited against non-usage charges
1169 $class->charged_sql($start, $end, %opt) . ' - ' .
1170 $class->credited_sql($start, $end, %opt).')';
1179 my ($class, $start, $end, %opt) = @_;
1180 my $s = $start ? "AND cust_credit_bill._date <= $start" : '';
1181 my $e = $end ? "AND cust_credit_bill._date > $end" : '';
1183 $opt{setuprecur} =~ /^s/ ? 'setup' :
1184 $opt{setuprecur} =~ /^r/ ? 'recur' :
1186 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1188 my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
1189 FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
1190 WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1191 $s $e $setuprecur )";
1193 if ( $opt{no_usage} ) {
1194 # cap the amount credited at the sum of non-usage charges
1195 "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
1207 setup and recur shouldn't be separate fields. There should be one "amount"
1208 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1210 A line item with both should really be two separate records (preserving
1211 sdate and edate for setup fees for recurring packages - that information may
1212 be valuable later). Invoice generation (cust_main::bill), invoice printing
1213 (cust_bill), tax reports (report_tax.cgi) and line item reports
1214 (cust_bill_pkg.cgi) would need to be updated.
1216 owed_setup and owed_recur could then be repaced by just owed, and
1217 cust_bill::open_cust_bill_pkg and
1218 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1222 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1223 from the base documentation.