cust_refund and cust_pay get custnums
[freeside.git] / FS / FS / cust_bill.pm
index 30db469..2ca4b52 100644 (file)
@@ -1,21 +1,41 @@
 package FS::cust_bill;
 
 use strict;
 package FS::cust_bill;
 
 use strict;
-use vars qw( @ISA $conf $add1 $add2 $add3 $add4 );
+use vars qw( @ISA $conf $invoice_template $money_char );
+use vars qw( $invoice_lines @buf ); #yuck
 use Date::Format;
 use Date::Format;
+use Text::Template;
 use FS::Record qw( qsearch qsearchs );
 use FS::cust_main;
 use FS::cust_bill_pkg;
 use FS::cust_credit;
 use FS::cust_pay;
 use FS::cust_pkg;
 use FS::Record qw( qsearch qsearchs );
 use FS::cust_main;
 use FS::cust_bill_pkg;
 use FS::cust_credit;
 use FS::cust_pay;
 use FS::cust_pkg;
+use FS::cust_credit_bill;
 
 @ISA = qw( FS::Record );
 
 #ask FS::UID to run this stuff for us later
 $FS::UID::callback{'FS::cust_bill'} = sub { 
 
 @ISA = qw( FS::Record );
 
 #ask FS::UID to run this stuff for us later
 $FS::UID::callback{'FS::cust_bill'} = sub { 
+
   $conf = new FS::Conf;
   $conf = new FS::Conf;
-  ( $add1, $add2, $add3, $add4 ) = ( $conf->config('address'), '', '', '', '' );
+
+  $money_char = $conf->config('money_char') || '$';  
+
+  my @invoice_template = $conf->config('invoice_template')
+    or die "cannot load config file invoice_template";
+  $invoice_lines = 0;
+  foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy
+    /invoice_lines\((\d+)\)/;
+    $invoice_lines += $1;
+  }
+  die "no invoice_lines() functions in template?" unless $invoice_lines;
+  $invoice_template = new Text::Template (
+    TYPE   => 'ARRAY',
+    SOURCE => [ map "$_\n", @invoice_template ],
+  ) or die "can't create new Text::Template object: $Text::Template::ERROR";
+  $invoice_template->compile()
+    or die "can't compile template: $Text::Template::ERROR";
 };
 
 =head1 NAME
 };
 
 =head1 NAME
@@ -45,13 +65,17 @@ FS::cust_bill - Object methods for cust_bill records
 
   @cust_pay_objects = $cust_bill->cust_pay;
 
 
   @cust_pay_objects = $cust_bill->cust_pay;
 
+  $tax_amount = $record->tax;
+
   @lines = $cust_bill->print_text;
   @lines = $cust_bill->print_text $time;
 
 =head1 DESCRIPTION
 
   @lines = $cust_bill->print_text;
   @lines = $cust_bill->print_text $time;
 
 =head1 DESCRIPTION
 
