use Date::Language;
use Text::Template 1.20;
use File::Temp 0.14;
+use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
+use IO::Scalar;
use HTML::Entities;
-use Locale::Country;
use Cwd;
use FS::UID;
use FS::Misc qw( send_email );
if ( $cust_main->country eq $countrydefault ) {
$invoice_data{'country'} = '';
} else {
- $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
+ $invoice_data{'country'} = &$escape_function($cust_main->bill_country_full);
}
my @address = ();
my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
# my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
#my $balance_due = $self->owed + $pr_total - $cr_total;
- my $balance_due = $self->owed + $pr_total;
+ my $balance_due = $self->owed;
+ if ( $self->enable_previous ) {
+ $balance_due += $pr_total;
+ }
+ # otherwise the previous balance is not shown, so including it in the
+ # balance due is just confusing
# the sum of amount owed on all invoices
# (this is used in the summary & on the payment coupon)
# "balance_date_range" unfortunately is unsuitable for this, since it
# cares about application dates. We want to know the sum of all
# _top-level transactions_ dated before the last invoice.
+ #
+ # still do this for the "Previous Balance" line of the summary block
my @sql =
map "$_ WHERE _date <= ? AND custnum = ?", (
"SELECT COALESCE( SUM(charged), 0 ) FROM cust_bill",
# longer stored in the database)
$invoice_data{'true_previous_balance'} = $last_bill_balance;
- # the change in balance from immediately after that invoice
- # to immediately before this one
- my $before_this_bill_balance = 0;
+ # Now, get all applications of credits/payments dated on or after the
+ # previous bill, to invoices before the current bill. (The
+ # credit/payment date restriction prevents these from intersecting
+ # the "Previous Balance" set.)
+ # These are "adjustments". The past due balance will be shown as
+ # Previous Balance - Adjustments.
+ my $adjustments = 0;
+ @sql = map {
+ "SELECT COALESCE(SUM(y.amount),0) FROM $_ JOIN cust_bill USING (invnum)
+ WHERE cust_bill._date < ?
+ AND x._date >= ?
+ AND cust_bill.custnum = ?"
+ } "cust_credit AS x JOIN cust_credit_bill y USING (crednum)",
+ "cust_pay AS x JOIN cust_bill_pay y USING (paynum)"
+ ;
foreach (@sql) {
my $delta = FS::Record->scalar_sql(
$_,
- $self->_date - 1,
+ $self->_date,
+ $last_bill->_date,
$self->custnum,
);
- $before_this_bill_balance += $delta;
+ $adjustments += $delta;
}
- $invoice_data{'balance_adjustments'} =
- sprintf("%.2f", $last_bill_balance - $before_this_bill_balance);
+ $invoice_data{'balance_adjustments'} = sprintf("%.2f", $adjustments);
warn sprintf("BALANCE ADJUSTMENTS: %.2f\n\n",
$invoice_data{'balance_adjustments'}
if ($format eq 'latex');
}
- # let invoices use either of these as needed
- $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
- ? $cust_main->payinfo : '';
- $invoice_data{'po_line'} =
- ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
- ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
- : $nbsp;
+# if (well, probably when) we still need PO numbers in the brave new world of
+# 4.x, then we'll have to add them back as their own customer fields
+# # let invoices use either of these as needed
+# $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
+# ? $cust_main->payinfo : '';
+# $invoice_data{'po_line'} =
+# ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
+# ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
+# : $nbsp;
my %money_chars = ( 'latex' => '',
'html' => $conf->config('money_char') || '$',
if ( $multisection ) {
if ( $taxtotal > 0 ) {
+ # there are taxes, so prepare the section to be displayed.
+ # $taxtotal already includes any line items that were already in the
+ # section (fees, taxes that are charged as packages for some reason).
+ # also set 'summarized' to false so that this isn't a summary-only
+ # section.
$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);
+ $tax_section->{'summarized'} = '';
# append it if it's not already there
if ( !grep $tax_section, @sections ) {
# usage subtotals
if ( $conf->exists('usage_class_summary')
and $self->can('_items_usage_class_summary') ) {
- my @usage_subtotals = $self->_items_usage_class_summary(escape => $escape_function);
+ my @usage_subtotals = $self->_items_usage_class_summary(escape => $escape_function, 'money_char' => $other_money_char);
if ( @usage_subtotals ) {
unshift @sections, $usage_subtotals[0]->{section}; # do not summarize
unshift @detail_items, @usage_subtotals;
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->config('voip-cdr_email_attach') eq 'zip' ) {
+
+ my $data = join('', map "$_\n",
+ $self->call_details(prepend_billed_number=>1)
+ );
+
+ my $zip = new Archive::Zip;
+ my $file = $zip->addString( $data, 'usage-'.$self->invnum.'.csv' );
+ $file->desiredCompressionMethod( COMPRESSION_DEFLATED );
+
+ my $zipdata = '';
+ my $SH = IO::Scalar->new(\$zipdata);
+ my $status = $zip->writeToFileHandle($SH);
+ die "Error zipping CDR attachment: $!" unless $status == AZ_OK;
+
+ push @otherparts, build MIME::Entity
+ 'Type' => 'application/zip',
+ 'Encoding' => 'base64',
+ 'Data' => $zipdata,
+ 'Disposition' => 'attachment',
+ 'Filename' => 'usage-'. $self->invnum. '.zip',
+ ;
+
+ } else { # } elsif ( $conf->config('voip-cdr_email_attach') eq 'csv' ) {
+
+ 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',
+ ;
+
+ }
}
foreach my $sectionname (keys %{ $s->{$locationnum} }) {
my $section = {
'subtotal' => $s->{$locationnum}{$sectionname},
- 'post_total' => $post_total,
'sort_weight' => 0,
};
if ( $locationnum ) {
multisection: a flag indicating that this is a multisection invoice,
which does something complicated.
-preref_callback: coderef run for each line item, code should return HTML to be
-displayed before that line item (quotations only)
-
Returns a list of hashrefs, each of which may contain:
pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
}
my $summary_page = $opt{summary_page} || ''; #unused
my $multisection = defined($category) || defined($locationnum);
- my $discount_show_always = 0;
+ # this variable is the value of the config setting, not whether it applies
+ # to this particular line item.
+ my $discount_show_always = $conf->exists('discount-show-always');
my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
# if the current line item is waiting to go out, and the one we're about
# to start is not bundled, then push out the current one and start a new
# one.
+ if ( $d ) {
+ $d->{amount} = $d->{setup_amount} + $d->{recur_amount};
+ }
foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
if ( $_ && !$cust_bill_pkg->hidden ) {
$_->{amount} = sprintf( "%.2f", $_->{amount} );
if (exists($_->{unit_amount})) {
$_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
}
- push @b, { %$_ }
- if $_->{amount} != 0
- || $discount_show_always
- || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
- || ( $_->{_is_setup} && $_->{setup_show_zero} )
+ push @b, { %$_ };
+ # we already decided to create this display line; don't reconsider it
+ # now.
+ # if $_->{amount} != 0
+ # || $discount_show_always
+ # || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
+ # || ( $_->{_is_setup} && $_->{setup_show_zero} )
;
$_ = undef;
}
'no_usage' => $opt{'no_usage'},
);
- 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;
- # quotation_pkgs are never fees, so don't worry about the case where
- # part_pkg is undefined
-
- # and I guess they're never bundled either?
- if ( $cust_bill_pkg->setup != 0 ) {
- my $description = $desc;
- $description .= ' Setup'
- if $cust_bill_pkg->recur != 0
- || $discount_show_always
- || $cust_bill_pkg->recur_show_zero;
- #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),
- 'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitsetup),
- 'quantity' => $cust_bill_pkg->quantity,
- 'preref_html' => ( $opt{preref_callback}
- ? &{ $opt{preref_callback} }( $cust_bill_pkg )
- : ''
- ),
- };
- }
- if ( $cust_bill_pkg->recur != 0 ) {
- #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),
- 'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitrecur),
- 'quantity' => $cust_bill_pkg->quantity,
- 'preref_html' => ( $opt{preref_callback}
- ? &{ $opt{preref_callback} }( $cust_bill_pkg )
- : ''
- ),
- };
- }
-
- } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
+ if ( $cust_bill_pkg->pkgnum > 0 ) {
# a "normal" package line item (not a quotation, not a fee, not a tax)
warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
if ( (!$type || $type eq 'S')
&& ( $cust_bill_pkg->setup != 0
|| $cust_bill_pkg->setup_show_zero
+ || ($discount_show_always and $cust_bill_pkg->unitsetup > 0)
)
)
{
warn "$me _items_cust_bill_pkg adding setup\n"
if $DEBUG > 1;
+ # append the word 'Setup' to the setup line if there's going to be
+ # a recur line for the same package (i.e. not a one-time charge)
my $description = $desc;
$description .= ' Setup'
if $cust_bill_pkg->recur != 0
- || $discount_show_always
+ || ($discount_show_always and $cust_bill_pkg->unitrecur > 0)
|| $cust_bill_pkg->recur_show_zero;
$description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
}
+ # should we show a recur line?
+ # if type eq 'S', then NO, because we've been told not to.
+ # otherwise, show the recur line if:
+ # - there's a recurring charge
+ # - or recur_show_zero is on
+ # - or there's a positive unitrecur (so it's been discounted to zero)
+ # and discount-show-always is on
if ( ( !$type || $type eq 'R' || $type eq 'U' )
&& (
$cust_bill_pkg->recur != 0
- || $cust_bill_pkg->setup == 0
- || $discount_show_always
+ || !defined($s)
+ || ($discount_show_always and $cust_bill_pkg->unitrecur > 0)
|| $cust_bill_pkg->recur_show_zero
)
)
+ $cust_bill_pkg->recur)
};
- } # if quotation / package line item / other line item
+ } # if package line item / other line item
# decide whether to show active discounts here
if (
# $item_discount->{amount} is negative
if ( $d and $cust_bill_pkg->hidden ) {
- $d->{amount} += $item_discount->{amount};
+ $d->{setup_amount} += $item_discount->{setup_amount};
+ $d->{recur_amount} += $item_discount->{recur_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};
-
- }
+ $s->{amount} -= $item_discount->{setup_amount} if $s;
+ $r->{amount} -= $item_discount->{recur_amount} if $r;
} # 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
- && $conf->exists('discount-show-always'));
+ }
+ # discount amount is internally split up
+ if ( $d ) {
+ $d->{amount} = $d->{setup_amount} + $d->{recur_amount};
}
foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
$_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
}
- push @b, { %$_ }
- if $_->{amount} != 0
- || $discount_show_always
- || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
- || ( $_->{_is_setup} && $_->{setup_show_zero} )
+ push @b, { %$_ };
+ #if $_->{amount} != 0
+ # || $discount_show_always
+ # || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
+ # || ( $_->{_is_setup} && $_->{setup_show_zero} )
}
}