Merge branch '3.x-pre' of git.freeside.biz:/home/git/freeside into FREESIDE_3_BRANCH
authorJeremy Davis <jeremyd@freeside.biz>
Fri, 26 Jun 2015 21:38:02 +0000 (17:38 -0400)
committerJeremy Davis <jeremyd@freeside.biz>
Fri, 26 Jun 2015 21:38:02 +0000 (17:38 -0400)
FS/FS/Conf.pm
FS/FS/Schema.pm
FS/FS/Template_Mixin.pm
FS/FS/invoice_conf.pm
conf/invoice_html
conf/invoice_latex
httemplate/edit/invoice_conf.html
httemplate/elements/menu.html
httemplate/search/report_rt_cust.html [new file with mode: 0644]
httemplate/search/rt_cust.html [new file with mode: 0644]
httemplate/search/svc_acct.cgi

index d211361..5ea3555 100644 (file)
@@ -679,10 +679,12 @@ invoice_latexfooter
 invoice_latexsmallfooter
 invoice_latexnotes
 invoice_latexcoupon
+invoice_latexwatermark
 invoice_html
 invoice_htmlreturnaddress
 invoice_htmlfooter
 invoice_htmlnotes
+invoice_htmlwatermark
 logo.png
 logo.eps
 );
@@ -1379,6 +1381,15 @@ sub reason_type_options {
   },
 
   {
+    'key'         => 'invoice_htmlwatermark',
+    'section'     => 'invoicing',
+    'description' => 'Watermark for HTML invoices. Appears in a semitransparent positioned DIV overlaid on the main invoice container.',
+    'type'        => 'textarea',
+    'per_agent'   => 1,
+    'per_locale'  => 1,
+  },
+
+  {
     'key'         => 'invoice_latex',
     'section'     => 'invoicing',
     'description' => 'Optional LaTeX template for typeset PostScript invoices.  See the <a href="http://www.freeside.biz/mediawiki/index.php/Freeside:2.1:Documentation:Administration#Typeset_.28LaTeX.29_invoice_templates">billing documentation</a> for details.',
@@ -1566,6 +1577,15 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'invoice_latexwatermark',
+    'section'     => 'invoicing',
+    'description' => 'Watermark for LaTeX invoices. See "texdoc background" for information on what this can contain. The content itself should be enclosed in braces, optionally followed by a comma and any formatting options.',
+    'type'        => 'textarea',
+    'per_agent'   => 1,
+    'per_locale'  => 1,
+  },
+
+  {
     'key'         => 'invoice_email_pdf',
     'section'     => 'invoicing',
     'description' => 'Send PDF invoice as an attachment to emailed invoices.  By default, includes the HTML invoice as the email body, unless invoice_email_pdf_note is set.',
index bab179f..fc6d301 100644 (file)
@@ -4724,6 +4724,8 @@ sub tables_hashref {
         'latexsmallfooter',     'text',     'NULL', '', '', '',
         'latexreturnaddress',   'text',     'NULL', '', '', '',
         'with_latexcoupon',     'char',     'NULL', '1', '', '',
+        'htmlwatermark',        'text',     'NULL', '', '', '',
+        'latexwatermark',       'text',     'NULL', '', '', '',
         'lpr',                  'varchar',  'NULL', $char_d, '', '',
       ],
       'primary_key' => 'confnum',
index 44d44e1..0d7c1f1 100644 (file)
@@ -818,35 +818,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',
@@ -860,6 +861,8 @@ sub print_generic {
       die $error;
     }
 
+    # fill in variables
+
     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
 
     $invoice_data{$include} =~ s/\n+$//
index da448b8..d88c89a 100644 (file)
@@ -49,6 +49,8 @@ and supports the FS::Conf interface.  The following fields are supported:
 
 =item htmlreturnaddress - return address (HTML)
 
+=item htmlwatermark - watermark to show in background (HTML)
+
 =item latexnotes - "notes" section (LaTeX)
 
 =item latexfooter - footer (LaTeX)
@@ -59,6 +61,8 @@ and supports the FS::Conf interface.  The following fields are supported:
 
 =item latexsmallfooter - footer for pages after the first (LaTeX)
 
+=item latexwatermark - watermark to show in background (LaTeX)
+
 =item with_latexcoupon - 'Y' to print the payment coupon (LaTeX)
 
 =item lpr - command to print the invoice (passed on stdin as a PDF)
@@ -185,11 +189,13 @@ sub check {
     || $self->ut_anything('htmlfooter')
     || $self->ut_anything('htmlsummary')
     || $self->ut_anything('htmlreturnaddress')
+    || $self->ut_anything('htmlwatermark')
     || $self->ut_anything('latexnotes')
     || $self->ut_anything('latexfooter')
     || $self->ut_anything('latexsummary')
     || $self->ut_anything('latexsmallfooter')
     || $self->ut_anything('latexreturnaddress')
+    || $self->ut_anything('latexwatermark')
     # flags
     || $self->ut_flag('with_latexcoupon')
   ;
index 06ee775..dfd87c7 100644 (file)
@@ -1,6 +1,18 @@
 <STYLE TYPE="text/css">
-.invoice { font-family: sans-serif; font-size: 10pt }
-.invoice_header { font-size: 10pt }
+.invoice {
+  font-family: sans-serif;
+  font-size: 10pt;
+  display: inline-block;
+  padding: 4pt;
+  border: 1px solid black;
+  background-color: white;
+  min-width: 625px;
+  position: relative;
+}
+.invoice_header {
+  font-size: 10pt;
+  border-spacing: 4pt;
+}
 .invoice_headerright TH { border-top: 2px solid #000000; border-bottom: 2px solid #000000 }
 .invoice_headerright TD { font-size: 10pt; empty-cells: show }
 .invoice_summary TH { border-bottom: 2px solid #000000 }
 .invoice_extdesc TD { font-size: 8pt }
 .invoice_totaldesc TD { font-size: 10pt; empty-cells: show }
 .allcaps { text-transform:uppercase; font-size: 12pt }
+.watermark-box {
+  z-index: 10;
+  position: absolute;
+  text-align: center;
+  padding: 0;
+  filter: alpha(opacity=25);
+  opacity: 0.25;
+  width: 100%;
+  height: 100%;
+  display: table;
+  pointer-events: none;
+}
+.watermark-content {
+  display: table-cell;
+  vertical-align: middle;
+  font-size: 96pt;
+}
 </STYLE>
 
-<table class="invoice" bgcolor="#ffffff" WIDTH=625 CELLSPACING=8 STYLE="border:1px solid #000000"><tr><td>
+<div class="invoice">
+
+  <DIV CLASS="watermark-box"><DIV CLASS="watermark-content">
+      <%= $watermark %>
+  </DIV></DIV>
 
   <table class="invoice_header" width="100%">
     <tr>
   <hr NOSHADE SIZE=2 COLOR="#000000">
   <p align="center" <%= $smallerfooter ? 'STYLE="font-size:75%;"' : '' %>><%= $footer %>
 
-</td></tr></table>
+</div>
index 40ec703..c7c696b 100644 (file)
 \usepackage{graphicx}                  % required for logo graphic\r
 \usepackage[utf8]{inputenc}             % multilanguage support\r
 \usepackage[T1]{fontenc}\r
+[@-- if ( length($watermark) ) {\r
+  $OUT .= '\r
+\usepackage{background}\r
+\backgroundsetup{\r
+  placement=center,\r
+  opacity=0.25,\r
+  color=black,\r
+  angle=0,\r
+  contents=' . $watermark . '\r
+}';\r
+}\r
+'';\r
+--@]\r
 \r
 \addtolength{\voffset}{-0.0cm}         % top margin to top of header\r
 \addtolength{\hoffset}{-0.6cm}         % left margin on page\r
index 7122653..861114b 100644 (file)
@@ -49,6 +49,7 @@ my @fields = (
         'Footer',
         'Summary header',
         'Return address',
+        'Watermark',
         'Small footer',
         'Enable coupon',
       ),
@@ -59,6 +60,7 @@ my @fields = (
       { field => 'latexfooter',         %textarea },
       { field => 'latexsummary',        %textarea },
       { field => 'latexreturnaddress',  %textarea },
+      { field => 'latexwatermark',      %textarea },
       { field => 'latexsmallfooter',    %textarea },
       { field => 'with_latexcoupon', type => 'checkbox', value => 'Y' },
 
@@ -68,6 +70,7 @@ my @fields = (
       { field => 'htmlfooter',          %textarea },
       { field => 'htmlsummary',         %textarea },
       { field => 'htmlreturnaddress',   %textarea },
+      { field => 'htmlwatermark',      %textarea },
       # logo
 
       { type  => 'columnend' },
@@ -87,12 +90,14 @@ my %labels = (
     latexfooter
     latexsummary
     latexreturnaddress
+    latexwatermark
     with_latexcoupon
     latexsmallfooter
     htmlnotes
     htmlfooter
     htmlsummary
     htmlreturnaddress
+    htmlwatermark
   ) ),
 
 ); 
index b4d019a..f96c05e 100644 (file)
@@ -290,7 +290,8 @@ $report_rating{'Unrateable CDRs'} = [ $fsurl.'search/cdr.html?freesidestatus=fai
   if $curuser->access_right("Usage: Unrateable CDRs");
 if ( $curuser->access_right("Usage: Time worked") ) {
   $report_rating{'Time worked'} = [ $fsurl.'search/report_rt_transaction.html', '' ];
-  $report_rating{'Time worked summary'} = [ $fsurl.'search/report_rt_ticket.html', '' ];
+  $report_rating{'Time worked summary per ticket'} = [ $fsurl.'search/report_rt_ticket.html', '' ];
+  $report_rating{'Time worked summary per customer'} = [ $fsurl.'search/report_rt_cust.html', '' ];
 }
 
 tie my %report_ticketing_statistics, 'Tie::IxHash',
diff --git a/httemplate/search/report_rt_cust.html b/httemplate/search/report_rt_cust.html
new file mode 100644 (file)
index 0000000..07d497f
--- /dev/null
@@ -0,0 +1,40 @@
+<& /elements/header.html, 'Time worked per-customer summary' &>
+
+<FORM ACTION="rt_cust.html" METHOD="GET">
+
+<TABLE>
+
+  <& /elements/tr-select-cust_main-status.html,
+                'label' => emt('Status'),
+  &>
+
+  <& /elements/tr-input-beginning_ending.html &>
+
+<!--
+  <& /elements/tr-select.html,
+      label       => 'Time category:',
+      field       => 'category',
+      options     => [ '', 'development', 'support' ],
+      option_labels => { '' => 'all' },
+      curr_value  => 'development',
+  &>
+
+  <& /elements/tr-select-otaker.html &>
+-->
+
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="Search">
+
+</FORM>
+
+<& /elements/footer.html &>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('List rating data');
+
+my $conf = new FS::Conf;
+
+</%init>
diff --git a/httemplate/search/rt_cust.html b/httemplate/search/rt_cust.html
new file mode 100644 (file)
index 0000000..7c31e97
--- /dev/null
@@ -0,0 +1,174 @@
+<& elements/search.html,
+     'title'         => 'Time worked per-customer summary',
+     'name_singular' => 'customer',
+     'query'       => $sql_query,
+     'count_query' => $count_query,
+     'header'      => [ FS::UI::Web::cust_header(
+                          $cgi->param('cust_fields')
+                        ),
+                        @extra_headers,
+                        'Support time',
+                        #'Development time',
+                        'Unclassified time',
+                      ],
+     'fields'      => [
+       \&FS::UI::Web::cust_fields,
+       @extra_fields,
+       $support_time_sub,
+       $unclass_time_sub,
+     ],
+     'color'       => [ FS::UI::Web::cust_colors(),
+                        map '', @extra_fields
+                      ],
+     'style'       => [ FS::UI::Web::cust_styles(),
+                        map '', @extra_fields
+                      ],
+     'align'       => [ FS::UI::Web::cust_aligns(),
+                        map '', @extra_fields
+                      ],
+     'links'       => [ ( map { $_ ne 'Cust. Status' ? $link : '' }
+                              FS::UI::Web::cust_header(
+                                         $cgi->param('cust_fields')
+                                                      )
+                        ),
+                        map '', @extra_fields
+                      ],
+
+&>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('List rating data')
+;
+
+#false laziness w/cust_main.html (we're really only filtering on status for now)
+
+my %search_hash = ();
+
+#$search_hash{'query'} = $cgi->keywords;
+
+#scalars
+my @scalars = qw (
+  agentnum salesnum status address city county state zip country                
+  invoice_terms                                                                 
+  no_censustract with_geocode with_email tax no_tax POST no_POST                
+  custbatch usernum                                                             
+  cancelled_pkgs                                                                
+  cust_fields flattened_pkgs                                                    
+  all_tags                                                                      
+  all_pkg_classnums                                                             
+  any_pkg_status                                                                
+);                                                                              
+                                                                                
+for my $param ( @scalars ) {                                                    
+  $search_hash{$param} = scalar( $cgi->param($param) )                          
+    if length($cgi->param($param));                                             
+}                                                                               
+                                                                                
+#lists                                                                          
+for my $param (qw( classnum refnum tagnum pkg_classnum )) {                     
+  $search_hash{$param} = [ $cgi->param($param) ];                               
+}
+
+###
+# etc
+###
+
+my $sql_query = FS::cust_main::Search->search(\%search_hash);
+my $count_query   = delete($sql_query->{'count_query'});
+my @extra_headers = @{ delete($sql_query->{'extra_headers'}) };
+my @extra_fields  = @{ delete($sql_query->{'extra_fields'})  };
+
+my $link = [ "${p}view/cust_main.cgi?", 'custnum' ];
+
+#eofalse (cust_main.html)
+
+#false laziness / cribbed from search/rt_ticket.html
+
+my $twhere = "
+  WHERE Transactions.ObjectType = 'RT::Ticket'
+"; #AND Transactions.ObjectId = Tickets.Id
+
+my $transaction_time = "
+CASE transactions.type when 'Set'
+  THEN (to_number(newvalue,'999999')-to_number(oldvalue, '999999')) * 60
+  ELSE timetaken*60
+END";
+
+$twhere .= "
+  AND (    ( Transactions.Type = 'Set'
+             AND Transactions.Field = 'TimeWorked'
+             AND Transactions.NewValue != Transactions.OldValue )
+        OR ( Transactions.Type IN ( 'Create', 'Comment', 'Correspond', 'Touch' )
+             AND Transactions.TimeTaken > 0
+           )
+      )";
+
+my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
+# TIMESTAMP is Pg-specific... ?
+if ( $beginning > 0 ) {
+  $beginning = "TIMESTAMP '". time2str('%Y-%m-%d %X', $beginning). "'";
+  $twhere .= " AND Transactions.Created >= $beginning ";
+}
+if ( $ending < 4294967295 ) {
+  $ending =    "TIMESTAMP '". time2str('%Y-%m-%d %X', $ending).    "'";
+  $twhere .= " AND Transactions.Created <= $ending    ";
+}
+
+my $transactions = "FROM Transactions $twhere";
+
+#eofalse (rt_ticket.html)
+
+my $support_time_sub = sub {
+  my $cust_main = shift;
+  my $sec = 0;
+  foreach my $ticket ($cust_main->tickets) {
+
+    my $TimeType = FS::Record->scalar_sql(
+      "SELECT Content FROM ObjectCustomFieldValues
+                      JOIN CustomFields
+                      ON (ObjectCustomFieldValues.CustomField = CustomFields.Id)
+         WHERE CustomFields.Name = 'TimeType'
+           AND ObjectCustomFieldValues.ObjectType = 'RT::Ticket'
+           AND ObjectCustomFieldValues.Disabled = 0
+           AND ObjectId = ". $ticket->{id}
+    );
+    next unless $TimeType eq 'support';
+
+    $sec += FS::Record->scalar_sql(
+      "SELECT SUM($transaction_time) $transactions ".
+      " AND Transactions.ObjectId = ". $ticket->{id}
+    );
+  }
+
+  (($sec < 0) ? '-' : '' ). int(abs($sec)/3600)."h".sprintf("%02d",(abs($sec)%3600)/60)."m";
+
+};
+
+my $unclass_time_sub = sub {
+  my $cust_main = shift;
+  my $sec = 0;
+  foreach my $ticket ($cust_main->tickets) {
+
+    my $TimeType = FS::Record->scalar_sql(
+      "SELECT Content FROM ObjectCustomFieldValues
+                      JOIN CustomFields
+                      ON (ObjectCustomFieldValues.CustomField = CustomFields.Id)
+         WHERE CustomFields.Name = 'TimeType'
+           AND ObjectCustomFieldValues.ObjectType = 'RT::Ticket'
+           AND ObjectCustomFieldValues.Disabled = 0
+           AND ObjectId = ". $ticket->{id}
+    );
+    next unless $TimeType eq '';
+
+    $sec += FS::Record->scalar_sql(
+      "SELECT SUM($transaction_time) $transactions ".
+      " AND Transactions.ObjectId = ". $ticket->{id}
+    );
+  }
+
+  (($sec < 0) ? '-' : '' ). int(abs($sec)/3600)."h".sprintf("%02d",(abs($sec)%3600)/60)."m";
+
+};
+
+</%init>
index b9e5a7c..58764f8 100755 (executable)
@@ -148,10 +148,34 @@ if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
   if ( $sortby eq 'seconds' ) {
     my $tot_time = 0;
     push @header, emt('Time');
-    push @fields, sub { my $svc_acct = shift;
-                        $tot_time += $svc_acct->seconds;
-                        format_time($svc_acct->seconds);
-                      };
+
+    if ( $conf->exists('svc_acct-display_paid_time_remaining') ) {
+      push @fields, sub { my $svc_acct = shift;
+                          my $seconds = $svc_acct->seconds;
+                          my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
+                          my $part_pkg = $cust_pkg->part_pkg;
+
+                          $tot_time += $svc_acct->seconds;
+
+                          $timepermonth = $part_pkg->option('seconds');
+                          $timepermonth = $timepermonth / $part_pkg->freq
+                            if $part_pkg->freq =~ /^\d+$/ && $part_pkg->freq != 0;
+                          my $recur = $part_pkg->base_recur($cust_pkg);
+
+                          return format_time($seconds)
+                            unless $timepermonth && $recur;
+
+                          format_time($seconds).
+                            sprintf(' (%.2fx monthly)', $seconds / $timepermonth );
+
+                        };
+    } else {
+      push @fields, sub { my $svc_acct = shift;
+                          $tot_time += $svc_acct->seconds;
+                          format_time($svc_acct->seconds);
+                        };
+    }
+
     push @links, '';
     $align .= 'r';
     push @color, '';