Merge branch 'master' of git.freeside.biz:/home/git/freeside
authorJonathan Prykop <jonathan@freeside.biz>
Thu, 29 Jan 2015 01:28:13 +0000 (19:28 -0600)
committerJonathan Prykop <jonathan@freeside.biz>
Thu, 29 Jan 2015 01:28:13 +0000 (19:28 -0600)
18 files changed:
FS/FS/Conf.pm
FS/FS/Report/Tax.pm
FS/FS/Schema.pm
FS/FS/Template_Mixin.pm
FS/FS/contact_email.pm
FS/FS/cust_main.pm
FS/FS/cust_main_note.pm
FS/FS/cust_pay.pm
FS/FS/discount.pm
FS/FS/prospect_main.pm
httemplate/edit/cust_main_note.cgi
httemplate/edit/part_pkg.cgi
httemplate/edit/process/bulk-svc_phone.html
httemplate/edit/process/cust_main_note.cgi
httemplate/search/report_tax.cgi
httemplate/view/cust_main/notes/notes.html
httemplate/view/cust_main/payment_history/voided_invoice.html
httemplate/view/cust_main/payment_history/voided_payment.html

index 279a4df..239e304 100644 (file)
@@ -773,7 +773,7 @@ sub reason_type_options {
   {
     'key'         => 'log_sent_mail',
     'section'     => 'notification',
-    'description' => 'Enable logging of template-generated email.',
+    'description' => 'Enable logging of all sent email.',
     'type'        => 'checkbox',
   },
 
index f3f441d..23c1645 100644 (file)
@@ -41,13 +41,9 @@ sub report_internal {
 
   my ($taxname, $country, %breakdown);
 
-  # purify taxname properly here, as we're going to include it in lots of 
-  # SQL statements using single quotes only
-  if ( $opt{taxname} =~ /^([\w\s]+)$/ ) {
-    $taxname = $1;
-  } else {
-    die "taxname required"; # UI prevents this
-  }
+  # taxname can contain arbitrary punctuation; escape it properly and 
+  # include $taxname unquoted elsewhere
+  $taxname = dbh->quote($opt{'taxname'});
 
   if ( $opt{country} =~ /^(\w\w)$/ ) {
     $country = $1;
@@ -103,7 +99,7 @@ sub report_internal {
      GROUP BY billpkgnum, taxnum";
 
   my $where = "WHERE cust_bill._date >= $beginning AND cust_bill._date <= $ending ".
-              "AND COALESCE(cust_main_county.taxname,'Tax') = '$taxname' ".
+              "AND COALESCE(cust_main_county.taxname,'Tax') = $taxname ".
               "AND cust_main_county.country = '$country'";
   # SELECT/GROUP clauses for first-level queries
   my $select = "SELECT ";
@@ -370,14 +366,14 @@ sub report_internal {
       SELECT 1 FROM cust_tax_exempt_pkg
         JOIN cust_main_county USING (taxnum)
         WHERE cust_tax_exempt_pkg.billpkgnum = cust_bill_pkg.billpkgnum
-          AND COALESCE(cust_main_county.taxname,'Tax') = '$taxname'
+          AND COALESCE(cust_main_county.taxname,'Tax') = $taxname
           AND cust_tax_exempt_pkg.creditbillpkgnum IS NULL
     )
     AND NOT EXISTS(
       SELECT 1 FROM cust_bill_pkg_tax_location
         JOIN cust_main_county USING (taxnum)
         WHERE cust_bill_pkg_tax_location.taxable_billpkgnum = cust_bill_pkg.billpkgnum
-          AND COALESCE(cust_main_county.taxname,'Tax') = '$taxname'
+          AND COALESCE(cust_main_county.taxname,'Tax') = $taxname
     )
   ";
   warn "\nOUTSIDE:\n$sql_outside\n" if $DEBUG;
index ca6d169..b7611c1 100644 (file)
@@ -2013,13 +2013,14 @@ sub tables_hashref {
 
     'cust_main_note' => {
       'columns' => [
-        'notenum',  'serial',  '',     '', '', '', 
-        'custnum',  'int',  '',     '', '', '', 
-        'classnum',      'int',     'NULL', '', '', '', 
-        '_date',    @date_type, '', '', 
-        'otaker',   'varchar', 'NULL',    32, '', '', 
-        'usernum',   'int', 'NULL', '', '', '',
-        'comments', 'text', 'NULL', '', '', '', 
+        'notenum',   'serial',    '',  '', '', '', 
+        'custnum',      'int',    '',  '', '', '', 
+        'classnum',     'int', 'NULL', '', '', '', 
+        '_date',          @date_type,      '', '', 
+        'otaker',   'varchar', 'NULL', 32, '', '', 
+        'usernum',      'int', 'NULL', '', '', '',
+        'comments',    'text', 'NULL', '', '', '', 
+        'sticky',       'int',     '', '',  0, '',
       ],
       'primary_key'  => 'notenum',
       'unique'       => [],
index 1fed7f1..9669ac2 100644 (file)
@@ -347,8 +347,8 @@ sub print_generic {
 
   if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
     #change this to a die when the old code is removed
-    # it's been almost ten years, changing it to a die on the next release.
-    warn "old-style invoice template $templatefile; ".
+    # it's been almost ten years, changing it to a die
+    die "old-style invoice template $templatefile; ".
          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
          #$old_latex = 'true';
          #@invoice_template = _translate_old_latex_format(@invoice_template);
@@ -1172,6 +1172,12 @@ sub print_generic {
            join(', ', map "$_=>".$line_item->{$_}, keys %$line_item). "\n"
         if $DEBUG > 1;
 
+      push @buf, ( [ $line_item->{'description'},
+                     $money_char. sprintf("%10.2f", $line_item->{'amount'}),
+                   ],
+                   map { [ " ". $_, '' ] } @{$line_item->{'ext_description'}},
+                 );
+
       $line_item->{'ref'} = $line_item->{'pkgnum'};
       $line_item->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; # mt()?
       $line_item->{'section'} = $section;
@@ -1184,11 +1190,6 @@ sub print_generic {
       $line_item->{'ext_description'} ||= [];
  
       push @detail_items, $line_item;
-      push @buf, ( [ $line_item->{'description'},
-                     $money_char. sprintf("%10.2f", $line_item->{'amount'}),
-                   ],
-                   map { [ " ". $_, '' ] } @{$line_item->{'ext_description'}},
-                 );
     }
 
     if ( $section->{'description'} ) {
@@ -3448,8 +3449,29 @@ sub _items_cust_bill_pkg {
                 ext_description => \@ext,
               };
               foreach my $cust_bill_pkg_discount (@discounts) {
-                my $def = $cust_bill_pkg_discount->cust_pkg_discount->discount;
-                push @ext, &{$escape_function}( $def->description );
+                my $discount = $cust_bill_pkg_discount->cust_pkg_discount->discount;
+                my $discount_desc = $discount->description_short;
+
+                if ($discount->months) {
+
+                  # calculate months remaining after this invoice
+                  my $used = FS::Record->scalar_sql(
+                    'SELECT SUM(months) FROM cust_bill_pkg_discount
+                      JOIN cust_bill_pkg USING (billpkgnum)
+                      JOIN cust_bill USING (invnum)
+                      WHERE pkgdiscountnum = ? AND _date <= ?',
+                    $cust_bill_pkg_discount->pkgdiscountnum,
+                    $self->_date
+                  );
+                  $used ||= 0;
+                  my $remaining = sprintf('%.2f', $discount->months - $used);
+                  # append "for X months (Y months remaining)"
+                  $discount_desc .= $self->mt(' for [quant,_1,month] ([quant,_2,month] remaining)',
+                    $cust_bill_pkg_discount->months,
+                    $remaining
+                  );
+                } # else it's not time-limited
+                push @ext, &{$escape_function}($discount_desc);
               }
             }
 
index a0ff273..8d6bbbf 100644 (file)
@@ -2,6 +2,7 @@ package FS::contact_email;
 use base qw( FS::Record );
 
 use strict;
+use FS::Msgcat qw( gettext );
 
 =head1 NAME
 
index e4766f5..1ed1d4a 100644 (file)
@@ -2139,8 +2139,8 @@ Returns all notes (see L<FS::cust_main_note>) for this customer.
 
 sub notes {
   my($self,$orderby_classnum) = (shift,shift);
-  my $orderby = "_DATE DESC";
-  $orderby = "CLASSNUM ASC, $orderby" if $orderby_classnum;
+  my $orderby = "sticky DESC, _date DESC";
+  $orderby = "classnum ASC, $orderby" if $orderby_classnum;
   qsearch( 'cust_main_note',
            { 'custnum' => $self->custnum },
            '',
index 7125807..ee63883 100644 (file)
@@ -112,6 +112,7 @@ sub check {
     || $self->ut_numbern('_date')
     || $self->ut_textn('otaker')
     || $self->ut_anything('comments')
+    || $self->ut_numbern('sticky')
   ;
   return $error if $error;
 
index 0020a82..e44278d 100644 (file)
@@ -945,10 +945,11 @@ sub _upgrade_data {  #class method
 
     #not the most efficient, but hey, it only has to run once
 
-    my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
-                "  AND usernum IS NULL ".
-                "  AND 0 < ( SELECT COUNT(*) FROM cust_main                 ".
-                "              WHERE cust_main.custnum = cust_pay.custnum ) ";
+    my $where = " WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' )
+                    AND usernum IS NULL
+                    AND EXISTS ( SELECT 1 FROM cust_main                    
+                                   WHERE cust_main.custnum = cust_pay.custnum )
+                ";
 
     my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
 
index 0561f9c..361e0b4 100644 (file)
@@ -173,7 +173,12 @@ sub description_short {
   my $conf = new FS::Conf;
   my $money_char = $conf->config('money_char') || '$';  
 
-  my $desc = $self->name ? $self->name.': ' : '';
+  my $desc;
+  if ( $self->name ) {
+    $desc = $self->name . ': ';
+  } else {
+    $desc = 'Discount of ';
+  }
   $desc .= $money_char. sprintf('%.2f/month', $self->amount)
     if $self->amount > 0;
 
index f30508d..b160343 100644 (file)
@@ -405,6 +405,11 @@ sub search {
 
 }
 
+# stub this so that calling ->cust_bill doesn't return an empty string
+sub cust_bill {
+  return;
+}
+
 =back
 
 =head1 BUGS
index 6159056..a089db2 100755 (executable)
     <BR>
 % }
 
-% if( $FS::CurrentUser::CurrentUser->option('disable_html_editor') ) {
-  <TEXTAREA NAME="comment_plain" ROWS="12" COLS="60"><% 
-  join '', split /<br \/>|&nbsp;/, $comment 
-  %></TEXTAREA>
-% }
-% else {
-<% include('/elements/htmlarea.html', 'field' => 'comment_html',
-                                      'curr_value' => $comment) %>
+% if ( $FS::CurrentUser::CurrentUser->option('disable_html_editor') ) {
+    <TEXTAREA NAME="comment_plain" ROWS="12" COLS="60"><% 
+    join '', split /<br \/>|&nbsp;/, $comment 
+    %></TEXTAREA>
+% } else {
+    <& /elements/htmlarea.html, 'field'      => 'comment_html',
+                                'curr_value' => $comment
+    &>
 % }
 
-<BR><BR>
+<BR>
+
+<& /elements/checkbox.html, 'field'      => 'sticky',
+                            'value'      => 1,
+                            'curr_value' => $sticky,
+&>
+Sticky note<BR><BR>
+
 <INPUT TYPE="submit" VALUE="<% $notenum ? emt("Apply changes") : emt("Add Note") %>">
 
 </FORM>
@@ -42,6 +49,7 @@ my $conf = new FS::Conf;
 my $comment;
 my $notenum = '';
 my $classnum;
+my $sticky = 0;
 if ( $cgi->param('error') ) {
   $comment     = $cgi->param('comment');
   $classnum = $cgi->param('classnum');
@@ -52,6 +60,7 @@ if ( $cgi->param('error') ) {
   die "no such note: ". $notenum unless $note;
   $comment = $note->comments;
   $classnum = $note->classnum;
+  $sticky = $note->sticky;
 }
 
 $comment =~ s/\r//g; # remove weird line breaks to protect FCKeditor
index 778a026..1702a6d 100755 (executable)
@@ -967,8 +967,17 @@ my $html_bottom = sub {
             'msg'       => q|You must set the 'suspend_credit_type' option in Configuration->Settings to gain access to this option.|,
             'are_met'   => sub{
                 my $conf = new FS::conf;
-                my @suspend_credit_type_conf = qsearch('conf', { 'name' => 'suspend_credit_type' } );
-                return 1 if (exists($suspend_credit_type_conf[0]) && $suspend_credit_type_conf[0]->{Hash}{value});
+                my @conf_info = qsearch('conf', { 'name' => 'suspend_credit_type' } );
+                return 1 if (exists($conf_info[0]) && $conf_info[0]->{Hash}{value});
+                return 0;
+            }
+        },
+        'unused_credit_cancel' => {
+            'msg'       => q|You must set the 'cancel_credit_type' option in Configuration->Settings to gain access to this option.|,
+            'are_met'   => sub{
+                my $conf = new FS::conf;
+                my @conf_info = qsearch('conf', { 'name' => 'cancel_credit_type' } );
+                return 1 if (exists($conf_info[0]) && $conf_info[0]->{Hash}{value});
                 return 0;
             }
         }
index 5a1fbc6..db486de 100644 (file)
@@ -25,7 +25,7 @@ my $num_avail = $1;
 errorpage("There are only $num_avail available")
   if $end - $start + 1 > $num_avail;
 
-foreach my $phonenum ( $start .. $end ) {
+foreach my $phonenum ( "$start" .. "$end" ) {
 
   my $svc_phone = new FS::svc_phone {
     'phonenum' => $phonenum,
index 227297e..53e616a 100755 (executable)
@@ -33,6 +33,7 @@ my $new = new FS::cust_main_note ( {
   _date    => time,
   usernum  => $FS::CurrentUser::CurrentUser->usernum,
   comments => $comment,
+  sticky   => scalar( $cgi->param('sticky') ),
 } );
 
 my $error;
index 83f2fc5..491cd42 100644 (file)
@@ -151,7 +151,7 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px }
         <% emt('Out of taxable region') %>
       </TD>
       <TD STYLE="text-align: right">
-        <A HREF="<% $saleslink %>;out=1;taxname=<% $params{taxname} %>">
+        <A HREF="<% $saleslink %>;out=1;taxname=<% encode_entities($params{'taxname'}) %>">
           <% $money_sprintf->( $report->{outside } ) %>
         </A>
       </TD>
@@ -188,8 +188,9 @@ if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
   $agentname = $agent->agentname;
 }
 
-if ( $cgi->param('taxname') =~ /^([\w ]+)$/ ) {
-  $params{taxname} = $1;
+# allow anything in here; FS::Report::Tax will treat it as unsafe
+if ( length($cgi->param('taxname')) ) {
+  $params{taxname} = $cgi->param('taxname');
 } else {
   die "taxname required";
 }
index 6a7a06a..fa45a68 100644 (file)
 
 % my $bgcolor1 = '#eeeeee';
 % my $bgcolor2 = '#ffffff';
+% my %sticky_color = ( '#eeeeee' => '#ffff66',
+%                      '#ffffff' => '#ffffb8',
+%                    );
+%
 % my $bgcolor = '';
 % my $last_classnum = -1;
 % my $skipheader = 0;
@@ -56,7 +60,7 @@
 %                                             ";notenum=$notenum",
 %                            'actionlabel' => emt('Edit customer note'),
 %                            'width'       => 616,
-%                            'height'      => 538, #575
+%                            'height'      => 575,
 %                            'frame'       => 'top',
 %                        );
 %   my $clickjs = qq!onclick="$onclick"!;
 % $last_classnum = $note->classnum;
 % }
 
+% my $color = $note->sticky ? $sticky_color{$bgcolor} : $bgcolor;
+
     <TR>
-      <% note_datestr($note,$conf,$bgcolor) %>
-      <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+      <% note_datestr($note,$conf,$color) %>
+      <TD CLASS="grid" BGCOLOR="<% $color %>">
         &nbsp;<% $note->usernum ? $note->access_user->name : $note->otaker %>
       </TD>
 % if ($conf->exists('note-classes') && $conf->config('note-classes') == 1) {
-      <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+      <TD CLASS="grid" BGCOLOR="<% $color %>">
        <% $note->classname %>   
       </TD>
 % }
-      <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+      <TD CLASS="grid" BGCOLOR="<% $color %>">
         <% $note->comments | defang %>
       </TD>
 % if($edit) {
-      <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $edit %></TD>
+      <TD CLASS="grid" BGCOLOR="<% $color %>"><% $edit %></TD>
 % }
     </TR>
 
index f9ff307..ba51b3b 100644 (file)
@@ -1,5 +1,5 @@
 <DEL><% $link %><% $invoice %><% $link ? '</A>' : '' %></DEL>
-<I><% mt("voided [_1]", time2str($date_format, $cust_bill_void->void_date) ) |h %> 
+<I><% mt("voided ([_1]) [_2]", $cust_bill_void->reason, time2str($date_format, $cust_bill_void->void_date) ) |h %> 
 % my $void_user = $cust_bill_void->void_access_user;
 % if ($void_user) {
     by <% $void_user->username %></I>
index a8194a7..207ab9c 100644 (file)
@@ -1,5 +1,5 @@
 <DEL><% mt("Payment [_1] by [_2]", $info, $cust_pay_void->otaker ) |h %></DEL>
-<I><% mt("voided [_1]", time2str($date_format, $cust_pay_void->void_date) ) |h %> 
+<I><% mt("voided ([_1]) [_2]", $cust_pay_void->reason, time2str($date_format, $cust_pay_void->void_date) ) |h %> 
 % my $void_user = $cust_pay_void->void_access_user;
 % if ($void_user) {
     by <% $void_user->username %></I>