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_pay_pkg;
14 use FS::cust_credit_bill_pkg;
15 use FS::cust_tax_exempt_pkg;
16 use FS::cust_bill_pkg_tax_location;
17 use FS::cust_bill_pkg_tax_rate_location;
18 use FS::cust_tax_adjustment;
20 @ISA = qw( FS::cust_main_Mixin FS::Record );
23 $me = '[FS::cust_bill_pkg]';
27 FS::cust_bill_pkg - Object methods for cust_bill_pkg records
31 use FS::cust_bill_pkg;
33 $record = new FS::cust_bill_pkg \%hash;
34 $record = new FS::cust_bill_pkg { 'column' => 'value' };
36 $error = $record->insert;
38 $error = $record->check;
42 An FS::cust_bill_pkg object represents an invoice line item.
43 FS::cust_bill_pkg inherits from FS::Record. The following fields are currently
54 invoice (see L<FS::cust_bill>)
58 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)
60 =item pkgpart_override
62 optional package definition (see L<FS::part_pkg>) override
74 starting date of recurring fee
78 ending date of recurring fee
82 Line item description (overrides normal package description)
86 If not set, defaults to 1
90 If not set, defaults to setup
94 If not set, defaults to recur
98 If set to Y, indicates data should not appear as separate line item on invoice
102 sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">. Also
103 see L<Time::Local> and L<Date::Parse> for conversion functions.
111 Creates a new line item. To add the line item to the database, see
112 L<"insert">. Line items are normally created by calling the bill method of a
113 customer object (see L<FS::cust_main>).
117 sub table { 'cust_bill_pkg'; }
121 Adds this line item to the database. If there is an error, returns the error,
122 otherwise returns false.
129 local $SIG{HUP} = 'IGNORE';
130 local $SIG{INT} = 'IGNORE';
131 local $SIG{QUIT} = 'IGNORE';
132 local $SIG{TERM} = 'IGNORE';
133 local $SIG{TSTP} = 'IGNORE';
134 local $SIG{PIPE} = 'IGNORE';
136 my $oldAutoCommit = $FS::UID::AutoCommit;
137 local $FS::UID::AutoCommit = 0;
140 my $error = $self->SUPER::insert;
142 $dbh->rollback if $oldAutoCommit;
146 if ( $self->get('details') ) {
147 foreach my $detail ( @{$self->get('details')} ) {
148 my $cust_bill_pkg_detail = new FS::cust_bill_pkg_detail {
149 'billpkgnum' => $self->billpkgnum,
150 'format' => (ref($detail) ? $detail->[0] : '' ),
151 'detail' => (ref($detail) ? $detail->[1] : $detail ),
152 'amount' => (ref($detail) ? $detail->[2] : '' ),
153 'classnum' => (ref($detail) ? $detail->[3] : '' ),
154 'phonenum' => (ref($detail) ? $detail->[4] : '' ),
155 'duration' => (ref($detail) ? $detail->[5] : '' ),
156 'regionname' => (ref($detail) ? $detail->[6] : '' ),
158 $error = $cust_bill_pkg_detail->insert;
160 $dbh->rollback if $oldAutoCommit;
161 return "error inserting cust_bill_pkg_detail: $error";
166 if ( $self->get('display') ) {
167 foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
168 $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
169 $error = $cust_bill_pkg_display->insert;
171 $dbh->rollback if $oldAutoCommit;
172 return "error inserting cust_bill_pkg_display: $error";
177 if ( $self->get('discounts') ) {
178 foreach my $cust_bill_pkg_discount ( @{$self->get('discounts')} ) {
179 $cust_bill_pkg_discount->billpkgnum($self->billpkgnum);
180 $error = $cust_bill_pkg_discount->insert;
182 $dbh->rollback if $oldAutoCommit;
183 return "error inserting cust_bill_pkg_discount: $error";
188 if ( $self->_cust_tax_exempt_pkg ) {
189 foreach my $cust_tax_exempt_pkg ( @{$self->_cust_tax_exempt_pkg} ) {
190 $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
191 $error = $cust_tax_exempt_pkg->insert;
193 $dbh->rollback if $oldAutoCommit;
194 return "error inserting cust_tax_exempt_pkg: $error";
199 my $tax_location = $self->get('cust_bill_pkg_tax_location');
200 if ( $tax_location ) {
201 foreach my $cust_bill_pkg_tax_location ( @$tax_location ) {
202 $cust_bill_pkg_tax_location->billpkgnum($self->billpkgnum);
203 $error = $cust_bill_pkg_tax_location->insert;
205 $dbh->rollback if $oldAutoCommit;
206 return "error inserting cust_bill_pkg_tax_location: $error";
211 my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
212 if ( $tax_rate_location ) {
213 foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
214 $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
215 $error = $cust_bill_pkg_tax_rate_location->insert;
217 $dbh->rollback if $oldAutoCommit;
218 return "error inserting cust_bill_pkg_tax_rate_location: $error";
223 my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
224 if ( $cust_tax_adjustment ) {
225 $cust_tax_adjustment->billpkgnum($self->billpkgnum);
226 $error = $cust_tax_adjustment->replace;
228 $dbh->rollback if $oldAutoCommit;
229 return "error replacing cust_tax_adjustment: $error";
233 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
247 local $SIG{HUP} = 'IGNORE';
248 local $SIG{INT} = 'IGNORE';
249 local $SIG{QUIT} = 'IGNORE';
250 local $SIG{TERM} = 'IGNORE';
251 local $SIG{TSTP} = 'IGNORE';
252 local $SIG{PIPE} = 'IGNORE';
254 my $oldAutoCommit = $FS::UID::AutoCommit;
255 local $FS::UID::AutoCommit = 0;
258 foreach my $table (qw(
260 cust_bill_pkg_display
261 cust_bill_pkg_tax_location
262 cust_bill_pkg_tax_rate_location
268 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
269 my $error = $linked->delete;
271 $dbh->rollback if $oldAutoCommit;
278 foreach my $cust_tax_adjustment (
279 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
281 $cust_tax_adjustment->billpkgnum(''); #NULL
282 my $error = $cust_tax_adjustment->replace;
284 $dbh->rollback if $oldAutoCommit;
289 my $error = $self->SUPER::delete(@_);
291 $dbh->rollback if $oldAutoCommit;
295 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
301 #alas, bin/follow-tax-rename
303 #=item replace OLD_RECORD
305 #Currently unimplemented. This would be even more of an accounting nightmare
306 #than deleteing the items. Just don't do it.
311 # return "Can't modify cust_bill_pkg records!";
316 Checks all fields to make sure this is a valid line item. If there is an
317 error, returns the error, otherwise returns false. Called by the insert
326 $self->ut_numbern('billpkgnum')
327 || $self->ut_snumber('pkgnum')
328 || $self->ut_number('invnum')
329 || $self->ut_money('setup')
330 || $self->ut_money('recur')
331 || $self->ut_numbern('sdate')
332 || $self->ut_numbern('edate')
333 || $self->ut_textn('itemdesc')
334 || $self->ut_textn('itemcomment')
335 || $self->ut_enum('hidden', [ '', 'Y' ])
337 return $error if $error;
339 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
340 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
341 return "Unknown pkgnum ". $self->pkgnum
342 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
345 return "Unknown invnum"
346 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
353 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
359 carp "$me $self -> cust_pkg" if $DEBUG;
360 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
365 Returns the package definition for this invoice line item.
371 if ( $self->pkgpart_override ) {
372 qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
375 my $cust_pkg = $self->cust_pkg;
376 $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
383 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
389 qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
392 =item previous_cust_bill_pkg
394 Returns the previous cust_bill_pkg for this package, if any.
398 sub previous_cust_bill_pkg {
400 return unless $self->sdate;
402 'table' => 'cust_bill_pkg',
403 'hashref' => { 'pkgnum' => $self->pkgnum,
404 'sdate' => { op=>'<', value=>$self->sdate },
406 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
410 =item details [ OPTION => VALUE ... ]
412 Returns an array of detail information for the invoice line item.
414 Currently available options are: I<format> I<escape_function>
416 If I<format> is set to html or latex then the array members are improved
417 for tabular appearance in those environments if possible.
419 If I<escape_function> is set then the array members are processed by this
420 function before being returned.
425 my ( $self, %opt ) = @_;
426 my $format = $opt{format} || '';
427 my $escape_function = $opt{escape_function} || sub { shift };
428 return () unless defined dbdef->table('cust_bill_pkg_detail');
430 eval "use Text::CSV_XS;";
432 my $csv = new Text::CSV_XS;
434 my $format_sub = sub { my $detail = shift;
435 $csv->parse($detail) or return "can't parse $detail";
436 join(' - ', map { &$escape_function($_) }
441 $format_sub = sub { my $detail = shift;
442 $csv->parse($detail) or return "can't parse $detail";
443 join('</TD><TD>', map { &$escape_function($_) }
447 if $format eq 'html';
449 $format_sub = sub { my $detail = shift;
450 $csv->parse($detail) or return "can't parse $detail";
451 #join(' & ', map { '\small{'. &$escape_function($_). '}' }
455 foreach ($csv->fields) {
456 $result .= ' & ' if $column > 1;
457 if ($column > 6) { # KLUDGE ALERT!
458 $result .= '\multicolumn{1}{l}{\scriptsize{'.
459 &$escape_function($_). '}}';
461 $result .= '\scriptsize{'. &$escape_function($_). '}';
467 if $format eq 'latex';
469 $format_sub = $opt{format_function} if $opt{format_function};
471 map { ( $_->format eq 'C'
472 ? &{$format_sub}( $_->detail, $_ )
473 : &{$escape_function}( $_->detail )
476 qsearch ({ 'table' => 'cust_bill_pkg_detail',
477 'hashref' => { 'billpkgnum' => $self->billpkgnum },
478 'order_by' => 'ORDER BY detailnum',
480 #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum });
485 Returns a description for this line item. For typical line items, this is the
486 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
487 For one-shot line items and named taxes, it is the I<itemdesc> field of this
488 line item, and for generic taxes, simply returns "Tax".
495 if ( $self->pkgnum > 0 ) {
496 $self->itemdesc || $self->part_pkg->pkg;
498 my $desc = $self->itemdesc || 'Tax';
499 $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
506 Returns the amount owed (still outstanding) on this line item's setup fee,
507 which is the amount of the line item minus all payment applications (see
508 L<FS::cust_bill_pay_pkg> and credit applications (see
509 L<FS::cust_credit_bill_pkg>).
515 $self->owed('setup', @_);
520 Returns the amount owed (still outstanding) on this line item's recurring fee,
521 which is the amount of the line item minus all payment applications (see
522 L<FS::cust_bill_pay_pkg> and credit applications (see
523 L<FS::cust_credit_bill_pkg>).
529 $self->owed('recur', @_);
532 # modeled after cust_bill::owed...
534 my( $self, $field ) = @_;
535 my $balance = $self->$field();
536 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
537 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
538 $balance = sprintf( '%.2f', $balance );
539 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
545 my( $self, $field ) = @_;
546 my $balance = $self->$field();
547 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
548 $balance = sprintf( '%.2f', $balance );
549 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
553 sub cust_bill_pay_pkg {
554 my( $self, $field ) = @_;
555 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
556 'setuprecur' => $field,
561 sub cust_credit_bill_pkg {
562 my( $self, $field ) = @_;
563 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
564 'setuprecur' => $field,
571 Returns the number of billing units (for tax purposes) represented by this,
578 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
586 my( $self, $value ) = @_;
587 if ( defined($value) ) {
588 $self->setfield('quantity', $value);
590 $self->getfield('quantity') || 1;
598 my( $self, $value ) = @_;
599 if ( defined($value) ) {
600 $self->setfield('unitsetup', $value);
602 $self->getfield('unitsetup') eq ''
603 ? $self->getfield('setup')
604 : $self->getfield('unitsetup');
612 my( $self, $value ) = @_;
613 if ( defined($value) ) {
614 $self->setfield('unitrecur', $value);
616 $self->getfield('unitrecur') eq ''
617 ? $self->getfield('recur')
618 : $self->getfield('unitrecur');
623 Returns a list of cust_bill_pkg objects each with no more than a single class
624 (including setup or recur) of charge.
630 # XXX this goes away with cust_bill_pkg refactor
632 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
633 my %cust_bill_pkg = ();
635 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
636 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
639 #split setup and recur
640 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
641 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
642 $cust_bill_pkg->set('details', []);
643 $cust_bill_pkg->recur(0);
644 $cust_bill_pkg->unitrecur(0);
645 $cust_bill_pkg->type('');
646 $cust_bill_pkg_recur->setup(0);
647 $cust_bill_pkg_recur->unitsetup(0);
648 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
652 #split usage from recur
653 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
654 if exists($cust_bill_pkg{recur});
655 warn "usage is $usage\n" if $DEBUG > 1;
657 my $cust_bill_pkg_usage =
658 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
659 $cust_bill_pkg_usage->recur( $usage );
660 $cust_bill_pkg_usage->type( 'U' );
661 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
662 $cust_bill_pkg{recur}->recur( $recur );
663 $cust_bill_pkg{recur}->type( '' );
664 $cust_bill_pkg{recur}->set('details', []);
665 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
668 #subdivide usage by usage_class
669 if (exists($cust_bill_pkg{''})) {
670 foreach my $class (grep { $_ } $self->usage_classes) {
671 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
672 my $cust_bill_pkg_usage =
673 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
674 $cust_bill_pkg_usage->recur( $usage );
675 $cust_bill_pkg_usage->set('details', []);
676 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
677 $cust_bill_pkg{''}->recur( $classless );
678 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
680 delete $cust_bill_pkg{''} unless $cust_bill_pkg{''}->recur;
683 # # sort setup,recur,'', and the rest numeric && return
684 # my @result = map { $cust_bill_pkg{$_} }
685 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
686 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
688 # keys %cust_bill_pkg;
697 Returns the amount of the charge associated with usage class CLASSNUM if
698 CLASSNUM is defined. Otherwise returns the total charge associated with
704 my( $self, $classnum ) = @_;
708 if ( $self->get('details') ) {
712 grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) }
713 @{ $self->get('details') };
717 my $hashref = { 'billpkgnum' => $self->billpkgnum };
718 $hashref->{ 'classnum' } = $classnum if defined($classnum);
719 @values = map { $_->amount } qsearch('cust_bill_pkg_detail', $hashref);
723 foreach ( @values ) {
731 Returns a list of usage classnums associated with this invoice line's
739 if ( $self->get('details') ) {
742 foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
743 $seen{ $detail->[3] } = 1;
750 qsearch({ table => 'cust_bill_pkg_detail',
751 hashref => { billpkgnum => $self->billpkgnum },
752 select => 'DISTINCT classnum',
759 =item cust_bill_pkg_display [ type => TYPE ]
761 Returns an array of display information for the invoice line item optionally
766 sub cust_bill_pkg_display {
767 my ( $self, %opt ) = @_;
770 new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
772 return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
774 my $type = $opt{type} if exists $opt{type};
777 if ( $self->get('display') ) {
778 @result = grep { defined($type) ? ($type eq $_->type) : 1 }
779 @{ $self->get('display') };
781 my $hashref = { 'billpkgnum' => $self->billpkgnum };
782 $hashref->{type} = $type if defined($type);
784 @result = qsearch ({ 'table' => 'cust_bill_pkg_display',
785 'hashref' => { 'billpkgnum' => $self->billpkgnum },
786 'order_by' => 'ORDER BY billpkgdisplaynum',
790 push @result, $default unless ( scalar(@result) || $type );
796 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
797 # and FS::cust_main::bill
799 sub _cust_tax_exempt_pkg {
802 $self->{Hash}->{_cust_tax_exempt_pkg} or
803 $self->{Hash}->{_cust_tax_exempt_pkg} = [];
807 =item cust_bill_pkg_tax_Xlocation
809 Returns the list of associated cust_bill_pkg_tax_location and/or
810 cust_bill_pkg_tax_rate_location objects
814 sub cust_bill_pkg_tax_Xlocation {
817 my %hash = ( 'billpkgnum' => $self->billpkgnum );
820 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
821 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
826 =item cust_bill_pkg_detail [ CLASSNUM ]
828 Returns the list of associated cust_bill_pkg_detail objects
829 The optional CLASSNUM argument will limit the details to the specified usage
834 sub cust_bill_pkg_detail {
836 my $classnum = shift || '';
838 my %hash = ( 'billpkgnum' => $self->billpkgnum );
839 $hash{classnum} = $classnum if $classnum;
841 qsearch ( 'cust_bill_pkg_detail', { %hash } ),
849 setup and recur shouldn't be separate fields. There should be one "amount"
850 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
852 A line item with both should really be two separate records (preserving
853 sdate and edate for setup fees for recurring packages - that information may
854 be valuable later). Invoice generation (cust_main::bill), invoice printing
855 (cust_bill), tax reports (report_tax.cgi) and line item reports
856 (cust_bill_pkg.cgi) would need to be updated.
858 owed_setup and owed_recur could then be repaced by just owed, and
859 cust_bill::open_cust_bill_pkg and
860 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
864 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
865 from the base documentation.