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