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 });
472 =item details_header [ OPTION => VALUE ... ]
474 Returns a list representing an invoice line item detail header, if any.
475 This relies on the behavior of voip_cdr in that it expects the header
476 to be the first CSV formatted detail (as is expected by invoice generation
477 routines). Returns the empty list otherwise.
483 return '' unless defined dbdef->table('cust_bill_pkg_detail');
485 eval "use Text::CSV_XS;";
487 my $csv = new Text::CSV_XS;
490 qsearch ({ 'table' => 'cust_bill_pkg_detail',
491 'hashref' => { 'billpkgnum' => $self->billpkgnum,
494 'order_by' => 'ORDER BY detailnum LIMIT 1',
496 return() unless scalar(@detail);
497 $csv->parse($detail[0]->detail) or return ();
503 Returns a description for this line item. For typical line items, this is the
504 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
505 For one-shot line items and named taxes, it is the I<itemdesc> field of this
506 line item, and for generic taxes, simply returns "Tax".
513 if ( $self->pkgnum > 0 ) {
514 $self->itemdesc || $self->part_pkg->pkg;
516 my $desc = $self->itemdesc || 'Tax';
517 $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
524 Returns the amount owed (still outstanding) on this line item's setup fee,
525 which is the amount of the line item minus all payment applications (see
526 L<FS::cust_bill_pay_pkg> and credit applications (see
527 L<FS::cust_credit_bill_pkg>).
533 $self->owed('setup', @_);
538 Returns the amount owed (still outstanding) on this line item's recurring fee,
539 which is the amount of the line item minus all payment applications (see
540 L<FS::cust_bill_pay_pkg> and credit applications (see
541 L<FS::cust_credit_bill_pkg>).
547 $self->owed('recur', @_);
550 # modeled after cust_bill::owed...
552 my( $self, $field ) = @_;
553 my $balance = $self->$field();
554 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
555 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
556 $balance = sprintf( '%.2f', $balance );
557 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
561 sub cust_bill_pay_pkg {
562 my( $self, $field ) = @_;
563 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
564 'setuprecur' => $field,
569 sub cust_credit_bill_pkg {
570 my( $self, $field ) = @_;
571 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
572 'setuprecur' => $field,
579 Returns the number of billing units (for tax purposes) represented by this,
586 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
594 my( $self, $value ) = @_;
595 if ( defined($value) ) {
596 $self->setfield('quantity', $value);
598 $self->getfield('quantity') || 1;
606 my( $self, $value ) = @_;
607 if ( defined($value) ) {
608 $self->setfield('unitsetup', $value);
610 $self->getfield('unitsetup') eq ''
611 ? $self->getfield('setup')
612 : $self->getfield('unitsetup');
620 my( $self, $value ) = @_;
621 if ( defined($value) ) {
622 $self->setfield('unitrecur', $value);
624 $self->getfield('unitrecur') eq ''
625 ? $self->getfield('recur')
626 : $self->getfield('unitrecur');
631 Returns a list of cust_bill_pkg objects each with no more than a single class
632 (including setup or recur) of charge.
638 # XXX this goes away with cust_bill_pkg refactor
640 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
641 my %cust_bill_pkg = ();
643 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
644 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
647 #split setup and recur
648 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
649 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
650 $cust_bill_pkg->set('details', []);
651 $cust_bill_pkg->recur(0);
652 $cust_bill_pkg->unitrecur(0);
653 $cust_bill_pkg->type('');
654 $cust_bill_pkg_recur->setup(0);
655 $cust_bill_pkg_recur->unitsetup(0);
656 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
660 #split usage from recur
661 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage );
662 warn "usage is $usage\n" if $DEBUG > 1;
664 my $cust_bill_pkg_usage =
665 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
666 $cust_bill_pkg_usage->recur( $usage );
667 $cust_bill_pkg_usage->type( 'U' );
668 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
669 $cust_bill_pkg{recur}->recur( $recur );
670 $cust_bill_pkg{recur}->type( '' );
671 $cust_bill_pkg{recur}->set('details', []);
672 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
675 #subdivide usage by usage_class
676 if (exists($cust_bill_pkg{''})) {
677 foreach my $class (grep { $_ } $self->usage_classes) {
678 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
679 my $cust_bill_pkg_usage =
680 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
681 $cust_bill_pkg_usage->recur( $usage );
682 $cust_bill_pkg_usage->set('details', []);
683 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
684 $cust_bill_pkg{''}->recur( $classless );
685 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
687 warn "Unexpected classless usage value: ". $cust_bill_pkg{''}->recur
688 if ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur < 0);
689 delete $cust_bill_pkg{''}
690 unless ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur > 0);
693 # # sort setup,recur,'', and the rest numeric && return
694 # my @result = map { $cust_bill_pkg{$_} }
695 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
696 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
698 # keys %cust_bill_pkg;
707 Returns the amount of the charge associated with usage class CLASSNUM if
708 CLASSNUM is defined. Otherwise returns the total charge associated with
714 my( $self, $classnum ) = @_;
718 if ( $self->get('details') ) {
722 grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) }
723 @{ $self->get('details') };
727 my $hashref = { 'billpkgnum' => $self->billpkgnum };
728 $hashref->{ 'classnum' } = $classnum if defined($classnum);
729 @values = map { $_->amount } qsearch('cust_bill_pkg_detail', $hashref);
733 foreach ( @values ) {
741 Returns a list of usage classnums associated with this invoice line's
749 if ( $self->get('details') ) {
752 foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
753 $seen{ $detail->[3] } = 1;
760 qsearch({ table => 'cust_bill_pkg_detail',
761 hashref => { billpkgnum => $self->billpkgnum },
762 select => 'DISTINCT classnum',
769 =item cust_bill_pkg_display [ type => TYPE ]
771 Returns an array of display information for the invoice line item optionally
776 sub cust_bill_pkg_display {
777 my ( $self, %opt ) = @_;
780 new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
782 return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
784 my $type = $opt{type} if exists $opt{type};
787 if ( scalar( $self->get('display') ) ) {
788 @result = grep { defined($type) ? ($type eq $_->type) : 1 }
789 @{ $self->get('display') };
791 my $hashref = { 'billpkgnum' => $self->billpkgnum };
792 $hashref->{type} = $type if defined($type);
794 @result = qsearch ({ 'table' => 'cust_bill_pkg_display',
795 'hashref' => { 'billpkgnum' => $self->billpkgnum },
796 'order_by' => 'ORDER BY billpkgdisplaynum',
800 push @result, $default unless ( scalar(@result) || $type );
806 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
807 # and FS::cust_main::bill
809 sub _cust_tax_exempt_pkg {
812 $self->{Hash}->{_cust_tax_exempt_pkg} or
813 $self->{Hash}->{_cust_tax_exempt_pkg} = [];
818 =item cust_bill_pkg_detail [ CLASSNUM ]
820 Returns the list of associated cust_bill_pkg_detail objects
821 The optional CLASSNUM argument will limit the details to the specified usage
826 sub cust_bill_pkg_detail {
828 my $classnum = shift || '';
830 my %hash = ( 'billpkgnum' => $self->billpkgnum );
831 $hash{classnum} = $classnum if $classnum;
833 qsearch ( 'cust_bill_pkg_detail', { %hash } ),
841 setup and recur shouldn't be separate fields. There should be one "amount"
842 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
844 A line item with both should really be two separate records (preserving
845 sdate and edate for setup fees for recurring packages - that information may
846 be valuable later). Invoice generation (cust_main::bill), invoice printing
847 (cust_bill), tax reports (report_tax.cgi) and line item reports
848 (cust_bill_pkg.cgi) would need to be updated.
850 owed_setup and owed_recur could then be repaced by just owed, and
851 cust_bill::open_cust_bill_pkg and
852 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
856 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
857 from the base documentation.