-An FS::cust_bill object represents an invoice.  FS::cust_bill inherits from
-FS::Record.  The following fields are currently supported:
+An FS::cust_bill object represents an invoice; a declaration that a customer
+owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
+(see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
+following fields are currently supported:
 
 =over 4
 
 
 =over 4
 
@@ -64,9 +88,6 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 
 =item charged - amount of this invoice
 
 
 =item charged - amount of this invoice
 
-=item owed - amount still outstanding on this invoice, which is charged minus
-all payments (see L<FS::cust_pay>).
-
 =item printed - how many times this invoice has been printed automatically
 (see L<FS::cust_main/"collect">).
 
 =item printed - how many times this invoice has been printed automatically
 (see L<FS::cust_main/"collect">).
 
@@ -91,21 +112,6 @@ sub table { 'cust_bill'; }
 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
 returns the error, otherwise returns false.
 
 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
 returns the error, otherwise returns false.
 
-When adding new invoices, owed must be charged (or null, in which case it is
-automatically set to charged).
-
-=cut
-
-sub insert {
-  my $self = shift;
-
-  $self->owed( $self->charged ) if $self->owed eq '';
-  return "owed != charged!"
-    unless $self->owed == $self->charged;
-
-  $self->SUPER::insert;
-}
-
 =item delete
 
 Currently unimplemented.  I don't remove invoices because there would then be
 =item delete
 
 Currently unimplemented.  I don't remove invoices because there would then be
@@ -122,9 +128,8 @@ sub delete {
 Replaces the OLD_RECORD with this one in the database.  If there is an error,
 returns the error, otherwise returns false.
 
 Replaces the OLD_RECORD with this one in the database.  If there is an error,
 returns the error, otherwise returns false.
 
-Only owed and printed may be changed.  Owed is normally updated by creating and
-inserting a payment (see L<FS::cust_pay>).  Printed is normally updated by
-calling the collect method of a customer object (see L<FS::cust_main>).
+Only printed may be changed.  printed is normally updated by calling the
+collect method of a customer object (see L<FS::cust_main>).
 
 =cut
 
 
 =cut
 
@@ -134,7 +139,6 @@ sub replace {
   #return "Can't change _date!" unless $old->_date eq $new->_date;
   return "Can't change _date!" unless $old->_date == $new->_date;
   return "Can't change charged!" unless $old->charged == $new->charged;
   #return "Can't change _date!" unless $old->_date eq $new->_date;
   return "Can't change _date!" unless $old->_date == $new->_date;
   return "Can't change charged!" unless $old->charged == $new->charged;
-  return "(New) owed can't be > (new) charged!" if $new->owed > $new->charged;
 
   $new->SUPER::replace($old);
 }
 
   $new->SUPER::replace($old);
 }
@@ -155,7 +159,6 @@ sub check {
     || $self->ut_number('custnum')
     || $self->ut_numbern('_date')
     || $self->ut_money('charged')
     || $self->ut_number('custnum')
     || $self->ut_numbern('_date')
     || $self->ut_money('charged')
-    || $self->ut_money('owed')
     || $self->ut_numbern('printed')
   ;
   return $error if $error;
     || $self->ut_numbern('printed')
   ;
   return $error if $error;
@@ -201,39 +204,104 @@ sub cust_bill_pkg {
 
 =item cust_credit
 
 
 =item cust_credit
 
-Returns a list consisting of the total previous credited (see
-L<FS::cust_credit>) for this customer, followed by the previous outstanding
-credits (FS::cust_credit objects).
+Depreciated.  See the cust_credited method.
+
+ #Returns a list consisting of the total previous credited (see
+ #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
+ #outstanding credits (FS::cust_credit objects).
 
 =cut
 
 sub cust_credit {
 
 =cut
 
 sub cust_credit {
-  my $self = shift;
-  my $total = 0;
-  my @cust_credit = sort { $a->_date <=> $b->date }
-    grep { $_->credited != 0 && $_->_date < $self->_date }
-      qsearch('cust_credit', { 'custnum' => $self->custnum } )
-  ;
-  foreach (@cust_credit) { $total += $_->credited; }
-  $total, @cust_credit;
+  use Carp;
+  croak "FS::cust_bill->cust_credit depreciated; see ".
+        "FS::cust_bill->cust_credit_bill";
+  #my $self = shift;
+  #my $total = 0;
+  #my @cust_credit = sort { $a->_date <=> $b->_date }
+  #  grep { $_->credited != 0 && $_->_date < $self->_date }
+  #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
+  #;
+  #foreach (@cust_credit) { $total += $_->credited; }
+  #$total, @cust_credit;
 }
 
 =item cust_pay
 
 }
 
 =item cust_pay
 
-Returns all payments (see L<FS::cust_pay>) for this invoice.
+Depreciated.  See the cust_bill_pay method.
+
+#Returns all payments (see L<FS::cust_pay>) for this invoice.
 
 =cut
 
 sub cust_pay {
 
 =cut
 
 sub cust_pay {
+  use Carp;
+  croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
+  #my $self = shift;
+  #sort { $a->_date <=> $b->_date }
+  #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
+  #;
+}
+
+=item cust_bill_pay
+
+Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
+
+=cut
+
+sub cust_bill_pay {
   my $self = shift;
   sort { $a->_date <=> $b->date }
   my $self = shift;
   sort { $a->_date <=> $b->date }
-    qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
+    qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
+}
+
+=item cust_credited
+
+Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
+
+=cut
+
+sub cust_credited {
+  my $self = shift;
+  sort { $a->_date <=> $b->_date }
+    qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
   ;
 }
 
   ;
 }
 
+=item tax
+
+Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
+
+=cut
+
+sub tax {
+  my $self = shift;
+  my $total = 0;
+  my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
+                                             'pkgnum' => 0 } );
+  foreach (@taxlines) { $total += $_->setup; }
+  $total;
+}
+
+=item owed
+
+Returns the amount owed (still outstanding) on this invoice, which is charged
+minus all payment applications (see L<FS::cust_bill_pay>) and credit
+applications (see L<FS::cust_credit_bill>).
+
+=cut
+
+sub owed {
+  my $self = shift;
+  my $balance = $self->charged;
+  $balance -= $_->amount foreach ( $self->cust_bill_pay );
+  $balance -= $_->amount foreach ( $self->cust_credited );
+  $balance = sprintf( "%.2f", $balance);
+}
+
 =item print_text [TIME];
 
 =item print_text [TIME];
 
-Returns an ASCII invoice, as a list of lines.
+Returns an text invoice, as a list of lines.
 
 TIME an optional value used to control the printing of overdue messages.  The
 default is now.  It isn't the date of the invoice; that's the `_date' field.
 
 TIME an optional value used to control the printing of overdue messages.  The
 default is now.  It isn't the date of the invoice; that's the `_date' field.
@@ -246,57 +314,33 @@ sub print_text {
 
   my( $self, $today ) = ( shift, shift );
   $today ||= time;
 
   my( $self, $today ) = ( shift, shift );
   $today ||= time;
-  my $invnum = $self->invnum;
+#  my $invnum = $self->invnum;
   my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
     unless $cust_main->payname;
 
   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
   my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
     unless $cust_main->payname;
 
   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
-  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
-  my $balance_due = $self->owed + $pr_total - $cr_total;
+#  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
+  #my $balance_due = $self->owed + $pr_total - $cr_total;
+  my $balance_due = $self->owed + $pr_total;
 
 
-  #overdue?
-  my $overdue = ( 
-    $balance_due > 0
-    && $today > $self->_date 
-    && $self->printed > 1
-  );
-
-  #printing bits here (yuck!)
-
-  my @collect = ();
-
-  my($description,$amount);
-  my(@buf);
-
-  #format address
-  my($l,@address)=(0,'','','','','','','');
-  $address[$l++] =
-    $cust_main->payname.
-      ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
-        ? " (P.O. #". $cust_main->payinfo. ")"
-        : ''
-      )
-  ;
-  $address[$l++]=$cust_main->company if $cust_main->company;
-  $address[$l++]=$cust_main->address1;
-  $address[$l++]=$cust_main->address2 if $cust_main->address2;
-  $address[$l++]=$cust_main->city. ", ". $cust_main->state. "  ".
-                 $cust_main->zip;
-  $address[$l++]=$cust_main->country unless $cust_main->country eq 'US';
+  #my @collect = ();
+  #my($description,$amount);
+  @buf = ();
 
   #previous balance
   foreach ( @pr_cust_bill ) {
 
   #previous balance
   foreach ( @pr_cust_bill ) {
-    push @buf, (
+    push @buf, [
       "Previous Balance, Invoice #". $_->invnum. 
                  " (". time2str("%x",$_->_date). ")",
       "Previous Balance, Invoice #". $_->invnum. 
                  " (". time2str("%x",$_->_date). ")",
-      '$'. sprintf("%10.2f",$_->owed)
-    );
+      $money_char. sprintf("%10.2f",$_->owed)
+    ];
   }
   if (@pr_cust_bill) {
   }
   if (@pr_cust_bill) {
-    push @buf,('','-----------');
-    push @buf,('Total Previous Balance','$' . sprintf("%10.2f",$pr_total ) );
-    push @buf,('','');
+    push @buf,['','-----------'];
+    push @buf,[ 'Total Previous Balance',
+                $money_char. sprintf("%10.2f",$pr_total ) ];
+    push @buf,['',''];
   }
 
   #new charges
   }
 
   #new charges
@@ -309,117 +353,135 @@ sub print_text {
       my($pkg)=$part_pkg->pkg;
 
       if ( $_->setup != 0 ) {
       my($pkg)=$part_pkg->pkg;
 
       if ( $_->setup != 0 ) {
-        push @buf, ( "$pkg Setup",'$' . sprintf("%10.2f",$_->setup) );
-        push @buf, map { "  ". $_->[0]. ": ". $_->[1], '' } $cust_pkg->labels;
+        push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
+        push @buf,
+          map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
       }
 
       if ( $_->recur != 0 ) {
       }
 
       if ( $_->recur != 0 ) {
-        push @buf, (
+        push @buf, [
           "$pkg (" . time2str("%x",$_->sdate) . " - " .
                                 time2str("%x",$_->edate) . ")",
           "$pkg (" . time2str("%x",$_->sdate) . " - " .
                                 time2str("%x",$_->edate) . ")",
-          '$' . sprintf("%10.2f",$_->recur)
-        );
-        push @buf, map { "  ". $_->[0]. ": ". $_->[1], '' } $cust_pkg->labels;
+          $money_char. sprintf("%10.2f",$_->recur)
+        ];
+        push @buf,
+          map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
       }
 
     } else { #pkgnum Tax
       }
 
     } else { #pkgnum Tax
-      push @buf,("Tax",'$' . sprintf("%10.2f",$_->setup) ) 
+      push @buf,["Tax", $money_char. sprintf("%10.2f",$_->setup) ] 
         if $_->setup != 0;
     }
   }
 
         if $_->setup != 0;
     }
   }
 
