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