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 'duration' => (ref($detail) ? $detail->[5] : '' ),
157 'regionname' => (ref($detail) ? $detail->[6] : '' ),
159 $error = $cust_bill_pkg_detail->insert;
161 $dbh->rollback if $oldAutoCommit;
162 return "error inserting cust_bill_pkg_detail: $error";
167 if ( $self->get('display') ) {
168 foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
169 $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
170 $error = $cust_bill_pkg_display->insert;
172 $dbh->rollback if $oldAutoCommit;
173 return "error inserting cust_bill_pkg_display: $error";
178 if ( $self->get('discounts') ) {
179 foreach my $cust_bill_pkg_discount ( @{$self->get('discounts')} ) {
180 $cust_bill_pkg_discount->billpkgnum($self->billpkgnum);
181 $error = $cust_bill_pkg_discount->insert;
183 $dbh->rollback if $oldAutoCommit;
184 return "error inserting cust_bill_pkg_discount: $error";
189 if ( $self->_cust_tax_exempt_pkg ) {
190 foreach my $cust_tax_exempt_pkg ( @{$self->_cust_tax_exempt_pkg} ) {
191 $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
192 $error = $cust_tax_exempt_pkg->insert;
194 $dbh->rollback if $oldAutoCommit;
195 return "error inserting cust_tax_exempt_pkg: $error";
200 my $tax_location = $self->get('cust_bill_pkg_tax_location');
201 if ( $tax_location ) {
202 foreach my $cust_bill_pkg_tax_location ( @$tax_location ) {
203 $cust_bill_pkg_tax_location->billpkgnum($self->billpkgnum);
204 $error = $cust_bill_pkg_tax_location->insert;
206 $dbh->rollback if $oldAutoCommit;
207 return "error inserting cust_bill_pkg_tax_location: $error";
212 my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
213 if ( $tax_rate_location ) {
214 foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
215 $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
216 $error = $cust_bill_pkg_tax_rate_location->insert;
218 $dbh->rollback if $oldAutoCommit;
219 return "error inserting cust_bill_pkg_tax_rate_location: $error";
224 my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
225 if ( $cust_tax_adjustment ) {
226 $cust_tax_adjustment->billpkgnum($self->billpkgnum);
227 $error = $cust_tax_adjustment->replace;
229 $dbh->rollback if $oldAutoCommit;
230 return "error replacing cust_tax_adjustment: $error";
234 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
248 local $SIG{HUP} = 'IGNORE';
249 local $SIG{INT} = 'IGNORE';
250 local $SIG{QUIT} = 'IGNORE';
251 local $SIG{TERM} = 'IGNORE';
252 local $SIG{TSTP} = 'IGNORE';
253 local $SIG{PIPE} = 'IGNORE';
255 my $oldAutoCommit = $FS::UID::AutoCommit;
256 local $FS::UID::AutoCommit = 0;
259 foreach my $table (qw(
261 cust_bill_pkg_display
262 cust_bill_pkg_tax_location
263 cust_bill_pkg_tax_rate_location
269 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
270 my $error = $linked->delete;
272 $dbh->rollback if $oldAutoCommit;
279 foreach my $cust_tax_adjustment (
280 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
282 $cust_tax_adjustment->billpkgnum(''); #NULL
283 my $error = $cust_tax_adjustment->replace;
285 $dbh->rollback if $oldAutoCommit;
290 my $error = $self->SUPER::delete(@_);
292 $dbh->rollback if $oldAutoCommit;
296 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
302 #alas, bin/follow-tax-rename
304 #=item replace OLD_RECORD
306 #Currently unimplemented. This would be even more of an accounting nightmare
307 #than deleteing the items. Just don't do it.
312 # return "Can't modify cust_bill_pkg records!";
317 Checks all fields to make sure this is a valid line item. If there is an
318 error, returns the error, otherwise returns false. Called by the insert
327 $self->ut_numbern('billpkgnum')
328 || $self->ut_snumber('pkgnum')
329 || $self->ut_number('invnum')
330 || $self->ut_money('setup')
331 || $self->ut_money('recur')
332 || $self->ut_numbern('sdate')
333 || $self->ut_numbern('edate')
334 || $self->ut_textn('itemdesc')
335 || $self->ut_textn('itemcomment')
336 || $self->ut_enum('hidden', [ '', 'Y' ])
338 return $error if $error;
340 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
341 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
342 return "Unknown pkgnum ". $self->pkgnum
343 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
346 return "Unknown invnum"
347 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
354 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
360 carp "$me $self -> cust_pkg" if $DEBUG;
361 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
366 Returns the package definition for this invoice line item.
372 if ( $self->pkgpart_override ) {
373 qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
376 my $cust_pkg = $self->cust_pkg;
377 $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
384 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
390 qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
393 =item previous_cust_bill_pkg
395 Returns the previous cust_bill_pkg for this package, if any.
399 sub previous_cust_bill_pkg {
401 return unless $self->sdate;
403 'table' => 'cust_bill_pkg',
404 'hashref' => { 'pkgnum' => $self->pkgnum,
405 'sdate' => { op=>'<', value=>$self->sdate },
407 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
411 =item details [ OPTION => VALUE ... ]
413 Returns an array of detail information for the invoice line item.
415 Currently available options are: I<format>, I<escape_function> and
418 If I<format> is set to html or latex then the array members are improved
419 for tabular appearance in those environments if possible.
421 If I<escape_function> is set then the array members are processed by this
422 function before being returned.
424 I<format_function> overrides the normal HTML or LaTeX function for returning
425 formatted CDRs. It can be set to a subroutine which returns an empty list
426 to skip usage detail:
428 'format_function' => sub { () },
433 my ( $self, %opt ) = @_;
434 my $format = $opt{format} || '';
435 my $escape_function = $opt{escape_function} || sub { shift };
436 return () unless defined dbdef->table('cust_bill_pkg_detail');
438 eval "use Text::CSV_XS;";
440 my $csv = new Text::CSV_XS;
442 my $format_sub = sub { my $detail = shift;
443 $csv->parse($detail) or return "can't parse $detail";
444 join(' - ', map { &$escape_function($_) }
449 $format_sub = sub { my $detail = shift;
450 $csv->parse($detail) or return "can't parse $detail";
451 join('</TD><TD>', map { &$escape_function($_) }
455 if $format eq 'html';
457 $format_sub = sub { my $detail = shift;
458 $csv->parse($detail) or return "can't parse $detail";
459 #join(' & ', map { '\small{'. &$escape_function($_). '}' }
463 foreach ($csv->fields) {
464 $result .= ' & ' if $column > 1;
465 if ($column > 6) { # KLUDGE ALERT!
466 $result .= '\multicolumn{1}{l}{\scriptsize{'.
467 &$escape_function($_). '}}';
469 $result .= '\scriptsize{'. &$escape_function($_). '}';
475 if $format eq 'latex';
477 $format_sub = $opt{format_function} if $opt{format_function};
479 map { ( $_->format eq 'C'
480 ? &{$format_sub}( $_->detail, $_ )
481 : &{$escape_function}( $_->detail )
484 qsearch ({ 'table' => 'cust_bill_pkg_detail',
485 'hashref' => { 'billpkgnum' => $self->billpkgnum },
486 'order_by' => 'ORDER BY detailnum',
488 #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum });
491 =item details_header [ OPTION => VALUE ... ]
493 Returns a list representing an invoice line item detail header, if any.
494 This relies on the behavior of voip_cdr in that it expects the header
495 to be the first CSV formatted detail (as is expected by invoice generation
496 routines). Returns the empty list otherwise.
502 return '' unless defined dbdef->table('cust_bill_pkg_detail');
504 eval "use Text::CSV_XS;";
506 my $csv = new Text::CSV_XS;
509 qsearch ({ 'table' => 'cust_bill_pkg_detail',
510 'hashref' => { 'billpkgnum' => $self->billpkgnum,
513 'order_by' => 'ORDER BY detailnum LIMIT 1',
515 return() unless scalar(@detail);
516 $csv->parse($detail[0]->detail) or return ();
522 Returns a description for this line item. For typical line items, this is the
523 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
524 For one-shot line items and named taxes, it is the I<itemdesc> field of this
525 line item, and for generic taxes, simply returns "Tax".
532 if ( $self->pkgnum > 0 ) {
533 $self->itemdesc || $self->part_pkg->pkg;
535 my $desc = $self->itemdesc || 'Tax';
536 $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
543 Returns the amount owed (still outstanding) on this line item's setup fee,
544 which is the amount of the line item minus all payment applications (see
545 L<FS::cust_bill_pay_pkg> and credit applications (see
546 L<FS::cust_credit_bill_pkg>).
552 $self->owed('setup', @_);
557 Returns the amount owed (still outstanding) on this line item's recurring fee,
558 which is the amount of the line item minus all payment applications (see
559 L<FS::cust_bill_pay_pkg> and credit applications (see
560 L<FS::cust_credit_bill_pkg>).
566 $self->owed('recur', @_);
569 # modeled after cust_bill::owed...
571 my( $self, $field ) = @_;
572 my $balance = $self->$field();
573 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
574 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
575 $balance = sprintf( '%.2f', $balance );
576 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
582 my( $self, $field ) = @_;
583 my $balance = $self->$field();
584 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
585 $balance = sprintf( '%.2f', $balance );
586 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
590 sub cust_bill_pay_pkg {
591 my( $self, $field ) = @_;
592 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
593 'setuprecur' => $field,
598 sub cust_credit_bill_pkg {
599 my( $self, $field ) = @_;
600 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
601 'setuprecur' => $field,
608 Returns the number of billing units (for tax purposes) represented by this,
615 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
623 my( $self, $value ) = @_;
624 if ( defined($value) ) {
625 $self->setfield('quantity', $value);
627 $self->getfield('quantity') || 1;
635 my( $self, $value ) = @_;
636 if ( defined($value) ) {
637 $self->setfield('unitsetup', $value);
639 $self->getfield('unitsetup') eq ''
640 ? $self->getfield('setup')
641 : $self->getfield('unitsetup');
649 my( $self, $value ) = @_;
650 if ( defined($value) ) {
651 $self->setfield('unitrecur', $value);
653 $self->getfield('unitrecur') eq ''
654 ? $self->getfield('recur')
655 : $self->getfield('unitrecur');
660 Returns a list of cust_bill_pkg objects each with no more than a single class
661 (including setup or recur) of charge.
667 # XXX this goes away with cust_bill_pkg refactor
669 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
670 my %cust_bill_pkg = ();
672 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
673 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
676 #split setup and recur
677 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
678 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
679 $cust_bill_pkg->set('details', []);
680 $cust_bill_pkg->recur(0);
681 $cust_bill_pkg->unitrecur(0);
682 $cust_bill_pkg->type('');
683 $cust_bill_pkg_recur->setup(0);
684 $cust_bill_pkg_recur->unitsetup(0);
685 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
689 #split usage from recur
690 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
691 if exists($cust_bill_pkg{recur});
692 warn "usage is $usage\n" if $DEBUG > 1;
694 my $cust_bill_pkg_usage =
695 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
696 $cust_bill_pkg_usage->recur( $usage );
697 $cust_bill_pkg_usage->type( 'U' );
698 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
699 $cust_bill_pkg{recur}->recur( $recur );
700 $cust_bill_pkg{recur}->type( '' );
701 $cust_bill_pkg{recur}->set('details', []);
702 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
705 #subdivide usage by usage_class
706 if (exists($cust_bill_pkg{''})) {
707 foreach my $class (grep { $_ } $self->usage_classes) {
708 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
709 my $cust_bill_pkg_usage =
710 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
711 $cust_bill_pkg_usage->recur( $usage );
712 $cust_bill_pkg_usage->set('details', []);
713 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
714 $cust_bill_pkg{''}->recur( $classless );
715 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
717 warn "Unexpected classless usage value: ". $cust_bill_pkg{''}->recur
718 if ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur < 0);
719 delete $cust_bill_pkg{''}
720 unless ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur > 0);
723 # # sort setup,recur,'', and the rest numeric && return
724 # my @result = map { $cust_bill_pkg{$_} }
725 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
726 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
728 # keys %cust_bill_pkg;
737 Returns the amount of the charge associated with usage class CLASSNUM if
738 CLASSNUM is defined. Otherwise returns the total charge associated with
744 my( $self, $classnum ) = @_;
748 if ( $self->get('details') ) {
752 grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) }
753 @{ $self->get('details') };
757 my $hashref = { 'billpkgnum' => $self->billpkgnum };
758 $hashref->{ 'classnum' } = $classnum if defined($classnum);
759 @values = map { $_->amount } qsearch('cust_bill_pkg_detail', $hashref);
763 foreach ( @values ) {
771 Returns a list of usage classnums associated with this invoice line's
779 if ( $self->get('details') ) {
782 foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
783 $seen{ $detail->[3] } = 1;
790 qsearch({ table => 'cust_bill_pkg_detail',
791 hashref => { billpkgnum => $self->billpkgnum },
792 select => 'DISTINCT classnum',
799 =item cust_bill_pkg_display [ type => TYPE ]
801 Returns an array of display information for the invoice line item optionally
806 sub cust_bill_pkg_display {
807 my ( $self, %opt ) = @_;
810 new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
812 return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
814 my $type = $opt{type} if exists $opt{type};
817 if ( $self->get('display') ) {
818 @result = grep { defined($type) ? ($type eq $_->type) : 1 }
819 @{ $self->get('display') };
821 my $hashref = { 'billpkgnum' => $self->billpkgnum };
822 $hashref->{type} = $type if defined($type);
824 @result = qsearch ({ 'table' => 'cust_bill_pkg_display',
825 'hashref' => { 'billpkgnum' => $self->billpkgnum },
826 'order_by' => 'ORDER BY billpkgdisplaynum',
830 push @result, $default unless ( scalar(@result) || $type );
836 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
837 # and FS::cust_main::bill
839 sub _cust_tax_exempt_pkg {
842 $self->{Hash}->{_cust_tax_exempt_pkg} or
843 $self->{Hash}->{_cust_tax_exempt_pkg} = [];
847 =item cust_bill_pkg_tax_Xlocation
849 Returns the list of associated cust_bill_pkg_tax_location and/or
850 cust_bill_pkg_tax_rate_location objects
854 sub cust_bill_pkg_tax_Xlocation {
857 my %hash = ( 'billpkgnum' => $self->billpkgnum );
860 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
861 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
866 =item cust_bill_pkg_detail [ CLASSNUM ]
868 Returns the list of associated cust_bill_pkg_detail objects
869 The optional CLASSNUM argument will limit the details to the specified usage
874 sub cust_bill_pkg_detail {
876 my $classnum = shift || '';
878 my %hash = ( 'billpkgnum' => $self->billpkgnum );
879 $hash{classnum} = $classnum if $classnum;
881 qsearch( 'cust_bill_pkg_detail', \%hash ),
885 =item cust_bill_pkg_discount
887 Returns the list of associated cust_bill_pkg_discount objects.
891 sub cust_bill_pkg_discount {
893 qsearch( 'cust_bill_pkg_discount', { 'billpkgnum' => $self->billpkgnum } );
896 =item recur_show_zero
900 sub recur_show_zero {
904 #&& $self->cust_pkg->part_pkg->recur_show_zero;
906 shift->_X_show_zero('recur');
910 sub setup_show_zero {
911 shift->_X_show_zero('setup');
915 my( $self, $what ) = @_;
917 return 0 unless $self->$what() == 0 && $self->pkgnum;
919 $self->cust_pkg->_X_show_zero($what);
926 setup and recur shouldn't be separate fields. There should be one "amount"
927 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
929 A line item with both should really be two separate records (preserving
930 sdate and edate for setup fees for recurring packages - that information may
931 be valuable later). Invoice generation (cust_main::bill), invoice printing
932 (cust_bill), tax reports (report_tax.cgi) and line item reports
933 (cust_bill_pkg.cgi) would need to be updated.
935 owed_setup and owed_recur could then be repaced by just owed, and
936 cust_bill::open_cust_bill_pkg and
937 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
941 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
942 from the base documentation.