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>
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.
427 my ( $self, %opt ) = @_;
428 my $format = $opt{format} || '';
429 my $escape_function = $opt{escape_function} || sub { shift };
430 return () unless defined dbdef->table('cust_bill_pkg_detail');
432 eval "use Text::CSV_XS;";
434 my $csv = new Text::CSV_XS;
436 my $format_sub = sub { my $detail = shift;
437 $csv->parse($detail) or return "can't parse $detail";
438 join(' - ', map { &$escape_function($_) }
443 $format_sub = sub { my $detail = shift;
444 $csv->parse($detail) or return "can't parse $detail";
445 join('</TD><TD>', map { &$escape_function($_) }
449 if $format eq 'html';
451 $format_sub = sub { my $detail = shift;
452 $csv->parse($detail) or return "can't parse $detail";
453 #join(' & ', map { '\small{'. &$escape_function($_). '}' }
457 foreach ($csv->fields) {
458 $result .= ' & ' if $column > 1;
459 if ($column > 6) { # KLUDGE ALERT!
460 $result .= '\multicolumn{1}{l}{\scriptsize{'.
461 &$escape_function($_). '}}';
463 $result .= '\scriptsize{'. &$escape_function($_). '}';
469 if $format eq 'latex';
471 $format_sub = $opt{format_function} if $opt{format_function};
473 map { ( $_->format eq 'C'
474 ? &{$format_sub}( $_->detail, $_ )
475 : &{$escape_function}( $_->detail )
478 qsearch ({ 'table' => 'cust_bill_pkg_detail',
479 'hashref' => { 'billpkgnum' => $self->billpkgnum },
480 'order_by' => 'ORDER BY detailnum',
482 #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum });
485 =item details_header [ OPTION => VALUE ... ]
487 Returns a list representing an invoice line item detail header, if any.
488 This relies on the behavior of voip_cdr in that it expects the header
489 to be the first CSV formatted detail (as is expected by invoice generation
490 routines). Returns the empty list otherwise.
496 return '' unless defined dbdef->table('cust_bill_pkg_detail');
498 eval "use Text::CSV_XS;";
500 my $csv = new Text::CSV_XS;
503 qsearch ({ 'table' => 'cust_bill_pkg_detail',
504 'hashref' => { 'billpkgnum' => $self->billpkgnum,
507 'order_by' => 'ORDER BY detailnum LIMIT 1',
509 return() unless scalar(@detail);
510 $csv->parse($detail[0]->detail) or return ();
516 Returns a description for this line item. For typical line items, this is the
517 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
518 For one-shot line items and named taxes, it is the I<itemdesc> field of this
519 line item, and for generic taxes, simply returns "Tax".
526 if ( $self->pkgnum > 0 ) {
527 $self->itemdesc || $self->part_pkg->pkg;
529 my $desc = $self->itemdesc || 'Tax';
530 $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
537 Returns the amount owed (still outstanding) on this line item's setup fee,
538 which is the amount of the line item minus all payment applications (see
539 L<FS::cust_bill_pay_pkg> and credit applications (see
540 L<FS::cust_credit_bill_pkg>).
546 $self->owed('setup', @_);
551 Returns the amount owed (still outstanding) on this line item's recurring fee,
552 which is the amount of the line item minus all payment applications (see
553 L<FS::cust_bill_pay_pkg> and credit applications (see
554 L<FS::cust_credit_bill_pkg>).
560 $self->owed('recur', @_);
563 # modeled after cust_bill::owed...
565 my( $self, $field ) = @_;
566 my $balance = $self->$field();
567 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
568 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
569 $balance = sprintf( '%.2f', $balance );
570 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
576 my( $self, $field ) = @_;
577 my $balance = $self->$field();
578 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
579 $balance = sprintf( '%.2f', $balance );
580 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
584 sub cust_bill_pay_pkg {
585 my( $self, $field ) = @_;
586 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
587 'setuprecur' => $field,
592 sub cust_credit_bill_pkg {
593 my( $self, $field ) = @_;
594 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
595 'setuprecur' => $field,
602 Returns the number of billing units (for tax purposes) represented by this,
609 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
617 my( $self, $value ) = @_;
618 if ( defined($value) ) {
619 $self->setfield('quantity', $value);
621 $self->getfield('quantity') || 1;
629 my( $self, $value ) = @_;
630 if ( defined($value) ) {
631 $self->setfield('unitsetup', $value);
633 $self->getfield('unitsetup') eq ''
634 ? $self->getfield('setup')
635 : $self->getfield('unitsetup');
643 my( $self, $value ) = @_;
644 if ( defined($value) ) {
645 $self->setfield('unitrecur', $value);
647 $self->getfield('unitrecur') eq ''
648 ? $self->getfield('recur')
649 : $self->getfield('unitrecur');
654 Returns a list of cust_bill_pkg objects each with no more than a single class
655 (including setup or recur) of charge.
661 # XXX this goes away with cust_bill_pkg refactor
663 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
664 my %cust_bill_pkg = ();
666 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
667 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
670 #split setup and recur
671 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
672 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
673 $cust_bill_pkg->set('details', []);
674 $cust_bill_pkg->recur(0);
675 $cust_bill_pkg->unitrecur(0);
676 $cust_bill_pkg->type('');
677 $cust_bill_pkg_recur->setup(0);
678 $cust_bill_pkg_recur->unitsetup(0);
679 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
683 #split usage from recur
684 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
685 if exists($cust_bill_pkg{recur});
686 warn "usage is $usage\n" if $DEBUG > 1;
688 my $cust_bill_pkg_usage =
689 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
690 $cust_bill_pkg_usage->recur( $usage );
691 $cust_bill_pkg_usage->type( 'U' );
692 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
693 $cust_bill_pkg{recur}->recur( $recur );
694 $cust_bill_pkg{recur}->type( '' );
695 $cust_bill_pkg{recur}->set('details', []);
696 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
699 #subdivide usage by usage_class
700 if (exists($cust_bill_pkg{''})) {
701 foreach my $class (grep { $_ } $self->usage_classes) {
702 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
703 my $cust_bill_pkg_usage =
704 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
705 $cust_bill_pkg_usage->recur( $usage );
706 $cust_bill_pkg_usage->set('details', []);
707 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
708 $cust_bill_pkg{''}->recur( $classless );
709 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
711 warn "Unexpected classless usage value: ". $cust_bill_pkg{''}->recur
712 if ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur < 0);
713 delete $cust_bill_pkg{''}
714 unless ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur > 0);
717 # # sort setup,recur,'', and the rest numeric && return
718 # my @result = map { $cust_bill_pkg{$_} }
719 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
720 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
722 # keys %cust_bill_pkg;
731 Returns the amount of the charge associated with usage class CLASSNUM if
732 CLASSNUM is defined. Otherwise returns the total charge associated with
738 my( $self, $classnum ) = @_;
742 if ( $self->get('details') ) {
746 grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) }
747 @{ $self->get('details') };
751 my $hashref = { 'billpkgnum' => $self->billpkgnum };
752 $hashref->{ 'classnum' } = $classnum if defined($classnum);
753 @values = map { $_->amount } qsearch('cust_bill_pkg_detail', $hashref);
757 foreach ( @values ) {
765 Returns a list of usage classnums associated with this invoice line's
773 if ( $self->get('details') ) {
776 foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
777 $seen{ $detail->[3] } = 1;
784 qsearch({ table => 'cust_bill_pkg_detail',
785 hashref => { billpkgnum => $self->billpkgnum },
786 select => 'DISTINCT classnum',
793 =item cust_bill_pkg_display [ type => TYPE ]
795 Returns an array of display information for the invoice line item optionally
800 sub cust_bill_pkg_display {
801 my ( $self, %opt ) = @_;
804 new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
806 return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
808 my $type = $opt{type} if exists $opt{type};
811 if ( $self->get('display') ) {
812 @result = grep { defined($type) ? ($type eq $_->type) : 1 }
813 @{ $self->get('display') };
815 my $hashref = { 'billpkgnum' => $self->billpkgnum };
816 $hashref->{type} = $type if defined($type);
818 @result = qsearch ({ 'table' => 'cust_bill_pkg_display',
819 'hashref' => { 'billpkgnum' => $self->billpkgnum },
820 'order_by' => 'ORDER BY billpkgdisplaynum',
824 push @result, $default unless ( scalar(@result) || $type );
830 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
831 # and FS::cust_main::bill
833 sub _cust_tax_exempt_pkg {
836 $self->{Hash}->{_cust_tax_exempt_pkg} or
837 $self->{Hash}->{_cust_tax_exempt_pkg} = [];
841 =item cust_bill_pkg_tax_Xlocation
843 Returns the list of associated cust_bill_pkg_tax_location and/or
844 cust_bill_pkg_tax_rate_location objects
848 sub cust_bill_pkg_tax_Xlocation {
851 my %hash = ( 'billpkgnum' => $self->billpkgnum );
854 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
855 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
860 =item cust_bill_pkg_detail [ CLASSNUM ]
862 Returns the list of associated cust_bill_pkg_detail objects
863 The optional CLASSNUM argument will limit the details to the specified usage
868 sub cust_bill_pkg_detail {
870 my $classnum = shift || '';
872 my %hash = ( 'billpkgnum' => $self->billpkgnum );
873 $hash{classnum} = $classnum if $classnum;
875 qsearch ( 'cust_bill_pkg_detail', { %hash } ),
879 =item cust_bill_pkg_discount
881 Returns the list of associated cust_bill_pkg_discount objects.
885 sub cust_bill_pkg_discount {
887 qsearch ( 'cust_bill_pkg_discount', { 'billpkgnum' => $self->billpkgnum } );
894 setup and recur shouldn't be separate fields. There should be one "amount"
895 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
897 A line item with both should really be two separate records (preserving
898 sdate and edate for setup fees for recurring packages - that information may
899 be valuable later). Invoice generation (cust_main::bill), invoice printing
900 (cust_bill), tax reports (report_tax.cgi) and line item reports
901 (cust_bill_pkg.cgi) would need to be updated.
903 owed_setup and owed_recur could then be repaced by just owed, and
904 cust_bill::open_cust_bill_pkg and
905 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
909 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
910 from the base documentation.