);
# but NOT $conf
use vars qw( $invoice_lines @buf ); #yuck
-use List::Util qw(sum first);
+use List::Util qw(sum); #can't import first, it conflicts with cust_main.first
use Date::Format;
use Date::Language;
use Text::Template 1.20;
|| $cust_main->contact_firstlast ),
#global config
- 'ship_enable' => $conf->exists('invoice-ship_address'),
+ 'ship_enable' => $cust_main->invoice_ship_address || $conf->exists('invoice-ship_address'),
'unitprices' => $conf->exists('invoice-unitprice'),
'smallernotes' => $conf->exists('invoice-smallernotes'),
'smallerfooter' => $conf->exists('invoice-smallerfooter'),
# XXX should be an FS::cust_bill method to set the defaults, instead
# of checking the type here
+ # info from customer's last invoice before this one, for some
+ # summary formats
+ $invoice_data{'last_bill'} = {};
+
my $last_bill = $self->previous_bill;
if ( $last_bill ) {
# ($pr_total is used elsewhere but not as $previous_balance)
$invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
- $invoice_data{'last_bill'} = {
- '_date' => $last_bill->_date, #unformatted
- };
+ $invoice_data{'last_bill'}{'_date'} = $last_bill->_date; #unformatted
my (@payments, @credits);
# for formats that itemize previous payments
foreach my $cust_pay ( qsearch('cust_pay', {
$invoice_data{'previous_payments'} = [];
$invoice_data{'previous_credits'} = [];
}
-
- # info from customer's last invoice before this one, for some
- # summary formats
- $invoice_data{'last_bill'} = {};
-
+
if ( $conf->exists('invoice_usesummary', $agentnum) ) {
$invoice_data{'summarypage'} = $summarypage = 1;
}
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+$//
if $DEBUG > 1;
my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
- my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) ||
+ my $multisection = $self->has_sections;
+ $conf->exists($tc.'sections', $cust_main->agentnum) ||
$conf->exists($tc.'sections_by_location', $cust_main->agentnum);
$invoice_data{'multisection'} = $multisection;
my $late_sections;
# create a tax section if we don't yet have one
my $tax_description = 'Taxes, Surcharges, and Fees';
- my $tax_section = first { $_->{description} eq $tax_description } @sections;
+ my $tax_section =
+ List::Util::first { $_->{description} eq $tax_description } @sections;
if (!$tax_section) {
$tax_section = { 'description' => $tax_description };
- push @sections, $tax_section if $multisection;
}
$tax_section->{tax_section} = 1; # mark this section as containing taxes
# if this is an existing tax section, we're merging the tax items into it.
#$tax_section->{'sort_weight'} = $tax_weight;
my @items_tax = $self->_items_tax;
+ push @sections, $tax_section if $multisection and @items_tax > 0;
+
foreach my $tax ( @items_tax ) {
$taxtotal += $tax->{'amount'};
];
}
-
+
if ( @items_tax ) {
my $total = {};
$total->{'total_item'} = $self->mt('Sub-total');
}
$invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
- push @buf,['','-----------'];
- push @buf,[$self->mt(
- (!$self->enable_previous)
- ? 'Total Charges'
- : 'Total New Charges'
- ),
- $money_char. sprintf("%10.2f",$self->charged) ];
- push @buf,['',''];
-
###
# Totals
###
);
my $embolden_function = $embolden_functions{$format};
- if ( $self->can('_items_total') ) { # quotations
-
- $self->_items_total(\@total_items);
+ if ( $multisection ) {
- foreach ( @total_items ) {
- $_->{'total_item'} = &$embolden_function( $_->{'total_item'} );
- $_->{'total_amount'} = &$embolden_function( $other_money_char.
- $_->{'total_amount'}
- );
+ if ( $adjust_section->{'sort_weight'} ) {
+ $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
+ $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
+ } else{
+ $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
+ $other_money_char. sprintf('%.2f', $self->charged );
}
- } else { #normal invoice case
+ }
+
+ if ( $self->can('_items_total') ) { # should always be true now
- # calculate total, possibly including total owed on previous
- # invoices
- my $total = {};
- my $item = 'Total';
- $item = $conf->config('previous_balance-exclude_from_total')
- || 'Total New Charges'
- if $conf->exists('previous_balance-exclude_from_total');
- my $amount = $self->charged;
- if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
- $amount += $pr_total;
- }
+ # even for multisection, need plain text version
+
+ my @new_total_items = $self->_items_total;
- $total->{'total_item'} = &$embolden_function($self->mt($item));
- $total->{'total_amount'} =
- &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
- if ( $multisection ) {
- if ( $adjust_section->{'sort_weight'} ) {
- $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
- $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
- } else {
- $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
- $other_money_char. sprintf('%.2f', $self->charged );
- }
- } else {
- push @total_items, $total;
- }
push @buf,['','-----------'];
- push @buf,[$item,
- $money_char.
- sprintf( '%10.2f', $amount )
- ];
- push @buf,['',''];
+
+ foreach ( @new_total_items ) {
+ my ($item, $amount) = ($_->{'total_item'}, $_->{'total_amount'});
+ $_->{'total_item'} = &$embolden_function( $item );
+ $_->{'total_amount'} = &$embolden_function( $other_money_char.$amount );
+ # but if it's multisection, don't append to @total_items. the adjust
+ # section has all this stuff
+ push @total_items, $_ if !$multisection;
+ push @buf, [ $item, $money_char.sprintf('%10.2f',$amount) ];
+ }
+
+ push @buf, [ '', '' ];
# if we're showing previous invoices, also show previous
# credits and payments
and $self->can('_items_credits')
and $self->can('_items_payments') )
{
- #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
# credits
my $credittotal = 0;
foreach my $credit (
- $self->_items_credits( 'template' => $template, 'trim_len' => 60 )
+ $self->_items_credits( 'template' => $template, 'trim_len' => 40 )
) {
my $total;
if ( $multisection && !$adjust_section->{sort_weight} ) {
$adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
$total->{'total_amount'};
- }else{
+ } else {
push @total_items, $total;
}
push @buf,['','-----------'];
# invoice history "section" (not really a section)
# not to be included in any subtotals, completely independent of
# everything...
- if ( $conf->exists('previous_invoice_history') ) {
+ if ( $conf->exists('previous_invoice_history') and $cust_main->isa('FS::cust_main') ) {
my %history;
my %monthorder;
foreach my $cust_bill ( $cust_main->cust_bill ) {
sub notice_name { '('.shift->table.')'; }
-sub template_conf { 'invoice_'; }
+# this is not supposed to happen
+sub template_conf { warn "bare FS::Template_Mixin::template_conf";
+ 'invoice_';
+}
# helper routine for generating date ranges
sub _prior_month30s {
sub balance_due_msg {
my $self = shift;
my $msg = $self->mt('Balance Due');
- return $msg unless $self->terms;
- if ( $self->due_date ) {
- $msg .= ' - ' . $self->mt('Please pay by'). ' '.
- $self->due_date2str('short');
- } elsif ( $self->terms ) {
- $msg .= ' - '. $self->terms;
+ return $msg unless $self->terms; # huh?
+ if ( !$self->conf->exists('invoice_show_prior_due_date')
+ or $self->conf->exists('invoice_sections') ) {
+ # if enabled, the due date is shown with Total New Charges (see
+ # _items_total) and not here
+ # (yes, or if invoice_sections is enabled; this is just for compatibility)
+ if ( $self->due_date ) {
+ $msg .= ' - ' . $self->mt('Please pay by'). ' '.
+ $self->due_date2str('short');
+ } elsif ( $self->terms ) {
+ $msg .= ' - '. $self->mt($self->terms);
+ }
}
$msg;
}
alternate template name, optional
-=item print_text
-
-text attachment arrayref, optional
-
=item subject
email subject, optional
my $tc = $self->template_conf;
- if ( $conf->exists($tc.'html') ) {
+ my @text; # array of lines
+ my $html; # a big string
+ my @related_parts; # will contain the text/HTML alternative, and images
+ my $related; # will contain the multipart/related object
- warn "$me creating HTML/text multipart message"
- if $DEBUG;
+ if ( $conf->exists($tc. 'email_pdf') ) {
+ if ( my $msgnum = $conf->config($tc.'email_pdf_msgnum') ) {
- $return{'nobody'} = 1;
+ warn "$me using '${tc}email_pdf_msgnum' in multipart message"
+ if $DEBUG;
- my $alternative = build MIME::Entity
- 'Type' => 'multipart/alternative',
- #'Encoding' => '7bit',
- 'Disposition' => 'inline'
- ;
+ my $msg_template = FS::msg_template->by_key($msgnum)
+ or die "${tc}email_pdf_msgnum $msgnum not found\n";
+ my %prepared = $msg_template->prepare(
+ cust_main => $self->cust_main,
+ object => $self
+ );
+
+ @text = split(/(?=\n)/, $prepared{'text_body'});
+ $html = $prepared{'html_body'};
- my $data = '';
- if ( $conf->exists($tc. 'email_pdf')
- and scalar($conf->config($tc. 'email_pdf_note')) ) {
+ } elsif ( my @note = $conf->config($tc.'email_pdf_note') ) {
warn "$me using '${tc}email_pdf_note' in multipart message"
if $DEBUG;
- $data = [ map { $_ . "\n" }
- $conf->config($tc.'email_pdf_note')
- ];
+ @text = $conf->config($tc.'email_pdf_note');
+ $html = join('<BR>', @text);
+
+ } # else use the plain text invoice
+ }
+
+ if (!@text) {
+
+ if ( $conf->config($tc.'template') ) {
+
+ warn "$me generating plain text invoice"
+ if $DEBUG;
+
+ # 'print_text' argument is no longer used
+ @text = $self->print_text(\%args);
} else {
- warn "$me not using '${tc}email_pdf_note' in multipart message"
+ warn "$me no plain text version exists; sending empty message body"
if $DEBUG;
- if ( ref($args{'print_text'}) eq 'ARRAY' ) {
- $data = $args{'print_text'};
- } elsif ( $conf->exists($tc.'template') ) { #plaintext invoice_template
- $data = [ $self->print_text(\%args) ];
- }
}
- if ( $data ) {
- $alternative->attach(
- 'Type' => 'text/plain',
- 'Encoding' => 'quoted-printable',
- 'Charset' => 'UTF-8',
- #'Encoding' => '7bit',
- 'Data' => $data,
- 'Disposition' => 'inline',
- );
- }
+ }
- my $htmldata;
- my $image = '';
- my $barcode = '';
- if ( $conf->exists($tc.'email_pdf')
- and scalar($conf->config($tc.'email_pdf_note')) ) {
+ my $text_part = build MIME::Entity (
+ 'Type' => 'text/plain',
+ 'Encoding' => 'quoted-printable',
+ 'Charset' => 'UTF-8',
+ #'Encoding' => '7bit',
+ 'Data' => \@text,
+ 'Disposition' => 'inline',
+ );
- $htmldata = join('<BR>', $conf->config($tc.'email_pdf_note') );
+ if (!$html) {
- } else {
+ if ( $conf->exists($tc.'html') ) {
+ warn "$me generating HTML invoice"
+ if $DEBUG;
$args{'from'} =~ /\@([\w\.\-]+)/;
my $from = $1 || 'example.com';
}
my $image_data = $conf->config_binary( $logo, $agentnum);
- $image = build MIME::Entity
+ push @related_parts, build MIME::Entity
'Type' => 'image/png',
'Encoding' => 'base64',
'Data' => $image_data,
if ( ref($self) eq 'FS::cust_bill' && $conf->exists('invoice-barcode') ) {
my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
- $barcode = build MIME::Entity
+ push @related_parts, build MIME::Entity
'Type' => 'image/png',
'Encoding' => 'base64',
'Data' => $self->invoice_barcode(0),
$args{'barcode_cid'} = $barcode_content_id;
}
- $htmldata = $self->print_html({ 'cid'=>$content_id, %args });
+ $html = $self->print_html({ 'cid'=>$content_id, %args });
+ }
+
+ }
+
+ if ( $html ) {
+
+ warn "$me creating HTML/text multipart message"
+ if $DEBUG;
+
+ $return{'nobody'} = 1;
+
+ my $alternative = build MIME::Entity
+ 'Type' => 'multipart/alternative',
+ #'Encoding' => '7bit',
+ 'Disposition' => 'inline'
+ ;
+
+ if ( @text ) {
+ $alternative->add_part($text_part);
}
$alternative->attach(
' </title>',
' </head>',
' <body bgcolor="#e8e8e8">',
- $htmldata,
+ $html,
' </body>',
'</html>',
],
#'Filename' => 'invoice.pdf',
);
+ unshift @related_parts, $alternative;
- my @otherparts = ();
- if ( ref($self) eq 'FS::cust_bill' && $cust_main->email_csv_cdr ) {
+ $related = build MIME::Entity 'Type' => 'multipart/related',
+ 'Encoding' => '7bit';
- push @otherparts, build MIME::Entity
- 'Type' => 'text/csv',
- 'Encoding' => '7bit',
- 'Data' => [ map { "$_\n" }
- $self->call_details('prepend_billed_number' => 1)
- ],
- 'Disposition' => 'attachment',
- 'Filename' => 'usage-'. $self->invnum. '.csv',
- ;
-
- }
-
- if ( $conf->exists($tc.'email_pdf') ) {
-
- #attaching pdf too:
- # multipart/mixed
- # multipart/related
- # multipart/alternative
- # text/plain
- # text/html
- # image/png
- # application/pdf
-
- my $related = build MIME::Entity 'Type' => 'multipart/related',
- 'Encoding' => '7bit';
-
- #false laziness w/Misc::send_email
- $related->head->replace('Content-type',
- $related->mime_type.
- '; boundary="'. $related->head->multipart_boundary. '"'.
- '; type=multipart/alternative'
- );
-
- $related->add_part($alternative);
-
- $related->add_part($image) if $image;
-
- my $pdf = build MIME::Entity $self->mimebuild_pdf(\%args);
+ #false laziness w/Misc::send_email
+ $related->head->replace('Content-type',
+ $related->mime_type.
+ '; boundary="'. $related->head->multipart_boundary. '"'.
+ '; type=multipart/alternative'
+ );
- $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
+ $related->add_part($_) foreach @related_parts;
- } else {
+ }
- #no other attachment:
- # multipart/related
- # multipart/alternative
- # text/plain
- # text/html
- # image/png
+ my @otherparts = ();
+ if ( ref($self) eq 'FS::cust_bill' && $cust_main->email_csv_cdr ) {
- $return{'content-type'} = 'multipart/related';
- if ($conf->exists('invoice-barcode') && $barcode) {
- $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
- } else {
- $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
- }
- $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
- #$return{'disposition'} = 'inline';
+ push @otherparts, build MIME::Entity
+ 'Type' => 'text/csv',
+ 'Encoding' => '7bit',
+ 'Data' => [ map { "$_\n" }
+ $self->call_details('prepend_billed_number' => 1)
+ ],
+ 'Disposition' => 'attachment',
+ 'Filename' => 'usage-'. $self->invnum. '.csv',
+ ;
- }
-
- } else {
+ }
- if ( $conf->exists($tc.'email_pdf') ) {
- warn "$me creating PDF attachment"
- if $DEBUG;
+ if ( $conf->exists($tc.'email_pdf') ) {
- #mime parts arguments a la MIME::Entity->build().
- $return{'mimeparts'} = [
- { $self->mimebuild_pdf(\%args) }
- ];
- }
-
- if ( $conf->exists($tc.'email_pdf')
- and scalar($conf->config($tc.'email_pdf_note')) ) {
+ #attaching pdf too:
+ # multipart/mixed
+ # multipart/related
+ # multipart/alternative
+ # text/plain
+ # text/html
+ # image/png
+ # application/pdf
- warn "$me using '${tc}email_pdf_note'"
- if $DEBUG;
- $return{'body'} = [ map { $_ . "\n" }
- $conf->config($tc.'email_pdf_note')
- ];
+ my $pdf = build MIME::Entity $self->mimebuild_pdf(\%args);
+ push @otherparts, $pdf;
+ }
+ if (@otherparts) {
+ $return{'content-type'} = 'multipart/mixed'; # of the outer container
+ if ( $html ) {
+ $return{'mimeparts'} = [ $related, @otherparts ];
+ $return{'type'} = 'multipart/related'; # of the first part
} else {
-
- warn "$me not using '${tc}email_pdf_note'"
- if $DEBUG;
- if ( ref($args{'print_text'}) eq 'ARRAY' ) {
- $return{'body'} = $args{'print_text'};
- } else {
- $return{'body'} = [ $self->print_text(\%args) ];
- }
-
+ $return{'mimeparts'} = [ $text_part, @otherparts ];
+ $return{'type'} = 'text/plain';
}
-
+ } elsif ( $html ) { # no PDF or CSV, strip the outer container
+ $return{'mimeparts'} = \@related_parts;
+ $return{'content-type'} = 'multipart/related';
+ $return{'type'} = 'multipart/alternative';
+ } else { # no HTML either
+ $return{'body'} = \@text;
+ $return{'content-type'} = 'text/plain';
}
%return;
my $multisection = defined($category) || defined($locationnum);
my $discount_show_always = 0;
- my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
+ my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
- # and location labels
+
+ # for location labels: use default location on the invoice date
+ my $default_locationnum;
+ if ( $self->custnum ) {
+ my $h_cust_main;
+ my @h_search = FS::h_cust_main->sql_h_search($self->_date);
+ $h_cust_main = qsearchs({
+ 'table' => 'h_cust_main',
+ 'hashref' => { custnum => $self->custnum },
+ 'extra_sql' => $h_search[1],
+ 'addl_from' => $h_search[3],
+ }) || $cust_main;
+ $default_locationnum = $h_cust_main->ship_locationnum;
+ } elsif ( $self->prospectnum ) {
+ my $cust_location = qsearchs('cust_location',
+ { prospectnum => $self->prospectnum,
+ disabled => '' });
+ $default_locationnum = $cust_location->locationnum if $cust_location;
+ }
my @b = (); # accumulator for the line item hashes that we'll return
my ($s, $r, $u, $d) = ( undef, undef, undef, undef );
);
if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
+ # XXX this should be pulled out into quotation_pkg
warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
if $DEBUG > 1;
push @d, @svc_labels
unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
- my $lnum = $cust_main ? $cust_main->ship_locationnum
- : $self->prospect_main->locationnum;
# show the location label if it's not the customer's default
# location, and we're not grouping items by location already
- if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
+ if ( $cust_pkg->locationnum != $default_locationnum
+ and !defined($locationnum) ) {
my $loc = $cust_pkg->location_label;
$loc = substr($loc, 0, $maxlength). '...'
if $format eq 'latex' && length($loc) > $maxlength;
warn "$me _items_cust_bill_pkg done adding service details\n"
if $DEBUG > 1;
- my $lnum = $cust_main ? $cust_main->ship_locationnum
- : $self->prospect_main->locationnum;
# show the location label if it's not the customer's default
# location, and we're not grouping items by location already
- if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
+ if ( $cust_pkg->locationnum != $default_locationnum
+ and !defined($locationnum) ) {
my $loc = $cust_pkg->location_label;
$loc = substr($loc, 0, $maxlength). '...'
if $format eq 'latex' && length($loc) > $maxlength;