X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2FTemplate_Mixin.pm;h=a5fb5579f021417523726398bbee98092058fed4;hb=c70ef4f4ec2a61b32c7e6aee40b3130cfd4381e5;hp=996cb55a406842fa10aea2983661f85e929edbcb;hpb=167c155032545af623972ffb449455810d0a2b78;p=freeside.git diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index 996cb55a4..a5fb5579f 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -7,7 +7,7 @@ use vars qw( $DEBUG $me ); # but NOT $conf use vars qw( $invoice_lines @buf ); #yuck -use List::Util qw(sum); +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; @@ -316,9 +316,6 @@ sub print_generic { unless $format =~ /^(latex|html|template)$/; my $cust_main = $self->cust_main || $self->prospect_main; - $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') ) - unless $cust_main->payname - && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/; my $locale = $params{'locale'} || $cust_main->locale; @@ -347,8 +344,8 @@ sub print_generic { if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) { #change this to a die when the old code is removed - # it's been almost ten years, changing it to a die on the next release. - warn "old-style invoice template $templatefile; ". + # it's been almost ten years, changing it to a die + die "old-style invoice template $templatefile; ". "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n"; #$old_latex = 'true'; #@invoice_template = _translate_old_latex_format(@invoice_template); @@ -565,12 +562,14 @@ sub print_generic { 'custnum' => $cust_main->display_custnum, 'prospectnum' => $cust_main->prospectnum, 'agent_custid' => &$escape_function($cust_main->agent_custid), - ( map { $_ => &$escape_function($cust_main->$_()) } qw( - payname company address1 address2 city state zip fax - )), + ( map { $_ => &$escape_function($cust_main->$_()) } + qw( company address1 address2 city state zip fax ) + ), + 'payname' => &$escape_function( $cust_main->invoice_attn + || $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'), @@ -655,10 +654,10 @@ sub print_generic { my @address = (); $invoice_data{'address'} = \@address; push @address, - $cust_main->payname. - ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo - ? " (P.O. #". $cust_main->payinfo. ")" - : '' + $invoice_data{'payname'}. + ( $cust_main->po_number + ? " (P.O. #". $cust_main->po_number. ")" + : '' ) ; push @address, $cust_main->company @@ -691,11 +690,12 @@ sub print_generic { # (this is used in the summary & on the payment coupon) $invoice_data{'balance'} = sprintf("%.2f", $balance_due); - # info from customer's last invoice before this one, for some - # summary formats - $invoice_data{'last_bill'} = {}; + # flag telling this invoice to have a first-page summary + my $summarypage = ''; if ( $self->custnum && $self->invnum ) { + # XXX should be an FS::cust_bill method to set the defaults, instead + # of checking the type here my $last_bill = $self->previous_bill; if ( $last_bill ) { @@ -801,13 +801,16 @@ sub print_generic { $invoice_data{'previous_payments'} = []; $invoice_data{'previous_credits'} = []; } - } # if this is an invoice - my $summarypage = ''; - if ( $conf->exists('invoice_usesummary', $agentnum) ) { - $summarypage = 1; - } - $invoice_data{'summarypage'} = $summarypage; + # 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; + } + + } # if this is an invoice warn "$me substituting variables in notes, footer, smallfooter\n" if $DEBUG > 1; @@ -904,29 +907,6 @@ sub print_generic { warn "$me generating sections\n" if $DEBUG > 1; - my $taxtotal = 0; - my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'), - 'subtotal' => $taxtotal, # adjusted below - 'tax_section' => 1, - }; - my $tax_weight = _pkg_category($tax_section->{description}) - ? _pkg_category($tax_section->{description})->weight - : 0; - $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : ''; - $tax_section->{'sort_weight'} = $tax_weight; - - my $adjusttotal = 0; - my $adjust_section = { - 'description' => $self->mt('Credits, Payments, and Adjustments'), - 'adjust_section' => 1, - 'subtotal' => 0, # adjusted below - }; - my $adjust_weight = _pkg_category($adjust_section->{description}) - ? _pkg_category($adjust_section->{description})->weight - : 0; - $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : ''; - $adjust_section->{'sort_weight'} = $adjust_weight; - my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y'; my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) || $conf->exists($tc.'sections_by_location', $cust_main->agentnum); @@ -967,6 +947,21 @@ sub print_generic { $previous_section = $default_section; } + my $adjust_section = { + 'description' => $self->mt('Credits, Payments, and Adjustments'), + 'adjust_section' => 1, + 'subtotal' => 0, # adjusted below + }; + my $adjust_weight = _pkg_category($adjust_section->{description}) + ? _pkg_category($adjust_section->{description})->weight + : 0; + $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : ''; + # Note: 'sort_weight' here is actually a flag telling whether there is an + # explicit package category for the adjust section. If so, certain behavior + # happens. + $adjust_section->{'sort_weight'} = $adjust_weight; + + if ( $multisection ) { ($extra_sections, $extra_lines) = $self->_items_extra_usage_sections($escape_function_nonbsp, $format) @@ -1172,6 +1167,12 @@ sub print_generic { join(', ', map "$_=>".$line_item->{$_}, keys %$line_item). "\n" if $DEBUG > 1; + push @buf, ( [ $line_item->{'description'}, + $money_char. sprintf("%10.2f", $line_item->{'amount'}), + ], + map { [ " ". $_, '' ] } @{$line_item->{'ext_description'}}, + ); + $line_item->{'ref'} = $line_item->{'pkgnum'}; $line_item->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; # mt()? $line_item->{'section'} = $section; @@ -1184,11 +1185,6 @@ sub print_generic { $line_item->{'ext_description'} ||= []; push @detail_items, $line_item; - push @buf, ( [ $line_item->{'description'}, - $money_char. sprintf("%10.2f", $line_item->{'amount'}), - ], - map { [ " ". $_, '' ] } @{$line_item->{'ext_description'}}, - ); } if ( $section->{'description'} ) { @@ -1215,6 +1211,27 @@ sub print_generic { warn "$me adding taxes\n" if $DEBUG > 1; + # create a tax section if we don't yet have one + my $tax_description = 'Taxes, Surcharges, and Fees'; + 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. + # grab the taxtotal that's already there, strip the money symbol if any + my $taxtotal = $tax_section->{'subtotal'} || 0; + $taxtotal =~ s/^\Q$other_money_char\E//; + + # this does nothing + #my $tax_weight = _pkg_category($tax_section->{description}) + # ? _pkg_category($tax_section->{description})->weight + # : 0; + #$tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : ''; + #$tax_section->{'sort_weight'} = $tax_weight; + my @items_tax = $self->_items_tax; foreach my $tax ( @items_tax ) { @@ -1249,7 +1266,7 @@ sub print_generic { ]; } - + if ( @items_tax ) { my $total = {}; $total->{'total_item'} = $self->mt('Sub-total'); @@ -1257,14 +1274,20 @@ sub print_generic { $other_money_char. sprintf('%.2f', $self->charged - $taxtotal ); if ( $multisection ) { - $tax_section->{'subtotal'} = $other_money_char. - sprintf('%.2f', $taxtotal); - $tax_section->{'pretotal'} = 'New charges sub-total '. - $total->{'total_amount'}; - if ( $taxtotal ) { - push @sections, $tax_section; - push @summary_subtotals, $tax_section; + if ( $taxtotal > 0 ) { + $tax_section->{'subtotal'} = $other_money_char. + sprintf('%.2f', $taxtotal); + $tax_section->{'pretotal'} = 'New charges sub-total '. + $total->{'total_amount'}; + $tax_section->{'description'} = $self->mt($tax_description); + + # append it if it's not already there + if ( !grep $tax_section, @sections ) { + push @sections, $tax_section; + push @summary_subtotals, $tax_section; + } } + } else { unshift @total_items, $total; } @@ -1280,7 +1303,6 @@ sub print_generic { $money_char. sprintf("%10.2f",$self->charged) ]; push @buf,['','']; - ### # Totals ### @@ -1294,13 +1316,12 @@ sub print_generic { if ( $self->can('_items_total') ) { # quotations - $self->_items_total(\@total_items); + my @new_total_items = $self->_items_total; - foreach ( @total_items ) { + foreach ( @new_total_items ) { $_->{'total_item'} = &$embolden_function( $_->{'total_item'} ); - $_->{'total_amount'} = &$embolden_function( $other_money_char. - $_->{'total_amount'} - ); + $_->{'total_amount'} = &$embolden_function( $other_money_char.$_->{'total_amount'}); + push @total_items, $_; } } else { #normal invoice case @@ -1349,14 +1370,13 @@ sub print_generic { # credits my $credittotal = 0; foreach my $credit ( - $self->_items_credits( 'template' => $template, 'trim_len' => 60 ) + $self->_items_credits( 'template' => $template, 'trim_len' => 50 ) ) { my $total; $total->{'total_item'} = &$escape_function($credit->{'description'}); $credittotal += $credit->{'amount'}; $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'}; - $adjusttotal += $credit->{'amount'}; if ( $multisection ) { push @detail_items, { ext_description => [], @@ -1390,7 +1410,6 @@ sub print_generic { $total->{'total_item'} = &$escape_function($payment->{'description'}); $paymenttotal += $payment->{'amount'}; $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'}; - $adjusttotal += $payment->{'amount'}; if ( $multisection ) { push @detail_items, { ext_description => [], @@ -1412,7 +1431,10 @@ sub print_generic { if ( $multisection ) { $adjust_section->{'subtotal'} = $other_money_char. - sprintf('%.2f', $adjusttotal); + sprintf('%.2f', $credittotal + $paymenttotal); + + #why this? because {sort_weight} forces the adjust_section to appear + #in @extra_sections instead of @sections. obviously. push @sections, $adjust_section unless $adjust_section->{sort_weight}; # do not summarize; adjustments there are shown according to @@ -1521,7 +1543,7 @@ sub print_generic { # 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 ) { @@ -2014,10 +2036,6 @@ sender address, required alternate template name, optional -=item print_text - -text attachment arrayref, optional - =item subject email subject, optional @@ -2061,61 +2079,61 @@ sub generate_email { 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 + ); - my $data = ''; - if ( $conf->exists($tc. 'email_pdf') - and scalar($conf->config($tc. 'email_pdf_note')) ) { + @text = split(/(?=\n)/, $prepared{'text_body'}); + $html = $prepared{'html_body'}; + + } 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('
', @text); + + } # else use the plain text invoice + } - } else { + if (!@text) { - warn "$me not using '${tc}email_pdf_note' in multipart message" - 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) ]; - } + warn "$me generating plain text invoice" + if $DEBUG; - } + # 'print_text' argument is no longer used + @text = $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('
', $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'; @@ -2134,7 +2152,7 @@ sub generate_email { } 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, @@ -2144,7 +2162,7 @@ sub generate_email { 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), @@ -2154,7 +2172,26 @@ sub generate_email { $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( @@ -2167,7 +2204,7 @@ sub generate_email { ' ', ' ', ' ', - $htmldata, + $html, ' ', '', ], @@ -2175,104 +2212,68 @@ sub generate_email { #'Filename' => 'invoice.pdf', ); + unshift @related_parts, $alternative; - my @otherparts = (); - if ( ref($self) eq 'FS::cust_bill' && $cust_main->email_csv_cdr ) { - - 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 = build MIME::Entity 'Type' => 'multipart/related', + 'Encoding' => '7bit'; - $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; @@ -2789,11 +2790,16 @@ equivalent to $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ]) -The only OPTIONS accepted is 'section', which may point to a hashref -with a key named 'condensed', which may have a true value. If it -does, this method tries to merge identical items into items with -'quantity' equal to the number of items (not the sum of their -separate quantities, for some reason). +OPTIONS are passed through to _items_cust_bill_pkg, and should include +'format' and 'escape_function' at minimum. + +To produce items for a specific invoice section, OPTIONS should include +'section', a hashref containing 'category' and/or 'locationnum' keys. + +'section' may also contain a key named 'condensed'. If this is present +and has a true value, _items_pkg will try to merge identical items into items +with 'quantity' equal to the number of items (not the sum of their separate +quantities, for some reason). =cut @@ -2825,6 +2831,8 @@ sub _items_fee { my $self = shift; my %options = @_; my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg; + my $escape_function = $options{escape_function}; + my @items; foreach my $cust_bill_pkg (@cust_bill_pkg) { # cache this, so we don't look it up again in every section @@ -2859,13 +2867,19 @@ sub _items_fee { } foreach (sort keys(%base_invnums)) { next if $_ == $self->invnum; + # per convention, we must escape ext_description lines push @ext_desc, - $self->mt('from invoice \\#[_1] on [_2]', $_, $base_invnums{$_}); + &{$escape_function}( + $self->mt('from invoice #[_1] on [_2]', $_, $base_invnums{$_}) + ); } + my $desc = $part_fee->itemdesc_locale($self->cust_main->locale); + # but not escape the base description line + push @items, { feepart => $cust_bill_pkg->feepart, amount => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur), - description => $part_fee->itemdesc_locale($self->cust_main->locale), + description => $desc, ext_description => \@ext_desc # sdate/edate? }; @@ -2998,7 +3012,25 @@ sub _items_cust_bill_pkg { my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50; 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 ); @@ -3079,6 +3111,7 @@ sub _items_cust_bill_pkg { ); 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; @@ -3092,7 +3125,9 @@ sub _items_cust_bill_pkg { if $cust_bill_pkg->recur != 0 || $discount_show_always || $cust_bill_pkg->recur_show_zero; - push @b, { + #push @b, { + # keep it consistent, please + $s = { 'pkgnum' => $cust_bill_pkg->pkgpart, #so it displays in Ref 'description' => $description, 'amount' => sprintf("%.2f", $cust_bill_pkg->setup), @@ -3105,7 +3140,8 @@ sub _items_cust_bill_pkg { }; } if ( $cust_bill_pkg->recur != 0 ) { - push @b, { + #push @b, { + $r = { 'pkgnum' => $cust_bill_pkg->pkgpart, #so it displays in Ref 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")", 'amount' => sprintf("%.2f", $cust_bill_pkg->recur), @@ -3179,11 +3215,10 @@ sub _items_cust_bill_pkg { 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; @@ -3281,11 +3316,10 @@ sub _items_cust_bill_pkg { 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; @@ -3398,89 +3432,6 @@ sub _items_cust_bill_pkg { } # recurring or usage with recurring charge - # decide whether to show active discounts here - if ( - # case 1: we are showing a single line for the package - ( !$type ) - # case 2: we are showing a setup line for a package that has - # no base recurring fee - or ( $type eq 'S' and $cust_bill_pkg->unitrecur == 0 ) - # case 3: we are showing a recur line for a package that has - # a base recurring fee - or ( $type eq 'R' and $cust_bill_pkg->unitrecur > 0 ) - ) { - - # the line item hashref for the line that will show the original - # price - # (use the recur or single line for the package, unless we're - # showing a setup line for a package with no recurring fee) - my $active_line = $r; - if ( $type eq 'S' ) { - $active_line = $s; - } - - my @discounts = $cust_bill_pkg->cust_bill_pkg_discount; - # special case: if there are old "discount details" on this line - # item, don't show discount line items - if ( FS::cust_bill_pkg_detail->count( - "detail LIKE 'Includes discount%' AND billpkgnum = " . - $cust_bill_pkg->billpkgnum - ) > 0 ) { - @discounts = (); - } - if ( @discounts ) { - warn "$me _items_cust_bill_pkg including discounts for ". - $cust_bill_pkg->billpkgnum."\n" - if $DEBUG; - my $discount_amount = sum( map {$_->amount} @discounts ); - # if multiple discounts apply to the same package, how to display - # them? ext_description lines, apparently - # - # # discount amounts are negative - if ( $d and $cust_bill_pkg->hidden ) { - $d->{amount} -= $discount_amount; - } else { - my @ext; - $d = { - _is_discount => 1, - description => $self->mt('Discount'), - amount => -1 * $discount_amount, - ext_description => \@ext, - }; - foreach my $cust_bill_pkg_discount (@discounts) { - my $discount = $cust_bill_pkg_discount->cust_pkg_discount->discount; - my $discount_desc = $discount->description_short; - - if ($discount->months) { - - # calculate months remaining after this invoice - my $used = FS::Record->scalar_sql( - 'SELECT SUM(months) FROM cust_bill_pkg_discount - JOIN cust_bill_pkg USING (billpkgnum) - JOIN cust_bill USING (invnum) - WHERE pkgdiscountnum = ? AND _date <= ?', - $cust_bill_pkg_discount->pkgdiscountnum, - $self->_date - ); - $used ||= 0; - my $remaining = sprintf('%.2f', $discount->months - $used); - # append "for X months (Y months remaining)" - $discount_desc .= $self->mt(' for [quant,_1,month] ([quant,_2,month] remaining)', - $cust_bill_pkg_discount->months, - $remaining - ); - } # else it's not time-limited - push @ext, &{$escape_function}($discount_desc); - } - } - - # update the active line (before the discount) to show the - # original price (whether this is a hidden line or not) - $active_line->{amount} += $discount_amount; - - } # if there are any discounts - } # if this is an appropriate place to show discounts - } else { # taxes and fees warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n" @@ -3495,6 +3446,56 @@ sub _items_cust_bill_pkg { } # if quotation / package line item / other line item + # decide whether to show active discounts here + if ( + # case 1: we are showing a single line for the package + ( !$type ) + # case 2: we are showing a setup line for a package that has + # no base recurring fee + or ( $type eq 'S' and $cust_bill_pkg->unitrecur == 0 ) + # case 3: we are showing a recur line for a package that has + # a base recurring fee + or ( $type eq 'R' and $cust_bill_pkg->unitrecur > 0 ) + ) { + + my $item_discount = $cust_bill_pkg->_item_discount; + if ( $item_discount ) { + # $item_discount->{amount} is negative + + if ( $d and $cust_bill_pkg->hidden ) { + $d->{amount} += $item_discount->{amount}; + } else { + $d = $item_discount; + $_ = &{$escape_function}($_) foreach @{ $d->{ext_description} }; + } + + # update the active line (before the discount) to show the + # original price (whether this is a hidden line or not) + # + # quotation discounts keep track of setup and recur; invoice + # discounts currently don't + if ( exists $item_discount->{setup_amount} ) { + + $s->{amount} -= $item_discount->{setup_amount} if $s; + $r->{amount} -= $item_discount->{recur_amount} if $r; + + } else { + + # $active_line is the line item hashref for the line that will + # show the original price + # (use the recur or single line for the package, unless we're + # showing a setup line for a package with no recurring fee) + my $active_line = $r; + if ( $type eq 'S' ) { + $active_line = $s; + } + $active_line->{amount} -= $item_discount->{amount}; + + } + + } # if there are any discounts + } # if this is an appropriate place to show discounts + } # foreach $display $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount