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;
149 if (ref($detail) eq 'FS::cust_bill_pkg_detail') {
150 $cust_bill_pkg_detail = $detail;
151 $cust_bill_pkg_detail->billpkgnum($self->billpkgnum);
153 $cust_bill_pkg_detail = new FS::cust_bill_pkg_detail {
154 'billpkgnum' => $self->billpkgnum,
155 'format' => (ref($detail) ? $detail->[0] : '' ),
156 'detail' => (ref($detail) ? $detail->[1] : $detail ),
157 'amount' => (ref($detail) ? $detail->[2] : '' ),
158 'classnum' => (ref($detail) ? $detail->[3] : '' ),
159 'phonenum' => (ref($detail) ? $detail->[4] : '' ),
160 'duration' => (ref($detail) ? $detail->[5] : '' ),
161 'regionname' => (ref($detail) ? $detail->[6] : '' ),
164 $error = $cust_bill_pkg_detail->insert;
166 $dbh->rollback if $oldAutoCommit;
167 return "error inserting cust_bill_pkg_detail: $error";
172 if ( $self->get('display') ) {
173 foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
174 $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
175 $error = $cust_bill_pkg_display->insert;
177 $dbh->rollback if $oldAutoCommit;
178 return "error inserting cust_bill_pkg_display: $error";
183 if ( $self->get('discounts') ) {
184 foreach my $cust_bill_pkg_discount ( @{$self->get('discounts')} ) {
185 $cust_bill_pkg_discount->billpkgnum($self->billpkgnum);
186 $error = $cust_bill_pkg_discount->insert;
188 $dbh->rollback if $oldAutoCommit;
189 return "error inserting cust_bill_pkg_discount: $error";
194 if ( $self->_cust_tax_exempt_pkg ) {
195 foreach my $cust_tax_exempt_pkg ( @{$self->_cust_tax_exempt_pkg} ) {
196 $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
197 $error = $cust_tax_exempt_pkg->insert;
199 $dbh->rollback if $oldAutoCommit;
200 return "error inserting cust_tax_exempt_pkg: $error";
205 my $tax_location = $self->get('cust_bill_pkg_tax_location');
206 if ( $tax_location ) {
207 foreach my $cust_bill_pkg_tax_location ( @$tax_location ) {
208 $cust_bill_pkg_tax_location->billpkgnum($self->billpkgnum);
209 $error = $cust_bill_pkg_tax_location->insert;
211 $dbh->rollback if $oldAutoCommit;
212 return "error inserting cust_bill_pkg_tax_location: $error";
217 my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
218 if ( $tax_rate_location ) {
219 foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
220 $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
221 $error = $cust_bill_pkg_tax_rate_location->insert;
223 $dbh->rollback if $oldAutoCommit;
224 return "error inserting cust_bill_pkg_tax_rate_location: $error";
229 my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
230 if ( $cust_tax_adjustment ) {
231 $cust_tax_adjustment->billpkgnum($self->billpkgnum);
232 $error = $cust_tax_adjustment->replace;
234 $dbh->rollback if $oldAutoCommit;
235 return "error replacing cust_tax_adjustment: $error";
239 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
253 local $SIG{HUP} = 'IGNORE';
254 local $SIG{INT} = 'IGNORE';
255 local $SIG{QUIT} = 'IGNORE';
256 local $SIG{TERM} = 'IGNORE';
257 local $SIG{TSTP} = 'IGNORE';
258 local $SIG{PIPE} = 'IGNORE';
260 my $oldAutoCommit = $FS::UID::AutoCommit;
261 local $FS::UID::AutoCommit = 0;
264 foreach my $table (qw(
266 cust_bill_pkg_display
267 cust_bill_pkg_tax_location
268 cust_bill_pkg_tax_rate_location
274 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
275 my $error = $linked->delete;
277 $dbh->rollback if $oldAutoCommit;
284 foreach my $cust_tax_adjustment (
285 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
287 $cust_tax_adjustment->billpkgnum(''); #NULL
288 my $error = $cust_tax_adjustment->replace;
290 $dbh->rollback if $oldAutoCommit;
295 my $error = $self->SUPER::delete(@_);
297 $dbh->rollback if $oldAutoCommit;
301 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
307 #alas, bin/follow-tax-rename
309 #=item replace OLD_RECORD
311 #Currently unimplemented. This would be even more of an accounting nightmare
312 #than deleteing the items. Just don't do it.
317 # return "Can't modify cust_bill_pkg records!";
322 Checks all fields to make sure this is a valid line item. If there is an
323 error, returns the error, otherwise returns false. Called by the insert
332 $self->ut_numbern('billpkgnum')
333 || $self->ut_snumber('pkgnum')
334 || $self->ut_number('invnum')
335 || $self->ut_money('setup')
336 || $self->ut_money('recur')
337 || $self->ut_numbern('sdate')
338 || $self->ut_numbern('edate')
339 || $self->ut_textn('itemdesc')
340 || $self->ut_textn('itemcomment')
341 || $self->ut_enum('hidden', [ '', 'Y' ])
343 return $error if $error;
345 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
346 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
347 return "Unknown pkgnum ". $self->pkgnum
348 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
351 return "Unknown invnum"
352 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
359 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
365 carp "$me $self -> cust_pkg" if $DEBUG;
366 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
371 Returns the package definition for this invoice line item.
377 if ( $self->pkgpart_override ) {
378 qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
381 my $cust_pkg = $self->cust_pkg;
382 $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
389 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
395 qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
398 =item previous_cust_bill_pkg
400 Returns the previous cust_bill_pkg for this package, if any.
404 sub previous_cust_bill_pkg {
406 return unless $self->sdate;
408 'table' => 'cust_bill_pkg',
409 'hashref' => { 'pkgnum' => $self->pkgnum,
410 'sdate' => { op=>'<', value=>$self->sdate },
412 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
416 =item details [ OPTION => VALUE ... ]
418 Returns an array of detail information for the invoice line item.
420 Currently available options are: I<format> I<escape_function>
422 If I<format> is set to html or latex then the array members are improved
423 for tabular appearance in those environments if possible.
425 If I<escape_function> is set then the array members are processed by this
426 function before being returned.
431 my ( $self, %opt ) = @_;
432 my $format = $opt{format} || '';
433 my $escape_function = $opt{escape_function} || sub { shift };
434 return () unless defined dbdef->table('cust_bill_pkg_detail');
436 eval "use Text::CSV_XS;";
438 my $csv = new Text::CSV_XS;
440 my $format_sub = sub { my $detail = shift;
441 $csv->parse($detail) or return "can't parse $detail";
442 join(' - ', map { &$escape_function($_) }
447 $format_sub = sub { my $detail = shift;
448 $csv->parse($detail) or return "can't parse $detail";
449 join('</TD><TD>', map { &$escape_function($_) }
453 if $format eq 'html';
455 $format_sub = sub { my $detail = shift;
456 $csv->parse($detail) or return "can't parse $detail";
457 #join(' & ', map { '\small{'. &$escape_function($_). '}' }
461 foreach ($csv->fields) {
462 $result .= ' & ' if $column > 1;
463 if ($column > 6) { # KLUDGE ALERT!
464 $result .= '\multicolumn{1}{l}{\scriptsize{'.
465 &$escape_function($_). '}}';
467 $result .= '\scriptsize{'. &$escape_function($_). '}';
473 if $format eq 'latex';
475 $format_sub = $opt{format_function} if $opt{format_function};
477 map { ( $_->format eq 'C'
478 ? &{$format_sub}( $_->detail, $_ )
479 : &{$escape_function}( $_->detail )
482 qsearch ({ 'table' => 'cust_bill_pkg_detail',
483 'hashref' => { 'billpkgnum' => $self->billpkgnum },
484 'order_by' => 'ORDER BY detailnum',
486 #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum });
489 =item details_header [ OPTION => VALUE ... ]
491 Returns a list representing an invoice line item detail header, if any.
492 This relies on the behavior of voip_cdr in that it expects the header
493 to be the first CSV formatted detail (as is expected by invoice generation
494 routines). Returns the empty list otherwise.
500 return '' unless defined dbdef->table('cust_bill_pkg_detail');
502 eval "use Text::CSV_XS;";
504 my $csv = new Text::CSV_XS;
507 qsearch ({ 'table' => 'cust_bill_pkg_detail',
508 'hashref' => { 'billpkgnum' => $self->billpkgnum,
511 'order_by' => 'ORDER BY detailnum LIMIT 1',
513 return() unless scalar(@detail);
514 $csv->parse($detail[0]->detail) or return ();
520 Returns a description for this line item. For typical line items, this is the
521 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
522 For one-shot line items and named taxes, it is the I<itemdesc> field of this
523 line item, and for generic taxes, simply returns "Tax".
530 if ( $self->pkgnum > 0 ) {
531 $self->itemdesc || $self->part_pkg->pkg;
533 my $desc = $self->itemdesc || 'Tax';
534 $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
541 Returns the amount owed (still outstanding) on this line item's setup fee,
542 which is the amount of the line item minus all payment applications (see
543 L<FS::cust_bill_pay_pkg> and credit applications (see
544 L<FS::cust_credit_bill_pkg>).
550 $self->owed('setup', @_);
555 Returns the amount owed (still outstanding) on this line item's recurring fee,
556 which is the amount of the line item minus all payment applications (see
557 L<FS::cust_bill_pay_pkg> and credit applications (see
558 L<FS::cust_credit_bill_pkg>).
564 $self->owed('recur', @_);
567 # modeled after cust_bill::owed...
569 my( $self, $field ) = @_;
570 my $balance = $self->$field();
571 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
572 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
573 $balance = sprintf( '%.2f', $balance );
574 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
580 my( $self, $field ) = @_;
581 my $balance = $self->$field();
582 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
583 $balance = sprintf( '%.2f', $balance );
584 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
588 sub cust_bill_pay_pkg {
589 my( $self, $field ) = @_;
590 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
591 'setuprecur' => $field,
596 sub cust_credit_bill_pkg {
597 my( $self, $field ) = @_;
598 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
599 'setuprecur' => $field,
606 Returns the number of billing units (for tax purposes) represented by this,
613 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
621 my( $self, $value ) = @_;
622 if ( defined($value) ) {
623 $self->setfield('quantity', $value);
625 $self->getfield('quantity') || 1;
633 my( $self, $value ) = @_;
634 if ( defined($value) ) {
635 $self->setfield('unitsetup', $value);
637 $self->getfield('unitsetup') eq ''
638 ? $self->getfield('setup')
639 : $self->getfield('unitsetup');
647 my( $self, $value ) = @_;
648 if ( defined($value) ) {
649 $self->setfield('unitrecur', $value);
651 $self->getfield('unitrecur') eq ''
652 ? $self->getfield('recur')
653 : $self->getfield('unitrecur');
658 Returns a list of cust_bill_pkg objects each with no more than a single class
659 (including setup or recur) of charge.
665 # XXX this goes away with cust_bill_pkg refactor
667 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
668 my %cust_bill_pkg = ();
670 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
671 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
674 #split setup and recur
675 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
676 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
677 $cust_bill_pkg->set('details', []);
678 $cust_bill_pkg->recur(0);
679 $cust_bill_pkg->unitrecur(0);
680 $cust_bill_pkg->type('');
681 $cust_bill_pkg_recur->setup(0);
682 $cust_bill_pkg_recur->unitsetup(0);
683 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
687 #split usage from recur
688 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
689 if exists($cust_bill_pkg{recur});
690 warn "usage is $usage\n" if $DEBUG > 1;
692 my $cust_bill_pkg_usage =
693 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
694 $cust_bill_pkg_usage->recur( $usage );
695 $cust_bill_pkg_usage->type( 'U' );
696 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
697 $cust_bill_pkg{recur}->recur( $recur );
698 $cust_bill_pkg{recur}->type( '' );
699 $cust_bill_pkg{recur}->set('details', []);
700 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
703 #subdivide usage by usage_class
704 if (exists($cust_bill_pkg{''})) {
705 foreach my $class (grep { $_ } $self->usage_classes) {
706 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
707 my $cust_bill_pkg_usage =
708 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
709 $cust_bill_pkg_usage->recur( $usage );
710 $cust_bill_pkg_usage->set('details', []);
711 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
712 $cust_bill_pkg{''}->recur( $classless );
713 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
715 warn "Unexpected classless usage value: ". $cust_bill_pkg{''}->recur
716 if ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur < 0);
717 delete $cust_bill_pkg{''}
718 unless ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur > 0);
721 # # sort setup,recur,'', and the rest numeric && return
722 # my @result = map { $cust_bill_pkg{$_} }
723 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
724 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
726 # keys %cust_bill_pkg;
735 Returns the amount of the charge associated with usage class CLASSNUM if
736 CLASSNUM is defined. Otherwise returns the total charge associated with
742 my( $self, $classnum ) = @_;
746 if ( $self->get('details') ) {
750 grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) }
751 @{ $self->get('details') };
755 my $hashref = { 'billpkgnum' => $self->billpkgnum };
756 $hashref->{ 'classnum' } = $classnum if defined($classnum);
757 @values = map { $_->amount } qsearch('cust_bill_pkg_detail', $hashref);
761 foreach ( @values ) {
769 Returns a list of usage classnums associated with this invoice line's
777 if ( $self->get('details') ) {
780 foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
781 $seen{ $detail->[3] } = 1;
788 qsearch({ table => 'cust_bill_pkg_detail',
789 hashref => { billpkgnum => $self->billpkgnum },
790 select => 'DISTINCT classnum',
797 =item cust_bill_pkg_display [ type => TYPE ]
799 Returns an array of display information for the invoice line item optionally
804 sub cust_bill_pkg_display {
805 my ( $self, %opt ) = @_;
808 new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
810 return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
812 my $type = $opt{type} if exists $opt{type};
815 if ( $self->get('display') ) {
816 @result = grep { defined($type) ? ($type eq $_->type) : 1 }
817 @{ $self->get('display') };
819 my $hashref = { 'billpkgnum' => $self->billpkgnum };
820 $hashref->{type} = $type if defined($type);
822 @result = qsearch ({ 'table' => 'cust_bill_pkg_display',
823 'hashref' => { 'billpkgnum' => $self->billpkgnum },
824 'order_by' => 'ORDER BY billpkgdisplaynum',
828 push @result, $default unless ( scalar(@result) || $type );
834 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
835 # and FS::cust_main::bill
837 sub _cust_tax_exempt_pkg {
840 $self->{Hash}->{_cust_tax_exempt_pkg} or
841 $self->{Hash}->{_cust_tax_exempt_pkg} = [];
845 =item cust_bill_pkg_tax_Xlocation
847 Returns the list of associated cust_bill_pkg_tax_location and/or
848 cust_bill_pkg_tax_rate_location objects
852 sub cust_bill_pkg_tax_Xlocation {
855 my %hash = ( 'billpkgnum' => $self->billpkgnum );
858 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
859 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
864 =item cust_bill_pkg_detail [ CLASSNUM ]
866 Returns the list of associated cust_bill_pkg_detail objects
867 The optional CLASSNUM argument will limit the details to the specified usage
872 sub cust_bill_pkg_detail {
874 my $classnum = shift || '';
876 my %hash = ( 'billpkgnum' => $self->billpkgnum );
877 $hash{classnum} = $classnum if $classnum;
880 'table' => 'cust_bill_pkg_detail',
881 'hashref' => { %hash },
882 'order_by' => 'ORDER BY detailnum',
891 setup and recur shouldn't be separate fields. There should be one "amount"
892 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
894 A line item with both should really be two separate records (preserving
895 sdate and edate for setup fees for recurring packages - that information may
896 be valuable later). Invoice generation (cust_main::bill), invoice printing
897 (cust_bill), tax reports (report_tax.cgi) and line item reports
898 (cust_bill_pkg.cgi) would need to be updated.
900 owed_setup and owed_recur could then be repaced by just owed, and
901 cust_bill::open_cust_bill_pkg and
902 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
906 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
907 from the base documentation.