add northern cyprus, RT#39335
[freeside.git] / FS / FS / Template_Mixin.pm
index 2479ef6..65a8484 100644 (file)
@@ -13,7 +13,6 @@ use Date::Language;
 use Text::Template 1.20;
 use File::Temp 0.14;
 use HTML::Entities;
-use Locale::Country;
 use Cwd;
 use FS::UID;
 use FS::Misc qw( send_email );
@@ -648,7 +647,7 @@ sub print_generic {
   if ( $cust_main->country eq $countrydefault ) {
     $invoice_data{'country'} = '';
   } else {
-    $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
+    $invoice_data{'country'} = &$escape_function($cust_main->bill_country_full);
   }
 
   my @address = ();
@@ -684,7 +683,12 @@ sub print_generic {
   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 $balance_due = $self->owed + $pr_total;
+  my $balance_due = $self->owed;
+  if ( $self->enable_previous ) {
+    $balance_due += $pr_total;
+  }
+  # otherwise the previous balance is not shown, so including it in the
+  # balance due is just confusing
 
   # the sum of amount owed on all invoices
   # (this is used in the summary & on the payment coupon)
@@ -707,6 +711,8 @@ sub print_generic {
       # "balance_date_range" unfortunately is unsuitable for this, since it
       # cares about application dates.  We want to know the sum of all 
       # _top-level transactions_ dated before the last invoice.
+      #
+      # still do this for the "Previous Balance" line of the summary block
       my @sql =
         map "$_ WHERE _date <= ? AND custnum = ?", (
           "SELECT      COALESCE( SUM(charged), 0 ) FROM cust_bill",
@@ -739,19 +745,31 @@ sub print_generic {
       # longer stored in the database)
       $invoice_data{'true_previous_balance'} = $last_bill_balance;
 
-      # the change in balance from immediately after that invoice
-      # to immediately before this one
-      my $before_this_bill_balance = 0;
+      # Now, get all applications of credits/payments dated on or after the
+      # previous bill, to invoices before the current bill. (The
+      # credit/payment date restriction prevents these from intersecting
+      # the "Previous Balance" set.)
+      # These are "adjustments". The past due balance will be shown as
+      # Previous Balance - Adjustments.
+      my $adjustments = 0;
+      @sql = map {
+        "SELECT COALESCE(SUM(y.amount),0) FROM $_ JOIN cust_bill USING (invnum)
+         WHERE cust_bill._date < ?
+           AND x._date >= ?
+           AND cust_bill.custnum = ?"
+        } "cust_credit AS x JOIN cust_credit_bill y USING (crednum)",
+          "cust_pay    AS x JOIN cust_bill_pay    y USING (paynum)"
+      ;
       foreach (@sql) {
         my $delta = FS::Record->scalar_sql(
           $_,
-          $self->_date - 1,
+          $self->_date,
+          $last_bill->_date,
           $self->custnum,
         );
-        $before_this_bill_balance += $delta;
+        $adjustments += $delta;
       }
-      $invoice_data{'balance_adjustments'} =
-        sprintf("%.2f", $last_bill_balance - $before_this_bill_balance);
+      $invoice_data{'balance_adjustments'} = sprintf("%.2f", $adjustments);
 
       warn sprintf("BALANCE ADJUSTMENTS: %.2f\n\n",
                    $invoice_data{'balance_adjustments'}
@@ -817,35 +835,36 @@ sub print_generic {
   my @include = ( [ $tc,        'notes' ],
                   [ 'invoice_', 'footer' ],
                   [ 'invoice_', 'smallfooter', ],
+                  [ 'invoice_', 'watermark' ],
                 );
   push @include, [ $tc,        'coupon', ]
     unless $params{'no_coupon'};
 
   foreach my $i (@include) {
 
+    # load the configuration for this sub-template
+
     my($base, $include) = @$i;
 
     my $inc_file = $conf->key_orbase("$base$format$include", $template);
-    my @inc_src;
-
-    if ( $conf->exists($inc_file, $agentnum)
-         && length( $conf->config($inc_file, $agentnum) ) ) {
-
-      @inc_src = $conf->config($inc_file, $agentnum);
-
-    } else {
-
-      $inc_file = $conf->key_orbase("${base}latex$include", $template);
 
-      my $convert_map = $convert_maps{$format}{$include};
-
-      @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
-                       s/--\@\]/$delimiters{$format}[1]/g;
-                       $_;
-                     } 
-                 &$convert_map( $conf->config($inc_file, $agentnum) );
+    my @inc_src = $conf->config($inc_file, $agentnum);
+    if (!@inc_src) {
+      my $converter = $convert_maps{$format}{$include};
+      if ( $converter ) {
+        # then attempt to convert LaTeX to the requested format
+        $inc_file = $conf->key_orbase($base.'latex'.$include, $template);
+        @inc_src = &$converter( $conf->config($inc_file, $agentnum) );
+        foreach (@inc_src) {
+          # this isn't included in the convert_maps
+          my ($open, $close) = @{ $delimiters{$format} };
+          s/\[\@--/$open/g;
+          s/--\@\]/$close/g;
+        }
+      }
+    } # else @inc_src is empty and that's fine
 
-    }
+    # make a Text::Template out of it
 
     my $inc_tt = new Text::Template (
       TYPE       => 'ARRAY',
@@ -859,6 +878,8 @@ sub print_generic {
       die $error;
     }
 
+    # fill in variables
+
     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
 
     $invoice_data{$include} =~ s/\n+$//
@@ -906,7 +927,8 @@ sub print_generic {
     if $DEBUG > 1;
 
   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
-  my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) ||
+  my $multisection = $self->has_sections;
+  $conf->exists($tc.'sections', $cust_main->agentnum) ||
                      $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
   $invoice_data{'multisection'} = $multisection;
   my $late_sections;
@@ -1215,7 +1237,6 @@ sub print_generic {
     List::Util::first { $_->{description} eq $tax_description } @sections;
   if (!$tax_section) {
     $tax_section = { 'description' => $tax_description };
-    push @sections, $tax_section if $multisection;
   }
   $tax_section->{tax_section} = 1; # mark this section as containing taxes
   # if this is an existing tax section, we're merging the tax items into it.
@@ -1231,6 +1252,8 @@ sub print_generic {
   #$tax_section->{'sort_weight'} = $tax_weight;
 
   my @items_tax = $self->_items_tax;
+  push @sections, $tax_section if $multisection and @items_tax > 0;
+
   foreach my $tax ( @items_tax ) {
 
     $taxtotal += $tax->{'amount'};
@@ -1273,11 +1296,17 @@ sub print_generic {
 
     if ( $multisection ) {
       if ( $taxtotal > 0 ) {
+        # there are taxes, so prepare the section to be displayed.
+        # $taxtotal already includes any line items that were already in the
+        # section (fees, taxes that are charged as packages for some reason).
+        # also set 'summarized' to false so that this isn't a summary-only
+        # section.
         $tax_section->{'subtotal'} = $other_money_char.
                                      sprintf('%.2f', $taxtotal);
         $tax_section->{'pretotal'} = 'New charges sub-total '.
                                      $total->{'total_amount'};
         $tax_section->{'description'} = $self->mt($tax_description);
+        $tax_section->{'summarized'} = '';
 
         # append it if it's not already there
         if ( !grep $tax_section, @sections ) {
@@ -1292,15 +1321,6 @@ sub print_generic {
   }
   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
 
-  push @buf,['','-----------'];
-  push @buf,[$self->mt( 
-              (!$self->enable_previous)
-               ? 'Total Charges'
-               : 'Total New Charges'
-             ),
-             $money_char. sprintf("%10.2f",$self->charged) ];
-  push @buf,['',''];
-
   ###
   # Totals
   ###
@@ -1312,50 +1332,37 @@ sub print_generic {
   );
   my $embolden_function = $embolden_functions{$format};
 
-  if ( $self->can('_items_total') ) { # quotations
-
-    my @new_total_items = $self->_items_total;
+  if ( $multisection ) {
 
-    foreach ( @new_total_items ) {
-      $_->{'total_item'}   = &$embolden_function( $_->{'total_item'} );
-      $_->{'total_amount'} = &$embolden_function( $other_money_char.$_->{'total_amount'});
-      push @total_items, $_;
+    if ( $adjust_section->{'sort_weight'} ) {
+      $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
+        $other_money_char.  sprintf("%.2f", ($self->billing_balance || 0) );
+    } else{
+      $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
+        $other_money_char.  sprintf('%.2f', $self->charged );
     }
 
-  } else { #normal invoice case
+  }
+  
+  if ( $self->can('_items_total') ) { # should always be true now
 
-    # calculate total, possibly including total owed on previous
-    # invoices
-    my $total = {};
-    my $item = 'Total';
-    $item = $conf->config('previous_balance-exclude_from_total')
-         || 'Total New Charges'
-      if $conf->exists('previous_balance-exclude_from_total');
-    my $amount = $self->charged;
-    if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
-      $amount += $pr_total;
-    }
+    # even for multisection, need plain text version
+
+    my @new_total_items = $self->_items_total;
 
-    $total->{'total_item'} = &$embolden_function($self->mt($item));
-    $total->{'total_amount'} =
-      &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
-    if ( $multisection ) {
-      if ( $adjust_section->{'sort_weight'} ) {
-        $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
-          $other_money_char.  sprintf("%.2f", ($self->billing_balance || 0) );
-      } else {
-        $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
-          $other_money_char.  sprintf('%.2f', $self->charged );
-      } 
-    } else {
-      push @total_items, $total;
-    }
     push @buf,['','-----------'];
-    push @buf,[$item,
-               $money_char.
-               sprintf( '%10.2f', $amount )
-              ];
-    push @buf,['',''];
+
+    foreach ( @new_total_items ) {
+      my ($item, $amount) = ($_->{'total_item'}, $_->{'total_amount'});
+      $_->{'total_item'}   = &$embolden_function( $item );
+      $_->{'total_amount'} = &$embolden_function( $other_money_char.$amount );
+      # but if it's multisection, don't append to @total_items. the adjust
+      # section has all this stuff
+      push @total_items, $_ if !$multisection;
+      push @buf, [ $item, $money_char.sprintf('%10.2f',$amount) ];
+    }
+
+    push @buf, [ '', '' ];
 
     # if we're showing previous invoices, also show previous
     # credits and payments 
@@ -1363,7 +1370,6 @@ sub print_generic {
           and $self->can('_items_credits')
           and $self->can('_items_payments') )
       {
-      #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
     
       # credits
       my $credittotal = 0;
@@ -1455,7 +1461,7 @@ sub print_generic {
         if ( $multisection && !$adjust_section->{sort_weight} ) {
           $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
                                            $total->{'total_amount'};
-        }else{
+        } else {
           push @total_items, $total;
         }
         push @buf,['','-----------'];
@@ -1531,7 +1537,7 @@ sub print_generic {
   # usage subtotals
   if ( $conf->exists('usage_class_summary')
        and $self->can('_items_usage_class_summary') ) {
-    my @usage_subtotals = $self->_items_usage_class_summary(escape => $escape_function);
+    my @usage_subtotals = $self->_items_usage_class_summary(escape => $escape_function, 'money_char' => $other_money_char);
     if ( @usage_subtotals ) {
       unshift @sections, $usage_subtotals[0]->{section}; # do not summarize
       unshift @detail_items, @usage_subtotals;
@@ -1670,7 +1676,10 @@ sub print_generic {
 
 sub notice_name { '('.shift->table.')'; }
 
-sub template_conf { 'invoice_'; }
+# this is not supposed to happen
+sub template_conf { warn "bare FS::Template_Mixin::template_conf";
+  'invoice_';
+}
 
 # helper routine for generating date ranges
 sub _prior_month30s {
@@ -1921,12 +1930,18 @@ sub due_date2str {
 sub balance_due_msg {
   my $self = shift;
   my $msg = $self->mt('Balance Due');
-  return $msg unless $self->terms;
-  if ( $self->due_date ) {
-    $msg .= ' - ' . $self->mt('Please pay by'). ' '.
-      $self->due_date2str('short');
-  } elsif ( $self->terms ) {
-    $msg .= ' - '. $self->terms;
+  return $msg unless $self->terms; # huh?
+  if ( !$self->conf->exists('invoice_show_prior_due_date')
+       or $self->conf->exists('invoice_sections') ) {
+    # if enabled, the due date is shown with Total New Charges (see 
+    # _items_total) and not here
+    # (yes, or if invoice_sections is enabled; this is just for compatibility)
+    if ( $self->due_date ) {
+      $msg .= ' - ' . $self->mt('Please pay by'). ' '.
+        $self->due_date2str('short');
+    } elsif ( $self->terms ) {
+      $msg .= ' - '. $self->mt($self->terms);
+    }
   }
   $msg;
 }
@@ -2110,11 +2125,20 @@ sub generate_email {
 
   if (!@text) {
 
-    warn "$me generating plain text invoice"
-      if $DEBUG;
+    if ( $conf->config($tc.'template') ) {
 
-    # 'print_text' argument is no longer used
-    @text = $self->print_text(\%args);
+      warn "$me generating plain text invoice"
+        if $DEBUG;
+
+      # 'print_text' argument is no longer used
+      @text = $self->print_text(\%args);
+
+    } else {
+
+      warn "$me no plain text version exists; sending empty message body"
+        if $DEBUG;
+
+    }
 
   }
 
@@ -2496,7 +2520,6 @@ sub _items_sections {
       foreach my $sectionname (keys %{ $s->{$locationnum} }) {
         my $section = {
                         'subtotal'    => $s->{$locationnum}{$sectionname},
-                        'post_total'  => $post_total,
                         'sort_weight' => 0,
                       };
         if ( $locationnum ) {
@@ -2976,9 +2999,6 @@ location (whichever is defined).
 multisection: a flag indicating that this is a multisection invoice,
 which does something complicated.
 
-preref_callback: coderef run for each line item, code should return HTML to be
-displayed before that line item (quotations only)
-
 Returns a list of hashrefs, each of which may contain:
 
 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and 
@@ -3005,7 +3025,9 @@ sub _items_cust_bill_pkg {
   }
   my $summary_page = $opt{summary_page} || ''; #unused
   my $multisection = defined($category) || defined($locationnum);
-  my $discount_show_always = 0;
+  # this variable is the value of the config setting, not whether it applies
+  # to this particular line item.
+  my $discount_show_always = $conf->exists('discount-show-always');
 
   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
 
@@ -3038,6 +3060,9 @@ sub _items_cust_bill_pkg {
     # if the current line item is waiting to go out, and the one we're about
     # to start is not bundled, then push out the current one and start a new
     # one.
+    if ( $d ) {
+      $d->{amount} = $d->{setup_amount} + $d->{recur_amount};
+    }
     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
       if ( $_ && !$cust_bill_pkg->hidden ) {
         $_->{amount}      = sprintf( "%.2f", $_->{amount} );
@@ -3045,11 +3070,13 @@ sub _items_cust_bill_pkg {
         if (exists($_->{unit_amount})) {
           $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
         }
-        push @b, { %$_ }
-          if $_->{amount} != 0
-          || $discount_show_always
-          || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
-          || (   $_->{_is_setup} && $_->{setup_show_zero} )
+        push @b, { %$_ };
+        # we already decided to create this display line; don't reconsider it
+        # now.
+        #  if $_->{amount} != 0
+        #  || $discount_show_always
+        #  || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
+        #  || (   $_->{_is_setup} && $_->{setup_show_zero} )
         ;
         $_ = undef;
       }
@@ -3108,51 +3135,7 @@ sub _items_cust_bill_pkg {
                           'no_usage'        => $opt{'no_usage'},
                         );
 
-      if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
-        # XXX this should be pulled out into quotation_pkg
-
-        warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
-          if $DEBUG > 1;
-        # quotation_pkgs are never fees, so don't worry about the case where
-        # part_pkg is undefined
-
-        # and I guess they're never bundled either?
-        if ( $cust_bill_pkg->setup != 0 ) {
-          my $description = $desc;
-          $description .= ' Setup'
-            if $cust_bill_pkg->recur != 0
-            || $discount_show_always
-            || $cust_bill_pkg->recur_show_zero;
-          #push @b, {
-          # keep it consistent, please
-          $s = {
-            'pkgnum'      => $cust_bill_pkg->pkgpart, #so it displays in Ref
-            'description' => $description,
-            'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
-            'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitsetup),
-            'quantity'    => $cust_bill_pkg->quantity,
-            'preref_html' => ( $opt{preref_callback}
-                                 ? &{ $opt{preref_callback} }( $cust_bill_pkg )
-                                 : ''
-                             ),
-          };
-        }
-        if ( $cust_bill_pkg->recur != 0 ) {
-          #push @b, {
-          $r = {
-            'pkgnum'      => $cust_bill_pkg->pkgpart, #so it displays in Ref
-            'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
-            'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
-            'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitrecur),
-            'quantity'    => $cust_bill_pkg->quantity,
-           'preref_html'  => ( $opt{preref_callback}
-                                 ? &{ $opt{preref_callback} }( $cust_bill_pkg )
-                                 : ''
-                             ),
-          };
-        }
-
-      } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
+      if ( $cust_bill_pkg->pkgnum > 0 ) {
         # a "normal" package line item (not a quotation, not a fee, not a tax)
 
         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
@@ -3176,6 +3159,7 @@ sub _items_cust_bill_pkg {
         if (    (!$type || $type eq 'S')
              && (    $cust_bill_pkg->setup != 0
                   || $cust_bill_pkg->setup_show_zero
+                  || ($discount_show_always and $cust_bill_pkg->unitsetup > 0)
                 )
            )
          {
@@ -3183,10 +3167,12 @@ sub _items_cust_bill_pkg {
           warn "$me _items_cust_bill_pkg adding setup\n"
             if $DEBUG > 1;
 
+          # append the word 'Setup' to the setup line if there's going to be
+          # a recur line for the same package (i.e. not a one-time charge) 
           my $description = $desc;
           $description .= ' Setup'
             if $cust_bill_pkg->recur != 0
-            || $discount_show_always
+            || ($discount_show_always and $cust_bill_pkg->unitrecur > 0)
             || $cust_bill_pkg->recur_show_zero;
 
           $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
@@ -3250,11 +3236,18 @@ sub _items_cust_bill_pkg {
 
         }
 
+        # should we show a recur line?
+        # if type eq 'S', then NO, because we've been told not to.
+        # otherwise, show the recur line if:
+        # - there's a recurring charge
+        # - or recur_show_zero is on
+        # - or there's a positive unitrecur (so it's been discounted to zero)
+        #   and discount-show-always is on
         if (    ( !$type || $type eq 'R' || $type eq 'U' )
              && (
                      $cust_bill_pkg->recur != 0
-                  || $cust_bill_pkg->setup == 0
-                  || $discount_show_always
+                  || !defined($s)
+                  || ($discount_show_always and $cust_bill_pkg->unitrecur > 0)
                   || $cust_bill_pkg->recur_show_zero
                 )
            )
@@ -3442,7 +3435,7 @@ sub _items_cust_bill_pkg {
                                            + $cust_bill_pkg->recur)
         };
 
-      } # if quotation / package line item / other line item
+      } # if package line item / other line item
 
       # decide whether to show active discounts here
       if (
@@ -3461,7 +3454,8 @@ sub _items_cust_bill_pkg {
           # $item_discount->{amount} is negative
 
           if ( $d and $cust_bill_pkg->hidden ) {
-            $d->{amount}      += $item_discount->{amount};
+            $d->{setup_amount} += $item_discount->{setup_amount};
+            $d->{recur_amount} += $item_discount->{recur_amount};
           } else {
             $d = $item_discount;
             $_ = &{$escape_function}($_) foreach @{ $d->{ext_description} };
@@ -3469,36 +3463,20 @@ sub _items_cust_bill_pkg {
 
           # update the active line (before the discount) to show the 
           # original price (whether this is a hidden line or not)
-          #
-          # quotation discounts keep track of setup and recur; invoice 
-          # discounts currently don't
-          if ( exists $item_discount->{setup_amount} ) {
 
-            $s->{amount} -= $item_discount->{setup_amount} if $s;
-            $r->{amount} -= $item_discount->{recur_amount} if $r;
-
-          } else {
-
-            # $active_line is the line item hashref for the line that will
-            # show the original price
-            # (use the recur or single line for the package, unless we're 
-            # showing a setup line for a package with no recurring fee)
-            my $active_line = $r;
-            if ( $type eq 'S' ) {
-              $active_line = $s;
-            }
-            $active_line->{amount} -= $item_discount->{amount};
-
-          }
+          $s->{amount} -= $item_discount->{setup_amount} if $s;
+          $r->{amount} -= $item_discount->{recur_amount} if $r;
 
         } # if there are any discounts
       } # if this is an appropriate place to show discounts
 
     } # foreach $display
 
-    $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
-                                && $conf->exists('discount-show-always'));
+  }
 
+  # discount amount is internally split up
+  if ( $d ) {
+    $d->{amount} = $d->{setup_amount} + $d->{recur_amount};
   }
 
   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
@@ -3510,11 +3488,11 @@ sub _items_cust_bill_pkg {
         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
       }
 
-      push @b, { %$_ }
-        if $_->{amount} != 0
-        || $discount_show_always
-        || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
-        || (   $_->{_is_setup} && $_->{setup_show_zero} )
+      push @b, { %$_ };
+      #if $_->{amount} != 0
+      #  || $discount_show_always
+      #  || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
+      #  || (   $_->{_is_setup} && $_->{setup_show_zero} )
     }
   }