Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / FS / FS / Template_Mixin.pm
index 95d001e..37dcf2a 100644 (file)
@@ -7,7 +7,7 @@ use vars qw( $DEBUG $me
            );
              # but NOT $conf
 use vars qw( $invoice_lines @buf ); #yuck
-use List::Util qw(sum first);
+use List::Util qw(sum); #can't import first, it conflicts with cust_main.first
 use Date::Format;
 use Date::Language;
 use Text::Template 1.20;
@@ -316,9 +316,6 @@ sub print_generic {
     unless $format =~ /^(latex|html|template)$/;
 
   my $cust_main = $self->cust_main || $self->prospect_main;
-  $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
-    unless $cust_main->payname
-        && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
 
   my $locale = $params{'locale'} || $cust_main->locale;
 
@@ -565,12 +562,14 @@ sub print_generic {
     'custnum'         => $cust_main->display_custnum,
     'prospectnum'     => $cust_main->prospectnum,
     'agent_custid'    => &$escape_function($cust_main->agent_custid),
-    ( map { $_ => &$escape_function($cust_main->$_()) } qw(
-      payname company address1 address2 city state zip fax
-    )),
+    ( map { $_ => &$escape_function($cust_main->$_()) }
+        qw( company address1 address2 city state zip fax )
+    ),
+    'payname'         => &$escape_function( $cust_main->invoice_attn
+                                             || $cust_main->contact_firstlast ),
 
     #global config
-    'ship_enable'     => $conf->exists('invoice-ship_address'),
+    'ship_enable'     => $cust_main->invoice_ship_address || $conf->exists('invoice-ship_address'),
     'unitprices'      => $conf->exists('invoice-unitprice'),
     'smallernotes'    => $conf->exists('invoice-smallernotes'),
     'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
@@ -655,10 +654,10 @@ sub print_generic {
   my @address = ();
   $invoice_data{'address'} = \@address;
   push @address,
-    $cust_main->payname.
-      ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
-        ? " (P.O. #". $cust_main->payinfo. ")"
-        : ''
+    $invoice_data{'payname'}.
+      ( $cust_main->po_number
+          ? " (P.O. #". $cust_main->po_number. ")"
+          : ''
       )
   ;
   push @address, $cust_main->company
@@ -698,6 +697,10 @@ sub print_generic {
     # XXX should be an FS::cust_bill method to set the defaults, instead
     # of checking the type here
 
+    # info from customer's last invoice before this one, for some 
+    # summary formats
+    $invoice_data{'last_bill'} = {};
     my $last_bill = $self->previous_bill;
     if ( $last_bill ) {
 
@@ -758,9 +761,7 @@ sub print_generic {
       # ($pr_total is used elsewhere but not as $previous_balance)
       $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
 
-      $invoice_data{'last_bill'} = {
-        '_date'     => $last_bill->_date, #unformatted
-      };
+      $invoice_data{'last_bill'}{'_date'} = $last_bill->_date; #unformatted
       my (@payments, @credits);
       # for formats that itemize previous payments
       foreach my $cust_pay ( qsearch('cust_pay', {
@@ -802,11 +803,7 @@ sub print_generic {
       $invoice_data{'previous_payments'} = [];
       $invoice_data{'previous_credits'} = [];
     }
-
-    # info from customer's last invoice before this one, for some 
-    # summary formats
-    $invoice_data{'last_bill'} = {};
-  
     if ( $conf->exists('invoice_usesummary', $agentnum) ) {
       $invoice_data{'summarypage'} = $summarypage = 1;
     }
@@ -820,35 +817,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',
@@ -862,6 +860,8 @@ sub print_generic {
       die $error;
     }
 
+    # fill in variables
+
     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
 
     $invoice_data{$include} =~ s/\n+$//
@@ -909,7 +909,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;
@@ -1214,10 +1215,10 @@ sub print_generic {
 
   # create a tax section if we don't yet have one
   my $tax_description = 'Taxes, Surcharges, and Fees';
-  my $tax_section = first { $_->{description} eq $tax_description } @sections;
+  my $tax_section =
+    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.
@@ -1233,6 +1234,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'};
@@ -1266,7 +1269,7 @@ sub print_generic {
               ];
 
   }
-  
   if ( @items_tax ) {
     my $total = {};
     $total->{'total_item'} = $self->mt('Sub-total');
@@ -1294,15 +1297,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
   ###
@@ -1314,51 +1308,37 @@ sub print_generic {
   );
   my $embolden_function = $embolden_functions{$format};
 
-  if ( $self->can('_items_total') ) { # quotations
-
-    $self->_items_total(\@total_items);
+  if ( $multisection ) {
 
-    foreach ( @total_items ) {
-      $_->{'total_item'}   = &$embolden_function( $_->{'total_item'} );
-      $_->{'total_amount'} = &$embolden_function( $other_money_char.
-                                                   $_->{'total_amount'}
-                                                );
+    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 
@@ -1366,12 +1346,11 @@ 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;
       foreach my $credit (
-        $self->_items_credits( 'template' => $template, 'trim_len' => 60 )
+        $self->_items_credits( 'template' => $template, 'trim_len' => 40 )
       ) {
 
         my $total;
@@ -1458,7 +1437,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,['','-----------'];
@@ -1544,7 +1523,7 @@ sub print_generic {
   # invoice history "section" (not really a section)
   # not to be included in any subtotals, completely independent of 
   # everything...
-  if ( $conf->exists('previous_invoice_history') ) {
+  if ( $conf->exists('previous_invoice_history') and $cust_main->isa('FS::cust_main') ) {
     my %history;
     my %monthorder;
     foreach my $cust_bill ( $cust_main->cust_bill ) {
@@ -1673,7 +1652,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 {
@@ -1924,12 +1906,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;
 }
@@ -2037,10 +2025,6 @@ sender address, required
 
 alternate template name, optional
 
-=item print_text
-
-text attachment arrayref, optional
-
 =item subject
 
 email subject, optional
@@ -2084,61 +2068,70 @@ sub generate_email {
 
   my $tc = $self->template_conf;
 
-  if ( $conf->exists($tc.'html') ) {
+  my @text; # array of lines
+  my $html; # a big string
+  my @related_parts; # will contain the text/HTML alternative, and images
+  my $related; # will contain the multipart/related object
 
-    warn "$me creating HTML/text multipart message"
-      if $DEBUG;
+  if ( $conf->exists($tc. 'email_pdf') ) {
+    if ( my $msgnum = $conf->config($tc.'email_pdf_msgnum') ) {
 
-    $return{'nobody'} = 1;
+      warn "$me using '${tc}email_pdf_msgnum' in multipart message"
+        if $DEBUG;
 
-    my $alternative = build MIME::Entity
-      'Type'        => 'multipart/alternative',
-      #'Encoding'    => '7bit',
-      'Disposition' => 'inline'
-    ;
+      my $msg_template = FS::msg_template->by_key($msgnum)
+        or die "${tc}email_pdf_msgnum $msgnum not found\n";
+      my %prepared = $msg_template->prepare(
+        cust_main => $self->cust_main,
+        object    => $self
+      );
+
+      @text = split(/(?=\n)/, $prepared{'text_body'});
+      $html = $prepared{'html_body'};
 
-    my $data = '';
-    if ( $conf->exists($tc. 'email_pdf')
-         and scalar($conf->config($tc. 'email_pdf_note')) ) {
+    } elsif ( my @note = $conf->config($tc.'email_pdf_note') ) {
 
       warn "$me using '${tc}email_pdf_note' in multipart message"
         if $DEBUG;
-      $data = [ map { $_ . "\n" }
-                    $conf->config($tc.'email_pdf_note')
-              ];
+      @text = $conf->config($tc.'email_pdf_note');
+      $html = join('<BR>', @text);
+  
+    } # else use the plain text invoice
+  }
+
+  if (!@text) {
+
+    if ( $conf->config($tc.'template') ) {
+
+      warn "$me generating plain text invoice"
+        if $DEBUG;
+
+      # 'print_text' argument is no longer used
+      @text = $self->print_text(\%args);
 
     } else {
 
-      warn "$me not using '${tc}email_pdf_note' in multipart message"
+      warn "$me no plain text version exists; sending empty message body"
         if $DEBUG;
-      if ( ref($args{'print_text'}) eq 'ARRAY' ) {
-        $data = $args{'print_text'};
-      } elsif ( $conf->exists($tc.'template') ) { #plaintext invoice_template
-        $data = [ $self->print_text(\%args) ];
-      }
 
     }
 
-    if ( $data ) {
-      $alternative->attach(
-        'Type'        => 'text/plain',
-        'Encoding'    => 'quoted-printable',
-        'Charset'     => 'UTF-8',
-        #'Encoding'    => '7bit',
-        'Data'        => $data,
-        'Disposition' => 'inline',
-      );
-    }
+  }
 
-    my $htmldata;
-    my $image = '';
-    my $barcode = '';
-    if ( $conf->exists($tc.'email_pdf')
-         and scalar($conf->config($tc.'email_pdf_note')) ) {
+  my $text_part = build MIME::Entity (
+    'Type'        => 'text/plain',
+    'Encoding'    => 'quoted-printable',
+    'Charset'     => 'UTF-8',
+    #'Encoding'    => '7bit',
+    'Data'        => \@text,
+    'Disposition' => 'inline',
+  );
 
-      $htmldata = join('<BR>', $conf->config($tc.'email_pdf_note') );
+  if (!$html) {
 
-    } else {
+    if ( $conf->exists($tc.'html') ) {
+      warn "$me generating HTML invoice"
+        if $DEBUG;
 
       $args{'from'} =~ /\@([\w\.\-]+)/;
       my $from = $1 || 'example.com';
@@ -2157,7 +2150,7 @@ sub generate_email {
       }
       my $image_data = $conf->config_binary( $logo, $agentnum);
 
-      $image = build MIME::Entity
+      push @related_parts, build MIME::Entity
         'Type'       => 'image/png',
         'Encoding'   => 'base64',
         'Data'       => $image_data,
@@ -2167,7 +2160,7 @@ sub generate_email {
    
       if ( ref($self) eq 'FS::cust_bill' && $conf->exists('invoice-barcode') ) {
         my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
-        $barcode = build MIME::Entity
+        push @related_parts, build MIME::Entity
           'Type'       => 'image/png',
           'Encoding'   => 'base64',
           'Data'       => $self->invoice_barcode(0),
@@ -2177,7 +2170,26 @@ sub generate_email {
         $args{'barcode_cid'} = $barcode_content_id;
       }
 
-      $htmldata = $self->print_html({ 'cid'=>$content_id, %args });
+      $html = $self->print_html({ 'cid'=>$content_id, %args });
+    }
+
+  }
+
+  if ( $html ) {
+
+    warn "$me creating HTML/text multipart message"
+      if $DEBUG;
+
+    $return{'nobody'} = 1;
+
+    my $alternative = build MIME::Entity
+      'Type'        => 'multipart/alternative',
+      #'Encoding'    => '7bit',
+      'Disposition' => 'inline'
+    ;
+
+    if ( @text ) {
+      $alternative->add_part($text_part);
     }
 
     $alternative->attach(
@@ -2190,7 +2202,7 @@ sub generate_email {
                          '    </title>',
                          '  </head>',
                          '  <body bgcolor="#e8e8e8">',
-                         $htmldata,
+                         $html,
                          '  </body>',
                          '</html>',
                        ],
@@ -2198,104 +2210,68 @@ sub generate_email {
       #'Filename'    => 'invoice.pdf',
     );
 
+    unshift @related_parts, $alternative;
 
-    my @otherparts = ();
-    if ( ref($self) eq 'FS::cust_bill' && $cust_main->email_csv_cdr ) {
+    $related = build MIME::Entity 'Type'     => 'multipart/related',
+                                  'Encoding' => '7bit';
 
-      push @otherparts, build MIME::Entity
-        'Type'        => 'text/csv',
-        'Encoding'    => '7bit',
-        'Data'        => [ map { "$_\n" }
-                             $self->call_details('prepend_billed_number' => 1)
-                         ],
-        'Disposition' => 'attachment',
-        'Filename'    => 'usage-'. $self->invnum. '.csv',
-      ;
-
-    }
-
-    if ( $conf->exists($tc.'email_pdf') ) {
-
-      #attaching pdf too:
-      # multipart/mixed
-      #   multipart/related
-      #     multipart/alternative
-      #       text/plain
-      #       text/html
-      #     image/png
-      #   application/pdf
-
-      my $related = build MIME::Entity 'Type'     => 'multipart/related',
-                                       'Encoding' => '7bit';
-
-      #false laziness w/Misc::send_email
-      $related->head->replace('Content-type',
-        $related->mime_type.
-        '; boundary="'. $related->head->multipart_boundary. '"'.
-        '; type=multipart/alternative'
-      );
-
-      $related->add_part($alternative);
-
-      $related->add_part($image) if $image;
-
-      my $pdf = build MIME::Entity $self->mimebuild_pdf(\%args);
+    #false laziness w/Misc::send_email
+    $related->head->replace('Content-type',
+      $related->mime_type.
+      '; boundary="'. $related->head->multipart_boundary. '"'.
+      '; type=multipart/alternative'
+    );
 
-      $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
+    $related->add_part($_) foreach @related_parts;
 
-    } else {
+  }
 
-      #no other attachment:
-      # multipart/related
-      #   multipart/alternative
-      #     text/plain
-      #     text/html
-      #   image/png
+  my @otherparts = ();
+  if ( ref($self) eq 'FS::cust_bill' && $cust_main->email_csv_cdr ) {
 
-      $return{'content-type'} = 'multipart/related';
-      if ($conf->exists('invoice-barcode') && $barcode) {
-        $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
-      } else {
-        $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
-      }
-      $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
-      #$return{'disposition'} = 'inline';
+    push @otherparts, build MIME::Entity
+      'Type'        => 'text/csv',
+      'Encoding'    => '7bit',
+      'Data'        => [ map { "$_\n" }
+                           $self->call_details('prepend_billed_number' => 1)
+                       ],
+      'Disposition' => 'attachment',
+      'Filename'    => 'usage-'. $self->invnum. '.csv',
+    ;
 
-    }
-  
-  } else {
+  }
 
-    if ( $conf->exists($tc.'email_pdf') ) {
-      warn "$me creating PDF attachment"
-        if $DEBUG;
+  if ( $conf->exists($tc.'email_pdf') ) {
 
-      #mime parts arguments a la MIME::Entity->build().
-      $return{'mimeparts'} = [
-        { $self->mimebuild_pdf(\%args) }
-      ];
-    }
-  
-    if ( $conf->exists($tc.'email_pdf')
-         and scalar($conf->config($tc.'email_pdf_note')) ) {
+    #attaching pdf too:
+    # multipart/mixed
+    #   multipart/related
+    #     multipart/alternative
+    #       text/plain
+    #       text/html
+    #     image/png
+    #   application/pdf
 
-      warn "$me using '${tc}email_pdf_note'"
-        if $DEBUG;
-      $return{'body'} = [ map { $_ . "\n" }
-                              $conf->config($tc.'email_pdf_note')
-                        ];
+    my $pdf = build MIME::Entity $self->mimebuild_pdf(\%args);
+    push @otherparts, $pdf;
+  }
 
+  if (@otherparts) {
+    $return{'content-type'} = 'multipart/mixed'; # of the outer container
+    if ( $html ) {
+      $return{'mimeparts'} = [ $related, @otherparts ];
+      $return{'type'} = 'multipart/related'; # of the first part
     } else {
-
-      warn "$me not using '${tc}email_pdf_note'"
-        if $DEBUG;
-      if ( ref($args{'print_text'}) eq 'ARRAY' ) {
-        $return{'body'} = $args{'print_text'};
-      } else {
-        $return{'body'} = [ $self->print_text(\%args) ];
-      }
-
+      $return{'mimeparts'} = [ $text_part, @otherparts ];
+      $return{'type'} = 'text/plain';
     }
-
+  } elsif ( $html ) { # no PDF or CSV, strip the outer container
+    $return{'mimeparts'} = \@related_parts;
+    $return{'content-type'} = 'multipart/related';
+    $return{'type'} = 'multipart/alternative';
+  } else { # no HTML either
+    $return{'body'} = \@text;
+    $return{'content-type'} = 'text/plain';
   }
 
   %return;
@@ -3031,10 +3007,28 @@ sub _items_cust_bill_pkg {
   my $multisection = defined($category) || defined($locationnum);
   my $discount_show_always = 0;
 
-  my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
+  my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
 
   my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
-                                   # and location labels
+
+  # for location labels: use default location on the invoice date
+  my $default_locationnum;
+  if ( $self->custnum ) {
+    my $h_cust_main;
+    my @h_search = FS::h_cust_main->sql_h_search($self->_date);
+    $h_cust_main = qsearchs({
+        'table'     => 'h_cust_main',
+        'hashref'   => { custnum => $self->custnum },
+        'extra_sql' => $h_search[1],
+        'addl_from' => $h_search[3],
+    }) || $cust_main;
+    $default_locationnum = $h_cust_main->ship_locationnum;
+  } elsif ( $self->prospectnum ) {
+    my $cust_location = qsearchs('cust_location',
+      { prospectnum => $self->prospectnum,
+        disabled => '' });
+    $default_locationnum = $cust_location->locationnum if $cust_location;
+  }
 
   my @b = (); # accumulator for the line item hashes that we'll return
   my ($s, $r, $u, $d) = ( undef, undef, undef, undef );
@@ -3115,6 +3109,7 @@ sub _items_cust_bill_pkg {
                         );
 
       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;
@@ -3218,11 +3213,10 @@ sub _items_cust_bill_pkg {
 
             push @d, @svc_labels
               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
-            my $lnum = $cust_main ? $cust_main->ship_locationnum
-                                  : $self->prospect_main->locationnum;
             # show the location label if it's not the customer's default
             # location, and we're not grouping items by location already
-            if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
+            if ( $cust_pkg->locationnum != $default_locationnum
+                  and !defined($locationnum) ) {
               my $loc = $cust_pkg->location_label;
               $loc = substr($loc, 0, $maxlength). '...'
                 if $format eq 'latex' && length($loc) > $maxlength;
@@ -3320,11 +3314,10 @@ sub _items_cust_bill_pkg {
             warn "$me _items_cust_bill_pkg done adding service details\n"
               if $DEBUG > 1;
 
-            my $lnum = $cust_main ? $cust_main->ship_locationnum
-                                  : $self->prospect_main->locationnum;
             # show the location label if it's not the customer's default
             # location, and we're not grouping items by location already
-            if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
+            if ( $cust_pkg->locationnum != $default_locationnum
+                  and !defined($locationnum) ) {
               my $loc = $cust_pkg->location_label;
               $loc = substr($loc, 0, $maxlength). '...'
                 if $format eq 'latex' && length($loc) > $maxlength;