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