-  push @buf,('','-----------');
-  push @buf,('Total New Charges',
-             '$' . sprintf("%10.2f",$self->charged) );
-  push @buf,('','');
+  push @buf,['','-----------'];
+  push @buf,['Total New Charges',
+             $money_char. sprintf("%10.2f",$self->charged) ];
+  push @buf,['',''];
 
 
-  push @buf,('','-----------');
-  push @buf,('Total Charges',
-             '$' . sprintf("%10.2f",$self->charged + $pr_total) );
-  push @buf,('','');
+  push @buf,['','-----------'];
+  push @buf,['Total Charges',
+             $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
+  push @buf,['',''];
 
   #credits
 
   #credits
-  foreach ( @cr_cust_credit ) {
-    push @buf,(
-      "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
-      '$' . sprintf("%10.2f",$_->credited)
-    );
+  foreach ( $self->cust_credited ) {
+
+    #something more elaborate if $_->amount ne $_->cust_credit->credited ?
+
+    push @buf,[
+      "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")",
+      $money_char. sprintf("%10.2f",$_->amount)
+    ];
   }
   }
+  #foreach ( @cr_cust_credit ) {
+  #  push @buf,[
+  #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
+  #    $money_char. sprintf("%10.2f",$_->credited)
+  #  ];
+  #}
 
   #get & print payments
 
   #get & print payments
