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