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