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->_cust_tax_exempt_pkg ) {
178 foreach my $cust_tax_exempt_pkg ( @{$self->_cust_tax_exempt_pkg} ) {
179 $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
180 $error = $cust_tax_exempt_pkg->insert;
182 $dbh->rollback if $oldAutoCommit;
183 return "error inserting cust_tax_exempt_pkg: $error";
188 my $tax_location = $self->get('cust_bill_pkg_tax_location');
189 if ( $tax_location ) {
190 foreach my $cust_bill_pkg_tax_location ( @$tax_location ) {
191 $cust_bill_pkg_tax_location->billpkgnum($self->billpkgnum);
192 $error = $cust_bill_pkg_tax_location->insert;
194 $dbh->rollback if $oldAutoCommit;
195 return "error inserting cust_bill_pkg_tax_location: $error";
200 my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
201 if ( $tax_rate_location ) {
202 foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
203 $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
204 $error = $cust_bill_pkg_tax_rate_location->insert;
206 $dbh->rollback if $oldAutoCommit;
207 return "error inserting cust_bill_pkg_tax_rate_location: $error";
212 my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
213 if ( $cust_tax_adjustment ) {
214 $cust_tax_adjustment->billpkgnum($self->billpkgnum);
215 $error = $cust_tax_adjustment->replace;
217 $dbh->rollback if $oldAutoCommit;
218 return "error replacing cust_tax_adjustment: $error";
222 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
236 local $SIG{HUP} = 'IGNORE';
237 local $SIG{INT} = 'IGNORE';
238 local $SIG{QUIT} = 'IGNORE';
239 local $SIG{TERM} = 'IGNORE';
240 local $SIG{TSTP} = 'IGNORE';
241 local $SIG{PIPE} = 'IGNORE';
243 my $oldAutoCommit = $FS::UID::AutoCommit;
244 local $FS::UID::AutoCommit = 0;
247 foreach my $table (qw(
249 cust_bill_pkg_display
250 cust_bill_pkg_tax_location
251 cust_bill_pkg_tax_rate_location
257 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
258 my $error = $linked->delete;
260 $dbh->rollback if $oldAutoCommit;
267 foreach my $cust_tax_adjustment (
268 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
270 $cust_tax_adjustment->billpkgnum(''); #NULL
271 my $error = $cust_tax_adjustment->replace;
273 $dbh->rollback if $oldAutoCommit;
278 my $error = $self->SUPER::delete(@_);
280 $dbh->rollback if $oldAutoCommit;
284 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
290 #alas, bin/follow-tax-rename
292 #=item replace OLD_RECORD
294 #Currently unimplemented. This would be even more of an accounting nightmare
295 #than deleteing the items. Just don't do it.
300 # return "Can't modify cust_bill_pkg records!";
305 Checks all fields to make sure this is a valid line item. If there is an
306 error, returns the error, otherwise returns false. Called by the insert
315 $self->ut_numbern('billpkgnum')
316 || $self->ut_snumber('pkgnum')
317 || $self->ut_number('invnum')
318 || $self->ut_money('setup')
319 || $self->ut_money('recur')
320 || $self->ut_numbern('sdate')
321 || $self->ut_numbern('edate')
322 || $self->ut_textn('itemdesc')
323 || $self->ut_textn('itemcomment')
324 || $self->ut_enum('hidden', [ '', 'Y' ])
326 return $error if $error;
328 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
329 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
330 return "Unknown pkgnum ". $self->pkgnum
331 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
334 return "Unknown invnum"
335 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
342 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
348 carp "$me $self -> cust_pkg" if $DEBUG;
349 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
354 Returns the package definition for this invoice line item.
360 if ( $self->pkgpart_override ) {
361 qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
364 my $cust_pkg = $self->cust_pkg;
365 $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
372 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
378 qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
381 =item previous_cust_bill_pkg
383 Returns the previous cust_bill_pkg for this package, if any.
387 sub previous_cust_bill_pkg {
389 return unless $self->sdate;
391 'table' => 'cust_bill_pkg',
392 'hashref' => { 'pkgnum' => $self->pkgnum,
393 'sdate' => { op=>'<', value=>$self->sdate },
395 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
399 =item details [ OPTION => VALUE ... ]
401 Returns an array of detail information for the invoice line item.
403 Currently available options are: I<format> I<escape_function>
405 If I<format> is set to html or latex then the array members are improved
406 for tabular appearance in those environments if possible.
408 If I<escape_function> is set then the array members are processed by this
409 function before being returned.
414 my ( $self, %opt ) = @_;
415 my $format = $opt{format} || '';
416 my $escape_function = $opt{escape_function} || sub { shift };
417 return () unless defined dbdef->table('cust_bill_pkg_detail');
419 eval "use Text::CSV_XS;";
421 my $csv = new Text::CSV_XS;
423 my $format_sub = sub { my $detail = shift;
424 $csv->parse($detail) or return "can't parse $detail";
425 join(' - ', map { &$escape_function($_) }
430 $format_sub = sub { my $detail = shift;
431 $csv->parse($detail) or return "can't parse $detail";
432 join('</TD><TD>', map { &$escape_function($_) }
436 if $format eq 'html';
438 $format_sub = sub { my $detail = shift;
439 $csv->parse($detail) or return "can't parse $detail";
440 #join(' & ', map { '\small{'. &$escape_function($_). '}' }
444 foreach ($csv->fields) {
445 $result .= ' & ' if $column > 1;
446 if ($column > 6) { # KLUDGE ALERT!
447 $result .= '\multicolumn{1}{l}{\scriptsize{'.
448 &$escape_function($_). '}}';
450 $result .= '\scriptsize{'. &$escape_function($_). '}';
456 if $format eq 'latex';
458 $format_sub = $opt{format_function} if $opt{format_function};
460 map { ( $_->format eq 'C'
461 ? &{$format_sub}( $_->detail, $_ )
462 : &{$escape_function}( $_->detail )
465 qsearch ({ 'table' => 'cust_bill_pkg_detail',
466 'hashref' => { 'billpkgnum' => $self->billpkgnum },
467 'order_by' => 'ORDER BY detailnum',
469 #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum });
474 Returns a description for this line item. For typical line items, this is the
475 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
476 For one-shot line items and named taxes, it is the I<itemdesc> field of this
477 line item, and for generic taxes, simply returns "Tax".
484 if ( $self->pkgnum > 0 ) {
485 $self->itemdesc || $self->part_pkg->pkg;
487 my $desc = $self->itemdesc || 'Tax';
488 $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
495 Returns the amount owed (still outstanding) on this line item's setup fee,
496 which is the amount of the line item minus all payment applications (see
497 L<FS::cust_bill_pay_pkg> and credit applications (see
498 L<FS::cust_credit_bill_pkg>).
504 $self->owed('setup', @_);
509 Returns the amount owed (still outstanding) on this line item's recurring fee,
510 which is the amount of the line item minus all payment applications (see
511 L<FS::cust_bill_pay_pkg> and credit applications (see
512 L<FS::cust_credit_bill_pkg>).
518 $self->owed('recur', @_);
521 # modeled after cust_bill::owed...
523 my( $self, $field ) = @_;
524 my $balance = $self->$field();
525 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
526 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
527 $balance = sprintf( '%.2f', $balance );
528 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
534 my( $self, $field ) = @_;
535 my $balance = $self->$field();
536 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
537 $balance = sprintf( '%.2f', $balance );
538 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
542 sub cust_bill_pay_pkg {
543 my( $self, $field ) = @_;
544 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
545 'setuprecur' => $field,
550 sub cust_credit_bill_pkg {
551 my( $self, $field ) = @_;
552 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
553 'setuprecur' => $field,
560 Returns the number of billing units (for tax purposes) represented by this,
567 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
575 my( $self, $value ) = @_;
576 if ( defined($value) ) {
577 $self->setfield('quantity', $value);
579 $self->getfield('quantity') || 1;
587 my( $self, $value ) = @_;
588 if ( defined($value) ) {
589 $self->setfield('unitsetup', $value);
591 $self->getfield('unitsetup') eq ''
592 ? $self->getfield('setup')
593 : $self->getfield('unitsetup');
601 my( $self, $value ) = @_;
602 if ( defined($value) ) {
603 $self->setfield('unitrecur', $value);
605 $self->getfield('unitrecur') eq ''
606 ? $self->getfield('recur')
607 : $self->getfield('unitrecur');
612 Returns a list of cust_bill_pkg objects each with no more than a single class
613 (including setup or recur) of charge.
619 # XXX this goes away with cust_bill_pkg refactor
621 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
622 my %cust_bill_pkg = ();
624 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
625 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
628 #split setup and recur
629 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
630 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
631 $cust_bill_pkg->set('details', []);
632 $cust_bill_pkg->recur(0);
633 $cust_bill_pkg->unitrecur(0);
634 $cust_bill_pkg->type('');
635 $cust_bill_pkg_recur->setup(0);
636 $cust_bill_pkg_recur->unitsetup(0);
637 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
641 #split usage from recur
642 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
643 if exists($cust_bill_pkg{recur});
644 warn "usage is $usage\n" if $DEBUG > 1;
646 my $cust_bill_pkg_usage =
647 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
648 $cust_bill_pkg_usage->recur( $usage );
649 $cust_bill_pkg_usage->type( 'U' );
650 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
651 $cust_bill_pkg{recur}->recur( $recur );
652 $cust_bill_pkg{recur}->type( '' );
653 $cust_bill_pkg{recur}->set('details', []);
654 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
657 #subdivide usage by usage_class
658 if (exists($cust_bill_pkg{''})) {
659 foreach my $class (grep { $_ } $self->usage_classes) {
660 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
661 my $cust_bill_pkg_usage =
662 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
663 $cust_bill_pkg_usage->recur( $usage );
664 $cust_bill_pkg_usage->set('details', []);
665 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
666 $cust_bill_pkg{''}->recur( $classless );
667 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
669 delete $cust_bill_pkg{''} unless $cust_bill_pkg{''}->recur;
672 # # sort setup,recur,'', and the rest numeric && return
673 # my @result = map { $cust_bill_pkg{$_} }
674 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
675 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
677 # keys %cust_bill_pkg;
686 Returns the amount of the charge associated with usage class CLASSNUM if
687 CLASSNUM is defined. Otherwise returns the total charge associated with
693 my( $self, $classnum ) = @_;
697 if ( $self->get('details') ) {
701 grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) }
702 @{ $self->get('details') };
706 my $hashref = { 'billpkgnum' => $self->billpkgnum };
707 $hashref->{ 'classnum' } = $classnum if defined($classnum);
708 @values = map { $_->amount } qsearch('cust_bill_pkg_detail', $hashref);
712 foreach ( @values ) {
720 Returns a list of usage classnums associated with this invoice line's
728 if ( $self->get('details') ) {
731 foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
732 $seen{ $detail->[3] } = 1;
739 qsearch({ table => 'cust_bill_pkg_detail',
740 hashref => { billpkgnum => $self->billpkgnum },
741 select => 'DISTINCT classnum',
748 =item cust_bill_pkg_display [ type => TYPE ]
750 Returns an array of display information for the invoice line item optionally
755 sub cust_bill_pkg_display {
756 my ( $self, %opt ) = @_;
759 new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
761 return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
763 my $type = $opt{type} if exists $opt{type};
766 if ( scalar( $self->get('display') ) ) {
767 @result = grep { defined($type) ? ($type eq $_->type) : 1 }
768 @{ $self->get('display') };
770 my $hashref = { 'billpkgnum' => $self->billpkgnum };
771 $hashref->{type} = $type if defined($type);
773 @result = qsearch ({ 'table' => 'cust_bill_pkg_display',
774 'hashref' => { 'billpkgnum' => $self->billpkgnum },
775 'order_by' => 'ORDER BY billpkgdisplaynum',
779 push @result, $default unless ( scalar(@result) || $type );
785 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
786 # and FS::cust_main::bill
788 sub _cust_tax_exempt_pkg {
791 $self->{Hash}->{_cust_tax_exempt_pkg} or
792 $self->{Hash}->{_cust_tax_exempt_pkg} = [];
796 =item cust_bill_pkg_tax_Xlocation
798 Returns the list of associated cust_bill_pkg_tax_location and/or
799 cust_bill_pkg_tax_rate_location objects
803 sub cust_bill_pkg_tax_Xlocation {
806 my %hash = ( 'billpkgnum' => $self->billpkgnum );
809 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
810 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
815 =item cust_bill_pkg_detail [ CLASSNUM ]
817 Returns the list of associated cust_bill_pkg_detail objects
818 The optional CLASSNUM argument will limit the details to the specified usage
823 sub cust_bill_pkg_detail {
825 my $classnum = shift || '';
827 my %hash = ( 'billpkgnum' => $self->billpkgnum );
828 $hash{classnum} = $classnum if $classnum;
830 qsearch ( 'cust_bill_pkg_detail', { %hash } ),
838 setup and recur shouldn't be separate fields. There should be one "amount"
839 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
841 A line item with both should really be two separate records (preserving
842 sdate and edate for setup fees for recurring packages - that information may
843 be valuable later). Invoice generation (cust_main::bill), invoice printing
844 (cust_bill), tax reports (report_tax.cgi) and line item reports
845 (cust_bill_pkg.cgi) would need to be updated.
847 owed_setup and owed_recur could then be repaced by just owed, and
848 cust_bill::open_cust_bill_pkg and
849 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
853 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
854 from the base documentation.