-  foreach ( $self->cust_pay ) {
-    push @buf,(
-      "Payment received ". time2str("%x",$_->_date ),
-      '$' . sprintf("%10.2f",$_->paid )
-    );
+  foreach ( $self->cust_bill_pay ) {
+
+    #something more elaborate if $_->amount ne ->cust_pay->paid ?
+
+    push @buf,[
+      "Payment received ". time2str("%x",$_->cust_pay->_date ),
+      $money_char. sprintf("%10.2f",$_->paid )
+    ];
   }
 
   #balance due
   }
 
   #balance due
-  push @buf,('','-----------');
-  push @buf,('Balance Due','$' . 
-    sprintf("%10.2f",$self->owed + $pr_total - $cr_total ) );
-
-  #now print
+  push @buf,['','-----------'];
+  push @buf,['Balance Due', $money_char. 
+    sprintf("%10.2f", $balance_due ) ];
+
+  #setup template variables
+  
+  package FS::cust_bill::_template; #!
+  use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
+
+  $invnum = $self->invnum;
+  $date = $self->_date;
+  $page = 1;
+
+  $total_pages =
+    int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
+  $total_pages++
+    if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
+
+
+  #format address (variable for the template)
+  my $l = 0;
+  @address = ( '', '', '', '', '', '' );
+  package FS::cust_bill; #!
+  $FS::cust_bill::_template::address[$l++] =
+    $cust_main->payname.
+      ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
+        ? " (P.O. #". $cust_main->payinfo. ")"
+        : ''
+      )
+  ;
+  $FS::cust_bill::_template::address[$l++] = $cust_main->company
+    if $cust_main->company;
+  $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
+  $FS::cust_bill::_template::address[$l++] = $cust_main->address2
+    if $cust_main->address2;
+  $FS::cust_bill::_template::address[$l++] =
+    $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
+  $FS::cust_bill::_template::address[$l++] = $cust_main->country
+    unless $cust_main->country eq 'US';
+
+  #overdue? (variable for the template)
+  $FS::cust_bill::_template::overdue = ( 
+    $balance_due > 0
+    && $today > $self->_date 
+#    && $self->printed > 1
+    && $self->printed > 0
+  );
 
 
-  my $tot_lines = 50; #should be configurable
-   #header is 17 lines
-  my $tot_pages = int( scalar(@buf) / ( 2 * ( $tot_lines - 17 ) ) );
-  $tot_pages++ if scalar(@buf) % ( 2 * ( $tot_lines - 17 ) );
+  #and subroutine for the template
 
 
-  my $page = 1;
+  sub FS::cust_bill::_template::invoice_lines {
+    my $lines = shift;
+    map { 
+      scalar(@buf) ? shift @buf : [ '', '' ];
+    }
+    ( 1 .. $lines );
+  }
+    
+  $FS::cust_bill::_template::page = 1;
   my $lines;
   my $lines;
