add (unfinished) credit card surcharge, part 1
[freeside.git] / FS / FS / cust_bill.pm
index bb392e8..62ab87f 100644 (file)
@@ -1,7 +1,8 @@
 package FS::cust_bill;
 
 use strict;
 package FS::cust_bill;
 
 use strict;
-use vars qw( @ISA $DEBUG $me $conf $money_char $date_format $rdate_format );
+use vars qw( @ISA $DEBUG $me $conf
+             $money_char $date_format $rdate_format $date_format_long );
 use vars qw( $invoice_lines @buf ); #yuck
 use Fcntl qw(:flock); #for spool_csv
 use List::Util qw(min max);
 use vars qw( $invoice_lines @buf ); #yuck
 use Fcntl qw(:flock); #for spool_csv
 use List::Util qw(min max);
@@ -45,9 +46,10 @@ $me = '[FS::cust_bill]';
 #ask FS::UID to run this stuff for us later
 FS::UID->install_callback( sub { 
   $conf = new FS::Conf;
 #ask FS::UID to run this stuff for us later
 FS::UID->install_callback( sub { 
   $conf = new FS::Conf;
-  $money_char   = $conf->config('money_char')  || '$';  
-  $date_format  = $conf->config('date_format') || '%x';  
-  $rdate_format = $conf->config('date_format') || '%m/%d/%Y';  
+  $money_char       = $conf->config('money_char')       || '$';  
+  $date_format      = $conf->config('date_format')      || '%x'; #/YY
+  $rdate_format     = $conf->config('date_format')      || '%m/%d/%Y';  #/YYYY
+  $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
 } );
 
 =head1 NAME
 } );
 
 =head1 NAME
