From c0c5709fb022b83a482d0b35f7094505766d5868 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 18 Sep 2015 10:18:43 -0700 Subject: [PATCH] send commission reports by email, #33101 --- FS/FS/AccessRight.pm | 1 + FS/FS/Mason.pm | 4 + FS/FS/Schema.pm | 31 ++ FS/FS/cust_bill_pkg.pm | 10 +- FS/FS/cust_msg.pm | 1 + FS/FS/msg_template/email.pm | 12 + FS/FS/msg_template/http.pm | 4 + FS/FS/report_batch.pm | 321 +++++++++++++++++++++ FS/MANIFEST | 8 + FS/t/report_batch.t | 5 + .../elements/popup_link-send_report_batch.html | 28 ++ httemplate/misc/process/send-report.html | 7 + httemplate/misc/send-report.html | 166 +++++++++++ httemplate/search/cust_msg.html | 3 +- httemplate/search/report_sales_commission_pkg.html | 10 + httemplate/search/sales_commission_pkg.html | 7 + 16 files changed, 615 insertions(+), 3 deletions(-) create mode 100644 FS/FS/report_batch.pm create mode 100644 FS/t/report_batch.t create mode 100644 httemplate/elements/popup_link-send_report_batch.html create mode 100644 httemplate/misc/process/send-report.html create mode 100644 httemplate/misc/send-report.html diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index 9274ad858..53c7cf622 100644 --- a/FS/FS/AccessRight.pm +++ b/FS/FS/AccessRight.pm @@ -288,6 +288,7 @@ tie my %rights, 'Tie::IxHash', 'Billing event reports', 'Receivables report', 'Financial reports', + { rightname=>'Send reports to customers', global=>1 }, { rightname=> 'List inventory', global=>1 }, { rightname=>'View email logs', global=>1 }, { rightname=>'View system logs' }, diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index ae4f07cdb..98a75c8df 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -405,6 +405,10 @@ if ( -e $addl_handler_use_file ) { use FS::cust_pkg_reason_fee; use FS::part_svc_link; use FS::access_user_log; + use FS::report_batch; + use FS::report_batch; + use FS::report_batch; + use FS::report_batch; # Sammath Naur if ( $FS::Mason::addl_handler_use ) { diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 12211d1e1..85fbbeb8a 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -7124,6 +7124,37 @@ sub tables_hashref { ], }, + 'report_batch' => { + 'columns' => [ + 'reportbatchnum', 'serial', '', '', '', '', + 'reportname', 'varchar', '', 255, '', '', + 'agentnum', 'int', 'NULL', '', '', '', + 'send_date', @date_type, '', '', + 'sdate', @date_type, '', '', + 'edate', @date_type, '', '', + 'usernum', 'int', 'NULL', '', '', '', + 'msgnum', 'int', 'NULL', '', '', '', + # add report params here as necessary + ], + 'primary_key' => 'reportbatchnum', + 'unique' => [], + 'index' => [], + 'foreign_keys' => [ + { columns => [ 'agentnum' ], + table => 'agent', + references => [ 'agentnum' ], + }, + { columns => [ 'usernum' ], + table => 'access_user', + references => [ 'usernum' ], + }, + { columns => [ 'msgnum' ], + table => 'msg_template', + references => [ 'msgnum' ], + }, + ], + }, + # name type nullability length default local #'new_table' => { diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index 8233ce0d6..178042666 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -1124,7 +1124,10 @@ sub tax_locationnum { if ( $self->pkgnum ) { # normal sales return $self->cust_pkg->tax_locationnum; } elsif ( $self->feepart ) { # fees - return $self->cust_bill->cust_main->ship_locationnum; + my $custnum = $self->fee_origin->custnum; + if ( $custnum ) { + return FS::cust_main->by_key($custnum)->ship_locationnum; + } } else { # taxes return ''; } @@ -1135,7 +1138,10 @@ sub tax_location { if ( $self->pkgnum ) { # normal sales return $self->cust_pkg->tax_location; } elsif ( $self->feepart ) { # fees - return $self->cust_bill->cust_main->ship_location; + my $custnum = $self->fee_origin->custnum; + if ( $custnum ) { + return FS::cust_main->by_key($custnum)->ship_location; + } } else { # taxes return; } diff --git a/FS/FS/cust_msg.pm b/FS/FS/cust_msg.pm index db026808c..27272b8a3 100644 --- a/FS/FS/cust_msg.pm +++ b/FS/FS/cust_msg.pm @@ -148,6 +148,7 @@ sub check { 'invoice', 'receipt', 'admin', + 'report', ]) ; return $error if $error; diff --git a/FS/FS/msg_template/email.pm b/FS/FS/msg_template/email.pm index e6d5a5a99..377dbb17b 100644 --- a/FS/FS/msg_template/email.pm +++ b/FS/FS/msg_template/email.pm @@ -200,6 +200,12 @@ A hash reference of additional substitutions A string identifying the kind of message this is. Currently can be "invoice", "receipt", "admin", or null. Expand this list as necessary. +=item override_content + +A string to use as the HTML body; if specified, replaces the entire +body of the message. This should be used ONLY by L and may +go away in the future. + =back =cut @@ -265,6 +271,12 @@ sub prepare { warn "$me filling in body template\n" if $DEBUG; $body = $body_tmpl->fill_in( HASH => $hashref ); + # override $body if requested + if ( $opt{'override_content'} ) { + warn "$me overriding template body with requested content" if $DEBUG; + $body = $opt{'override_content'}; + } + ### # and email ### diff --git a/FS/FS/msg_template/http.pm b/FS/FS/msg_template/http.pm index 51dfcffc2..a2b0986ea 100644 --- a/FS/FS/msg_template/http.pm +++ b/FS/FS/msg_template/http.pm @@ -59,6 +59,10 @@ sub prepare { %$document, %$hashref }; + # put override content _somewhere_ so it can be used + if ( $opt{'override_content'} ) { + $document{'content'} = $opt{'override_content'}; + } my $request_content = $json->encode($document); warn "$me ".$self->prepare_url."\n" if $DEBUG; diff --git a/FS/FS/report_batch.pm b/FS/FS/report_batch.pm new file mode 100644 index 000000000..64412dfba --- /dev/null +++ b/FS/FS/report_batch.pm @@ -0,0 +1,321 @@ +package FS::report_batch; +use base qw( FS::Record ); + +use strict; +use FS::Record qw( qsearch qsearchs dbdef ); +use FS::msg_template; +use FS::cust_main; +use FS::Misc::DateTime qw(parse_datetime); +use FS::Mason qw(mason_interps); +use URI::Escape; +use HTML::Defang; + +our $DEBUG = 0; + +=head1 NAME + +FS::report_batch - Object methods for report_batch records + +=head1 SYNOPSIS + + use FS::report_batch; + + $record = new FS::report_batch \%hash; + $record = new FS::report_batch { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::report_batch object represents an order to send a batch of reports to +their respective customers or other contacts. FS::report_batch inherits from +FS::Record. The following fields are currently supported: + +=over 4 + +=item reportbatchnum + +primary key + +=item reportname + +The name of the report, which will be the same as the file name (minus any +directory names). There's an enumerated set of these; you can't use just any +report. + +=item send_date + +The date the report was sent. + +=item agentnum + +The agentnum to limit the report to, if any. + +=item sdate + +The start date of the report period. + +=item edate + +The end date of the report period. + +=item usernum + +The user who ordered the report. + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new report batch. To add the record to the database, see L<"insert">. + +=cut + +sub table { 'report_batch'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=item delete + +Deletes this record from the database. + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=item check + +Checks all fields to make sure this is a valid record. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('reportbatchnum') + || $self->ut_text('reportname') + || $self->ut_numbern('agentnum') + || $self->ut_numbern('sdate') + || $self->ut_numbern('edate') + || $self->ut_numbern('usernum') + ; + return $error if $error; + + $self->set('send_date', time); + + $self->SUPER::check; +} + +=back + +=head1 SUBROUTINES + +=over 4 + +=item process_send_report JOB, PARAMS + +Takes a hash of PARAMS, determines all contacts who need to receive a report, +and sends it to them. On completion, creates and stores a report_batch record. +JOB is a queue job to receive status messages. + +PARAMS can include: + +- reportname: the name of the report (listed in the C<%sendable_reports> hash). +Required. +- msgnum: the L to use for this report. Currently the +content of the template is ignored, but the subject line and From/Bcc addresses +are still used. Required. +- agentnum: the agent to limit the report to. +- beginning, ending: the date range to run the report, as human-readable +dates (I unix timestamps). + +=cut + +# trying to keep this data-driven, with parameters that tell how the report is +# to be handled rather than callbacks. +# - path: where under the document root the report is located +# - domain: which table to query for objects on which the report is run. +# Each record in that table produces one report. +# - cust_main: the method on that object that returns its linked customer (to +# which the report will be sent). If the table has a 'custnum' field, this +# can be omitted. +our %sendable_reports = ( + 'sales_commission_pkg' => { + 'name' => 'Sales commission per package', + 'path' => '/search/sales_commission_pkg.html', + 'domain' => 'sales', + 'cust_main' => 'sales_cust_main', + }, +); + +sub process_send_report { + my $job = shift; + my $param = shift; + + my $msgnum = $param->{'msgnum'}; + my $template = FS::msg_template->by_key($msgnum) + or die "msg_template $msgnum not found\n"; + + my $reportname = $param->{'reportname'}; + my $info = $sendable_reports{$reportname} + or die "don't know how to send report '$reportname'\n"; + + # the most important thing: which report is it? + my $path = $info->{'path'}; + + # find all targets for the report: + # - those matching the agentnum if there is one. + # - those that aren't disabled. + my $domain = $info->{domain}; + my $dbt = dbdef->table($domain); + my $hashref = {}; + if ( $param->{'agentnum'} and $dbt->column('agentnum') ) { + $hashref->{'agentnum'} = $param->{'agentnum'}; + } + if ( $dbt->column('disabled') ) { + $hashref->{'disabled'} = ''; + } + my @records = qsearch($domain, $hashref); + my $num_targets = scalar(@records); + return if $num_targets == 0; + my $sent = 0; + + my $outbuf; + my ($fs_interp) = mason_interps('standalone', 'outbuf' => \$outbuf); + # if generating the report fails, we want to capture the error and exit, + # not send it. + $fs_interp->error_mode('fatal'); + $fs_interp->error_format('brief'); + + # we have to at least have an RT::Handle + require RT; + RT::LoadConfig(); + RT::Init(); + + # hold onto all the reports until we're sure they generated correctly. + my %cust_main; + my %report_content; + + # grab the stylesheet + ### note: if we need the ability to support different stylesheets, this + ### is the place to put it in + eval { $fs_interp->exec('/elements/freeside.css') }; + die "couldn't load stylesheet via Mason: $@\n" if $@; + my $stylesheet = $outbuf; + + my $pkey = $dbt->primary_key; + foreach my $rec (@records) { + + $job->update_statustext(int( 100 * $sent / $num_targets )); + my $pkey_val = $rec->get($pkey); # e.g. sales.salesnum + + # find the customer we're sending to, and their email + my $cust_main; + if ( $info->{'cust_main'} ) { + my $cust_method = $info->{'cust_main'}; + $cust_main = $rec->$cust_method; + } elsif ( $rec->custnum ) { + $cust_main = FS::cust_main->by_key($rec->custnum); + } else { + warn "$pkey = $pkey_val has no custnum; not sending report\n"; + next; + } + my @email = $cust_main->invoicing_list_emailonly; + if (!@email) { + warn "$pkey = $pkey_val has no email destinations\n" if $DEBUG; + next; + } + + # params to send to the report (as if from the user's browser) + my @report_param = ( # maybe list these in $info + agentnum => $param->{'agentnum'}, + beginning => $param->{'beginning'}, + ending => $param->{'ending'}, + $pkey => $pkey_val, + _type => 'html-print', + ); + + # build a query string + my $query_string = ''; + while (@report_param) { + $query_string .= uri_escape(shift @report_param) + . '=' + . uri_escape(shift @report_param); + $query_string .= ';' if @report_param; + } + warn "$path?$query_string\n\n" if $DEBUG; + + # run the report! + $FS::Mason::Request::QUERY_STRING = $query_string; + $FS::Mason::Request::FSURL = ''; + $outbuf = ''; + eval { $fs_interp->exec($path) }; + die "creating report for $pkey = $pkey_val: $@" if $@; + + # make some adjustments to the report + my $html_defang; + $html_defang = HTML::Defang->new( + url_callback => sub { 1 }, # strip all URLs (they're not accessible) + tags_to_callback => [ 'body' ], # and after the BODY tag... + tags_callback => sub { + my $isEndTag = $_[4]; + $html_defang->add_to_output("\n\n") + unless $isEndTag; + }, + ); + $outbuf = $html_defang->defang($outbuf); + + $cust_main{ $cust_main->custnum } = $cust_main; + $report_content{ $cust_main->custnum } = $outbuf; + } # foreach $rec + + $job->update_statustext('Sending reports...'); + foreach my $custnum (keys %cust_main) { + # create an email message with the report as body + # change this when backporting to 3.x + $template->send( + cust_main => $cust_main{$custnum}, + object => $cust_main{$custnum}, + msgtype => 'report', + override_content => $report_content{$custnum}, + ); + } + + my $self = FS::report_batch->new({ + reportname => $param->{'reportname'}, + agentnum => $param->{'agentnum'}, + sdate => parse_datetime($param->{'beginning'}), + edate => parse_datetime($param->{'ending'}), + usernum => $job->usernum, + msgnum => $param->{'msgnum'}, + }); + my $error = $self->insert; + warn "error recording completion of report: $error\n" if $error; + +} + +=head1 SEE ALSO + +L + +=cut + +1; + diff --git a/FS/MANIFEST b/FS/MANIFEST index 5b73b728c..5041ccd68 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -850,3 +850,11 @@ FS/part_svc_link.pm t/part_svc_link.t FS/access_user_log.pm t/access_user_log.t +FS/report_batch.pm +t/report_batch.t +FS/report_batch.pm +t/report_batch.t +FS/report_batch.pm +t/report_batch.t +FS/report_batch.pm +t/report_batch.t diff --git a/FS/t/report_batch.t b/FS/t/report_batch.t new file mode 100644 index 000000000..42fc8936a --- /dev/null +++ b/FS/t/report_batch.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::report_batch; +$loaded=1; +print "ok 1\n"; diff --git a/httemplate/elements/popup_link-send_report_batch.html b/httemplate/elements/popup_link-send_report_batch.html new file mode 100644 index 000000000..5f4471054 --- /dev/null +++ b/httemplate/elements/popup_link-send_report_batch.html @@ -0,0 +1,28 @@ +<%doc> + +Example: + +<& /elements/popup_link-send_report_batch.html, + reportname => 'sales_commission_pkg', + label => 'Click here to send reports by email', +&> + +<& /elements/popup_link.html, $params &>\ +<%init> + +my $params = { 'closetext' => emt('Close') }; + +if (ref($_[0]) eq 'HASH') { + $params = { %$params, %{ $_[0] } }; +} else { + $params = { %$params, @_ }; +} + +$params->{'label'} ||= emt('Send reports by email'); +$params->{'actionlabel'} ||= emt('Send reports'); +#$params->{'width'} ||= 350; +$params->{'height'} ||= 650; + +$params->{'action'} = $fsurl. 'misc/send-report.html?reportname='. $params->{'reportname'}; + + diff --git a/httemplate/misc/process/send-report.html b/httemplate/misc/process/send-report.html new file mode 100644 index 000000000..3bceebc0c --- /dev/null +++ b/httemplate/misc/process/send-report.html @@ -0,0 +1,7 @@ +<%init> +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Send reports to customers'); + +my $server = FS::UI::Web::JSRPC->new('FS::report_batch::process_send_report', $cgi); + +<% $server->process %> diff --git a/httemplate/misc/send-report.html b/httemplate/misc/send-report.html new file mode 100644 index 000000000..557767a57 --- /dev/null +++ b/httemplate/misc/send-report.html @@ -0,0 +1,166 @@ +<%doc> + +Parameters: + +- reportname: the report name (per FS::report_batch) + + +<& /elements/header-popup.html, { title => $report_info->{name} } &> + +
+ + + + + <& /elements/tr-select-agent.html &> + + <& /elements/tr-td-label.html, label => emt('Message template') &> + + + + <& /elements/tr-input-beginning_ending.html &> + + <& /elements/progress-init.html, + 'OneTrueForm', + [ qw( reportname msgnum agentnum beginning ending ) ], + $p.'misc/process/send-report.html', + { message => 'Reports sent', + url => $cgi->referer } + &> + +
+ <& /elements/select-msg_template.html, field => 'msgnum' &> +
+ + +
+ + + +% if ( @report_history ) { +
+ + + + + + + + + +% my $row = 0; +% foreach my $report (@report_history) { +% my $agent = ($report->agentnum ? +% $report->agent->agent : 'All agents'); + + + + + + + +% $row++; +% } + +
<% emt('Report history') %>
AgentSent onDate rangeUser
<% $agent %><% time2str($date_format, $report->send_date) %><% time2str($date_format, $report->sdate) %><% time2str($date_format, $report->edate) %><% $report->access_user->username %>
+% } + +<& /elements/footer.html &> + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Send reports to customers'); + +$cgi->param('reportname') =~ /^(\w+)$/ + or die "bad reportname"; +my $reportname = $1; +my $report_info = $FS::report_batch::sendable_reports{$reportname} + or die "bad reportname"; + +my $date_format = FS::Conf->new->config('date_format') || '%x'; + +my @report_history = qsearch({ + table => 'report_batch', + hashref => { reportname => $reportname }, + order_by => ' ORDER BY send_date DESC', +}); + +# defaults per agent that could be selected for the report +my %agent; + +foreach my $report ( @report_history ) { + my $agentnum = $report->agentnum; + next if $agent{$agentnum}; + + # estimate the width of the report period, in months + my $last_sdate = DateTime->from_epoch( epoch => $report->sdate ); + my $last_edate = DateTime->from_epoch( epoch => $report->edate ); + + my $days = $last_sdate->delta_days( $last_edate )->delta_days; + my $months = sprintf('%.0f', $days / 6) / 5; + + my $next_sdate = $last_edate->clone->add(days => 1); + my $next_edate = $next_sdate->clone; + if ( $months >= 1 ) { # then treat as an interval in months + $next_edate->add( months => sprintf('%.0f', $months) ); + $next_edate->subtract(days => 1); + } else { # treat as a number of days + $next_edate->add( days => $days ); + } + + my $name = $agentnum ? FS::agent->by_key($agentnum)->agent : 'All agents'; + $agent{$agentnum} = { + name => $name, + beginning => $next_sdate->strftime($date_format), + ending => $next_edate->strftime($date_format), + msgnum => $report->msgnum, + }; +} + + diff --git a/httemplate/search/cust_msg.html b/httemplate/search/cust_msg.html index 401f52ebb..e9aece202 100644 --- a/httemplate/search/cust_msg.html +++ b/httemplate/search/cust_msg.html @@ -144,11 +144,12 @@ include('/elements/select.html', include('/elements/select.html', 'field' => 'msgtype', 'curr_value' => $cgi->param('msgtype') || '', - 'options' => [ '', 'invoice', 'receipt', 'admin' ], + 'options' => [ '', 'invoice', 'receipt', 'admin', 'report' ], 'labels' => { '' => '(any)', 'invoice' => 'Invoices', 'receipt' => 'Receipts', 'admin' => 'Admin notices', + 'report' => 'Reports', }, ) . ' diff --git a/httemplate/search/report_sales_commission_pkg.html b/httemplate/search/report_sales_commission_pkg.html index 6adf090e9..27906e0c3 100644 --- a/httemplate/search/report_sales_commission_pkg.html +++ b/httemplate/search/report_sales_commission_pkg.html @@ -1,5 +1,15 @@ <& /elements/header.html, 'Sales commission report per package' &> +% if ($FS::CurrentUser::CurrentUser->access_right('Send reports to customers')) +% { +

+<& /elements/popup_link-send_report_batch.html, + reportname => 'sales_commission_pkg', + label => emt('Send these reports by email'), +&> +

+% } +
diff --git a/httemplate/search/sales_commission_pkg.html b/httemplate/search/sales_commission_pkg.html index 2b5f2bb0a..9fbe22eca 100644 --- a/httemplate/search/sales_commission_pkg.html +++ b/httemplate/search/sales_commission_pkg.html @@ -1,12 +1,17 @@ %# still not a good way to do rows grouped by some field in a search.html %# report +%# (there is now, but we're not yet sponsored to switch this over to it) % if ( $type eq 'xls' ) { <% $data %>\ % } else { +% if ( $type eq 'html-print' ) { +<& /elements/header-popup.html, $title &> +% } else { <& /elements/header.html, $title &>

Download full results
as Excel spreadsheet

+% }
<& /elements/table-grid.html &> + + % my ($custnum, $sales, $commission, $row, $bgcolor) = (0, 0, 0, 0); % foreach my $cust_pkg ( @cust_pkg ) { % if ( $custnum ne $cust_pkg->custnum ) { -- 2.11.0
Package Sales Percentage Commission