4c1617f0ef29d30078ea828feb264095e84aa9a1
[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 =back
95
96 =head1 METHODS
97
98 =over 4
99
100 =item new HASHREF
101
102 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
103 Invoices are normally created by calling the bill method of a customer object
104 (see L<FS::cust_main>).
105
106 =cut
107
108 sub table { 'cust_bill'; }
109
110 =item insert
111
112 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
113 returns the error, otherwise returns false.
114
115 =item delete
116
117 Currently unimplemented.  I don't remove invoices because there would then be
118 no record you ever posted this invoice (which is bad, no?)
119
120 =cut
121
122 sub delete {
123   return "Can't remove invoice!"
124 }
125
126 =item replace OLD_RECORD
127
128 Replaces the OLD_RECORD with this one in the database.  If there is an error,
129 returns the error, otherwise returns false.
130
131 Only printed may be changed.  printed is normally updated by calling the
132 collect method of a customer object (see L<FS::cust_main>).
133
134 =cut
135
136 sub replace {
137   my( $new, $old ) = ( shift, shift );
138   return "Can't change custnum!" unless $old->custnum == $new->custnum;
139   #return "Can't change _date!" unless $old->_date eq $new->_date;
140   return "Can't change _date!" unless $old->_date == $new->_date;
141   return "Can't change charged!" unless $old->charged == $new->charged;
142
143   $new->SUPER::replace($old);
144 }
145
146 =item check
147
148 Checks all fields to make sure this is a valid invoice.  If there is an error,
149 returns the error, otherwise returns false.  Called by the insert and replace
150 methods.
151
152 =cut
153
154 sub check {
155   my $self = shift;
156
157   my $error =
158     $self->ut_numbern('invnum')
159     || $self->ut_number('custnum')
160     || $self->ut_numbern('_date')
161     || $self->ut_money('charged')
162     || $self->ut_numbern('printed')
163   ;
164   return $error if $error;
165
166   return "Unknown customer"
167     unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
168
169   $self->_date(time) unless $self->_date;
170
171   $self->printed(0) if $self->printed eq '';
172
173   ''; #no error
174 }
175
176 =item previous
177
178 Returns a list consisting of the total previous balance for this customer, 
179 followed by the previous outstanding invoices (as FS::cust_bill objects also).
180
181 =cut
182
183 sub previous {
184   my $self = shift;
185   my $total = 0;
186   my @cust_bill = sort { $a->_date <=> $b->_date }
187     grep { $_->owed != 0 && $_->_date < $self->_date }
188       qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) 
189   ;
190   foreach ( @cust_bill ) { $total += $_->owed; }
191   $total, @cust_bill;
192 }
193
194 =item cust_bill_pkg
195
196 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
197
198 =cut
199
200 sub cust_bill_pkg {
201   my $self = shift;
202   qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
203 }
204
205 =item cust_credit
206
207 Returns a list consisting of the total previous credited (see
208 L<FS::cust_credit>) and unapplied for this customer, followed by the previous
209 outstanding credits (FS::cust_credit objects).
210
211 =cut
212
213 sub cust_credit {
214   my $self = shift;
215   my $total = 0;
216   my @cust_credit = sort { $a->_date <=> $b->_date }
217     grep { $_->credited != 0 && $_->_date < $self->_date }
218       qsearch('cust_credit', { 'custnum' => $self->custnum } )
219   ;
220   foreach (@cust_credit) { $total += $_->credited; }
221   $total, @cust_credit;
222 }
223
224 =item cust_pay
225
226 Returns all payments (see L<FS::cust_pay>) for this invoice.
227
228 =cut
229
230 sub cust_pay {
231   my $self = shift;
232   sort { $a->_date <=> $b->_date }
233     qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
234   ;
235 }
236
237 =item cust_credited
238
239 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
240
241 =cut
242
243 sub cust_credited {
244   my $self = shift;
245   sort { $a->_date <=> $b->_date }
246     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
247   ;
248 }
249
250 =item tax
251
252 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
253
254 =cut
255
256 sub tax {
257   my $self = shift;
258   my $total = 0;
259   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
260                                              'pkgnum' => 0 } );
261   foreach (@taxlines) { $total += $_->setup; }
262   $total;
263 }
264
265 =item owed
266
267 Returns the amount owed (still outstanding) on this invoice, which is charged
268 minus all payments (see L<FS::cust_pay>) and applied credits
269 (see L<FS::cust_credit_bill>).
270
271 =cut
272
273 sub owed {
274   my $self = shift;
275   my $balance = $self->charged;
276   $balance -= $_->paid foreach ( $self->cust_pay );
277   $balance -= $_->amount foreach ( $self->cust_credited );
278   $balance = sprintf( "%.2f", $balance);
279 }
280
281 =item print_text [TIME];
282
283 Returns an text invoice, as a list of lines.
284
285 TIME an optional value used to control the printing of overdue messages.  The
286 default is now.  It isn't the date of the invoice; that's the `_date' field.
287 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
288 L<Time::Local> and L<Date::Parse> for conversion functions.
289
290 =cut
291
292 sub print_text {
293
294   my( $self, $today ) = ( shift, shift );
295   $today ||= time;
296 #  my $invnum = $self->invnum;
297   my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
298   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
299     unless $cust_main->payname;
300
301   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
302   my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
303   my $balance_due = $self->owed + $pr_total - $cr_total;
304
305   #
306
307   #my @collect = ();
308   #my($description,$amount);
309   @buf = ();
310
311   #previous balance
312   foreach ( @pr_cust_bill ) {
313     push @buf, [
314       "Previous Balance, Invoice #". $_->invnum. 
315                  " (". time2str("%x",$_->_date). ")",
316       $money_char. sprintf("%10.2f",$_->owed)
317     ];
318   }
319   if (@pr_cust_bill) {
320     push @buf,['','-----------'];
321     push @buf,[ 'Total Previous Balance',
322                 $money_char. sprintf("%10.2f",$pr_total ) ];
323     push @buf,['',''];
324   }
325
326   #new charges
327   foreach ( $self->cust_bill_pkg ) {
328
329     if ( $_->pkgnum ) {
330
331       my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
332       my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
333       my($pkg)=$part_pkg->pkg;
334
335       if ( $_->setup != 0 ) {
336         push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
337         push @buf,
338           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
339       }
340
341       if ( $_->recur != 0 ) {
342         push @buf, [
343           "$pkg (" . time2str("%x",$_->sdate) . " - " .
344                                 time2str("%x",$_->edate) . ")",
345           $money_char. sprintf("%10.2f",$_->recur)
346         ];
347         push @buf,
348           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
349       }
350
351     } else { #pkgnum Tax
352       push @buf,["Tax", $money_char. sprintf("%10.2f",$_->setup) ] 
353         if $_->setup != 0;
354     }
355   }
356
357   push @buf,['','-----------'];
358   push @buf,['Total New Charges',
359              $money_char. sprintf("%10.2f",$self->charged) ];
360   push @buf,['',''];
361
362   push @buf,['','-----------'];
363   push @buf,['Total Charges',
364              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
365   push @buf,['',''];
366
367   #credits
368   foreach ( $self->cust_credited ) {
369     push @buf,[
370       "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
371       $money_char. sprintf("%10.2f",$_->amount)
372     ];
373   }
374   foreach ( @cr_cust_credit ) {
375     push @buf,[
376       "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
377       $money_char. sprintf("%10.2f",$_->credited)
378     ];
379   }
380
381   #get & print payments
382   foreach ( $self->cust_pay ) {
383     push @buf,[
384       "Payment received ". time2str("%x",$_->_date ),
385       $money_char. sprintf("%10.2f",$_->paid )
386     ];
387   }
388
389   #balance due
390   push @buf,['','-----------'];
391   push @buf,['Balance Due', $money_char. 
392     sprintf("%10.2f",$self->owed + $pr_total - $cr_total ) ];
393
394   #setup template variables
395   
396   package FS::cust_bill::_template; #!
397   use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
398
399   $invnum = $self->invnum;
400   $date = $self->_date;
401   $page = 1;
402
403   $total_pages =
404     int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
405   $total_pages++
406     if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
407
408
409   #format address (variable for the template)
410   my $l = 0;
411   @address = ( '', '', '', '', '', '' );
412   package FS::cust_bill; #!
413   $FS::cust_bill::_template::address[$l++] =
414     $cust_main->payname.
415       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
416         ? " (P.O. #". $cust_main->payinfo. ")"
417         : ''
418       )
419   ;
420   $FS::cust_bill::_template::address[$l++] = $cust_main->company
421     if $cust_main->company;
422   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
423   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
424     if $cust_main->address2;
425   $FS::cust_bill::_template::address[$l++] =
426     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
427   $FS::cust_bill::_template::address[$l++] = $cust_main->country
428     unless $cust_main->country eq 'US';
429
430   #overdue? (variable for the template)
431   $FS::cust_bill::_template::overdue = ( 
432     $balance_due > 0
433     && $today > $self->_date 
434 #    && $self->printed > 1
435     && $self->printed > 0
436   );
437
438   #and subroutine for the template
439
440   sub FS::cust_bill::_template::invoice_lines {
441     my $lines = shift;
442     map { 
443       scalar(@buf) ? shift @buf : [ '', '' ];
444     }
445     ( 1 .. $lines );
446   }
447     
448   $FS::cust_bill::_template::page = 1;
449   my $lines;
450   my @collect;
451   while (@buf) {
452     push @collect, split("\n",
453       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
454     );
455     $FS::cust_bill::_template::page++;
456   }
457
458   map "$_\n", @collect;
459
460 }
461
462 =back
463
464 =head1 VERSION
465
466 $Id: cust_bill.pm,v 1.9 2001-09-01 21:52:19 jeff Exp $
467
468 =head1 BUGS
469
470 The delete method.
471
472 print_text formatting (and some logic :/) is in source, but needs to be
473 slurped in from a file.  Also number of lines ($=).
474
475 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
476 or something similar so the look can be completely customized?)
477
478 =head1 SEE ALSO
479
480 L<FS::Record>, L<FS::cust_main>, L<FS::cust_pay>, L<FS::cust_bill_pkg>,
481 L<FS::cust_credit>, schema.html from the base documentation.
482
483 =cut
484
485 1;
486