1 package FS::cust_bill_pkg;
4 use vars qw( @ISA $DEBUG $me );
6 use FS::Record qw( qsearch qsearchs dbdef dbh );
7 use FS::cust_main_Mixin;
11 use FS::cust_bill_pkg_detail;
12 use FS::cust_bill_pkg_display;
13 use FS::cust_bill_pkg_discount;
14 use FS::cust_bill_pay_pkg;
15 use FS::cust_credit_bill_pkg;
16 use FS::cust_tax_exempt_pkg;
17 use FS::cust_bill_pkg_tax_location;
18 use FS::cust_bill_pkg_tax_rate_location;
19 use FS::cust_tax_adjustment;
21 @ISA = qw( FS::cust_main_Mixin FS::Record );
24 $me = '[FS::cust_bill_pkg]';
28 FS::cust_bill_pkg - Object methods for cust_bill_pkg records
32 use FS::cust_bill_pkg;
34 $record = new FS::cust_bill_pkg \%hash;
35 $record = new FS::cust_bill_pkg { 'column' => 'value' };
37 $error = $record->insert;
39 $error = $record->check;
43 An FS::cust_bill_pkg object represents an invoice line item.
44 FS::cust_bill_pkg inherits from FS::Record. The following fields are currently
55 invoice (see L<FS::cust_bill>)
59 package (see L<FS::cust_pkg>) or 0 for the special virtual sales tax package, or -1 for the virtual line item (itemdesc is used for the line)
61 =item pkgpart_override
63 optional package definition (see L<FS::part_pkg>) override
75 starting date of recurring fee
79 ending date of recurring fee
83 Line item description (overrides normal package description)
87 If not set, defaults to 1
91 If not set, defaults to setup
95 If not set, defaults to recur
99 If set to Y, indicates data should not appear as separate line item on invoice
103 sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">. Also
104 see L<Time::Local> and L<Date::Parse> for conversion functions.
112 Creates a new line item. To add the line item to the database, see
113 L<"insert">. Line items are normally created by calling the bill method of a
114 customer object (see L<FS::cust_main>).
118 sub table { 'cust_bill_pkg'; }
122 Adds this line item to the database. If there is an error, returns the error,
123 otherwise returns false.
130 local $SIG{HUP} = 'IGNORE';
131 local $SIG{INT} = 'IGNORE';
132 local $SIG{QUIT} = 'IGNORE';
133 local $SIG{TERM} = 'IGNORE';
134 local $SIG{TSTP} = 'IGNORE';
135 local $SIG{PIPE} = 'IGNORE';
137 my $oldAutoCommit = $FS::UID::AutoCommit;
138 local $FS::UID::AutoCommit = 0;
141 my $error = $self->SUPER::insert;
143 $dbh->rollback if $oldAutoCommit;
147 if ( $self->get('details') ) {
148 foreach my $detail ( @{$self->get('details')} ) {
149 my $cust_bill_pkg_detail = new FS::cust_bill_pkg_detail {
150 'billpkgnum' => $self->billpkgnum,
151 'format' => (ref($detail) ? $detail->[0] : '' ),
152 'detail' => (ref($detail) ? $detail->[1] : $detail ),
153 'amount' => (ref($detail) ? $detail->[2] : '' ),
154 'classnum' => (ref($detail) ? $detail->[3] : '' ),
155 'phonenum' => (ref($detail) ? $detail->[4] : '' ),
156 'accountcode' => (ref($detail) ? $detail->[5] : '' ),
157 'duration' => (ref($detail) ? $detail->[6] : '' ),
158 'regionname' => (ref($detail) ? $detail->[7] : '' ),
160 $error = $cust_bill_pkg_detail->insert;
162 $dbh->rollback if $oldAutoCommit;
163 return "error inserting cust_bill_pkg_detail: $error";
168 if ( $self->get('display') ) {
169 foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
170 $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
171 $error = $cust_bill_pkg_display->insert;
173 $dbh->rollback if $oldAutoCommit;
174 return "error inserting cust_bill_pkg_display: $error";
179 if ( $self->get('discounts') ) {
180 foreach my $cust_bill_pkg_discount ( @{$self->get('discounts')} ) {
181 $cust_bill_pkg_discount->billpkgnum($self->billpkgnum);
182 $error = $cust_bill_pkg_discount->insert;
184 $dbh->rollback if $oldAutoCommit;
185 return "error inserting cust_bill_pkg_discount: $error";
190 if ( $self->_cust_tax_exempt_pkg ) {
191 foreach my $cust_tax_exempt_pkg ( @{$self->_cust_tax_exempt_pkg} ) {
192 $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
193 $error = $cust_tax_exempt_pkg->insert;
195 $dbh->rollback if $oldAutoCommit;
196 return "error inserting cust_tax_exempt_pkg: $error";
201 my $tax_location = $self->get('cust_bill_pkg_tax_location');
202 if ( $tax_location ) {
203 foreach my $cust_bill_pkg_tax_location ( @$tax_location ) {
204 $cust_bill_pkg_tax_location->billpkgnum($self->billpkgnum);
205 $error = $cust_bill_pkg_tax_location->insert;
207 $dbh->rollback if $oldAutoCommit;
208 return "error inserting cust_bill_pkg_tax_location: $error";
213 my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
214 if ( $tax_rate_location ) {
215 foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
216 $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
217 $error = $cust_bill_pkg_tax_rate_location->insert;
219 $dbh->rollback if $oldAutoCommit;
220 return "error inserting cust_bill_pkg_tax_rate_location: $error";
225 my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
226 if ( $cust_tax_adjustment ) {
227 $cust_tax_adjustment->billpkgnum($self->billpkgnum);
228 $error = $cust_tax_adjustment->replace;
230 $dbh->rollback if $oldAutoCommit;
231 return "error replacing cust_tax_adjustment: $error";
235 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
249 local $SIG{HUP} = 'IGNORE';
250 local $SIG{INT} = 'IGNORE';
251 local $SIG{QUIT} = 'IGNORE';
252 local $SIG{TERM} = 'IGNORE';
253 local $SIG{TSTP} = 'IGNORE';
254 local $SIG{PIPE} = 'IGNORE';
256 my $oldAutoCommit = $FS::UID::AutoCommit;
257 local $FS::UID::AutoCommit = 0;
260 foreach my $table (qw(
262 cust_bill_pkg_display
263 cust_bill_pkg_tax_location
264 cust_bill_pkg_tax_rate_location
270 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
271 my $error = $linked->delete;
273 $dbh->rollback if $oldAutoCommit;
280 foreach my $cust_tax_adjustment (
281 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
283 $cust_tax_adjustment->billpkgnum(''); #NULL
284 my $error = $cust_tax_adjustment->replace;
286 $dbh->rollback if $oldAutoCommit;
291 my $error = $self->SUPER::delete(@_);
293 $dbh->rollback if $oldAutoCommit;
297 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
303 #alas, bin/follow-tax-rename
305 #=item replace OLD_RECORD
307 #Currently unimplemented. This would be even more of an accounting nightmare
308 #than deleteing the items. Just don't do it.
313 # return "Can't modify cust_bill_pkg records!";
318 Checks all fields to make sure this is a valid line item. If there is an
319 error, returns the error, otherwise returns false. Called by the insert
328 $self->ut_numbern('billpkgnum')
329 || $self->ut_snumber('pkgnum')
330 || $self->ut_number('invnum')
331 || $self->ut_money('setup')
332 || $self->ut_money('recur')
333 || $self->ut_numbern('sdate')
334 || $self->ut_numbern('edate')
335 || $self->ut_textn('itemdesc')
336 || $self->ut_textn('itemcomment')
337 || $self->ut_enum('hidden', [ '', 'Y' ])
339 return $error if $error;
341 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
342 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
343 return "Unknown pkgnum ". $self->pkgnum
344 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
347 return "Unknown invnum"
348 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
355 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
361 carp "$me $self -> cust_pkg" if $DEBUG;
362 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
367 Returns the package definition for this invoice line item.
373 if ( $self->pkgpart_override ) {
374 qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
377 my $cust_pkg = $self->cust_pkg;
378 $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
385 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
391 qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
394 =item previous_cust_bill_pkg
396 Returns the previous cust_bill_pkg for this package, if any.
400 sub previous_cust_bill_pkg {
402 return unless $self->sdate;
404 'table' => 'cust_bill_pkg',
405 'hashref' => { 'pkgnum' => $self->pkgnum,
406 'sdate' => { op=>'<', value=>$self->sdate },
408 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
412 =item details [ OPTION => VALUE ... ]
414 Returns an array of detail information for the invoice line item.
416 Currently available options are: I<format>, I<escape_function> and
419 If I<format> is set to html or latex then the array members are improved
420 for tabular appearance in those environments if possible.
422 If I<escape_function> is set then the array members are processed by this
423 function before being returned.
425 I<format_function> overrides the normal HTML or LaTeX function for returning
426 formatted CDRs. It can be set to a subroutine which returns an empty list
427 to skip usage detail:
429 'format_function' => sub { () },
434 my ( $self, %opt ) = @_;
435 my $format = $opt{format} || '';
436 my $escape_function = $opt{escape_function} || sub { shift };
437 return () unless defined dbdef->table('cust_bill_pkg_detail');
439 eval "use Text::CSV_XS;";
441 my $csv = new Text::CSV_XS;
443 my $format_sub = sub { my $detail = shift;
444 $csv->parse($detail) or return "can't parse $detail";
445 join(' - ', map { &$escape_function($_) }
450 $format_sub = sub { my $detail = shift;
451 $csv->parse($detail) or return "can't parse $detail";
452 join('</TD><TD>', map { &$escape_function($_) }
456 if $format eq 'html';
458 $format_sub = sub { my $detail = shift;
459 $csv->parse($detail) or return "can't parse $detail";
460 #join(' & ', map { '\small{'. &$escape_function($_). '}' }
464 foreach ($csv->fields) {
465 $result .= ' & ' if $column > 1;
466 if ($column > 6) { # KLUDGE ALERT!
467 $result .= '\multicolumn{1}{l}{\scriptsize{'.
468 &$escape_function($_). '}}';
470 $result .= '\scriptsize{'. &$escape_function($_). '}';
476 if $format eq 'latex';
478 $format_sub = $opt{format_function} if $opt{format_function};
480 map { ( $_->format eq 'C'
481 ? &{$format_sub}( $_->detail, $_ )
482 : &{$escape_function}( $_->detail )
485 qsearch ({ 'table' => 'cust_bill_pkg_detail',
486 'hashref' => { 'billpkgnum' => $self->billpkgnum },
487 'order_by' => 'ORDER BY detailnum',
489 #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum });
492 =item details_header [ OPTION => VALUE ... ]
494 Returns a list representing an invoice line item detail header, if any.
495 This relies on the behavior of voip_cdr in that it expects the header
496 to be the first CSV formatted detail (as is expected by invoice generation
497 routines). Returns the empty list otherwise.
503 return '' unless defined dbdef->table('cust_bill_pkg_detail');
505 eval "use Text::CSV_XS;";
507 my $csv = new Text::CSV_XS;
510 qsearch ({ 'table' => 'cust_bill_pkg_detail',
511 'hashref' => { 'billpkgnum' => $self->billpkgnum,
514 'order_by' => 'ORDER BY detailnum LIMIT 1',
516 return() unless scalar(@detail);
517 $csv->parse($detail[0]->detail) or return ();
523 Returns a description for this line item. For typical line items, this is the
524 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
525 For one-shot line items and named taxes, it is the I<itemdesc> field of this
526 line item, and for generic taxes, simply returns "Tax".
533 if ( $self->pkgnum > 0 ) {
534 $self->itemdesc || $self->part_pkg->pkg;
536 my $desc = $self->itemdesc || 'Tax';
537 $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
544 Returns the amount owed (still outstanding) on this line item's setup fee,
545 which is the amount of the line item minus all payment applications (see
546 L<FS::cust_bill_pay_pkg> and credit applications (see
547 L<FS::cust_credit_bill_pkg>).
553 $self->owed('setup', @_);
558 Returns the amount owed (still outstanding) on this line item's recurring fee,
559 which is the amount of the line item minus all payment applications (see
560 L<FS::cust_bill_pay_pkg> and credit applications (see
561 L<FS::cust_credit_bill_pkg>).
567 $self->owed('recur', @_);
570 # modeled after cust_bill::owed...
572 my( $self, $field ) = @_;
573 my $balance = $self->$field();
574 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
575 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
576 $balance = sprintf( '%.2f', $balance );
577 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
583 my( $self, $field ) = @_;
584 my $balance = $self->$field();
585 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
586 $balance = sprintf( '%.2f', $balance );
587 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
591 sub cust_bill_pay_pkg {
592 my( $self, $field ) = @_;
593 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
594 'setuprecur' => $field,
599 sub cust_credit_bill_pkg {
600 my( $self, $field ) = @_;
601 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
602 'setuprecur' => $field,
609 Returns the number of billing units (for tax purposes) represented by this,
616 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
624 my( $self, $value ) = @_;
625 if ( defined($value) ) {
626 $self->setfield('quantity', $value);
628 $self->getfield('quantity') || 1;
636 my( $self, $value ) = @_;
637 if ( defined($value) ) {
638 $self->setfield('unitsetup', $value);
640 $self->getfield('unitsetup') eq ''
641 ? $self->getfield('setup')
642 : $self->getfield('unitsetup');
650 my( $self, $value ) = @_;
651 if ( defined($value) ) {
652 $self->setfield('unitrecur', $value);
654 $self->getfield('unitrecur') eq ''
655 ? $self->getfield('recur')
656 : $self->getfield('unitrecur');
661 Returns a list of cust_bill_pkg objects each with no more than a single class
662 (including setup or recur) of charge.
668 # XXX this goes away with cust_bill_pkg refactor
670 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
671 my %cust_bill_pkg = ();
673 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
674 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
677 #split setup and recur
678 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
679 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
680 $cust_bill_pkg->set('details', []);
681 $cust_bill_pkg->recur(0);
682 $cust_bill_pkg->unitrecur(0);
683 $cust_bill_pkg->type('');
684 $cust_bill_pkg_recur->setup(0);
685 $cust_bill_pkg_recur->unitsetup(0);
686 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
690 #split usage from recur
691 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
692 if exists($cust_bill_pkg{recur});
693 warn "usage is $usage\n" if $DEBUG > 1;
695 my $cust_bill_pkg_usage =
696 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
697 $cust_bill_pkg_usage->recur( $usage );
698 $cust_bill_pkg_usage->type( 'U' );
699 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
700 $cust_bill_pkg{recur}->recur( $recur );
701 $cust_bill_pkg{recur}->type( '' );
702 $cust_bill_pkg{recur}->set('details', []);
703 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
706 #subdivide usage by usage_class
707 if (exists($cust_bill_pkg{''})) {
708 foreach my $class (grep { $_ } $self->usage_classes) {
709 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
710 my $cust_bill_pkg_usage =
711 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
712 $cust_bill_pkg_usage->recur( $usage );
713 $cust_bill_pkg_usage->set('details', []);
714 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
715 $cust_bill_pkg{''}->recur( $classless );
716 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
718 warn "Unexpected classless usage value: ". $cust_bill_pkg{''}->recur
719 if ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur < 0);
720 delete $cust_bill_pkg{''}
721 unless ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur > 0);
724 # # sort setup,recur,'', and the rest numeric && return
725 # my @result = map { $cust_bill_pkg{$_} }
726 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
727 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
729 # keys %cust_bill_pkg;
738 Returns the amount of the charge associated with usage class CLASSNUM if
739 CLASSNUM is defined. Otherwise returns the total charge associated with
745 my( $self, $classnum ) = @_;
749 if ( $self->get('details') ) {
753 grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) }
754 @{ $self->get('details') };
758 my $hashref = { 'billpkgnum' => $self->billpkgnum };
759 $hashref->{ 'classnum' } = $classnum if defined($classnum);
760 @values = map { $_->amount } qsearch('cust_bill_pkg_detail', $hashref);
764 foreach ( @values ) {
772 Returns a list of usage classnums associated with this invoice line's
780 if ( $self->get('details') ) {
783 foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
784 $seen{ $detail->[3] } = 1;
791 qsearch({ table => 'cust_bill_pkg_detail',
792 hashref => { billpkgnum => $self->billpkgnum },
793 select => 'DISTINCT classnum',
800 =item cust_bill_pkg_display [ type => TYPE ]
802 Returns an array of display information for the invoice line item optionally
807 sub cust_bill_pkg_display {
808 my ( $self, %opt ) = @_;
811 new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
813 return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
815 my $type = $opt{type} if exists $opt{type};
818 if ( $self->get('display') ) {
819 @result = grep { defined($type) ? ($type eq $_->type) : 1 }
820 @{ $self->get('display') };
822 my $hashref = { 'billpkgnum' => $self->billpkgnum };
823 $hashref->{type} = $type if defined($type);
825 @result = qsearch ({ 'table' => 'cust_bill_pkg_display',
826 'hashref' => { 'billpkgnum' => $self->billpkgnum },
827 'order_by' => 'ORDER BY billpkgdisplaynum',
831 push @result, $default unless ( scalar(@result) || $type );
837 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
838 # and FS::cust_main::bill
840 sub _cust_tax_exempt_pkg {
843 $self->{Hash}->{_cust_tax_exempt_pkg} or
844 $self->{Hash}->{_cust_tax_exempt_pkg} = [];
848 =item cust_bill_pkg_tax_Xlocation
850 Returns the list of associated cust_bill_pkg_tax_location and/or
851 cust_bill_pkg_tax_rate_location objects
855 sub cust_bill_pkg_tax_Xlocation {
858 my %hash = ( 'billpkgnum' => $self->billpkgnum );
861 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
862 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
867 =item cust_bill_pkg_detail [ CLASSNUM ]
869 Returns the list of associated cust_bill_pkg_detail objects
870 The optional CLASSNUM argument will limit the details to the specified usage
875 sub cust_bill_pkg_detail {
877 my $classnum = shift || '';
879 my %hash = ( 'billpkgnum' => $self->billpkgnum );
880 $hash{classnum} = $classnum if $classnum;
882 qsearch( 'cust_bill_pkg_detail', \%hash ),
886 =item cust_bill_pkg_discount
888 Returns the list of associated cust_bill_pkg_discount objects.
892 sub cust_bill_pkg_discount {
894 qsearch( 'cust_bill_pkg_discount', { 'billpkgnum' => $self->billpkgnum } );
897 =item recur_show_zero
901 sub recur_show_zero {
905 #&& $self->cust_pkg->part_pkg->recur_show_zero;
907 shift->_X_show_zero('recur');
911 sub setup_show_zero {
912 shift->_X_show_zero('setup');
916 my( $self, $what ) = @_;
918 return 0 unless $self->$what() == 0 && $self->pkgnum;
920 $self->cust_pkg->_X_show_zero($what);
927 setup and recur shouldn't be separate fields. There should be one "amount"
928 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
930 A line item with both should really be two separate records (preserving
931 sdate and edate for setup fees for recurring packages - that information may
932 be valuable later). Invoice generation (cust_main::bill), invoice printing
933 (cust_bill), tax reports (report_tax.cgi) and line item reports
934 (cust_bill_pkg.cgi) would need to be updated.
936 owed_setup and owed_recur could then be repaced by just owed, and
937 cust_bill::open_cust_bill_pkg and
938 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
942 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
943 from the base documentation.