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] : '' ),
156 $error = $cust_bill_pkg_detail->insert;
158 $dbh->rollback if $oldAutoCommit;
159 return "error inserting cust_bill_pkg_detail: $error";
164 if ( $self->get('display') ) {
165 foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
166 $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
167 $error = $cust_bill_pkg_display->insert;
169 $dbh->rollback if $oldAutoCommit;
170 return "error inserting cust_bill_pkg_display: $error";
175 if ( $self->_cust_tax_exempt_pkg ) {
176 foreach my $cust_tax_exempt_pkg ( @{$self->_cust_tax_exempt_pkg} ) {
177 $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
178 $error = $cust_tax_exempt_pkg->insert;
180 $dbh->rollback if $oldAutoCommit;
181 return "error inserting cust_tax_exempt_pkg: $error";
186 my $tax_location = $self->get('cust_bill_pkg_tax_location');
187 if ( $tax_location ) {
188 foreach my $cust_bill_pkg_tax_location ( @$tax_location ) {
189 $cust_bill_pkg_tax_location->billpkgnum($self->billpkgnum);
190 $error = $cust_bill_pkg_tax_location->insert;
192 $dbh->rollback if $oldAutoCommit;
193 return "error inserting cust_bill_pkg_tax_location: $error";
198 my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
199 if ( $tax_rate_location ) {
200 foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
201 $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
202 $error = $cust_bill_pkg_tax_rate_location->insert;
204 $dbh->rollback if $oldAutoCommit;
205 return "error inserting cust_bill_pkg_tax_rate_location: $error";
210 my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
211 if ( $cust_tax_adjustment ) {
212 $cust_tax_adjustment->billpkgnum($self->billpkgnum);
213 $error = $cust_tax_adjustment->replace;
215 $dbh->rollback if $oldAutoCommit;
216 return "error replacing cust_tax_adjustment: $error";
220 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
234 local $SIG{HUP} = 'IGNORE';
235 local $SIG{INT} = 'IGNORE';
236 local $SIG{QUIT} = 'IGNORE';
237 local $SIG{TERM} = 'IGNORE';
238 local $SIG{TSTP} = 'IGNORE';
239 local $SIG{PIPE} = 'IGNORE';
241 my $oldAutoCommit = $FS::UID::AutoCommit;
242 local $FS::UID::AutoCommit = 0;
245 foreach my $table (qw(
247 cust_bill_pkg_display
248 cust_bill_pkg_tax_location
249 cust_bill_pkg_tax_rate_location
255 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
256 my $error = $linked->delete;
258 $dbh->rollback if $oldAutoCommit;
265 foreach my $cust_tax_adjustment (
266 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
268 $cust_tax_adjustment->billpkgnum(''); #NULL
269 my $error = $cust_tax_adjustment->replace;
271 $dbh->rollback if $oldAutoCommit;
276 my $error = $self->SUPER::delete(@_);
278 $dbh->rollback if $oldAutoCommit;
282 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
288 #alas, bin/follow-tax-rename
290 #=item replace OLD_RECORD
292 #Currently unimplemented. This would be even more of an accounting nightmare
293 #than deleteing the items. Just don't do it.
298 # return "Can't modify cust_bill_pkg records!";
303 Checks all fields to make sure this is a valid line item. If there is an
304 error, returns the error, otherwise returns false. Called by the insert
313 $self->ut_numbern('billpkgnum')
314 || $self->ut_snumber('pkgnum')
315 || $self->ut_number('invnum')
316 || $self->ut_money('setup')
317 || $self->ut_money('recur')
318 || $self->ut_numbern('sdate')
319 || $self->ut_numbern('edate')
320 || $self->ut_textn('itemdesc')
321 || $self->ut_textn('itemcomment')
322 || $self->ut_enum('hidden', [ '', 'Y' ])
324 return $error if $error;
326 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
327 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
328 return "Unknown pkgnum ". $self->pkgnum
329 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
332 return "Unknown invnum"
333 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
340 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
346 carp "$me $self -> cust_pkg" if $DEBUG;
347 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
352 Returns the package definition for this invoice line item.
358 if ( $self->pkgpart_override ) {
359 qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
362 my $cust_pkg = $self->cust_pkg;
363 $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
370 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
376 qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
379 =item previous_cust_bill_pkg
381 Returns the previous cust_bill_pkg for this package, if any.
385 sub previous_cust_bill_pkg {
387 return unless $self->sdate;
389 'table' => 'cust_bill_pkg',
390 'hashref' => { 'pkgnum' => $self->pkgnum,
391 'sdate' => { op=>'<', value=>$self->sdate },
393 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
397 =item details [ OPTION => VALUE ... ]
399 Returns an array of detail information for the invoice line item.
401 Currently available options are: I<format> I<escape_function>
403 If I<format> is set to html or latex then the array members are improved
404 for tabular appearance in those environments if possible.
406 If I<escape_function> is set then the array members are processed by this
407 function before being returned.
412 my ( $self, %opt ) = @_;
413 my $format = $opt{format} || '';
414 my $escape_function = $opt{escape_function} || sub { shift };
415 return () unless defined dbdef->table('cust_bill_pkg_detail');
417 eval "use Text::CSV_XS;";
419 my $csv = new Text::CSV_XS;
421 my $format_sub = sub { my $detail = shift;
422 $csv->parse($detail) or return "can't parse $detail";
423 join(' - ', map { &$escape_function($_) }
428 $format_sub = sub { my $detail = shift;
429 $csv->parse($detail) or return "can't parse $detail";
430 join('</TD><TD>', map { &$escape_function($_) }
434 if $format eq 'html';
436 $format_sub = sub { my $detail = shift;
437 $csv->parse($detail) or return "can't parse $detail";
438 #join(' & ', map { '\small{'. &$escape_function($_). '}' }
442 foreach ($csv->fields) {
443 $result .= ' & ' if $column > 1;
444 if ($column > 6) { # KLUDGE ALERT!
445 $result .= '\multicolumn{1}{l}{\scriptsize{'.
446 &$escape_function($_). '}}';
448 $result .= '\scriptsize{'. &$escape_function($_). '}';
454 if $format eq 'latex';
456 $format_sub = $opt{format_function} if $opt{format_function};
458 map { ( $_->format eq 'C'
459 ? &{$format_sub}( $_->detail, $_ )
460 : &{$escape_function}( $_->detail )
463 qsearch ({ 'table' => 'cust_bill_pkg_detail',
464 'hashref' => { 'billpkgnum' => $self->billpkgnum },
465 'order_by' => 'ORDER BY detailnum',
467 #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum });
472 Returns a description for this line item. For typical line items, this is the
473 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
474 For one-shot line items and named taxes, it is the I<itemdesc> field of this
475 line item, and for generic taxes, simply returns "Tax".
482 if ( $self->pkgnum > 0 ) {
483 $self->itemdesc || $self->part_pkg->pkg;
485 my $desc = $self->itemdesc || 'Tax';
486 $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
493 Returns the amount owed (still outstanding) on this line item's setup fee,
494 which is the amount of the line item minus all payment applications (see
495 L<FS::cust_bill_pay_pkg> and credit applications (see
496 L<FS::cust_credit_bill_pkg>).
502 $self->owed('setup', @_);
507 Returns the amount owed (still outstanding) on this line item's recurring fee,
508 which is the amount of the line item minus all payment applications (see
509 L<FS::cust_bill_pay_pkg> and credit applications (see
510 L<FS::cust_credit_bill_pkg>).
516 $self->owed('recur', @_);
519 # modeled after cust_bill::owed...
521 my( $self, $field ) = @_;
522 my $balance = $self->$field();
523 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
524 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
525 $balance = sprintf( '%.2f', $balance );
526 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
530 sub cust_bill_pay_pkg {
531 my( $self, $field ) = @_;
532 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
533 'setuprecur' => $field,
538 sub cust_credit_bill_pkg {
539 my( $self, $field ) = @_;
540 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
541 'setuprecur' => $field,
548 Returns the number of billing units (for tax purposes) represented by this,
555 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
563 my( $self, $value ) = @_;
564 if ( defined($value) ) {
565 $self->setfield('quantity', $value);
567 $self->getfield('quantity') || 1;
575 my( $self, $value ) = @_;
576 if ( defined($value) ) {
577 $self->setfield('unitsetup', $value);
579 $self->getfield('unitsetup') eq ''
580 ? $self->getfield('setup')
581 : $self->getfield('unitsetup');
589 my( $self, $value ) = @_;
590 if ( defined($value) ) {
591 $self->setfield('unitrecur', $value);
593 $self->getfield('unitrecur') eq ''
594 ? $self->getfield('recur')
595 : $self->getfield('unitrecur');
600 Returns a list of cust_bill_pkg objects each with no more than a single class
601 (including setup or recur) of charge.
607 # XXX this goes away with cust_bill_pkg refactor
609 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
610 my %cust_bill_pkg = ();
612 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
613 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
616 #split setup and recur
617 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
618 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
619 $cust_bill_pkg->set('details', []);
620 $cust_bill_pkg->recur(0);
621 $cust_bill_pkg->unitrecur(0);
622 $cust_bill_pkg->type('');
623 $cust_bill_pkg_recur->setup(0);
624 $cust_bill_pkg_recur->unitsetup(0);
625 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
629 #split usage from recur
630 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
631 if exists($cust_bill_pkg{recur});
632 warn "usage is $usage\n" if $DEBUG > 1;
634 my $cust_bill_pkg_usage =
635 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
636 $cust_bill_pkg_usage->recur( $usage );
637 $cust_bill_pkg_usage->type( 'U' );
638 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
639 $cust_bill_pkg{recur}->recur( $recur );
640 $cust_bill_pkg{recur}->type( '' );
641 $cust_bill_pkg{recur}->set('details', []);
642 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
645 #subdivide usage by usage_class
646 if (exists($cust_bill_pkg{''})) {
647 foreach my $class (grep { $_ } $self->usage_classes) {
648 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
649 my $cust_bill_pkg_usage =
650 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
651 $cust_bill_pkg_usage->recur( $usage );
652 $cust_bill_pkg_usage->set('details', []);
653 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
654 $cust_bill_pkg{''}->recur( $classless );
655 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
657 delete $cust_bill_pkg{''} unless $cust_bill_pkg{''}->recur;
660 # # sort setup,recur,'', and the rest numeric && return
661 # my @result = map { $cust_bill_pkg{$_} }
662 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
663 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
665 # keys %cust_bill_pkg;
674 Returns the amount of the charge associated with usage class CLASSNUM if
675 CLASSNUM is defined. Otherwise returns the total charge associated with
681 my( $self, $classnum ) = @_;
685 if ( $self->get('details') ) {
689 grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) }
690 @{ $self->get('details') };
694 my $hashref = { 'billpkgnum' => $self->billpkgnum };
695 $hashref->{ 'classnum' } = $classnum if defined($classnum);
696 @values = map { $_->amount } qsearch('cust_bill_pkg_detail', $hashref);
700 foreach ( @values ) {
708 Returns a list of usage classnums associated with this invoice line's
716 if ( $self->get('details') ) {
719 foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
720 $seen{ $detail->[3] } = 1;
727 qsearch({ table => 'cust_bill_pkg_detail',
728 hashref => { billpkgnum => $self->billpkgnum },
729 select => 'DISTINCT classnum',
736 =item cust_bill_pkg_display [ type => TYPE ]
738 Returns an array of display information for the invoice line item optionally
743 sub cust_bill_pkg_display {
744 my ( $self, %opt ) = @_;
747 new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
749 return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
751 my $type = $opt{type} if exists $opt{type};
754 if ( scalar( $self->get('display') ) ) {
755 @result = grep { defined($type) ? ($type eq $_->type) : 1 }
756 @{ $self->get('display') };
758 my $hashref = { 'billpkgnum' => $self->billpkgnum };
759 $hashref->{type} = $type if defined($type);
761 @result = qsearch ({ 'table' => 'cust_bill_pkg_display',
762 'hashref' => { 'billpkgnum' => $self->billpkgnum },
763 'order_by' => 'ORDER BY billpkgdisplaynum',
767 push @result, $default unless ( scalar(@result) || $type );
773 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
774 # and FS::cust_main::bill
776 sub _cust_tax_exempt_pkg {
779 $self->{Hash}->{_cust_tax_exempt_pkg} or
780 $self->{Hash}->{_cust_tax_exempt_pkg} = [];
784 =item cust_bill_pkg_tax_Xlocation
786 Returns the list of associated cust_bill_pkg_tax_location and/or
787 cust_bill_pkg_tax_rate_location objects
791 sub cust_bill_pkg_tax_Xlocation {
794 my %hash = ( 'billpkgnum' => $self->billpkgnum );
797 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
798 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
807 setup and recur shouldn't be separate fields. There should be one "amount"
808 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
810 A line item with both should really be two separate records (preserving
811 sdate and edate for setup fees for recurring packages - that information may
812 be valuable later). Invoice generation (cust_main::bill), invoice printing
813 (cust_bill), tax reports (report_tax.cgi) and line item reports
814 (cust_bill_pkg.cgi) would need to be updated.
816 owed_setup and owed_recur could then be repaced by just owed, and
817 cust_bill::open_cust_bill_pkg and
818 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
822 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
823 from the base documentation.