invoice_latexsmallfooter
invoice_latexnotes
invoice_latexcoupon
+invoice_latexwatermark
invoice_html
invoice_htmlreturnaddress
invoice_htmlfooter
invoice_htmlnotes
+invoice_htmlwatermark
logo.png
logo.eps
);
},
{
+ '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.',
},
{
+ '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.',
'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',
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',
die $error;
}
+ # fill in variables
+
$invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
$invoice_data{$include} =~ s/\n+$//
=item htmlreturnaddress - return address (HTML)
+=item htmlwatermark - watermark to show in background (HTML)
+
=item latexnotes - "notes" section (LaTeX)
=item latexfooter - footer (LaTeX)
=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)
|| $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')
;
<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>
\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
'Footer',
'Summary header',
'Return address',
+ 'Watermark',
'Small footer',
'Enable coupon',
),
{ field => 'latexfooter', %textarea },
{ field => 'latexsummary', %textarea },
{ field => 'latexreturnaddress', %textarea },
+ { field => 'latexwatermark', %textarea },
{ field => 'latexsmallfooter', %textarea },
{ field => 'with_latexcoupon', type => 'checkbox', value => 'Y' },
{ field => 'htmlfooter', %textarea },
{ field => 'htmlsummary', %textarea },
{ field => 'htmlreturnaddress', %textarea },
+ { field => 'htmlwatermark', %textarea },
# logo
{ type => 'columnend' },
latexfooter
latexsummary
latexreturnaddress
+ latexwatermark
with_latexcoupon
latexsmallfooter
htmlnotes
htmlfooter
htmlsummary
htmlreturnaddress
+ htmlwatermark
) ),
);
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',
--- /dev/null
+<& /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>
--- /dev/null
+<& 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>
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, '';