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