4 use vars qw(@ISA $conf $add1 $add2 $add3 $add4);
7 use FS::Record qw(fields qsearch qsearchs);
9 @ISA = qw(FS::Record Exporter);
13 ($add1,$add2,$add3,$add4) = $conf->config('address');
17 FS::cust_bill - Object methods for cust_bill records
23 $record = create FS::cust_bill \%hash;
24 $record = create FS::cust_bill { 'column' => 'value' };
26 $error = $record->insert;
28 $error = $new_record->replace($old_record);
30 $error = $record->delete;
32 $error = $record->check;
34 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
36 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
38 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
40 @cust_pay_objects = $cust_bill->cust_pay;
42 @lines = $cust_bill->print_text;
43 @lines = $cust_bill->print_text $time;
47 An FS::cust_bill object represents an invoice. FS::cust_bill inherits from
48 FS::Record. The following fields are currently supported:
52 =item invnum - primary key (assigned automatically for new invoices)
54 =item custnum - customer (see L<FS::cust_main>)
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.
59 =item charged - amount of this invoice
61 =item owed - amount still outstanding on this invoice, which is charged minus
62 all payments (see L<FS::cust_pay>).
64 =item printed - how many times this invoice has been printed automatically
65 (see L<FS::cust_main/"collect">).
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>).
82 my($proto,$hashref)=@_;
84 #now in FS::Record::new
86 #foreach $field (fields('cust_bill')) {
87 # $hashref->{$field}='' unless defined $hashref->{$field};
90 $proto->new('cust_bill',$hashref);
95 Adds this invoice to the database ("Posts" the invoice). If there is an error,
96 returns the error, otherwise returns false.
98 When adding new invoices, owed must be charged (or null, in which case it is
99 automatically set to charged).
106 $self->setfield('owed',$self->charged) if $self->owed eq '';
107 return "owed != charged!"
108 unless $self->owed == $self->charged;
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?)
122 return "Can't remove invoice!"
127 =item replace OLD_RECORD
129 Replaces the OLD_RECORD with this one in the database. If there is an error,
130 returns the error, otherwise returns false.
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>).
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');
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
166 return "Not a cust_bill record!" unless $self->table eq "cust_bill";
167 my($recref) = $self->hashref;
169 $recref->{invnum} =~ /^(\d*)$/ or return "Illegal invnum";
170 $recref->{invnum} = $1;
172 $recref->{custnum} =~ /^(\d+)$/ or return "Illegal custnum";
173 $recref->{custnum} = $1;
174 return "Unknown customer"
175 unless qsearchs('cust_main',{'custnum'=>$recref->{custnum}});
177 $recref->{_date} =~ /^(\d*)$/ or return "Illegal date";
178 $recref->{_date} = $recref->{_date} ? $1 : time;
180 #$recref->{charged} =~ /^(\d+(\.\d\d)?)$/ or return "Illegal charged";
181 $recref->{charged} =~ /^(\-?\d+(\.\d\d)?)$/ or return "Illegal charged";
182 $recref->{charged} = $1;
184 $recref->{owed} =~ /^(\-?\d+(\.\d\d)?)$/ or return "Illegal owed";
185 $recref->{owed} = $1;
187 $recref->{printed} =~ /^(\d*)$/ or return "Illegal printed";
188 $recref->{printed} = $1 || '0';
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).
203 my(@cust_bill) = sort { $a->_date <=> $b->_date }
204 grep { $_->owed != 0 && $_->_date < $self->_date }
205 qsearch('cust_bill',{ 'custnum' => $self->custnum } )
207 foreach (@cust_bill) { $total += $_->owed; }
213 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
219 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
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).
233 my(@cust_credit) = sort { $a->_date <=> $b->date }
234 grep { $_->credited != 0 && $_->_date < $self->_date }
235 qsearch('cust_credit', { 'custnum' => $self->custnum } )
237 foreach (@cust_credit) { $total += $_->credited; }
238 $total, @cust_credit;
243 Returns all payments (see L<FS::cust_pay>) for this invoice.
249 sort { $a->_date <=> $b->date }
250 qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
254 =item print_text [TIME];
256 Returns an ASCII invoice, as a list of lines.
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.
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;
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;
283 && $today > $self->_date
284 && $self->printed > 1
289 local($SIG{CHLD}) = sub { wait() };
291 my($pid)=open(CHILD,"-|");
292 die "Can't fork: $!" unless defined($pid);
295 my(@collect)=<CHILD>;
300 my($description,$amount);
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. " ".
323 $address[$l++]=$cust_main->country unless $cust_main->country eq 'US';
326 foreach ( @pr_cust_bill ) {
328 "Previous Balance, Invoice #". $_->invnum.
329 " (". time2str("%x",$_->_date). ")",
330 '$'. sprintf("%10.2f",$_->owed)
334 push @buf,('','-----------');
335 push @buf,('Total Previous Balance','$' . sprintf("%10.2f",$pr_total ) );
340 foreach ( $self->cust_bill_pkg ) {
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;
348 push @buf, ( "$pkg Setup",'$' . sprintf("%10.2f",$_->setup) )
351 "$pkg (" . time2str("%x",$_->sdate) . " - " .
352 time2str("%x",$_->edate) . ")",
353 '$' . sprintf("%10.2f",$_->recur)
357 push @buf,("Tax",'$' . sprintf("%10.2f",$_->setup) )
362 push @buf,('','-----------');
363 push @buf,('Total New Charges',
364 '$' . sprintf("%10.2f",$self->charged) );
367 push @buf,('','-----------');
368 push @buf,('Total Charges',
369 '$' . sprintf("%10.2f",$self->charged + $pr_total) );
373 foreach ( @cr_cust_credit ) {
375 "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
376 '$' . sprintf("%10.2f",$_->credited)
380 #get & print payments
381 foreach ( $self->cust_pay ) {
383 "Payment received ". time2str("%x",$_->_date ),
384 '$' . sprintf("%10.2f",$_->paid )
389 push @buf,('','-----------');
390 push @buf,('Balance Due','$' .
391 sprintf("%10.2f",$self->owed + $pr_total - $cr_total ) );
395 my($tot_pages)=int(scalar(@buf)/30); #15 lines, 2 values per line
396 $tot_pages++ if scalar(@buf) % 30;
399 $description=shift(@buf);
403 ($description,$amount)=('','');
413 @||||||||||||||||||| @<<<<<<< @<<<<<<<<<<<
415 ( $tot_pages != 1 ) ? "Page $% of $tot_pages" : '',
416 time2str("%x",( $self->_date )), "FS-$invnum"
420 @>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
422 @>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
424 @>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
426 @>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
429 @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
430 { $cust_main->payname,
431 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo )
432 ? "P.O. #". $cust_main->payinfo : ''
434 @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
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 @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
450 @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<
464 It doesn't properly override FS::Record yet.
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 ($=).
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?)
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?
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.
484 ivan@voicenet.com 97-jul-1
486 small fix for new API ivan@sisd.com 98-mar-14
488 charges can be negative ivan@sisd.com 98-jul-13
490 pod, ingegrate with FS::Invoice ivan@sisd.com 98-sep-20