5de1d929b9cb992aec68fe92c68326e3a2302e01
[freeside.git] / site_perl / cust_bill.pm
1 package FS::cust_bill;
2
3 use strict;
4 use vars qw( @ISA $conf $add1 $add2 $add3 $add4 );
5 use Date::Format;
6 use FS::Record qw( qsearch qsearchs );
7 use FS::cust_main;
8 use FS::cust_bill_pkg;
9 use FS::cust_credit;
10 use FS::cust_pay;
11 use FS::cust_pkg;
12
13 @ISA = qw( FS::Record );
14
15 #ask FS::UID to run this stuff for us later
16 $FS::UID::callback{'FS::cust_bill'} = sub { 
17   $conf = new FS::Conf;
18   ( $add1, $add2, $add3, $add4 ) = $conf->config('address');
19 };
20
21 =head1 NAME
22
23 FS::cust_bill - Object methods for cust_bill records
24
25 =head1 SYNOPSIS
26
27   use FS::cust_bill;
28
29   $record = new FS::cust_bill \%hash;
30   $record = new FS::cust_bill { 'column' => 'value' };
31
32   $error = $record->insert;
33
34   $error = $new_record->replace($old_record);
35
36   $error = $record->delete;
37
38   $error = $record->check;
39
40   ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
41
42   @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
43
44   ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
45
46   @cust_pay_objects = $cust_bill->cust_pay;
47
48   @lines = $cust_bill->print_text;
49   @lines = $cust_bill->print_text $time;
50
51 =head1 DESCRIPTION
52
53 An FS::cust_bill object represents an invoice.  FS::cust_bill inherits from
54 FS::Record.  The following fields are currently supported:
55
56 =over 4
57
58 =item invnum - primary key (assigned automatically for new invoices)
59
60 =item custnum - customer (see L<FS::cust_main>)
61
62 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
63 L<Time::Local> and L<Date::Parse> for conversion functions.
64
65 =item charged - amount of this invoice
66
67 =item owed - amount still outstanding on this invoice, which is charged minus
68 all payments (see L<FS::cust_pay>).
69
70 =item printed - how many times this invoice has been printed automatically
71 (see L<FS::cust_main/"collect">).
72
73 =back
74
75 =head1 METHODS
76
77 =over 4
78
79 =item new HASHREF
80
81 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
82 Invoices are normally created by calling the bill method of a customer object
83 (see L<FS::cust_main>).
84
85 =cut
86
87 sub table { 'cust_bill'; }
88
89 =item insert
90
91 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
92 returns the error, otherwise returns false.
93
94 When adding new invoices, owed must be charged (or null, in which case it is
95 automatically set to charged).
96
97 =cut
98
99 sub insert {
100   my $self = shift;
101
102   $self->owed( $self->charged ) if $self->owed eq '';
103   return "owed != charged!"
104     unless $self->owed == $self->charged;
105
106   $self->SUPER::insert;
107 }
108
109 =item delete
110
111 Currently unimplemented.  I don't remove invoices because there would then be
112 no record you ever posted this invoice (which is bad, no?)
113
114 =cut
115
116 sub delete {
117   return "Can't remove invoice!"
118 }
119
120 =item replace OLD_RECORD
121
122 Replaces the OLD_RECORD with this one in the database.  If there is an error,
123 returns the error, otherwise returns false.
124
125 Only owed and printed may be changed.  Owed is normally updated by creating and
126 inserting a payment (see L<FS::cust_pay>).  Printed is normally updated by
127 calling the collect method of a customer object (see L<FS::cust_main>).
128
129 =cut
130
131 sub replace {
132   my( $new, $old ) = ( shift, shift );
133   return "Can't change custnum!" unless $old->custnum == $new->custnum;
134   #return "Can't change _date!" unless $old->_date eq $new->_date;
135   return "Can't change _date!" unless $old->_date == $new->_date;
136   return "Can't change charged!" unless $old->charged == $new->charged;
137   return "(New) owed can't be > (new) charged!" if $new->owed > $new->charged;
138
139   $new->SUPER::replace($old);
140 }
141
142 =item check
143
144 Checks all fields to make sure this is a valid invoice.  If there is an error,
145 returns the error, otherwise returns false.  Called by the insert and replace
146 methods.
147
148 =cut
149
150 sub check {
151   my $self = shift;
152
153   my $error =
154     $self->ut_numbern('invnum')
155     || $self->ut_number('custnum')
156     || $self->ut_numbern('_date')
157     || $self->ut_money('charged')
158     || $self->ut_money('owed')
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 print_text [TIME];
235
236 Returns an ASCII invoice, as a list of lines.
237
238 TIME an optional value used to control the printing of overdue messages.  The
239 default is now.  It isn't the date of the invoice; that's the `_date' field.
240 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
241 L<Time::Local> and L<Date::Parse> for conversion functions.
242
243 =cut
244
245 sub print_text {
246
247   my( $self, $today ) = ( shift, shift );
248   $today ||= time;
249   my $invnum = $self->invnum;
250   my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
251   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
252     unless $cust_main->payname;
253
254   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
255   my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
256   my $balance_due = $self->owed + $pr_total - $cr_total;
257
258   #overdue?
259   my $overdue = ( 
260     $balance_due > 0
261     && $today > $self->_date 
262     && $self->printed > 1
263   );
264
265   #printing bits here (yuck!)
266
267   local($SIG{CHLD}) = sub { wait() };
268   $|=1;
269   my($pid)=open(CHILD,"-|");
270   die "Can't fork: $!" unless defined($pid); 
271
272   if ($pid) { #parent
273     my(@collect)=<CHILD>;
274     close CHILD;
275     return @collect;
276   } else { #child
277
278     my($description,$amount);
279     my(@buf);
280
281     #define format stuff
282     $%=0;
283     $= = 35;
284     local($^L) = <<END;
285
286
287
288
289
290
291
292 END
293
294     #format address
295     my($l,@address)=(0,'','','','','');
296     $address[$l++]=$cust_main->company if $cust_main->company;
297     $address[$l++]=$cust_main->address1;
298     $address[$l++]=$cust_main->address2 if $cust_main->address2;
299     $address[$l++]=$cust_main->city. ", ". $cust_main->state. "  ".
300                    $cust_main->zip;
301     $address[$l++]=$cust_main->country unless $cust_main->country eq 'US';
302
303     #previous balance
304     foreach ( @pr_cust_bill ) {
305       push @buf, (
306         "Previous Balance, Invoice #". $_->invnum. 
307                    " (". time2str("%x",$_->_date). ")",
308         '$'. sprintf("%10.2f",$_->owed)
309       );
310     }
311     if (@pr_cust_bill) {
312       push @buf,('','-----------');
313       push @buf,('Total Previous Balance','$' . sprintf("%10.2f",$pr_total ) );
314       push @buf,('','');
315     }
316
317     #new charges
318     foreach ( $self->cust_bill_pkg ) {
319
320       if ( $_->pkgnum ) {
321
322         my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
323         my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
324         my($pkg)=$part_pkg->pkg;
325
326         push @buf, ( "$pkg Setup",'$' . sprintf("%10.2f",$_->setup) )
327           if $_->setup != 0;
328         push @buf, (
329           "$pkg (" . time2str("%x",$_->sdate) . " - " .
330                                 time2str("%x",$_->edate) . ")",
331           '$' . sprintf("%10.2f",$_->recur)
332         ) if $_->recur != 0;
333
334       } else { #pkgnum Tax
335         push @buf,("Tax",'$' . sprintf("%10.2f",$_->setup) ) 
336           if $_->setup != 0;
337       }
338     }
339
340     push @buf,('','-----------');
341     push @buf,('Total New Charges',
342                '$' . sprintf("%10.2f",$self->charged) );
343     push @buf,('','');
344
345     push @buf,('','-----------');
346     push @buf,('Total Charges',
347                '$' . sprintf("%10.2f",$self->charged + $pr_total) );
348     push @buf,('','');
349
350     #credits
351     foreach ( @cr_cust_credit ) {
352       push @buf,(
353         "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
354         '$' . sprintf("%10.2f",$_->credited)
355       );
356     }
357
358     #get & print payments
359     foreach ( $self->cust_pay ) {
360       push @buf,(
361         "Payment received ". time2str("%x",$_->_date ),
362         '$' . sprintf("%10.2f",$_->paid )
363       );
364     }
365
366     #balance due
367     push @buf,('','-----------');
368     push @buf,('Balance Due','$' . 
369       sprintf("%10.2f",$self->owed + $pr_total - $cr_total ) );
370
371     #now print
372
373     my($tot_pages)=int(scalar(@buf)/30); #15 lines, 2 values per line
374     $tot_pages++ if scalar(@buf) % 30;
375
376     while (@buf) {
377       $description=shift(@buf);
378       $amount=shift(@buf);
379       write;
380     }
381       ($description,$amount)=('','');
382       write while ( $- );
383       print $^L;
384
385       exit; #kid
386
387     format STDOUT_TOP =
388
389                                       @|||||||||||||||||||
390                                      "Invoice"
391                                       @||||||||||||||||||| @<<<<<<< @<<<<<<<<<<<
392 {
393               ( $tot_pages != 1 ) ? "Page $% of $tot_pages" : '',
394   time2str("%x",( $self->_date )), "FS-$invnum"
395 }
396
397
398 @>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
399 $add1
400 @>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
401 $add2
402 @>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
403 $add3
404 @>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
405 $add4
406
407   @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<             @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
408 { $cust_main->payname,
409   ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo )
410   ? "P.O. #". $cust_main->payinfo : ''
411 }
412   @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<             @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
413 $address[0],''
414   @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<             @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
415 $address[1],$overdue ? "* This invoice is now PAST DUE! *" : ''
416   @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<             @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
417 $address[2],$overdue ? " Please forward payment promptly " : ''
418   @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<             @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
419 $address[3],$overdue ? "to avoid interruption of service." : ''
420   @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<             @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
421 $address[4],''
422
423
424
425 .
426
427     format STDOUT =
428   @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<
429   $description,$amount
430 .
431
432   } #endchild
433
434 }
435
436 =back
437
438 =head1 VERSION
439
440 $Id: cust_bill.pm,v 1.5 1999-01-18 21:58:03 ivan Exp $
441
442 =head1 BUGS
443
444 The delete method.
445
446 print_text formatting (and some logic :/) is in source as a format declaration,
447 which needs to be slurped in from a file.  the fork is rather kludgy as well.
448 It could be cleaned with swrite from man perlform, and the picture could be
449 put in a /var/spool/freeside/conf file.  Also number of lines ($=).
450
451 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
452 or something similar so the look can be completely customized?)
453
454 There is an off-by-one error in print_text which causes a visual error: "Page 1
455 of 2" printed on some single-page invoices?
456
457 =head1 SEE ALSO
458
459 L<FS::Record>, L<FS::cust_main>, L<FS::cust_pay>, L<FS::cust_bill_pkg>,
460 L<FS::cust_credit>, schema.html from the base documentation.
461
462 =head1 HISTORY
463
464 ivan@voicenet.com 97-jul-1
465
466 small fix for new API ivan@sisd.com 98-mar-14
467
468 charges can be negative ivan@sisd.com 98-jul-13
469
470 pod, ingegrate with FS::Invoice ivan@sisd.com 98-sep-20
471
472 $Log: cust_bill.pm,v $
473 Revision 1.5  1999-01-18 21:58:03  ivan
474 esthetic: eq and ne were used in a few places instead of == and !=
475
476 Revision 1.4  1998/12/29 11:59:36  ivan
477 mostly properly OO, some work still to be done with svc_ stuff
478
479 Revision 1.3  1998/11/13 09:56:53  ivan
480 change configuration file layout to support multiple distinct databases (with
481 own set of config files, export, etc.)
482
483 Revision 1.2  1998/11/07 10:24:24  ivan
484 don't use depriciated FS::Bill and FS::Invoice, other miscellania
485
486
487 =cut
488
489 1;
490