book closing schema changes
[freeside.git] / FS / FS / cust_bill.pm
1 package FS::cust_bill;
2
3 use strict;
4 use vars qw( @ISA $conf $invoice_template $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
6 use Date::Format;
7 use Text::Template;
8 use FS::Record qw( qsearch qsearchs );
9 use FS::cust_main;
10 use FS::cust_bill_pkg;
11 use FS::cust_credit;
12 use FS::cust_pay;
13 use FS::cust_pkg;
14 use FS::cust_credit_bill;
15
16 @ISA = qw( FS::Record );
17
18 #ask FS::UID to run this stuff for us later
19 $FS::UID::callback{'FS::cust_bill'} = sub { 
20
21   $conf = new FS::Conf;
22
23   $money_char = $conf->config('money_char') || '$';  
24
25   my @invoice_template = $conf->config('invoice_template')
26     or die "cannot load config file invoice_template";
27   $invoice_lines = 0;
28   foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy
29     /invoice_lines\((\d+)\)/;
30     $invoice_lines += $1;
31   }
32   die "no invoice_lines() functions in template?" unless $invoice_lines;
33   $invoice_template = new Text::Template (
34     TYPE   => 'ARRAY',
35     SOURCE => [ map "$_\n", @invoice_template ],
36   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
37   $invoice_template->compile()
38     or die "can't compile template: $Text::Template::ERROR";
39 };
40
41 =head1 NAME
42
43 FS::cust_bill - Object methods for cust_bill records
44
45 =head1 SYNOPSIS
46
47   use FS::cust_bill;
48
49   $record = new FS::cust_bill \%hash;
50   $record = new FS::cust_bill { 'column' => 'value' };
51
52   $error = $record->insert;
53
54   $error = $new_record->replace($old_record);
55
56   $error = $record->delete;
57
58   $error = $record->check;
59
60   ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
61
62   @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
63
64   ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
65
66   @cust_pay_objects = $cust_bill->cust_pay;
67
68   $tax_amount = $record->tax;
69
70   @lines = $cust_bill->print_text;
71   @lines = $cust_bill->print_text $time;
72
73 =head1 DESCRIPTION
74
75 An FS::cust_bill object represents an invoice; a declaration that a customer
76 owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
77 (see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
78 following fields are currently supported:
79
80 =over 4
81
82 =item invnum - primary key (assigned automatically for new invoices)
83
84 =item custnum - customer (see L<FS::cust_main>)
85
86 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
87 L<Time::Local> and L<Date::Parse> for conversion functions.
88
89 =item charged - amount of this invoice
90
91 =item printed - how many times this invoice has been printed automatically
92 (see L<FS::cust_main/"collect">).
93
94 =item closed - books closed flag, empty or `Y'
95
96 =back
97
98 =head1 METHODS
99
100 =over 4
101
102 =item new HASHREF
103
104 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
105 Invoices are normally created by calling the bill method of a customer object
106 (see L<FS::cust_main>).
107
108 =cut
109
110 sub table { 'cust_bill'; }
111
112 =item insert
113
114 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
115 returns the error, otherwise returns false.
116
117 =item delete
118
119 Currently unimplemented.  I don't remove invoices because there would then be
120 no record you ever posted this invoice (which is bad, no?)
121
122 =cut
123
124 sub delete {
125   my $self = shift;
126   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
127   $self->SUPER::delete(@_);
128 }
129
130 =item replace OLD_RECORD
131
132 Replaces the OLD_RECORD with this one in the database.  If there is an error,
133 returns the error, otherwise returns false.
134
135 Only printed may be changed.  printed is normally updated by calling the
136 collect method of a customer object (see L<FS::cust_main>).
137
138 =cut
139
140 sub replace {
141   my( $new, $old ) = ( shift, shift );
142   return "Can't change custnum!" unless $old->custnum == $new->custnum;
143   #return "Can't change _date!" unless $old->_date eq $new->_date;
144   return "Can't change _date!" unless $old->_date == $new->_date;
145   return "Can't change charged!" unless $old->charged == $new->charged;
146
147   $new->SUPER::replace($old);
148 }
149
150 =item check
151
152 Checks all fields to make sure this is a valid invoice.  If there is an error,
153 returns the error, otherwise returns false.  Called by the insert and replace
154 methods.
155
156 =cut
157
158 sub check {
159   my $self = shift;
160
161   my $error =
162     $self->ut_numbern('invnum')
163     || $self->ut_number('custnum')
164     || $self->ut_numbern('_date')
165     || $self->ut_money('charged')
166     || $self->ut_numbern('printed')
167     || $self->ut_enum('closed', [ '', 'Y' ])
168   ;
169   return $error if $error;
170
171   return "Unknown customer"
172     unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
173
174   $self->_date(time) unless $self->_date;
175
176   $self->printed(0) if $self->printed eq '';
177
178   ''; #no error
179 }
180
181 =item previous
182
183 Returns a list consisting of the total previous balance for this customer, 
184 followed by the previous outstanding invoices (as FS::cust_bill objects also).
185
186 =cut
187
188 sub previous {
189   my $self = shift;
190   my $total = 0;
191   my @cust_bill = sort { $a->_date <=> $b->_date }
192     grep { $_->owed != 0 && $_->_date < $self->_date }
193       qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) 
194   ;
195   foreach ( @cust_bill ) { $total += $_->owed; }
196   $total, @cust_bill;
197 }
198
199 =item cust_bill_pkg
200
201 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
202
203 =cut
204
205 sub cust_bill_pkg {
206   my $self = shift;
207   qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
208 }
209
210 =item cust_credit
211
212 Depreciated.  See the cust_credited method.
213
214  #Returns a list consisting of the total previous credited (see
215  #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
216  #outstanding credits (FS::cust_credit objects).
217
218 =cut
219
220 sub cust_credit {
221   use Carp;
222   croak "FS::cust_bill->cust_credit depreciated; see ".
223         "FS::cust_bill->cust_credit_bill";
224   #my $self = shift;
225   #my $total = 0;
226   #my @cust_credit = sort { $a->_date <=> $b->_date }
227   #  grep { $_->credited != 0 && $_->_date < $self->_date }
228   #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
229   #;
230   #foreach (@cust_credit) { $total += $_->credited; }
231   #$total, @cust_credit;
232 }
233
234 =item cust_pay
235
236 Depreciated.  See the cust_bill_pay method.
237
238 #Returns all payments (see L<FS::cust_pay>) for this invoice.
239
240 =cut
241
242 sub cust_pay {
243   use Carp;
244   croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
245   #my $self = shift;
246   #sort { $a->_date <=> $b->_date }
247   #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
248   #;
249 }
250
251 =item cust_bill_pay
252
253 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
254
255 =cut
256
257 sub cust_bill_pay {
258   my $self = shift;
259   sort { $a->_date <=> $b->_date }
260     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
261 }
262
263 =item cust_credited
264
265 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
266
267 =cut
268
269 sub cust_credited {
270   my $self = shift;
271   sort { $a->_date <=> $b->_date }
272     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
273   ;
274 }
275
276 =item tax
277
278 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
279
280 =cut
281
282 sub tax {
283   my $self = shift;
284   my $total = 0;
285   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
286                                              'pkgnum' => 0 } );
287   foreach (@taxlines) { $total += $_->setup; }
288   $total;
289 }
290
291 =item owed
292
293 Returns the amount owed (still outstanding) on this invoice, which is charged
294 minus all payment applications (see L<FS::cust_bill_pay>) and credit
295 applications (see L<FS::cust_credit_bill>).
296
297 =cut
298
299 sub owed {
300   my $self = shift;
301   my $balance = $self->charged;
302   $balance -= $_->amount foreach ( $self->cust_bill_pay );
303   $balance -= $_->amount foreach ( $self->cust_credited );
304   $balance = sprintf( "%.2f", $balance);
305   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
306   $balance;
307 }
308
309 =item print_text [TIME];
310
311 Returns an text invoice, as a list of lines.
312
313 TIME an optional value used to control the printing of overdue messages.  The
314 default is now.  It isn't the date of the invoice; that's the `_date' field.
315 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
316 L<Time::Local> and L<Date::Parse> for conversion functions.
317
318 =cut
319
320 sub print_text {
321
322   my( $self, $today ) = ( shift, shift );
323   $today ||= time;
324 #  my $invnum = $self->invnum;
325   my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
326   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
327     unless $cust_main->payname;
328
329   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
330 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
331   #my $balance_due = $self->owed + $pr_total - $cr_total;
332   my $balance_due = $self->owed + $pr_total;
333
334   #my @collect = ();
335   #my($description,$amount);
336   @buf = ();
337
338   #previous balance
339   foreach ( @pr_cust_bill ) {
340     push @buf, [
341       "Previous Balance, Invoice #". $_->invnum. 
342                  " (". time2str("%x",$_->_date). ")",
343       $money_char. sprintf("%10.2f",$_->owed)
344     ];
345   }
346   if (@pr_cust_bill) {
347     push @buf,['','-----------'];
348     push @buf,[ 'Total Previous Balance',
349                 $money_char. sprintf("%10.2f",$pr_total ) ];
350     push @buf,['',''];
351   }
352
353   #new charges
354   foreach ( $self->cust_bill_pkg ) {
355
356     if ( $_->pkgnum ) {
357
358       my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
359       my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
360       my($pkg)=$part_pkg->pkg;
361
362       if ( $_->setup != 0 ) {
363         push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
364         push @buf,
365           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
366       }
367
368       if ( $_->recur != 0 ) {
369         push @buf, [
370           "$pkg (" . time2str("%x",$_->sdate) . " - " .
371                                 time2str("%x",$_->edate) . ")",
372           $money_char. sprintf("%10.2f",$_->recur)
373         ];
374         push @buf,
375           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
376       }
377
378     } else { #pkgnum Tax
379       push @buf,["Tax", $money_char. sprintf("%10.2f",$_->setup) ] 
380         if $_->setup != 0;
381     }
382   }
383
384   push @buf,['','-----------'];
385   push @buf,['Total New Charges',
386              $money_char. sprintf("%10.2f",$self->charged) ];
387   push @buf,['',''];
388
389   push @buf,['','-----------'];
390   push @buf,['Total Charges',
391              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
392   push @buf,['',''];
393
394   #credits
395   foreach ( $self->cust_credited ) {
396
397     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
398
399     my $reason = substr($_->cust_credit->reason,0,32);
400     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
401     $reason = " ($reason) " if $reason;
402     push @buf,[
403       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
404         $reason,
405       $money_char. sprintf("%10.2f",$_->amount)
406     ];
407   }
408   #foreach ( @cr_cust_credit ) {
409   #  push @buf,[
410   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
411   #    $money_char. sprintf("%10.2f",$_->credited)
412   #  ];
413   #}
414
415   #get & print payments
416   foreach ( $self->cust_bill_pay ) {
417
418     #something more elaborate if $_->amount ne ->cust_pay->paid ?
419
420     push @buf,[
421       "Payment received ". time2str("%x",$_->cust_pay->_date ),
422       $money_char. sprintf("%10.2f",$_->amount )
423     ];
424   }
425
426   #balance due
427   push @buf,['','-----------'];
428   push @buf,['Balance Due', $money_char. 
429     sprintf("%10.2f", $balance_due ) ];
430
431   #setup template variables
432   
433   package FS::cust_bill::_template; #!
434   use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
435
436   $invnum = $self->invnum;
437   $date = $self->_date;
438   $page = 1;
439
440   $total_pages =
441     int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
442   $total_pages++
443     if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
444
445
446   #format address (variable for the template)
447   my $l = 0;
448   @address = ( '', '', '', '', '', '' );
449   package FS::cust_bill; #!
450   $FS::cust_bill::_template::address[$l++] =
451     $cust_main->payname.
452       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
453         ? " (P.O. #". $cust_main->payinfo. ")"
454         : ''
455       )
456   ;
457   $FS::cust_bill::_template::address[$l++] = $cust_main->company
458     if $cust_main->company;
459   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
460   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
461     if $cust_main->address2;
462   $FS::cust_bill::_template::address[$l++] =
463     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
464   $FS::cust_bill::_template::address[$l++] = $cust_main->country
465     unless $cust_main->country eq 'US';
466
467   #overdue? (variable for the template)
468   $FS::cust_bill::_template::overdue = ( 
469     $balance_due > 0
470     && $today > $self->_date 
471 #    && $self->printed > 1
472     && $self->printed > 0
473   );
474
475   #and subroutine for the template
476
477   sub FS::cust_bill::_template::invoice_lines {
478     my $lines = shift;
479     map { 
480       scalar(@buf) ? shift @buf : [ '', '' ];
481     }
482     ( 1 .. $lines );
483   }
484     
485   $FS::cust_bill::_template::page = 1;
486   my $lines;
487   my @collect;
488   while (@buf) {
489     push @collect, split("\n",
490       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
491     );
492     $FS::cust_bill::_template::page++;
493   }
494
495   map "$_\n", @collect;
496
497 }
498
499 =back
500
501 =head1 VERSION
502
503 $Id: cust_bill.pm,v 1.15 2002-01-28 06:57:23 ivan Exp $
504
505 =head1 BUGS
506
507 The delete method.
508
509 print_text formatting (and some logic :/) is in source, but needs to be
510 slurped in from a file.  Also number of lines ($=).
511
512 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
513 or something similar so the look can be completely customized?)
514
515 =head1 SEE ALSO
516
517 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS:;cust_pay>,
518 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
519 documentation.
520
521 =cut
522
523 1;
524