@@ -162,6 +164,45 @@ sub cust_unlinked_msg {
 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.
 
+=cut
+
+sub insert {
+  my $self = shift;
+  warn "$me insert called\n" if $DEBUG;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->SUPER::insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  if ( $self->get('cust_bill_pkg') ) {
+    foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
+      $cust_bill_pkg->invnum($self->invnum);
+      my $error = $cust_bill_pkg->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "can't create invoice line item: $error";
+      }
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
 =item delete
 
 This method now works but you probably shouldn't use it.  Instead, apply a
 =item delete
 
 This method now works but you probably shouldn't use it.  Instead, apply a
@@ -224,13 +265,13 @@ sub delete {
 
 }
 
 
 }
 
-=item replace OLD_RECORD
+=item replace [ OLD_RECORD ]
 
 
-Replaces the OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
+You can, but probably shouldn't modify invoices...
 
 
-Only printed may be changed.  printed is normally updated by calling the
-collect method of a customer object (see L<FS::cust_main>).
+Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
+supplied, replaces this record.  If there is an error, returns the error,
+otherwise returns false.
 
 =cut
 
 
 =cut
 
@@ -241,11 +282,11 @@ collect method of a customer object (see L<FS::cust_main>).
 
 sub replace_check {
   my( $new, $old ) = ( shift, shift );
 
 sub replace_check {
   my( $new, $old ) = ( shift, shift );
-  return "Can't change custnum!" unless $old->custnum == $new->custnum;
+  return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
   #return "Can't change _date!" unless $old->_date eq $new->_date;
   #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
-                                     || $old->charged == 0;
+  return "Can't change _date" unless $old->_date == $new->_date;
+  return "Can't change charged" unless $old->charged == $new->charged
+                                    || $old->charged == 0;
 
   '';
 }
 
   '';
 }
@@ -1200,8 +1241,14 @@ sub email {
   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
                             $self->cust_main->invoicing_list;
 
   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
                             $self->cust_main->invoicing_list;
 
-  #better to notify this person than silence
-  @invoicing_list = ($invoice_from) unless @invoicing_list;
+  if ( ! @invoicing_list ) { #no recipients
+    if ( $conf->exists('cust_bill-no_recipients-error') ) {
+      die 'No recipients for customer #'. $self->custnum;
+    } else {
+      #default: better to notify this person than silence
+      @invoicing_list = ($invoice_from);
+    }
+  }
 
   my $subject = $self->email_subject($template);
 
 
   my $subject = $self->email_subject($template);
 
@@ -1911,7 +1958,8 @@ sub realtime_lec {
 }
 
 sub realtime_bop {
 }
 
 sub realtime_bop {
-  my( $self, $method ) = @_;
+  my( $self, $method ) = (shift,shift);
+  my %opt = @_;
 
   my $cust_main = $self->cust_main;
   my $balance = $cust_main->balance;
 
   my $cust_main = $self->cust_main;
   my $balance = $cust_main->balance;
@@ -1940,6 +1988,7 @@ sub realtime_bop {
 #this didn't do what we want, it just calls apply_payments_and_credits
 #    'apply'       => 1,
     'apply_to_invoice' => 1,
 #this didn't do what we want, it just calls apply_payments_and_credits
 #    'apply'       => 1,
     'apply_to_invoice' => 1,
+    %opt,
  #what we want:
  #this changes application behavior: auto payments
                         #triggered against a specific invoice are now applied
  #what we want:
  #this changes application behavior: auto payments
                         #triggered against a specific invoice are now applied
@@ -2252,15 +2301,19 @@ sub print_generic {
   my $nbsp = $nbsps{$format};
 
   my %escape_functions = ( 'latex'    => \&_latex_escape,
   my $nbsp = $nbsps{$format};
 
   my %escape_functions = ( 'latex'    => \&_latex_escape,
-                           'html'     => \&encode_entities,
+                           'html'     => \&_html_escape_nbsp,#\&encode_entities,
                            'template' => sub { shift },
                          );
   my $escape_function = $escape_functions{$format};
                            'template' => sub { shift },
                          );
   my $escape_function = $escape_functions{$format};
+  my $escape_function_nonbsp = ($format eq 'html')
+                                 ? \&_html_escape : $escape_function;
 
 
-  my %date_formats = ( 'latex'    => '%b %o, %Y',
-                       'html'     => '%b&nbsp;%o,&nbsp;%Y',
+  my %date_formats = ( 'latex'    => $date_format_long,
+                       'html'     => $date_format_long,
                        'template' => '%s',
                      );
                        'template' => '%s',
                      );
+  $date_formats{'html'} =~ s/ /&nbsp;/g;
+
   my $date_format = $date_formats{$format};
 
   my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
   my $date_format = $date_formats{$format};
 
   my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
@@ -2337,7 +2390,7 @@ sub print_generic {
     #invoice info
     'invnum'          => $self->invnum,
     'date'            => time2str($date_format, $self->_date),
     #invoice info
     'invnum'          => $self->invnum,
     'date'            => time2str($date_format, $self->_date),
-    'today'           => time2str('%b %o, %Y', $today),
+    'today'           => time2str($date_format_long, $today),
     'terms'           => $self->terms,
     'template'        => $template, #params{'template'},
     'notice_name'     => ($params{'notice_name'} || 'Invoice'),#escape_function?
     'terms'           => $self->terms,
     'template'        => $template, #params{'template'},
     'notice_name'     => ($params{'notice_name'} || 'Invoice'),#escape_function?
@@ -2559,7 +2612,7 @@ sub print_generic {
   my $extra_lines = ();
   if ( $multisection ) {
     ($extra_sections, $extra_lines) =
   my $extra_lines = ();
   if ( $multisection ) {
     ($extra_sections, $extra_lines) =
-      $self->_items_extra_usage_sections($escape_function, $format)
+      $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
 
     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
 
     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
@@ -2568,13 +2621,13 @@ sub print_generic {
     push @sections,
       $self->_items_sections( $late_sections,      # this could stand a refactor
                               $summarypage,
     push @sections,
       $self->_items_sections( $late_sections,      # this could stand a refactor
                               $summarypage,
-                              $escape_function,
+                              $escape_function_nonbsp,
                               $extra_sections,
                               $format,             #bah
                             );
     if ($conf->exists('svc_phone_sections')) {
       my ($phone_sections, $phone_lines) =
                               $extra_sections,
                               $format,             #bah
                             );
     if ($conf->exists('svc_phone_sections')) {
       my ($phone_sections, $phone_lines) =
-        $self->_items_svc_phone_sections($escape_function, $format);
+        $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
       push @{$late_sections}, @$phone_sections;
       push @detail_items, @$phone_lines;
     }
       push @{$late_sections}, @$phone_sections;
       push @detail_items, @$phone_lines;
     }
@@ -2619,6 +2672,16 @@ sub print_generic {
                  $money_char. sprintf("%10.2f", $pr_total) ];
     push @buf, ['',''];
   }
                  $money_char. sprintf("%10.2f", $pr_total) ];
     push @buf, ['',''];
   }
+  if ( $conf->exists('svc_phone-did-summary') ) {
+      my ($didsummary,$minutes) = $self->_did_summary;
+      my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
+      push @detail_items, 
+       { 'description' => $didsummary_desc,
+           'ext_description' => [ $didsummary, $minutes ],
+       }
+       if !$multisection;
+  }
 
   foreach my $section (@sections, @$late_sections) {
 
 
   foreach my $section (@sections, @$late_sections) {
 
@@ -3041,7 +3104,6 @@ sub print_ps {
 
   my ($file, $lfile) = $self->print_latex(@_);
   my $ps = generate_ps($file);
 
   my ($file, $lfile) = $self->print_latex(@_);
   my $ps = generate_ps($file);
-  unlink($file.'.tex');
   unlink($lfile);
 
   $ps;
   unlink($lfile);
 
   $ps;
@@ -3070,7 +3132,6 @@ sub print_pdf {
 
   my ($file, $lfile) = $self->print_latex(@_);
   my $pdf = generate_pdf($file);
 
   my ($file, $lfile) = $self->print_latex(@_);
   my $pdf = generate_pdf($file);
-  unlink($file.'.tex');
   unlink($lfile);
 
   $pdf;
   unlink($lfile);
 
   $pdf;
@@ -3126,6 +3187,18 @@ sub _latex_escape {
   $value;
 }
 
   $value;
 }
 
+sub _html_escape {
+  my $value = shift;
+  encode_entities($value);
+  $value;
+}
+
+sub _html_escape_nbsp {
+  my $value = _html_escape(shift);
+  $value =~ s/ +/&nbsp;/g;
+  $value;
+}
+
 #utility methods for print_*
 
 sub _translate_old_latex_format {
 #utility methods for print_*
 
 sub _translate_old_latex_format {
@@ -3742,6 +3815,72 @@ sub _items_extra_usage_sections {
 
 }
 
 
 }
 
+sub _did_summary {
+    my $self = shift;
+    my $end = $self->_date;
+    my $start = $end - 2592000; # 30 days
+    my $cust_main = $self->cust_main;
+    my @pkgs = $cust_main->all_pkgs;
+    my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
+       = (0,0,0,0,0);
+    my @seen = ();
+    foreach my $pkg ( @pkgs ) {
+       my @h_cust_svc = $pkg->h_cust_svc($end);
+       foreach my $h_cust_svc ( @h_cust_svc ) {
+           next if grep {$_ eq $h_cust_svc->svcnum} @seen;
+           next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
+
+           my $inserted = $h_cust_svc->date_inserted;
+           my $deleted = $h_cust_svc->date_deleted;
+           my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
+           my $phone_deleted;
+           $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
+           
+# DID either activated or ported in; cannot be both for same DID simultaneously
+           if ($inserted >= $start && $inserted <= $end && $phone_inserted
+               && (!$phone_inserted->lnp_status 
+                   || $phone_inserted->lnp_status eq ''
+                   || $phone_inserted->lnp_status eq 'native')) {
+               $num_activated++;
+           }
+           else { # this one not so clean, should probably move to (h_)svc_phone
+                my $phone_portedin = qsearchs( 'h_svc_phone',
+                     { 'svcnum' => $h_cust_svc->svcnum, 
+                       'lnp_status' => 'portedin' },  
+                     FS::h_svc_phone->sql_h_searchs($end),  
+                   );
+                $num_portedin++ if $phone_portedin;
+           }
+
+# DID either deactivated or ported out;        cannot be both for same DID simultaneously
+           if($deleted >= $start && $deleted <= $end && $phone_deleted
+               && (!$phone_deleted->lnp_status 
+                   || $phone_deleted->lnp_status ne 'portingout')) {
+               $num_deactivated++;
+           } 
+           elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
+               && $phone_deleted->lnp_status 
+               && $phone_deleted->lnp_status eq 'portingout') {
+               $num_portedout++;
+           }
+
+           # increment usage minutes
+           my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
+           foreach my $cdr ( @cdrs ) {
+               $minutes += $cdr->billsec/60;
+           }
+
+           # don't look at this service again
+           push @seen, $h_cust_svc->svcnum;
+       }
+    }
+
+    $minutes = sprintf("%d", $minutes);
+    ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
+       . "$num_deactivated  Ported-Out: $num_portedout ",
+           "Total Minutes: $minutes");
+}
+
 sub _items_svc_phone_sections {
   my $self = shift;
   my $escape = shift;
 sub _items_svc_phone_sections {
   my $self = shift;
   my $escape = shift;
@@ -3996,19 +4135,23 @@ sub _items_cust_bill_pkg {
   my $summary_page = $opt{summary_page} || '';
   my $multilocation = $opt{multilocation} || '';
   my $multisection = $opt{multisection} || '';
   my $summary_page = $opt{summary_page} || '';
   my $multilocation = $opt{multilocation} || '';
   my $multisection = $opt{multisection} || '';
+  my $discount_show_always = 0;
 
   my @b = ();
   my ($s, $r, $u) = ( undef, undef, undef );
   foreach my $cust_bill_pkg ( @$cust_bill_pkg )
   {
 
 
   my @b = ();
   my ($s, $r, $u) = ( undef, undef, undef );
   foreach my $cust_bill_pkg ( @$cust_bill_pkg )
   {
 
+    $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
+                               && $conf->exists('discount-show-always'));
+
     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
       if ( $_ && !$cust_bill_pkg->hidden ) {
         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
         $_->{amount}      =~ s/^\-0\.00$/0.00/;
         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
         push @b, { %$_ }
     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
       if ( $_ && !$cust_bill_pkg->hidden ) {
         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
         $_->{amount}      =~ s/^\-0\.00$/0.00/;
         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
         push @b, { %$_ }
-          unless $_->{amount} == 0;
+          unless ( $_->{amount} == 0 && !$discount_show_always );
         $_ = undef;
       }
     }
         $_ = undef;
       }
     }
@@ -4047,15 +4190,20 @@ sub _items_cust_bill_pkg {
           unless ( $cust_pkg->part_pkg->hide_svc_detail
                 || $cust_bill_pkg->hidden )
           {
           unless ( $cust_pkg->part_pkg->hide_svc_detail
                 || $cust_bill_pkg->hidden )
           {
+
             push @d, map &{$escape_function}($_),
             push @d, map &{$escape_function}($_),
-                         $cust_pkg->h_labels_short($self->_date);
+                         $cust_pkg->h_labels_short($self->_date, undef, 'I')
+              unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
+
             if ( $multilocation ) {
               my $loc = $cust_pkg->location_label;
             if ( $multilocation ) {
               my $loc = $cust_pkg->location_label;
-              $loc = substr($desc, 0, 50). '...'
+              $loc = substr($loc, 0, 50). '...'
                 if $format eq 'latex' && length($loc) > 50;
               push @d, &{$escape_function}($loc);
             }
                 if $format eq 'latex' && length($loc) > 50;
               push @d, &{$escape_function}($loc);
             }
+
           }
           }
+
           push @d, $cust_bill_pkg->details(%details_opt)
             if $cust_bill_pkg->recur == 0;
 
           push @d, $cust_bill_pkg->details(%details_opt)
             if $cust_bill_pkg->recur == 0;
 
@@ -4077,7 +4225,8 @@ sub _items_cust_bill_pkg {
 
         }
 
 
         }
 
-        if ( ( $cust_bill_pkg->recur != 0  || $cust_bill_pkg->setup == 0 ) &&
+        if ( ( $cust_bill_pkg->recur != 0  || $cust_bill_pkg->setup == 0 || 
+               ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
              ( !$type || $type eq 'R' || $type eq 'U' )
            )
         {
              ( !$type || $type eq 'R' || $type eq 'U' )
            )
         {
@@ -4098,23 +4247,27 @@ sub _items_cust_bill_pkg {
           my @dates = ( $self->_date );
           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
           push @dates, $prev->sdate if $prev;
           my @dates = ( $self->_date );
           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
           push @dates, $prev->sdate if $prev;
+          push @dates, undef if !$prev;
 
           unless ( $cust_pkg->part_pkg->hide_svc_detail
                 || $cust_bill_pkg->itemdesc
                 || $cust_bill_pkg->hidden
                 || $is_summary && $type && $type eq 'U' )
           {
 
           unless ( $cust_pkg->part_pkg->hide_svc_detail
                 || $cust_bill_pkg->itemdesc
                 || $cust_bill_pkg->hidden
                 || $is_summary && $type && $type eq 'U' )
           {
+
             push @d, map &{$escape_function}($_),
             push @d, map &{$escape_function}($_),
-                         $cust_pkg->h_labels_short(@dates)
+                         $cust_pkg->h_labels_short(@dates, 'I')
                                                    #$cust_bill_pkg->edate,
                                                    #$cust_bill_pkg->sdate)
                                                    #$cust_bill_pkg->edate,
                                                    #$cust_bill_pkg->sdate)
-            ;
+              unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
+
             if ( $multilocation ) {
               my $loc = $cust_pkg->location_label;
             if ( $multilocation ) {
               my $loc = $cust_pkg->location_label;
-              $loc = substr($desc, 0, 50). '...'
+              $loc = substr($loc, 0, 50). '...'
                 if $format eq 'latex' && length($loc) > 50;
               push @d, &{$escape_function}($loc);
             }
                 if $format eq 'latex' && length($loc) > 50;
               push @d, &{$escape_function}($loc);
             }
+
           }
 
           push @d, $cust_bill_pkg->details(%details_opt)
           }
 
           push @d, $cust_bill_pkg->details(%details_opt)
@@ -4198,7 +4351,7 @@ sub _items_cust_bill_pkg {
       $_->{amount}      =~ s/^\-0\.00$/0.00/;
       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
       push @b, { %$_ }
       $_->{amount}      =~ s/^\-0\.00$/0.00/;
       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
       push @b, { %$_ }
-        unless $_->{amount} == 0;
+           unless ( $_->{amount} == 0 && !$discount_show_always );
     }
   }
 
     }
   }