X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fmsg_template.pm;h=5c6392cfc921ec6dce4ec0296d21cb88ad04eb6b;hp=d980ab9e2b566df51f54c6c7e43278d71f8dc461;hb=4e091a88af83ba9293a1747aaf754439dcc4cd39;hpb=53ea5a72067a9b0ebcd3417692c3884d6f91f74a diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index d980ab9e2..5c6392cfc 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -3,7 +3,7 @@ package FS::msg_template; use strict; use base qw( FS::Record ); use Text::Template; -use FS::Misc qw( generate_email send_email ); +use FS::Misc qw( generate_email send_email do_print ); use FS::Conf; use FS::Record qw( qsearch qsearchs ); use FS::UID qw( dbh ); @@ -16,6 +16,10 @@ use Date::Format qw( time2str ); use HTML::Entities qw( decode_entities encode_entities ) ; use HTML::FormatText; use HTML::TreeBuilder; +use Encode; + +use File::Temp; +use IPC::Run qw(run); use vars qw( $DEBUG $conf ); FS::UID->install_callback( sub { $conf = new FS::Conf; } ); @@ -262,6 +266,10 @@ The I field in the template takes precedence over this. Destination address. The default is to use the customer's invoicing_list addresses. Multiple addresses may be comma-separated. +=item substitutions + +A hash reference of additional substitutions + =back =cut @@ -269,8 +277,8 @@ invoicing_list addresses. Multiple addresses may be comma-separated. sub prepare { my( $self, %opt ) = @_; - my $cust_main = $opt{'cust_main'}; - my $object = $opt{'object'}; + my $cust_main = $opt{'cust_main'} or die 'cust_main required'; + my $object = $opt{'object'} or die 'object required'; # localization my $locale = $cust_main->locale || ''; @@ -324,8 +332,12 @@ sub prepare { } } } - $_ = encode_entities($_ || '') foreach values(%hash); + if ( $opt{substitutions} ) { + $hash{$_} = $opt{substitutions}->{$_} foreach keys %{$opt{substitutions}}; + } + + $_ = encode_entities($_ || '') foreach values(%hash); ### # clean up template @@ -385,8 +397,7 @@ sub prepare { $from_addr = scalar( $conf->config($opt{'from_config'}, $cust_main->agentnum) ); } - $from_addr ||= scalar( $conf->config('invoice_from', - $cust_main->agentnum) ); + $from_addr ||= $conf->invoice_from_full($cust_main->agentnum); } # my @cust_msg = (); # if ( $conf->exists('log_sent_mail') and !$opt{'preview'} ) { @@ -399,6 +410,10 @@ sub prepare { # @cust_msg = ('cust_msg' => $cust_msg); # } + my $text_body = encode('UTF-8', + HTML::FormatText->new(leftmargin => 0, rightmargin => 70) + ->format( HTML::TreeBuilder->new_from_content($body) ) + ); ( 'custnum' => $cust_main->custnum, 'msgnum' => $self->msgnum, @@ -407,8 +422,7 @@ sub prepare { 'bcc' => $self->bcc_addr || undef, 'subject' => $subject, 'html_body' => $body, - 'text_body' => HTML::FormatText->new(leftmargin => 0, rightmargin => 70 - )->format( HTML::TreeBuilder->new_from_content($body) ), + 'text_body' => $text_body ); } @@ -427,9 +441,53 @@ sub send { send_email(generate_email($self->prepare(@_))); } +=item render OPTION => VALUE ... + +Fills in the template and renders it to a PDF document. Returns the +name of the PDF file. + +Options are as for 'prepare', but 'from' and 'to' are meaningless. + +=cut + +# will also have options to set paper size, margins, etc. + +sub render { + my $self = shift; + eval "use PDF::WebKit"; + die $@ if $@; + my %opt = @_; + my %hash = $self->prepare(%opt); + my $html = $hash{'html_body'}; + + # Graphics/stylesheets should probably go in /var/www on the Freeside + # machine. + my $script_path = `/usr/bin/which freeside-wkhtmltopdf`; + chomp $script_path; + my $kit = PDF::WebKit->new(\$html); #%options + # hack to use our wrapper script + $kit->configure(sub { shift->wkhtmltopdf($script_path) }); + + $kit->to_pdf; +} + +=item print OPTIONS + +Render a PDF and send it to the printer. OPTIONS are as for 'render'. + +=cut + +sub print { + my( $self, %opt ) = @_; + do_print( [ $self->render(%opt) ], agentnum=>$opt{cust_main}->agentnum ); +} + # helper sub for package dates my $ymd = sub { $_[0] ? time2str('%Y-%m-%d', $_[0]) : '' }; +# helper sub for money amounts +my $money = sub { ($conf->money_char || '$') . sprintf('%.2f', $_[0] || 0) }; + # helper sub for usage-related messages my $usage_warning = sub { my $svc = shift; @@ -450,6 +508,17 @@ my $usage_warning = sub { # If you add anything, be sure to add a description in # httemplate/edit/msg_template.html. sub substitutions { + my $payinfo_sub = sub { + my $obj = shift; + ($obj->payby eq 'CARD' || $obj->payby eq 'CHEK') + ? $obj->paymask + : $obj->decrypt($obj->payinfo) + }; + my $payinfo_end = sub { + my $obj = shift; + my $payinfo = &$payinfo_sub($obj); + substr($payinfo, -4); + }; { 'cust_main' => [qw( display_custnum agentnum agent_name @@ -457,14 +526,12 @@ sub substitutions { name name_short contact contact_firstlast address1 address2 city county state zip country - daytime night fax + daytime night mobile fax has_ship_address - ship_last ship_first ship_company ship_name ship_name_short ship_contact ship_contact_firstlast ship_address1 ship_address2 ship_city ship_county ship_state ship_zip ship_country - ship_daytime ship_night ship_fax paymask payname paytype payip num_cancelled_pkgs num_ncancelled_pkgs num_pkgs @@ -477,6 +544,16 @@ sub substitutions { signupdate dundate packages recurdates ), + [ invoicing_email => sub { shift->invoicing_list_emailonly_scalar } ], + #compatibility: obsolete ship_ fields - use the non-ship versions + map ( + { my $field = $_; + [ "ship_$field" => sub { shift->$field } ] + } + qw( last first company daytime night fax ) + ), + # ship_name, ship_name_short, ship_contact, ship_contact_firstlast + # still work, though [ expdate => sub { shift->paydate_epoch } ], #compatibility [ signupdate_ymd => sub { $ymd->(shift->signupdate) } ], [ dundate_ymd => sub { $ymd->(shift->dundate) } ], @@ -493,6 +570,9 @@ sub substitutions { [ company_phonenum => sub { $conf->config('company_phonenum', shift->agentnum) } ], + [ selfservice_server_base_url => sub { + $conf->config('selfservice_server-base_url') #, shift->agentnum) + } ], ], # next_bill_date 'cust_pkg' => [qw( @@ -505,6 +585,8 @@ sub substitutions { labels_short ), [ pkg => sub { shift->part_pkg->pkg } ], + [ pkg_category => sub { shift->part_pkg->categoryname } ], + [ pkg_class => sub { shift->part_pkg->classname } ], [ cancel => sub { shift->getfield('cancel') } ], # grrr... [ start_ymd => sub { $ymd->(shift->getfield('start_date')) } ], [ setup_ymd => sub { $ymd->(shift->getfield('setup')) } ], @@ -514,11 +596,22 @@ sub substitutions { [ susp_ymd => sub { $ymd->(shift->getfield('susp')) } ], [ expire_ymd => sub { $ymd->(shift->getfield('expire')) } ], [ cancel_ymd => sub { $ymd->(shift->getfield('cancel')) } ], + + # not necessarily correct for non-flat packages + [ setup_fee => sub { shift->part_pkg->option('setup_fee') } ], + [ recur_fee => sub { shift->part_pkg->option('recur_fee') } ], + + [ freq_pretty => sub { shift->part_pkg->freq_pretty } ], + ], 'cust_bill' => [qw( invnum _date - )], + _date_pretty + due_date + ), + [ due_date2str => sub { shift->due_date2str('short') } ], + ], #XXX not really thinking about cust_bill substitutions quite yet # for welcome and limit warning messages @@ -572,11 +665,17 @@ sub substitutions { # overrides the one in cust_main in cases where a cust_pay is passed [ payby => sub { FS::payby->shortname(shift->payby) } ], [ date => sub { time2str("%a %B %o, %Y", shift->_date) } ], - [ payinfo => sub { - my $cust_pay = shift; - ($cust_pay->payby eq 'CARD' || $cust_pay->payby eq 'CHEK') ? - $cust_pay->paymask : $cust_pay->decrypt($cust_pay->payinfo) - } ], + [ 'payinfo' => $payinfo_sub ], + [ 'payinfo_end' => $payinfo_end ], + ], + # for refund receipts + 'cust_refund' => [ + 'refundnum', + [ refund => sub { sprintf("%.2f", shift->refund) } ], + [ payby => sub { FS::payby->shortname(shift->payby) } ], + [ date => sub { time2str("%a %B %o, %Y", shift->_date) } ], + [ 'payinfo' => $payinfo_sub ], + [ 'payinfo_end' => $payinfo_end ], ], # for payment decline messages # try to support all cust_pay fields @@ -588,11 +687,8 @@ sub substitutions { [ paid => sub { sprintf("%.2f", shift->paid) } ], [ payby => sub { FS::payby->shortname(shift->payby) } ], [ date => sub { time2str("%a %B %o, %Y", shift->_date) } ], - [ payinfo => sub { - my $pending = shift; - ($pending->payby eq 'CARD' || $pending->payby eq 'CHEK') ? - $pending->paymask : $pending->decrypt($pending->payinfo) - } ], + [ 'payinfo' => $payinfo_sub ], + [ 'payinfo_end' => $payinfo_end ], ], }; } @@ -626,6 +722,10 @@ sub agent { sub _upgrade_data { my ($self, %opts) = @_; + ### + # First move any historical templates in config to real message templates + ### + my @fixes = ( [ 'alerter_msgnum', 'alerter_template', '', '', '' ], [ 'cancel_msgnum', 'cancelmessage', 'cancelsubject', '', '' ], @@ -644,8 +744,7 @@ sub _upgrade_data { my $new = new FS::msg_template({ 'msgname' => $oldname, 'agentnum' => $agentnum, - 'from_addr' => ($from && $conf->config($from, $agentnum)) || - $conf->config('invoice_from', $agentnum), + 'from_addr' => ($from && $conf->config($from, $agentnum)) || '', 'bcc_addr' => ($bcc && $conf->config($from, $agentnum)) || '', 'subject' => ($subject && $conf->config($subject, $agentnum)) || '', 'mime_type' => 'text/html', @@ -659,20 +758,122 @@ sub _upgrade_data { $conf->delete($subject, $agentnum) if $subject; } } + + if ( $conf->exists('alert_expiration', $agentnum) ) { + my $msgnum = $conf->exists('alerter_msgnum', $agentnum); + my $template = FS::msg_template->by_key($msgnum) if $msgnum; + if (!$template) { + warn "template for alerter_msgnum $msgnum not found\n"; + next; + } + # this is now a set of billing events + foreach my $days (30, 15, 5) { + my $event = FS::part_event->new({ + 'agentnum' => $agentnum, + 'event' => "Card expiration warning - $days days", + 'eventtable' => 'cust_main', + 'check_freq' => '1d', + 'action' => 'notice', + 'disabled' => 'Y', #initialize first + }); + my $error = $event->insert( 'msgnum' => $msgnum ); + if ($error) { + warn "error creating expiration alert event:\n$error\n\n"; + next; + } + # make it work like before: + # only send each warning once before the card expires, + # only warn active customers, + # only warn customers with CARD/DCRD, + # only warn customers who get email invoices + my %conds = ( + 'once_every' => { 'run_delay' => '30d' }, + 'cust_paydate_within' => { 'within' => $days.'d' }, + 'cust_status' => { 'status' => { 'active' => 1 } }, + 'payby' => { 'payby' => { 'CARD' => 1, + 'DCRD' => 1, } + }, + 'message_email' => {}, + ); + foreach (keys %conds) { + my $condition = FS::part_event_condition->new({ + 'conditionname' => $_, + 'eventpart' => $event->eventpart, + }); + $error = $condition->insert( %{ $conds{$_} }); + if ( $error ) { + warn "error creating expiration alert event:\n$error\n\n"; + next; + } + } + $error = $event->initialize; + if ( $error ) { + warn "expiration alert event was created, but not initialized:\n$error\n\n"; + } + } # foreach $days + $conf->delete('alerter_msgnum', $agentnum); + $conf->delete('alert_expiration', $agentnum); + + } # if alerter_msgnum + } + + ### + # Move subject and body from msg_template to template_content + ### + foreach my $msg_template ( qsearch('msg_template', {}) ) { if ( $msg_template->subject || $msg_template->body ) { # create new default content my %content; - foreach ('subject','body') { - $content{$_} = $msg_template->$_; - $msg_template->setfield($_, ''); + $content{subject} = $msg_template->subject; + $msg_template->set('subject', ''); + + # work around obscure Pg/DBD bug + # https://rt.cpan.org/Public/Bug/Display.html?id=60200 + # (though the right fix is to upgrade DBD) + my $body = $msg_template->body; + if ( $body =~ /^x([0-9a-f]+)$/ ) { + # there should be no real message templates that look like that + warn "converting template body to TEXT\n"; + $body = pack('H*', $1); } + $content{body} = $body; + $msg_template->set('body', ''); my $error = $msg_template->replace(%content); die $error if $error; } } + + ### + # Add new-style default templates if missing + ### + $self->_populate_initial_data; + +} + +sub _populate_initial_data { #class method + #my($class, %opts) = @_; + #my $class = shift; + + eval "use FS::msg_template::InitialData;"; + die $@ if $@; + + my $initial_data = FS::msg_template::InitialData->_initial_data; + + foreach my $hash ( @$initial_data ) { + + next if $hash->{_conf} && $conf->config( $hash->{_conf} ); + + my $msg_template = new FS::msg_template($hash); + my $error = $msg_template->insert( @{ $hash->{_insert_args} || [] } ); + die $error if $error; + + $conf->set( $hash->{_conf}, $msg_template->msgnum ) if $hash->{_conf}; + + } + } sub eviscerate {