+  my @collect;
   while (@buf) {
   while (@buf) {
-    $lines = $tot_lines;
-    my @header = &header(
-      $page, $tot_pages, $self->_date, $self->invnum, @address
+    push @collect, split("\n",
+      $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
     );
     );
-    push @collect, @header;
-    $lines -= scalar(@header);
-
-    while ( $lines-- && @buf ) {
-      $description=shift(@buf);
-      $amount=shift(@buf);
-      push @collect, myswrite($description, $amount);
-    }
-    $page++;
-  }
-  while ( $lines-- ) {
-    push @collect, myswrite('', '');
+    $FS::cust_bill::_template::page++;
   }
 
   }
 
-  return @collect;
-
-  sub header { #17 lines
-    my ( $page, $tot_pages, $date, $invnum, @address ) = @_ ;
-    push @address, '', '', '', '';
-
-    my @return = ();
-    my $i = ' 'x32;
-    push @return,
-      '',
-      $i. 'Invoice',
-      $i. substr("Page $page of $tot_pages".' 'x10, 0, 20).
-        time2str("%x", $date ). "  FS-". $invnum,
-      '',
-      '',
-      $add1,
-      $add2,
-      $add3,
-      $add4,
-      '',
-      splice @address, 0, 7;
-    ;
-    return map $_. "\n", @return;
-  }
-
-  sub myswrite {
-    my $format = <<END;
-  @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<
-END
-    $^A = '';
-    formline( $format, @_ );
-    return $^A;
-  }
+  map "$_\n", @collect;
 
 }
 
 
 }
 
@@ -427,7 +489,7 @@ END
 
 =head1 VERSION
 
 
 =head1 VERSION
 
-$Id: cust_bill.pm,v 1.1 1999-08-04 09:03:53 ivan Exp $
+$Id: cust_bill.pm,v 1.10 2001-09-02 01:27:10 ivan Exp $
 
 =head1 BUGS
 
 
 =head1 BUGS
 
@@ -441,8 +503,9 @@ or something similar so the look can be completely customized?)
 
 =head1 SEE ALSO
 
 
 =head1 SEE ALSO
 
-L<FS::Record>, L<FS::cust_main>, L<FS::cust_pay>, L<FS::cust_bill_pkg>,
-L<FS::cust_credit>, schema.html from the base documentation.
+L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS:;cust_pay>,
+L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
+documentation.
 
 =cut
 
 
 =cut