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 warn $cust_bill_pkg_tax_location;
190 $error = $cust_bill_pkg_tax_location->insert;
193 $dbh->rollback if $oldAutoCommit;
194 return "error inserting cust_bill_pkg_tax_location: $error";
199 my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
200 if ( $tax_rate_location ) {
201 foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
202 $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
203 $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;
218 $dbh->rollback if $oldAutoCommit;
219 return "error replacing cust_tax_adjustment: $error";
223 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
237 local $SIG{HUP} = 'IGNORE';
238 local $SIG{INT} = 'IGNORE';
239 local $SIG{QUIT} = 'IGNORE';
240 local $SIG{TERM} = 'IGNORE';
241 local $SIG{TSTP} = 'IGNORE';
242 local $SIG{PIPE} = 'IGNORE';
244 my $oldAutoCommit = $FS::UID::AutoCommit;
245 local $FS::UID::AutoCommit = 0;
248 foreach my $table (qw(
250 cust_bill_pkg_display
251 cust_bill_pkg_tax_location
252 cust_bill_pkg_tax_rate_location
258 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
259 my $error = $linked->delete;
261 $dbh->rollback if $oldAutoCommit;
268 foreach my $cust_tax_adjustment (
269 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
271 $cust_tax_adjustment->billpkgnum(''); #NULL
272 my $error = $cust_tax_adjustment->replace;
274 $dbh->rollback if $oldAutoCommit;
279 my $error = $self->SUPER::delete(@_);
281 $dbh->rollback if $oldAutoCommit;
285 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
291 #alas, bin/follow-tax-rename
293 #=item replace OLD_RECORD
295 #Currently unimplemented. This would be even more of an accounting nightmare
296 #than deleteing the items. Just don't do it.
301 # return "Can't modify cust_bill_pkg records!";
306 Checks all fields to make sure this is a valid line item. If there is an
307 error, returns the error, otherwise returns false. Called by the insert
316 $self->ut_numbern('billpkgnum')
317 || $self->ut_snumber('pkgnum')
318 || $self->ut_number('invnum')
319 || $self->ut_money('setup')
320 || $self->ut_money('recur')
321 || $self->ut_numbern('sdate')
322 || $self->ut_numbern('edate')
323 || $self->ut_textn('itemdesc')
324 || $self->ut_textn('itemcomment')
325 || $self->ut_enum('hidden', [ '', 'Y' ])
327 return $error if $error;
329 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
330 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
331 return "Unknown pkgnum ". $self->pkgnum
332 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
335 return "Unknown invnum"
336 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
343 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
349 warn "$me $self -> cust_pkg";
350 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
355 Returns the package definition for this invoice line item.
361 if ( $self->pkgpart_override ) {
362 qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
365 my $cust_pkg = $self->cust_pkg;
366 $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
373 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
379 qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
382 =item previous_cust_bill_pkg
384 Returns the previous cust_bill_pkg for this package, if any.
388 sub previous_cust_bill_pkg {
390 return unless $self->sdate;
392 'table' => 'cust_bill_pkg',
393 'hashref' => { 'pkgnum' => $self->pkgnum,
394 'sdate' => { op=>'<', value=>$self->sdate },
396 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
400 =item details [ OPTION => VALUE ... ]
402 Returns an array of detail information for the invoice line item.
404 Currently available options are: I<format> I<escape_function>
406 If I<format> is set to html or latex then the array members are improved
407 for tabular appearance in those environments if possible.
409 If I<escape_function> is set then the array members are processed by this
410 function before being returned.
415 my ( $self, %opt ) = @_;
416 my $format = $opt{format} || '';
417 my $escape_function = $opt{escape_function} || sub { shift };
418 return () unless defined dbdef->table('cust_bill_pkg_detail');
420 eval "use Text::CSV_XS;";
422 my $csv = new Text::CSV_XS;
424 my $format_sub = sub { my $detail = shift;
425 $csv->parse($detail) or return "can't parse $detail";
426 join(' - ', map { &$escape_function($_) }
431 $format_sub = sub { my $detail = shift;
432 $csv->parse($detail) or return "can't parse $detail";
433 join('</TD><TD>', map { &$escape_function($_) }
437 if $format eq 'html';
439 $format_sub = sub { my $detail = shift;
440 $csv->parse($detail) or return "can't parse $detail";
441 #join(' & ', map { '\small{'. &$escape_function($_). '}' }
445 foreach ($csv->fields) {
446 $result .= ' & ' if $column > 1;
447 if ($column > 6) { # KLUDGE ALERT!
448 $result .= '\multicolumn{1}{l}{\scriptsize{'.
449 &$escape_function($_). '}}';
451 $result .= '\scriptsize{'. &$escape_function($_). '}';
457 if $format eq 'latex';
459 $format_sub = $opt{format_function} if $opt{format_function};
461 map { ( $_->format eq 'C'
462 ? &{$format_sub}( $_->detail, $_ )
463 : &{$escape_function}( $_->detail )
466 qsearch ({ 'table' => 'cust_bill_pkg_detail',
467 'hashref' => { 'billpkgnum' => $self->billpkgnum },
468 'order_by' => 'ORDER BY detailnum',
470 #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum });
475 Returns a description for this line item. For typical line items, this is the
476 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
477 For one-shot line items and named taxes, it is the I<itemdesc> field of this
478 line item, and for generic taxes, simply returns "Tax".
485 if ( $self->pkgnum > 0 ) {
486 $self->itemdesc || $self->part_pkg->pkg;
488 my $desc = $self->itemdesc || 'Tax';
489 $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
496 Returns the amount owed (still outstanding) on this line item's setup fee,
497 which is the amount of the line item minus all payment applications (see
498 L<FS::cust_bill_pay_pkg> and credit applications (see
499 L<FS::cust_credit_bill_pkg>).
505 $self->owed('setup', @_);
510 Returns the amount owed (still outstanding) on this line item's recurring fee,
511 which is the amount of the line item minus all payment applications (see
512 L<FS::cust_bill_pay_pkg> and credit applications (see
513 L<FS::cust_credit_bill_pkg>).
519 $self->owed('recur', @_);
522 # modeled after cust_bill::owed...
524 my( $self, $field ) = @_;
525 my $balance = $self->$field();
526 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
527 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
528 $balance = sprintf( '%.2f', $balance );
529 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
533 sub cust_bill_pay_pkg {
534 my( $self, $field ) = @_;
535 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
536 'setuprecur' => $field,
541 sub cust_credit_bill_pkg {
542 my( $self, $field ) = @_;
543 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
544 'setuprecur' => $field,
551 Returns the number of billing units (for tax purposes) represented by this,
558 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
566 my( $self, $value ) = @_;
567 if ( defined($value) ) {
568 $self->setfield('quantity', $value);
570 $self->getfield('quantity') || 1;
578 my( $self, $value ) = @_;
579 if ( defined($value) ) {
580 $self->setfield('unitsetup', $value);
582 $self->getfield('unitsetup') eq ''
583 ? $self->getfield('setup')
584 : $self->getfield('unitsetup');
592 my( $self, $value ) = @_;
593 if ( defined($value) ) {
594 $self->setfield('unitrecur', $value);
596 $self->getfield('unitrecur') eq ''
597 ? $self->getfield('recur')
598 : $self->getfield('unitrecur');
603 Returns a list of cust_bill_pkg objects each with no more than a single class
604 (including setup or recur) of charge.
610 # XXX this goes away with cust_bill_pkg refactor
612 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
613 my %cust_bill_pkg = ();
615 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
616 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
619 #split setup and recur
620 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
621 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
622 $cust_bill_pkg->set('details', []);
623 $cust_bill_pkg->recur(0);
624 $cust_bill_pkg->unitrecur(0);
625 $cust_bill_pkg->type('');
626 $cust_bill_pkg_recur->setup(0);
627 $cust_bill_pkg_recur->unitsetup(0);
628 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
632 #split usage from recur
633 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage );
634 warn "usage is $usage\n" if $DEBUG;
636 my $cust_bill_pkg_usage =
637 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
638 $cust_bill_pkg_usage->recur( $usage );
639 $cust_bill_pkg_usage->type( 'U' );
640 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
641 $cust_bill_pkg{recur}->recur( $recur );
642 $cust_bill_pkg{recur}->type( '' );
643 $cust_bill_pkg{recur}->set('details', []);
644 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
647 #subdivide usage by usage_class
648 if (exists($cust_bill_pkg{''})) {
649 foreach my $class (grep { $_ } $self->usage_classes) {
650 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
651 my $cust_bill_pkg_usage =
652 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
653 $cust_bill_pkg_usage->recur( $usage );
654 $cust_bill_pkg_usage->set('details', []);
655 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
656 $cust_bill_pkg{''}->recur( $classless );
657 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
659 delete $cust_bill_pkg{''} unless $cust_bill_pkg{''}->recur;
662 # # sort setup,recur,'', and the rest numeric && return
663 # my @result = map { $cust_bill_pkg{$_} }
664 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
665 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
667 # keys %cust_bill_pkg;
676 Returns the amount of the charge associated with usage class CLASSNUM if
677 CLASSNUM is defined. Otherwise returns the total charge associated with
683 my( $self, $classnum ) = @_;
687 if ( $self->get('details') ) {
691 grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) }
692 @{ $self->get('details') };
696 my $hashref = { 'billpkgnum' => $self->billpkgnum };
697 $hashref->{ 'classnum' } = $classnum if defined($classnum);
698 @values = map { $_->amount } qsearch('cust_bill_pkg_detail', $hashref);
702 foreach ( @values ) {
710 Returns a list of usage classnums associated with this invoice line's
718 if ( $self->get('details') ) {
721 foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
722 $seen{ $detail->[3] } = 1;
729 qsearch({ table => 'cust_bill_pkg_detail',
730 hashref => { billpkgnum => $self->billpkgnum },
731 select => 'DISTINCT classnum',
738 =item cust_bill_pkg_display [ type => TYPE ]
740 Returns an array of display information for the invoice line item optionally
745 sub cust_bill_pkg_display {
746 my ( $self, %opt ) = @_;
749 new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
751 return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
753 my $type = $opt{type} if exists $opt{type};
756 if ( scalar( $self->get('display') ) ) {
757 @result = grep { defined($type) ? ($type eq $_->type) : 1 }
758 @{ $self->get('display') };
760 my $hashref = { 'billpkgnum' => $self->billpkgnum };
761 $hashref->{type} = $type if defined($type);
763 @result = qsearch ({ 'table' => 'cust_bill_pkg_display',
764 'hashref' => { 'billpkgnum' => $self->billpkgnum },
765 'order_by' => 'ORDER BY billpkgdisplaynum',
769 push @result, $default unless ( scalar(@result) || $type );
775 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
776 # and FS::cust_main::bill
778 sub _cust_tax_exempt_pkg {
781 $self->{Hash}->{_cust_tax_exempt_pkg} or
782 $self->{Hash}->{_cust_tax_exempt_pkg} = [];
791 setup and recur shouldn't be separate fields. There should be one "amount"
792 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
794 A line item with both should really be two separate records (preserving
795 sdate and edate for setup fees for recurring packages - that information may
796 be valuable later). Invoice generation (cust_main::bill), invoice printing
797 (cust_bill), tax reports (report_tax.cgi) and line item reports
798 (cust_bill_pkg.cgi) would need to be updated.
800 owed_setup and owed_recur could then be repaced by just owed, and
801 cust_bill::open_cust_bill_pkg and
802 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
806 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
807 from the base documentation.