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