tax adjustments, RT#5595
[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   my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
179   if ( $cust_tax_adjustment ) {
180     $cust_tax_adjustment->billpkgnum($self->billpkgnum);
181     $error = $cust_tax_adjustment->replace;
182     warn $error;
183     if ( $error ) {
184       $dbh->rollback if $oldAutoCommit;
185       return $error;
186     }
187   }
188
189   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
190   '';
191
192 }
193
194 =item delete
195
196 Currently unimplemented.  I don't remove line items because there would then be
197 no record the items ever existed (which is bad, no?)
198
199 =cut
200
201 sub delete {
202   return "Can't delete cust_bill_pkg records!";
203 }
204
205 #alas, bin/follow-tax-rename
206 #
207 #=item replace OLD_RECORD
208 #
209 #Currently unimplemented.  This would be even more of an accounting nightmare
210 #than deleteing the items.  Just don't do it.
211 #
212 #=cut
213 #
214 #sub replace {
215 #  return "Can't modify cust_bill_pkg records!";
216 #}
217
218 =item check
219
220 Checks all fields to make sure this is a valid line item.  If there is an
221 error, returns the error, otherwise returns false.  Called by the insert
222 method.
223
224 =cut
225
226 sub check {
227   my $self = shift;
228
229   my $error =
230          $self->ut_numbern('billpkgnum')
231       || $self->ut_snumber('pkgnum')
232       || $self->ut_number('invnum')
233       || $self->ut_money('setup')
234       || $self->ut_money('recur')
235       || $self->ut_numbern('sdate')
236       || $self->ut_numbern('edate')
237       || $self->ut_textn('itemdesc')
238       || $self->ut_textn('itemcomment')
239   ;
240   return $error if $error;
241
242   #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
243   if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
244     return "Unknown pkgnum ". $self->pkgnum
245       unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
246   }
247
248   return "Unknown invnum"
249     unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
250
251   $self->SUPER::check;
252 }
253
254 =item cust_pkg
255
256 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
257
258 =cut
259
260 sub cust_pkg {
261   my $self = shift;
262   qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
263 }
264
265 =item part_pkg
266
267 Returns the package definition for this invoice line item.
268
269 =cut
270
271 sub part_pkg {
272   my $self = shift;
273   if ( $self->pkgpart_override ) {
274     qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
275   } else {
276     $self->cust_pkg->part_pkg;
277   }
278 }
279
280 =item cust_bill
281
282 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
283
284 =cut
285
286 sub cust_bill {
287   my $self = shift;
288   qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
289 }
290
291 =item previous_cust_bill_pkg
292
293 Returns the previous cust_bill_pkg for this package, if any.
294
295 =cut
296
297 sub previous_cust_bill_pkg {
298   my $self = shift;
299   qsearchs({
300     'table'    => 'cust_bill_pkg',
301     'hashref'  => { 'pkgnum' => $self->pkgnum,
302                     'sdate'  => { op=>'<', value=>$self->sdate },
303                   },
304     'order_by' => 'ORDER BY sdate DESC LIMIT 1',
305   });
306 }
307
308 =item details [ OPTION => VALUE ... ]
309
310 Returns an array of detail information for the invoice line item.
311
312 Currently available options are: I<format> I<escape_function>
313
314 If I<format> is set to html or latex then the array members are improved
315 for tabular appearance in those environments if possible.
316
317 If I<escape_function> is set then the array members are processed by this
318 function before being returned.
319
320 =cut
321
322 sub details {
323   my ( $self, %opt ) = @_;
324   my $format = $opt{format} || '';
325   my $escape_function = $opt{escape_function} || sub { shift };
326   return () unless defined dbdef->table('cust_bill_pkg_detail');
327
328   eval "use Text::CSV_XS;";
329   die $@ if $@;
330   my $csv = new Text::CSV_XS;
331
332   my $format_sub = sub { my $detail = shift;
333                          $csv->parse($detail) or return "can't parse $detail";
334                          join(' - ', map { &$escape_function($_) }
335                                      $csv->fields
336                              );
337                        };
338
339   $format_sub = sub { my $detail = shift;
340                       $csv->parse($detail) or return "can't parse $detail";
341                       join('</TD><TD>', map { &$escape_function($_) }
342                                         $csv->fields
343                           );
344                     }
345     if $format eq 'html';
346
347   $format_sub = sub { my $detail = shift;
348                       $csv->parse($detail) or return "can't parse $detail";
349                       #join(' & ', map { '\small{'. &$escape_function($_). '}' }
350                       #            $csv->fields );
351                       my $result = '';
352                       my $column = 1;
353                       foreach ($csv->fields) {
354                         $result .= ' & ' if $column > 1;
355                         if ($column > 6) {                     # KLUDGE ALERT!
356                           $result .= '\multicolumn{1}{l}{\scriptsize{'.
357                                      &$escape_function($_). '}}';
358                         }else{
359                           $result .= '\scriptsize{'.  &$escape_function($_). '}';
360                         }
361                         $column++;
362                       }
363                       $result;
364                     }
365     if $format eq 'latex';
366
367   $format_sub = $opt{format_function} if $opt{format_function};
368
369   map { ( $_->format eq 'C'
370           ? &{$format_sub}( $_->detail )
371           : &{$escape_function}( $_->detail )
372         )
373       }
374     qsearch ({ 'table'    => 'cust_bill_pkg_detail',
375                'hashref'  => { 'billpkgnum' => $self->billpkgnum },
376                'order_by' => 'ORDER BY detailnum',
377             });
378     #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum });
379 }
380
381 =item desc
382
383 Returns a description for this line item.  For typical line items, this is the
384 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
385 For one-shot line items and named taxes, it is the I<itemdesc> field of this
386 line item, and for generic taxes, simply returns "Tax".
387
388 =cut
389
390 sub desc {
391   my $self = shift;
392
393   if ( $self->pkgnum > 0 ) {
394     $self->itemdesc || $self->part_pkg->pkg;
395   } else {
396     my $desc = $self->itemdesc || 'Tax';
397     $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
398     $desc;
399   }
400 }
401
402 =item owed_setup
403
404 Returns the amount owed (still outstanding) on this line item's setup 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_setup {
412   my $self = shift;
413   $self->owed('setup', @_);
414 }
415
416 =item owed_recur
417
418 Returns the amount owed (still outstanding) on this line item's recurring fee,
419 which is the amount of the line item minus all payment applications (see
420 L<FS::cust_bill_pay_pkg> and credit applications (see
421 L<FS::cust_credit_bill_pkg>).
422
423 =cut
424
425 sub owed_recur {
426   my $self = shift;
427   $self->owed('recur', @_);
428 }
429
430 # modeled after cust_bill::owed...
431 sub owed {
432   my( $self, $field ) = @_;
433   my $balance = $self->$field();
434   $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
435   $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
436   $balance = sprintf( '%.2f', $balance );
437   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
438   $balance;
439 }
440
441 sub cust_bill_pay_pkg {
442   my( $self, $field ) = @_;
443   qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
444                                   'setuprecur' => $field,
445                                 }
446          );
447 }
448
449 sub cust_credit_bill_pkg {
450   my( $self, $field ) = @_;
451   qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
452                                      'setuprecur' => $field,
453                                    }
454          );
455 }
456
457 =item units
458
459 Returns the number of billing units (for tax purposes) represented by this,
460 line item.
461
462 =cut
463
464 sub units {
465   my $self = shift;
466   $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
467 }
468
469 =item quantity
470
471 =cut
472
473 sub quantity {
474   my( $self, $value ) = @_;
475   if ( defined($value) ) {
476     $self->setfield('quantity', $value);
477   }
478   $self->getfield('quantity') || 1;
479 }
480
481 =item unitsetup
482
483 =cut
484
485 sub unitsetup {
486   my( $self, $value ) = @_;
487   if ( defined($value) ) {
488     $self->setfield('unitsetup', $value);
489   }
490   $self->getfield('unitsetup') eq ''
491     ? $self->getfield('setup')
492     : $self->getfield('unitsetup');
493 }
494
495 =item unitrecur
496
497 =cut
498
499 sub unitrecur {
500   my( $self, $value ) = @_;
501   if ( defined($value) ) {
502     $self->setfield('unitrecur', $value);
503   }
504   $self->getfield('unitrecur') eq ''
505     ? $self->getfield('recur')
506     : $self->getfield('unitrecur');
507 }
508
509 =item disintegrate
510
511 Returns a list of cust_bill_pkg objects each with no more than a single class
512 (including setup or recur) of charge.
513
514 =cut
515
516 sub disintegrate {
517   my $self = shift;
518   # XXX this goes away with cust_bill_pkg refactor
519
520   my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
521   my %cust_bill_pkg = ();
522
523   $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
524   $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
525
526
527   #split setup and recur
528   if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
529     my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
530     $cust_bill_pkg->set('details', []);
531     $cust_bill_pkg->recur(0);
532     $cust_bill_pkg->unitrecur(0);
533     $cust_bill_pkg->type('');
534     $cust_bill_pkg_recur->setup(0);
535     $cust_bill_pkg_recur->unitsetup(0);
536     $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
537
538   }
539
540   #split usage from recur
541   my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage );
542   warn "usage is $usage\n" if $DEBUG;
543   if ($usage) {
544     my $cust_bill_pkg_usage =
545         new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
546     $cust_bill_pkg_usage->recur( $usage );
547     $cust_bill_pkg_usage->type( 'U' );
548     my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
549     $cust_bill_pkg{recur}->recur( $recur );
550     $cust_bill_pkg{recur}->type( '' );
551     $cust_bill_pkg{recur}->set('details', []);
552     $cust_bill_pkg{''} = $cust_bill_pkg_usage;
553   }
554
555   #subdivide usage by usage_class
556   if (exists($cust_bill_pkg{''})) {
557     foreach my $class (grep { $_ } $self->usage_classes) {
558       my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
559       my $cust_bill_pkg_usage =
560           new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
561       $cust_bill_pkg_usage->recur( $usage );
562       $cust_bill_pkg_usage->set('details', []);
563       my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
564       $cust_bill_pkg{''}->recur( $classless );
565       $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
566     }
567     delete $cust_bill_pkg{''} unless $cust_bill_pkg{''}->recur;
568   }
569
570 #  # sort setup,recur,'', and the rest numeric && return
571 #  my @result = map { $cust_bill_pkg{$_} }
572 #               sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
573 #                      ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
574 #                    }
575 #               keys %cust_bill_pkg;
576 #
577 #  return (@result);
578
579    %cust_bill_pkg;
580 }
581
582 =item usage CLASSNUM
583
584 Returns the amount of the charge associated with usage class CLASSNUM if
585 CLASSNUM is defined.  Otherwise returns the total charge associated with
586 usage.
587   
588 =cut
589
590 sub usage {
591   my( $self, $classnum ) = @_;
592   my $sum = 0;
593   my @values = ();
594
595   if ( $self->get('details') ) {
596
597     @values = 
598       map { $_->[2] }
599       grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) }
600       @{ $self->get('details') };
601
602   }else{
603
604     my $hashref = { 'billpkgnum' => $self->billpkgnum };
605     $hashref->{ 'classnum' } = $classnum if defined($classnum);
606     @values = map { $_->amount } qsearch('cust_bill_pkg_detail', $hashref);
607
608   }
609
610   foreach ( @values ) {
611     $sum += $_ if $_;
612   }
613   $sum;
614 }
615
616 =item usage_classes
617
618 Returns a list of usage classnums associated with this invoice line's
619 details.
620   
621 =cut
622
623 sub usage_classes {
624   my( $self ) = @_;
625
626   if ( $self->get('details') ) {
627
628     my %seen = ();
629     foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
630       $seen{ $detail->[3] } = 1;
631     }
632     keys %seen;
633
634   }else{
635
636     map { $_->classnum }
637         qsearch({ table   => 'cust_bill_pkg_detail',
638                   hashref => { billpkgnum => $self->billpkgnum },
639                   select  => 'DISTINCT classnum',
640                });
641
642   }
643
644 }
645
646 =item cust_bill_pkg_display [ type => TYPE ]
647
648 Returns an array of display information for the invoice line item optionally
649 limited to 'TYPE'.
650
651 =cut
652
653 sub cust_bill_pkg_display {
654   my ( $self, %opt ) = @_;
655
656   my $default =
657     new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
658
659   return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
660
661   my $type = $opt{type} if exists $opt{type};
662   my @result;
663
664   if ( scalar( $self->get('display') ) ) {
665     @result = grep { defined($type) ? ($type eq $_->type) : 1 }
666               @{ $self->get('display') };
667   }else{
668     my $hashref = { 'billpkgnum' => $self->billpkgnum };
669     $hashref->{type} = $type if defined($type);
670     
671     @result = qsearch ({ 'table'    => 'cust_bill_pkg_display',
672                          'hashref'  => { 'billpkgnum' => $self->billpkgnum },
673                          'order_by' => 'ORDER BY billpkgdisplaynum',
674                       });
675   }
676
677   push @result, $default unless ( scalar(@result) || $type );
678
679   @result;
680
681 }
682
683 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
684 # and FS::cust_main::bill
685
686 sub _cust_tax_exempt_pkg {
687   my ( $self ) = @_;
688
689   $self->{Hash}->{_cust_tax_exempt_pkg} or
690   $self->{Hash}->{_cust_tax_exempt_pkg} = [];
691
692 }
693
694
695 =back
696
697 =head1 BUGS
698
699 setup and recur shouldn't be separate fields.  There should be one "amount"
700 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
701
702 A line item with both should really be two separate records (preserving
703 sdate and edate for setup fees for recurring packages - that information may
704 be valuable later).  Invoice generation (cust_main::bill), invoice printing
705 (cust_bill), tax reports (report_tax.cgi) and line item reports 
706 (cust_bill_pkg.cgi) would need to be updated.
707
708 owed_setup and owed_recur could then be repaced by just owed, and
709 cust_bill::open_cust_bill_pkg and
710 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
711
712 =head1 SEE ALSO
713
714 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
715 from the base documentation.
716
717 =cut
718
719 1;
720