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