should hopefully fix old services showing up on invoices, RT#5451/RT#5514/RT#5564...
[freeside.git] / FS / FS / cust_bill_pkg.pm
1 package FS::cust_bill_pkg;
2
3 use strict;
4 use vars qw( @ISA $DEBUG );
5 use FS::Record qw( qsearch qsearchs dbdef dbh );
6 use FS::cust_main_Mixin;
7 use FS::cust_pkg;
8 use FS::part_pkg;
9 use FS::cust_bill;
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
16 @ISA = qw( FS::cust_main_Mixin FS::Record );
17
18 $DEBUG = 0;
19
20 =head1 NAME
21
22 FS::cust_bill_pkg - Object methods for cust_bill_pkg records
23
24 =head1 SYNOPSIS
25
26   use FS::cust_bill_pkg;
27
28   $record = new FS::cust_bill_pkg \%hash;
29   $record = new FS::cust_bill_pkg { 'column' => 'value' };
30
31   $error = $record->insert;
32
33   $error = $record->check;
34
35 =head1 DESCRIPTION
36
37 An FS::cust_bill_pkg object represents an invoice line item.
38 FS::cust_bill_pkg inherits from FS::Record.  The following fields are currently
39 supported:
40
41 =over 4
42
43 =item billpkgnum - primary key
44
45 =item invnum - invoice (see L<FS::cust_bill>)
46
47 =item pkgnum - 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)
48
49 =item pkgpart_override - optional package definition (see L<FS::part_pkg>) override
50 =item setup - setup fee
51
52 =item recur - recurring fee
53
54 =item sdate - starting date of recurring fee
55
56 =item edate - ending date of recurring fee
57
58 =item itemdesc - Line item description (overrides normal package description)
59
60 =item quantity - If not set, defaults to 1
61
62 =item unitsetup - If not set, defaults to setup
63
64 =item unitrecur - If not set, defaults to recur
65
66 =back
67
68 sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">.  Also
69 see L<Time::Local> and L<Date::Parse> for conversion functions.
70
71 =head1 METHODS
72
73 =over 4
74
75 =item new HASHREF
76
77 Creates a new line item.  To add the line item to the database, see
78 L<"insert">.  Line items are normally created by calling the bill method of a
79 customer object (see L<FS::cust_main>).
80
81 =cut
82
83 sub table { 'cust_bill_pkg'; }
84
85 =item insert
86
87 Adds this line item to the database.  If there is an error, returns the error,
88 otherwise returns false.
89
90 =cut
91
92 sub insert {
93   my $self = shift;
94
95   local $SIG{HUP} = 'IGNORE';
96   local $SIG{INT} = 'IGNORE';
97   local $SIG{QUIT} = 'IGNORE';
98   local $SIG{TERM} = 'IGNORE';
99   local $SIG{TSTP} = 'IGNORE';
100   local $SIG{PIPE} = 'IGNORE';
101
102   my $oldAutoCommit = $FS::UID::AutoCommit;
103   local $FS::UID::AutoCommit = 0;
104   my $dbh = dbh;
105
106   my $error = $self->SUPER::insert;
107   if ( $error ) {
108     $dbh->rollback if $oldAutoCommit;
109     return $error;
110   }
111
112   if ( defined dbdef->table('cust_bill_pkg_detail') && $self->get('details') ) {
113     foreach my $detail ( @{$self->get('details')} ) {
114       my $cust_bill_pkg_detail = new FS::cust_bill_pkg_detail {
115         'billpkgnum' => $self->billpkgnum,
116         'format'     => (ref($detail) ? $detail->[0] : '' ),
117         'detail'     => (ref($detail) ? $detail->[1] : $detail ),
118         'amount'     => (ref($detail) ? $detail->[2] : '' ),
119         'classnum'   => (ref($detail) ? $detail->[3] : '' ),
120       };
121       $error = $cust_bill_pkg_detail->insert;
122       if ( $error ) {
123         $dbh->rollback if $oldAutoCommit;
124         return $error;
125       }
126     }
127   }
128
129   if ( defined dbdef->table('cust_bill_pkg_display') && $self->get('display') ){
130     foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
131       $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
132       $error = $cust_bill_pkg_display->insert;
133       if ( $error ) {
134         $dbh->rollback if $oldAutoCommit;
135         return $error;
136       }
137     }
138   }
139
140   if ( $self->_cust_tax_exempt_pkg ) {
141     foreach my $cust_tax_exempt_pkg ( @{$self->_cust_tax_exempt_pkg} ) {
142       $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
143       $error = $cust_tax_exempt_pkg->insert;
144       if ( $error ) {
145         $dbh->rollback if $oldAutoCommit;
146         return $error;
147       }
148     }
149   }
150
151   my $tax_location = $self->get('cust_bill_pkg_tax_location');
152   if ( $tax_location ) {
153     foreach my $cust_bill_pkg_tax_location ( @$tax_location ) {
154       $cust_bill_pkg_tax_location->billpkgnum($self->billpkgnum);
155       warn $cust_bill_pkg_tax_location;
156       $error = $cust_bill_pkg_tax_location->insert;
157       warn $error;
158       if ( $error ) {
159         $dbh->rollback if $oldAutoCommit;
160         return $error;
161       }
162     }
163   }
164
165   my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
166   if ( $tax_rate_location ) {
167     foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
168       $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
169       $error = $cust_bill_pkg_tax_rate_location->insert;
170       warn $error;
171       if ( $error ) {
172         $dbh->rollback if $oldAutoCommit;
173         return $error;
174       }
175     }
176   }
177
178   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
179   '';
180
181 }
182
183 =item delete
184
185 Currently unimplemented.  I don't remove line items because there would then be
186 no record the items ever existed (which is bad, no?)
187
188 =cut
189
190 sub delete {
191   return "Can't delete cust_bill_pkg records!";
192 }
193
194 #alas, bin/follow-tax-rename
195 #
196 #=item replace OLD_RECORD
197 #
198 #Currently unimplemented.  This would be even more of an accounting nightmare
199 #than deleteing the items.  Just don't do it.
200 #
201 #=cut
202 #
203 #sub replace {
204 #  return "Can't modify cust_bill_pkg records!";
205 #}
206
207 =item check
208
209 Checks all fields to make sure this is a valid line item.  If there is an
210 error, returns the error, otherwise returns false.  Called by the insert
211 method.
212
213 =cut
214
215 sub check {
216   my $self = shift;
217
218   my $error =
219          $self->ut_numbern('billpkgnum')
220       || $self->ut_snumber('pkgnum')
221       || $self->ut_number('invnum')
222       || $self->ut_money('setup')
223       || $self->ut_money('recur')
224       || $self->ut_numbern('sdate')
225       || $self->ut_numbern('edate')
226       || $self->ut_textn('itemdesc')
227   ;
228   return $error if $error;
229
230   #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
231   if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
232     return "Unknown pkgnum ". $self->pkgnum
233       unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
234   }
235
236   return "Unknown invnum"
237     unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
238
239   $self->SUPER::check;
240 }
241
242 =item cust_pkg
243
244 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
245
246 =cut
247
248 sub cust_pkg {
249   my $self = shift;
250   qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
251 }
252
253 =item part_pkg
254
255 Returns the package definition for this invoice line item.
256
257 =cut
258
259 sub part_pkg {
260   my $self = shift;
261   if ( $self->pkgpart_override ) {
262     qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
263   } else {
264     $self->cust_pkg->part_pkg;
265   }
266 }
267
268 =item cust_bill
269
270 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
271
272 =cut
273
274 sub cust_bill {
275   my $self = shift;
276   qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
277 }
278
279 =item previous_cust_bill_pkg
280
281 Returns the previous cust_bill_pkg for this package, if any.
282
283 =cut
284
285 sub previous_cust_bill_pkg {
286   my $self = shift;
287   qsearchs({
288     'table'    => 'cust_bill_pkg',
289     'hashref'  => { 'pkgnum' => $self->pkgnum,
290                     'sdate'  => { op=>'<', value=>$self->sdate },
291                   },
292     'order_by' => 'ORDER BY sdate DESC LIMIT 1',
293   });
294 }
295
296 =item details [ OPTION => VALUE ... ]
297
298 Returns an array of detail information for the invoice line item.
299
300 Currently available options are: I<format> I<escape_function>
301
302 If I<format> is set to html or latex then the array members are improved
303 for tabular appearance in those environments if possible.
304
305 If I<escape_function> is set then the array members are processed by this
306 function before being returned.
307
308 =cut
309
310 sub details {
311   my ( $self, %opt ) = @_;
312   my $format = $opt{format} || '';
313   my $escape_function = $opt{escape_function} || sub { shift };
314   return () unless defined dbdef->table('cust_bill_pkg_detail');
315
316   eval "use Text::CSV_XS;";
317   die $@ if $@;
318   my $csv = new Text::CSV_XS;
319
320   my $format_sub = sub { my $detail = shift;
321                          $csv->parse($detail) or return "can't parse $detail";
322                          join(' - ', map { &$escape_function($_) }
323                                      $csv->fields
324                              );
325                        };
326
327   $format_sub = sub { my $detail = shift;
328                       $csv->parse($detail) or return "can't parse $detail";
329                       join('</TD><TD>', map { &$escape_function($_) }
330                                         $csv->fields
331                           );
332                     }
333     if $format eq 'html';
334
335   $format_sub = sub { my $detail = shift;
336                       $csv->parse($detail) or return "can't parse $detail";
337                       #join(' & ', map { '\small{'. &$escape_function($_). '}' }
338                       #            $csv->fields );
339                       my $result = '';
340                       my $column = 1;
341                       foreach ($csv->fields) {
342                         $result .= ' & ' if $column > 1;
343                         if ($column > 6) {                     # KLUDGE ALERT!
344                           $result .= '\multicolumn{1}{l}{\scriptsize{'.
345                                      &$escape_function($_). '}}';
346                         }else{
347                           $result .= '\scriptsize{'.  &$escape_function($_). '}';
348                         }
349                         $column++;
350                       }
351                       $result;
352                     }
353     if $format eq 'latex';
354
355   $format_sub = $opt{format_function} if $opt{format_function};
356
357   map { ( $_->format eq 'C'
358           ? &{$format_sub}( $_->detail )
359           : &{$escape_function}( $_->detail )
360         )
361       }
362     qsearch ({ 'table'    => 'cust_bill_pkg_detail',
363                'hashref'  => { 'billpkgnum' => $self->billpkgnum },
364                'order_by' => 'ORDER BY detailnum',
365             });
366     #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum });
367 }
368
369 =item desc
370
371 Returns a description for this line item.  For typical line items, this is the
372 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
373 For one-shot line items and named taxes, it is the I<itemdesc> field of this
374 line item, and for generic taxes, simply returns "Tax".
375
376 =cut
377
378 sub desc {
379   my $self = shift;
380
381   if ( $self->pkgnum > 0 ) {
382     $self->itemdesc || $self->part_pkg->pkg;
383   } else {
384     $self->itemdesc || 'Tax';
385   }
386 }
387
388 =item owed_setup
389
390 Returns the amount owed (still outstanding) on this line item's setup fee,
391 which is the amount of the line item minus all payment applications (see
392 L<FS::cust_bill_pay_pkg> and credit applications (see
393 L<FS::cust_credit_bill_pkg>).
394
395 =cut
396
397 sub owed_setup {
398   my $self = shift;
399   $self->owed('setup', @_);
400 }
401
402 =item owed_recur
403
404 Returns the amount owed (still outstanding) on this line item's recurring fee,
405 which is the amount of the line item minus all payment applications (see
406 L<FS::cust_bill_pay_pkg> and credit applications (see
407 L<FS::cust_credit_bill_pkg>).
408
409 =cut
410
411 sub owed_recur {
412   my $self = shift;
413   $self->owed('recur', @_);
414 }
415
416 # modeled after cust_bill::owed...
417 sub owed {
418   my( $self, $field ) = @_;
419   my $balance = $self->$field();
420   $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
421   $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
422   $balance = sprintf( '%.2f', $balance );
423   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
424   $balance;
425 }
426
427 sub cust_bill_pay_pkg {
428   my( $self, $field ) = @_;
429   qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
430                                   'setuprecur' => $field,
431                                 }
432          );
433 }
434
435 sub cust_credit_bill_pkg {
436   my( $self, $field ) = @_;
437   qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
438                                      'setuprecur' => $field,
439                                    }
440          );
441 }
442
443 =item units
444
445 Returns the number of billing units (for tax purposes) represented by this,
446 line item.
447
448 =cut
449
450 sub units {
451   my $self = shift;
452   $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
453 }
454
455 =item quantity
456
457 =cut
458
459 sub quantity {
460   my( $self, $value ) = @_;
461   if ( defined($value) ) {
462     $self->setfield('quantity', $value);
463   }
464   $self->getfield('quantity') || 1;
465 }
466
467 =item unitsetup
468
469 =cut
470
471 sub unitsetup {
472   my( $self, $value ) = @_;
473   if ( defined($value) ) {
474     $self->setfield('unitsetup', $value);
475   }
476   $self->getfield('unitsetup') eq ''
477     ? $self->getfield('setup')
478     : $self->getfield('unitsetup');
479 }
480
481 =item unitrecur
482
483 =cut
484
485 sub unitrecur {
486   my( $self, $value ) = @_;
487   if ( defined($value) ) {
488     $self->setfield('unitrecur', $value);
489   }
490   $self->getfield('unitrecur') eq ''
491     ? $self->getfield('recur')
492     : $self->getfield('unitrecur');
493 }
494
495 =item disintegrate
496
497 Returns a list of cust_bill_pkg objects each with no more than a single class
498 (including setup or recur) of charge.
499
500 =cut
501
502 sub disintegrate {
503   my $self = shift;
504   # XXX this goes away with cust_bill_pkg refactor
505
506   my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
507   my %cust_bill_pkg = ();
508
509   $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
510   $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
511
512
513   #split setup and recur
514   if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
515     my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
516     $cust_bill_pkg->set('details', []);
517     $cust_bill_pkg->recur(0);
518     $cust_bill_pkg->unitrecur(0);
519     $cust_bill_pkg->type('');
520     $cust_bill_pkg_recur->setup(0);
521     $cust_bill_pkg_recur->unitsetup(0);
522     $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
523
524   }
525
526   #split usage from recur
527   my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage );
528   warn "usage is $usage\n" if $DEBUG;
529   if ($usage) {
530     my $cust_bill_pkg_usage =
531         new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
532     $cust_bill_pkg_usage->recur( $usage );
533     $cust_bill_pkg_usage->type( 'U' );
534     my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
535     $cust_bill_pkg{recur}->recur( $recur );
536     $cust_bill_pkg{recur}->type( '' );
537     $cust_bill_pkg{recur}->set('details', []);
538     $cust_bill_pkg{''} = $cust_bill_pkg_usage;
539   }
540
541   #subdivide usage by usage_class
542   if (exists($cust_bill_pkg{''})) {
543     foreach my $class (grep { $_ } $self->usage_classes) {
544       my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
545       my $cust_bill_pkg_usage =
546           new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
547       $cust_bill_pkg_usage->recur( $usage );
548       $cust_bill_pkg_usage->set('details', []);
549       my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
550       $cust_bill_pkg{''}->recur( $classless );
551       $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
552     }
553     delete $cust_bill_pkg{''} unless $cust_bill_pkg{''}->recur;
554   }
555
556 #  # sort setup,recur,'', and the rest numeric && return
557 #  my @result = map { $cust_bill_pkg{$_} }
558 #               sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
559 #                      ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
560 #                    }
561 #               keys %cust_bill_pkg;
562 #
563 #  return (@result);
564
565    %cust_bill_pkg;
566 }
567
568 =item usage CLASSNUM
569
570 Returns the amount of the charge associated with usage class CLASSNUM if
571 CLASSNUM is defined.  Otherwise returns the total charge associated with
572 usage.
573   
574 =cut
575
576 sub usage {
577   my( $self, $classnum ) = @_;
578   my $sum = 0;
579   my @values = ();
580
581   if ( $self->get('details') ) {
582
583     @values = 
584       map { $_->[2] }
585       grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) }
586       @{ $self->get('details') };
587
588   }else{
589
590     my $hashref = { 'billpkgnum' => $self->billpkgnum };
591     $hashref->{ 'classnum' } = $classnum if defined($classnum);
592     @values = map { $_->amount } qsearch('cust_bill_pkg_detail', $hashref);
593
594   }
595
596   foreach ( @values ) {
597     $sum += $_ if $_;
598   }
599   $sum;
600 }
601
602 =item usage_classes
603
604 Returns a list of usage classnums associated with this invoice line's
605 details.
606   
607 =cut
608
609 sub usage_classes {
610   my( $self ) = @_;
611
612   if ( $self->get('details') ) {
613
614     my %seen = ();
615     foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
616       $seen{ $detail->[3] } = 1;
617     }
618     keys %seen;
619
620   }else{
621
622     map { $_->classnum }
623         qsearch({ table   => 'cust_bill_pkg_detail',
624                   hashref => { billpkgnum => $self->billpkgnum },
625                   select  => 'DISTINCT classnum',
626                });
627
628   }
629
630 }
631
632 =item cust_bill_pkg_display [ type => TYPE ]
633
634 Returns an array of display information for the invoice line item optionally
635 limited to 'TYPE'.
636
637 =cut
638
639 sub cust_bill_pkg_display {
640   my ( $self, %opt ) = @_;
641
642   my $default =
643     new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
644
645   return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
646
647   my $type = $opt{type} if exists $opt{type};
648   my @result;
649
650   if ( scalar( $self->get('display') ) ) {
651     @result = grep { defined($type) ? ($type eq $_->type) : 1 }
652               @{ $self->get('display') };
653   }else{
654     my $hashref = { 'billpkgnum' => $self->billpkgnum };
655     $hashref->{type} = $type if defined($type);
656     
657     @result = qsearch ({ 'table'    => 'cust_bill_pkg_display',
658                          'hashref'  => { 'billpkgnum' => $self->billpkgnum },
659                          'order_by' => 'ORDER BY billpkgdisplaynum',
660                       });
661   }
662
663   push @result, $default unless ( scalar(@result) || $type );
664
665   @result;
666
667 }
668
669 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
670 # and FS::cust_main::bill
671
672 sub _cust_tax_exempt_pkg {
673   my ( $self ) = @_;
674
675   $self->{Hash}->{_cust_tax_exempt_pkg} or
676   $self->{Hash}->{_cust_tax_exempt_pkg} = [];
677
678 }
679
680
681 =back
682
683 =head1 BUGS
684
685 setup and recur shouldn't be separate fields.  There should be one "amount"
686 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
687
688 A line item with both should really be two separate records (preserving
689 sdate and edate for setup fees for recurring packages - that information may
690 be valuable later).  Invoice generation (cust_main::bill), invoice printing
691 (cust_bill), tax reports (report_tax.cgi) and line item reports 
692 (cust_bill_pkg.cgi) would need to be updated.
693
694 owed_setup and owed_recur could then be repaced by just owed, and
695 cust_bill::open_cust_bill_pkg and
696 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
697
698 =head1 SEE ALSO
699
700 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
701 from the base documentation.
702
703 =cut
704
705 1;
706