1 package FS::cust_bill_pkg;
4 use vars qw( @ISA $DEBUG $me );
5 use FS::Record qw( qsearch qsearchs dbdef dbh );
6 use FS::cust_main_Mixin;
10 use FS::cust_bill_pkg_detail;
11 use FS::cust_bill_pkg_display;
12 use FS::cust_bill_pay_pkg;
13 use FS::cust_credit_bill_pkg;
14 use FS::cust_tax_exempt_pkg;
15 use FS::cust_bill_pkg_tax_location;
16 use FS::cust_bill_pkg_tax_rate_location;
17 use FS::cust_tax_adjustment;
19 @ISA = qw( FS::cust_main_Mixin FS::Record );
22 $me = '[FS::cust_bill_pkg]';
26 FS::cust_bill_pkg - Object methods for cust_bill_pkg records
30 use FS::cust_bill_pkg;
32 $record = new FS::cust_bill_pkg \%hash;
33 $record = new FS::cust_bill_pkg { 'column' => 'value' };
35 $error = $record->insert;
37 $error = $record->check;
41 An FS::cust_bill_pkg object represents an invoice line item.
42 FS::cust_bill_pkg inherits from FS::Record. The following fields are currently
53 invoice (see L<FS::cust_bill>)
57 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)
59 =item pkgpart_override
61 optional package definition (see L<FS::part_pkg>) override
73 starting date of recurring fee
77 ending date of recurring fee
81 Line item description (overrides normal package description)
85 If not set, defaults to 1
89 If not set, defaults to setup
93 If not set, defaults to recur
97 If set to Y, indicates data should not appear as separate line item on invoice
101 sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">. Also
102 see L<Time::Local> and L<Date::Parse> for conversion functions.
110 Creates a new line item. To add the line item to the database, see
111 L<"insert">. Line items are normally created by calling the bill method of a
112 customer object (see L<FS::cust_main>).
116 sub table { 'cust_bill_pkg'; }
120 Adds this line item to the database. If there is an error, returns the error,
121 otherwise returns false.
128 local $SIG{HUP} = 'IGNORE';
129 local $SIG{INT} = 'IGNORE';
130 local $SIG{QUIT} = 'IGNORE';
131 local $SIG{TERM} = 'IGNORE';
132 local $SIG{TSTP} = 'IGNORE';
133 local $SIG{PIPE} = 'IGNORE';
135 my $oldAutoCommit = $FS::UID::AutoCommit;
136 local $FS::UID::AutoCommit = 0;
139 my $error = $self->SUPER::insert;
141 $dbh->rollback if $oldAutoCommit;
145 if ( $self->get('details') ) {
146 foreach my $detail ( @{$self->get('details')} ) {
147 my $cust_bill_pkg_detail = new FS::cust_bill_pkg_detail {
148 'billpkgnum' => $self->billpkgnum,
149 'format' => (ref($detail) ? $detail->[0] : '' ),
150 'detail' => (ref($detail) ? $detail->[1] : $detail ),
151 'amount' => (ref($detail) ? $detail->[2] : '' ),
152 'classnum' => (ref($detail) ? $detail->[3] : '' ),
153 'phonenum' => (ref($detail) ? $detail->[4] : '' ),
155 $error = $cust_bill_pkg_detail->insert;
157 $dbh->rollback if $oldAutoCommit;
158 return "error inserting cust_bill_pkg_detail: $error";
163 if ( $self->get('display') ) {
164 foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
165 $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
166 $error = $cust_bill_pkg_display->insert;
168 $dbh->rollback if $oldAutoCommit;
169 return "error inserting cust_bill_pkg_display: $error";
174 if ( $self->_cust_tax_exempt_pkg ) {
175 foreach my $cust_tax_exempt_pkg ( @{$self->_cust_tax_exempt_pkg} ) {
176 $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
177 $error = $cust_tax_exempt_pkg->insert;
179 $dbh->rollback if $oldAutoCommit;
180 return "error inserting cust_tax_exempt_pkg: $error";
185 my $tax_location = $self->get('cust_bill_pkg_tax_location');
186 if ( $tax_location ) {
187 foreach my $cust_bill_pkg_tax_location ( @$tax_location ) {
188 $cust_bill_pkg_tax_location->billpkgnum($self->billpkgnum);
189 $error = $cust_bill_pkg_tax_location->insert;
191 $dbh->rollback if $oldAutoCommit;
192 return "error inserting cust_bill_pkg_tax_location: $error";
197 my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
198 if ( $tax_rate_location ) {
199 foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
200 $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
201 $error = $cust_bill_pkg_tax_rate_location->insert;
203 $dbh->rollback if $oldAutoCommit;
204 return "error inserting cust_bill_pkg_tax_rate_location: $error";
209 my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
210 if ( $cust_tax_adjustment ) {
211 $cust_tax_adjustment->billpkgnum($self->billpkgnum);
212 $error = $cust_tax_adjustment->replace;
214 $dbh->rollback if $oldAutoCommit;
215 return "error replacing cust_tax_adjustment: $error";
219 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
233 local $SIG{HUP} = 'IGNORE';
234 local $SIG{INT} = 'IGNORE';
235 local $SIG{QUIT} = 'IGNORE';
236 local $SIG{TERM} = 'IGNORE';
237 local $SIG{TSTP} = 'IGNORE';
238 local $SIG{PIPE} = 'IGNORE';
240 my $oldAutoCommit = $FS::UID::AutoCommit;
241 local $FS::UID::AutoCommit = 0;
244 foreach my $table (qw(
246 cust_bill_pkg_display
247 cust_bill_pkg_tax_location
248 cust_bill_pkg_tax_rate_location
254 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
255 my $error = $linked->delete;
257 $dbh->rollback if $oldAutoCommit;
264 foreach my $cust_tax_adjustment (
265 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
267 $cust_tax_adjustment->billpkgnum(''); #NULL
268 my $error = $cust_tax_adjustment->replace;
270 $dbh->rollback if $oldAutoCommit;
275 my $error = $self->SUPER::delete(@_);
277 $dbh->rollback if $oldAutoCommit;
281 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
287 #alas, bin/follow-tax-rename
289 #=item replace OLD_RECORD
291 #Currently unimplemented. This would be even more of an accounting nightmare
292 #than deleteing the items. Just don't do it.
297 # return "Can't modify cust_bill_pkg records!";
302 Checks all fields to make sure this is a valid line item. If there is an
303 error, returns the error, otherwise returns false. Called by the insert
312 $self->ut_numbern('billpkgnum')
313 || $self->ut_snumber('pkgnum')
314 || $self->ut_number('invnum')
315 || $self->ut_money('setup')
316 || $self->ut_money('recur')
317 || $self->ut_numbern('sdate')
318 || $self->ut_numbern('edate')
319 || $self->ut_textn('itemdesc')
320 || $self->ut_textn('itemcomment')
321 || $self->ut_enum('hidden', [ '', 'Y' ])
323 return $error if $error;
325 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
326 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
327 return "Unknown pkgnum ". $self->pkgnum
328 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
331 return "Unknown invnum"
332 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
339 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
345 #warn "$me $self -> cust_pkg"; #carp?
346 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
351 Returns the package definition for this invoice line item.
357 if ( $self->pkgpart_override ) {
358 qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
361 my $cust_pkg = $self->cust_pkg;
362 $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
369 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
375 qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
378 =item previous_cust_bill_pkg
380 Returns the previous cust_bill_pkg for this package, if any.
384 sub previous_cust_bill_pkg {
386 return unless $self->sdate;
388 'table' => 'cust_bill_pkg',
389 'hashref' => { 'pkgnum' => $self->pkgnum,
390 'sdate' => { op=>'<', value=>$self->sdate },
392 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
396 =item details [ OPTION => VALUE ... ]
398 Returns an array of detail information for the invoice line item.
400 Currently available options are: I<format> I<escape_function>
402 If I<format> is set to html or latex then the array members are improved
403 for tabular appearance in those environments if possible.
405 If I<escape_function> is set then the array members are processed by this
406 function before being returned.
411 my ( $self, %opt ) = @_;
412 my $format = $opt{format} || '';
413 my $escape_function = $opt{escape_function} || sub { shift };
414 return () unless defined dbdef->table('cust_bill_pkg_detail');
416 eval "use Text::CSV_XS;";
418 my $csv = new Text::CSV_XS;
420 my $format_sub = sub { my $detail = shift;
421 $csv->parse($detail) or return "can't parse $detail";
422 join(' - ', map { &$escape_function($_) }
427 $format_sub = sub { my $detail = shift;
428 $csv->parse($detail) or return "can't parse $detail";
429 join('</TD><TD>', map { &$escape_function($_) }
433 if $format eq 'html';
435 $format_sub = sub { my $detail = shift;
436 $csv->parse($detail) or return "can't parse $detail";
437 #join(' & ', map { '\small{'. &$escape_function($_). '}' }
441 foreach ($csv->fields) {
442 $result .= ' & ' if $column > 1;
443 if ($column > 6) { # KLUDGE ALERT!
444 $result .= '\multicolumn{1}{l}{\scriptsize{'.
445 &$escape_function($_). '}}';
447 $result .= '\scriptsize{'. &$escape_function($_). '}';
453 if $format eq 'latex';
455 $format_sub = $opt{format_function} if $opt{format_function};
457 map { ( $_->format eq 'C'
458 ? &{$format_sub}( $_->detail, $_ )
459 : &{$escape_function}( $_->detail )
462 qsearch ({ 'table' => 'cust_bill_pkg_detail',
463 'hashref' => { 'billpkgnum' => $self->billpkgnum },
464 'order_by' => 'ORDER BY detailnum',
466 #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum });
471 Returns a description for this line item. For typical line items, this is the
472 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
473 For one-shot line items and named taxes, it is the I<itemdesc> field of this
474 line item, and for generic taxes, simply returns "Tax".
481 if ( $self->pkgnum > 0 ) {
482 $self->itemdesc || $self->part_pkg->pkg;
484 my $desc = $self->itemdesc || 'Tax';
485 $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
492 Returns the amount owed (still outstanding) on this line item's setup fee,
493 which is the amount of the line item minus all payment applications (see
494 L<FS::cust_bill_pay_pkg> and credit applications (see
495 L<FS::cust_credit_bill_pkg>).
501 $self->owed('setup', @_);
506 Returns the amount owed (still outstanding) on this line item's recurring 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('recur', @_);
518 # modeled after cust_bill::owed...
520 my( $self, $field ) = @_;
521 my $balance = $self->$field();
522 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
523 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
524 $balance = sprintf( '%.2f', $balance );
525 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
529 sub cust_bill_pay_pkg {
530 my( $self, $field ) = @_;
531 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
532 'setuprecur' => $field,
537 sub cust_credit_bill_pkg {
538 my( $self, $field ) = @_;
539 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
540 'setuprecur' => $field,
547 Returns the number of billing units (for tax purposes) represented by this,
554 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
562 my( $self, $value ) = @_;
563 if ( defined($value) ) {
564 $self->setfield('quantity', $value);
566 $self->getfield('quantity') || 1;
574 my( $self, $value ) = @_;
575 if ( defined($value) ) {
576 $self->setfield('unitsetup', $value);
578 $self->getfield('unitsetup') eq ''
579 ? $self->getfield('setup')
580 : $self->getfield('unitsetup');
588 my( $self, $value ) = @_;
589 if ( defined($value) ) {
590 $self->setfield('unitrecur', $value);
592 $self->getfield('unitrecur') eq ''
593 ? $self->getfield('recur')
594 : $self->getfield('unitrecur');
599 Returns a list of cust_bill_pkg objects each with no more than a single class
600 (including setup or recur) of charge.
606 # XXX this goes away with cust_bill_pkg refactor
608 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
609 my %cust_bill_pkg = ();
611 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
612 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
615 #split setup and recur
616 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
617 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
618 $cust_bill_pkg->set('details', []);
619 $cust_bill_pkg->recur(0);
620 $cust_bill_pkg->unitrecur(0);
621 $cust_bill_pkg->type('');
622 $cust_bill_pkg_recur->setup(0);
623 $cust_bill_pkg_recur->unitsetup(0);
624 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
628 #split usage from recur
629 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage );
630 warn "usage is $usage\n" if $DEBUG;
632 my $cust_bill_pkg_usage =
633 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
634 $cust_bill_pkg_usage->recur( $usage );
635 $cust_bill_pkg_usage->type( 'U' );
636 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
637 $cust_bill_pkg{recur}->recur( $recur );
638 $cust_bill_pkg{recur}->type( '' );
639 $cust_bill_pkg{recur}->set('details', []);
640 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
643 #subdivide usage by usage_class
644 if (exists($cust_bill_pkg{''})) {
645 foreach my $class (grep { $_ } $self->usage_classes) {
646 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
647 my $cust_bill_pkg_usage =
648 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
649 $cust_bill_pkg_usage->recur( $usage );
650 $cust_bill_pkg_usage->set('details', []);
651 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
652 $cust_bill_pkg{''}->recur( $classless );
653 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
655 delete $cust_bill_pkg{''} unless $cust_bill_pkg{''}->recur;
658 # # sort setup,recur,'', and the rest numeric && return
659 # my @result = map { $cust_bill_pkg{$_} }
660 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
661 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
663 # keys %cust_bill_pkg;
672 Returns the amount of the charge associated with usage class CLASSNUM if
673 CLASSNUM is defined. Otherwise returns the total charge associated with
679 my( $self, $classnum ) = @_;
683 if ( $self->get('details') ) {
687 grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) }
688 @{ $self->get('details') };
692 my $hashref = { 'billpkgnum' => $self->billpkgnum };
693 $hashref->{ 'classnum' } = $classnum if defined($classnum);
694 @values = map { $_->amount } qsearch('cust_bill_pkg_detail', $hashref);
698 foreach ( @values ) {
706 Returns a list of usage classnums associated with this invoice line's
714 if ( $self->get('details') ) {
717 foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
718 $seen{ $detail->[3] } = 1;
725 qsearch({ table => 'cust_bill_pkg_detail',
726 hashref => { billpkgnum => $self->billpkgnum },
727 select => 'DISTINCT classnum',
734 =item cust_bill_pkg_display [ type => TYPE ]
736 Returns an array of display information for the invoice line item optionally
741 sub cust_bill_pkg_display {
742 my ( $self, %opt ) = @_;
745 new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
747 return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
749 my $type = $opt{type} if exists $opt{type};
752 if ( scalar( $self->get('display') ) ) {
753 @result = grep { defined($type) ? ($type eq $_->type) : 1 }
754 @{ $self->get('display') };
756 my $hashref = { 'billpkgnum' => $self->billpkgnum };
757 $hashref->{type} = $type if defined($type);
759 @result = qsearch ({ 'table' => 'cust_bill_pkg_display',
760 'hashref' => { 'billpkgnum' => $self->billpkgnum },
761 'order_by' => 'ORDER BY billpkgdisplaynum',
765 push @result, $default unless ( scalar(@result) || $type );
771 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
772 # and FS::cust_main::bill
774 sub _cust_tax_exempt_pkg {
777 $self->{Hash}->{_cust_tax_exempt_pkg} or
778 $self->{Hash}->{_cust_tax_exempt_pkg} = [];
787 setup and recur shouldn't be separate fields. There should be one "amount"
788 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
790 A line item with both should really be two separate records (preserving
791 sdate and edate for setup fees for recurring packages - that information may
792 be valuable later). Invoice generation (cust_main::bill), invoice printing
793 (cust_bill), tax reports (report_tax.cgi) and line item reports
794 (cust_bill_pkg.cgi) would need to be updated.
796 owed_setup and owed_recur could then be repaced by just owed, and
797 cust_bill::open_cust_bill_pkg and
798 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
802 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
803 from the base documentation.