multiple usage classes checkpoint
[freeside.git] / FS / FS / cust_bill_pkg.pm
1 package FS::cust_bill_pkg;
2
3 use strict;
4 use vars qw( @ISA );
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_pay_pkg;
12 use FS::cust_credit_bill_pkg;
13
14 @ISA = qw( FS::cust_main_Mixin FS::Record );
15
16 =head1 NAME
17
18 FS::cust_bill_pkg - Object methods for cust_bill_pkg records
19
20 =head1 SYNOPSIS
21
22   use FS::cust_bill_pkg;
23
24   $record = new FS::cust_bill_pkg \%hash;
25   $record = new FS::cust_bill_pkg { 'column' => 'value' };
26
27   $error = $record->insert;
28
29   $error = $new_record->replace($old_record);
30
31   $error = $record->delete;
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 type - can be set to U for usage; more later
51
52 =item setup - setup fee
53
54 =item recur - recurring fee
55
56 =item sdate - starting date of recurring fee
57
58 =item edate - ending date of recurring fee
59
60 =item itemdesc - Line item description (overrides normal package description)
61
62 =item section - Invoice section (overrides normal package section)
63
64 =duplicate - Indicates this item appears elsewhere on the invoice
65              (and should not be retaxed or reincluded in totals)
66
67 =post_total - A hint that this item should appear after invoice totals
68
69 =cut
70
71 sub section {
72   my ( $self, $value ) = @_;
73   if ( defined($value) ) {
74     $self->setfield('section', $value);
75   } else {
76     $self->getfield('section') || $self->part_pkg->categoryname;
77   }
78 }
79
80 =item quantity - If not set, defaults to 1
81
82 =item unitsetup - If not set, defaults to setup
83
84 =item unitrecur - If not set, defaults to recur
85
86 =back
87
88 sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">.  Also
89 see L<Time::Local> and L<Date::Parse> for conversion functions.
90
91 =head1 METHODS
92
93 =over 4
94
95 =item new HASHREF
96
97 Creates a new line item.  To add the line item to the database, see
98 L<"insert">.  Line items are normally created by calling the bill method of a
99 customer object (see L<FS::cust_main>).
100
101 =cut
102
103 sub table { 'cust_bill_pkg'; }
104
105 =item insert
106
107 Adds this line item to the database.  If there is an error, returns the error,
108 otherwise returns false.
109
110 =cut
111
112 sub insert {
113   my $self = shift;
114
115   local $SIG{HUP} = 'IGNORE';
116   local $SIG{INT} = 'IGNORE';
117   local $SIG{QUIT} = 'IGNORE';
118   local $SIG{TERM} = 'IGNORE';
119   local $SIG{TSTP} = 'IGNORE';
120   local $SIG{PIPE} = 'IGNORE';
121
122   my $oldAutoCommit = $FS::UID::AutoCommit;
123   local $FS::UID::AutoCommit = 0;
124   my $dbh = dbh;
125
126   my $error = $self->SUPER::insert;
127   if ( $error ) {
128     $dbh->rollback if $oldAutoCommit;
129     return $error;
130   }
131
132   unless ( defined dbdef->table('cust_bill_pkg_detail') && $self->get('details') ) {
133     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
134     return '';
135   }
136
137   foreach my $detail ( @{$self->get('details')} ) {
138     my $cust_bill_pkg_detail = new FS::cust_bill_pkg_detail {
139       'billpkgnum' => $self->billpkgnum,
140       'format'     => (ref($detail) ? $detail->[0] : '' ),
141       'detail'     => (ref($detail) ? $detail->[1] : $detail ),
142       'charge'     => (ref($detail) ? $detail->[2] : '' ),
143       'classnum'   => (ref($detail) ? $detail->[3] : '' ),
144     };
145     $error = $cust_bill_pkg_detail->insert;
146     if ( $error ) {
147       $dbh->rollback if $oldAutoCommit;
148       return $error;
149     }
150   }
151
152   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
153   '';
154
155 }
156
157 =item delete
158
159 Currently unimplemented.  I don't remove line items because there would then be
160 no record the items ever existed (which is bad, no?)
161
162 =cut
163
164 sub delete {
165   return "Can't delete cust_bill_pkg records!";
166 }
167
168 =item replace OLD_RECORD
169
170 Currently unimplemented.  This would be even more of an accounting nightmare
171 than deleteing the items.  Just don't do it.
172
173 =cut
174
175 sub replace {
176   return "Can't modify cust_bill_pkg records!";
177 }
178
179 =item check
180
181 Checks all fields to make sure this is a valid line item.  If there is an
182 error, returns the error, otherwise returns false.  Called by the insert
183 method.
184
185 =cut
186
187 sub check {
188   my $self = shift;
189
190   my $error =
191          $self->ut_numbern('billpkgnum')
192       || $self->ut_snumber('pkgnum')
193       || $self->ut_number('invnum')
194       || $self->ut_money('setup')
195       || $self->ut_money('recur')
196       || $self->ut_numbern('sdate')
197       || $self->ut_numbern('edate')
198       || $self->ut_textn('itemdesc')
199       || $self->ut_textn('section')
200       || $self->ut_enum('duplicate', [ '', 'Y' ])
201       || $self->ut_enum('post_total', [ '', 'Y' ])
202       || $self->ut_enum('type', [ '', 'U' ])      #only usage for now
203   ;
204   return $error if $error;
205
206   #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
207   if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
208     return "Unknown pkgnum ". $self->pkgnum
209       unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
210   }
211
212   return "Unknown invnum"
213     unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
214
215   $self->SUPER::check;
216 }
217
218 =item cust_pkg
219
220 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
221
222 =cut
223
224 sub cust_pkg {
225   my $self = shift;
226   qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
227 }
228
229 =item part_pkg
230
231 Returns the package definition for this invoice line item.
232
233 =cut
234
235 sub part_pkg {
236   my $self = shift;
237   if ( $self->pkgpart_override ) {
238     qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
239   } else {
240     $self->cust_pkg->part_pkg;
241   }
242 }
243
244 =item cust_bill
245
246 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
247
248 =cut
249
250 sub cust_bill {
251   my $self = shift;
252   qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
253 }
254
255 =item details [ OPTION => VALUE ... ]
256
257 Returns an array of detail information for the invoice line item.
258
259 Currently available options are: I<format> I<escape_function>
260
261 If I<format> is set to html or latex then the array members are improved
262 for tabular appearance in those environments if possible.
263
264 If I<escape_function> is set then the array members are processed by this
265 function before being returned.
266
267 =cut
268
269 sub details {
270   my ( $self, %opt ) = @_;
271   my $format = $opt{format} || '';
272   my $escape_function = $opt{escape_function} || sub { shift };
273   return () unless defined dbdef->table('cust_bill_pkg_detail');
274
275   eval "use Text::CSV_XS;";
276   die $@ if $@;
277   my $csv = new Text::CSV_XS;
278
279   my $format_sub = sub { my $detail = shift;
280                          $csv->parse($detail) or return "can't parse $detail";
281                          join(' - ', map { &$escape_function($_) }
282                                      $csv->fields
283                              );
284                        };
285
286   $format_sub = sub { my $detail = shift;
287                       $csv->parse($detail) or return "can't parse $detail";
288                       join('</TD><TD>', map { &$escape_function($_) }
289                                         $csv->fields
290                           );
291                     }
292     if $format eq 'html';
293
294   $format_sub = sub { my $detail = shift;
295                       $csv->parse($detail) or return "can't parse $detail";
296                       #join(' & ', map { '\small{'. &$escape_function($_). '}' }
297                       #            $csv->fields );
298                       my $result = '';
299                       my $column = 1;
300                       foreach ($csv->fields) {
301                         $result .= ' & ' if $column > 1;
302                         if ($column > 6) {                     # KLUDGE ALERT!
303                           $result .= '\multicolumn{1}{l}{\small{'.
304                                      &$escape_function($_). '}}';
305                         }else{
306                           $result .= '\small{'.  &$escape_function($_). '}';
307                         }
308                         $column++;
309                       }
310                       $result;
311                     }
312     if $format eq 'latex';
313
314   $format_sub = $opt{format_function} if $opt{format_function};
315
316   map { ( $_->format eq 'C'
317           ? &{$format_sub}( $_->detail )
318           : &{$escape_function}( $_->detail )
319         )
320       }
321     qsearch ({ 'table'    => 'cust_bill_pkg_detail',
322                'hashref'  => { 'billpkgnum' => $self->billpkgnum },
323                'order_by' => 'ORDER BY detailnum',
324             });
325     #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum });
326 }
327
328 =item desc
329
330 Returns a description for this line item.  For typical line items, this is the
331 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
332 For one-shot line items and named taxes, it is the I<itemdesc> field of this
333 line item, and for generic taxes, simply returns "Tax".
334
335 =cut
336
337 sub desc {
338   my $self = shift;
339
340   if ( $self->pkgnum > 0 ) {
341     $self->itemdesc || $self->part_pkg->pkg;
342   } else {
343     $self->itemdesc || 'Tax';
344   }
345 }
346
347 =item owed_setup
348
349 Returns the amount owed (still outstanding) on this line item's setup fee,
350 which is the amount of the line item minus all payment applications (see
351 L<FS::cust_bill_pay_pkg> and credit applications (see
352 L<FS::cust_credit_bill_pkg>).
353
354 =cut
355
356 sub owed_setup {
357   my $self = shift;
358   $self->owed('setup', @_);
359 }
360
361 =item owed_recur
362
363 Returns the amount owed (still outstanding) on this line item's recurring fee,
364 which is the amount of the line item minus all payment applications (see
365 L<FS::cust_bill_pay_pkg> and credit applications (see
366 L<FS::cust_credit_bill_pkg>).
367
368 =cut
369
370 sub owed_recur {
371   my $self = shift;
372   $self->owed('recur', @_);
373 }
374
375 # modeled after cust_bill::owed...
376 sub owed {
377   my( $self, $field ) = @_;
378   my $balance = $self->duplicate ? 0 : $self->$field();
379   $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
380   $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
381   $balance = sprintf( '%.2f', $balance );
382   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
383   $balance;
384 }
385
386 sub cust_bill_pay_pkg {
387   my( $self, $field ) = @_;
388   qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
389                                   'setuprecur' => $field,
390                                 }
391          );
392 }
393
394 sub cust_credit_bill_pkg {
395   my( $self, $field ) = @_;
396   qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
397                                      'setuprecur' => $field,
398                                    }
399          );
400 }
401
402 =item units
403
404 Returns the number of billing units (for tax purposes) represented by this,
405 line item.
406
407 =cut
408
409 sub units {
410   my $self = shift;
411   $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
412 }
413
414 =item quantity
415
416 =cut
417
418 sub quantity {
419   my( $self, $value ) = @_;
420   if ( defined($value) ) {
421     $self->setfield('quantity', $value);
422   }
423   $self->getfield('quantity') || 1;
424 }
425
426 =item unitsetup
427
428 =cut
429
430 sub unitsetup {
431   my( $self, $value ) = @_;
432   if ( defined($value) ) {
433     $self->setfield('unitsetup', $value);
434   }
435   $self->getfield('unitsetup') eq ''
436     ? $self->getfield('setup')
437     : $self->getfield('unitsetup');
438 }
439
440 =item unitrecur
441
442 =cut
443
444 sub unitrecur {
445   my( $self, $value ) = @_;
446   if ( defined($value) ) {
447     $self->setfield('unitrecur', $value);
448   }
449   $self->getfield('unitrecur') eq ''
450     ? $self->getfield('recur')
451     : $self->getfield('unitrecur');
452 }
453
454 =item separate_cdr
455
456 Returns true if this line item represents a cdr line item in its own section.
457   
458 =cut
459
460 # lame, but works for now
461 sub separate_cdr {
462   my( $self ) = shift;
463   $self->pkgnum && $self->section ne $self->part_pkg->categoryname;
464 }
465
466 =item usage CLASSNUM
467
468 Returns the amount of the charge associated with usage class CLASSNUM if
469 CLASSNUM is defined.  Otherwise returns the total charge associated with
470 usage.
471   
472 =cut
473
474 sub usage {
475   my( $self, $classnum ) = @_;
476   my $sum = 0;
477   my @values = ();
478
479   if ( $self->get('details') ) {
480
481     @values = 
482       map { $_->[2] }
483       grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) }
484       $self->get('details');
485
486   }else{
487
488     my $hashref = { 'billpkgnum' => $self->billpkgnum };
489     $hashref->{ 'classnum' } = $classnum if defined($classnum);
490     @values = map { $_->charge } qsearch('cust_bill_pkg_detail', $hashref);
491
492   }
493
494   foreach ( @values ) {
495     $sum += $_ if $_;
496   }
497   $sum;
498 }
499
500 =item usage_classes
501
502 Returns a list of usage classnums associated with this invoice line's
503 details.
504   
505 =cut
506
507 sub usage_classes {
508   my( $self ) = @_;
509
510   if ( $self->get('details') ) {
511
512     my %seen = ();
513     foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
514       $seen{ $detail->[3] } = 1;
515     }
516     keys %seen;
517
518   }else{
519
520     map { $_->classnum }
521         qsearch({ table   => 'cust_bill_pkg_detail',
522                   hashref => { billpkgnum => $self->billpkgnum },
523                   select  => 'DISTINCT classnum',
524                });
525
526   }
527
528 }
529
530 =back
531
532 =head1 BUGS
533
534 setup and recur shouldn't be separate fields.  There should be one "amount"
535 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
536
537 A line item with both should really be two separate records (preserving
538 sdate and edate for setup fees for recurring packages - that information may
539 be valuable later).  Invoice generation (cust_main::bill), invoice printing
540 (cust_bill), tax reports (report_tax.cgi) and line item reports 
541 (cust_bill_pkg.cgi) would need to be updated.
542
543 owed_setup and owed_recur could then be repaced by just owed, and
544 cust_bill::open_cust_bill_pkg and
545 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
546
547 =head1 SEE ALSO
548
549 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
550 from the base documentation.
551
552 =cut
553
554 1;
555