$DEBUG = 0;
$me = '[FS::Template_Mixin]';
-FS::UID->install_callback( sub {
+FS::UID->install_callback( sub {
my $conf = new FS::Conf; #global
- $money_char = $conf->config('money_char') || '$';
+ $money_char = $conf->config('money_char') || '$';
$date_format = $conf->config('date_format') || '%x'; #/YY
} );
It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
L<Time::Local> and L<Date::Parse> for conversion functions.
-I<template>, if specified, is the name of a suffix for alternate invoices.
+I<template>, if specified, is the name of a suffix for alternate invoices.
This is strongly deprecated; see L<FS::invoice_conf> for the right way to
customize invoice templates for different purposes.
close $lh;
$params{'logo_file'} = $lh->filename;
- if( $conf->exists('invoice-barcode')
+ if( $conf->exists('invoice-barcode')
&& $self->can('invoice_barcode')
&& $self->invnum ) { # don't try to barcode statements
my $png_file = $self->invoice_barcode($dir);
$eps_file = $1;
my $curr_dir = cwd();
- chdir($dir);
+ chdir($dir);
# after painfuly long experimentation, it was determined that sam2p won't
# accept : and other chars in the path, no matter how hard I tried to
# escape them, hence the chdir (and chdir back, just to be safe)
}
my @filled_in = $self->print_generic( %params );
-
+
my $fh = new File::Temp( TEMPLATE => $tmp_template,
DIR => $dir,
SUFFIX => '.tex',
=item template
-Dprecated. Used as a suffix for a configuration template. Please
+Dprecated. Used as a suffix for a configuration template. Please
don't use this, it deprecated in favor of more flexible alternatives.
=back
$templatefile .= "_$template"
if length($template) && $conf->exists($templatefile."_$template");
+ $self->set('_template',$template);
+
# the base template
my @invoice_template = map "$_\n", $conf->config($templatefile)
or die "cannot load config data $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);
- }
+ }
warn "$me print_generic creating T:T object\n"
if $DEBUG > 1;
# additional substitution could possibly cause breakage in existing templates
- my %convert_maps = (
+ my %convert_maps = (
'latex' => {
'notes' => sub { map "$_", @_ },
'footer' => sub { map "$_", @_ },
'html' => {
'notes' =>
sub {
- map {
+ map {
s/%%(.*)$/<!-- $1 -->/g;
s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
s/\\begin\{enumerate\}/<ol>/g;
sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
'returnaddress' =>
sub {
- map {
+ map {
s/~/ /g;
s/\\\\\*?\s*$/<BR>/;
s/\\hyphenation\{[\w\s\-]+}//;
'template' => {
'notes' =>
sub {
- map {
+ map {
s/%%.*$//g;
s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
s/\\begin\{enumerate\}//g;
sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
'returnaddress' =>
sub {
- map {
+ map {
s/~/ /g;
s/\\\\\*?\s*$/\n/; # dubious
s/\\hyphenation\{[\w\s\-]+}//;
'quotationnum' => $self->quotationnum,
'no_date' => $params{'no_date'},
'_date' => ( $params{'no_date'} ? '' : $self->_date ),
- # workaround for inconsistent behavior in the early plain text
+ # workaround for inconsistent behavior in the early plain text
# templates; see RT#28271
'date' => ( $params{'no_date'}
? ''
'smallernotes' => $conf->exists('invoice-smallernotes'),
'smallerfooter' => $conf->exists('invoice-smallerfooter'),
'balance_due_below_line' => $conf->exists('balance_due_below_line'),
-
+
#layout info -- would be fancy to calc some of this and bury the template
# here in the code
'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
#quotations have $name
$invoice_data{'name'} = $invoice_data{'payname'};
-
+
#localization
$invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
# prototype here to silence warnings
$invoice_data{'bill_period'} = '';
$invoice_data{'bill_period'} =
- $self->time2str_local('%e %h', $min_sdate, $format)
+ $self->time2str_local('%e %h', $min_sdate, $format)
. " to " .
$self->time2str_local('%e %h', $max_edate, $format)
if ($max_edate != 0 && $min_sdate != 999999999999);
my $pkg_class =
qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
$invoice_data{finance_section} = $pkg_class->categoryname;
- }
+ }
$invoice_data{finance_amount} = '0.00';
$invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
$invoice_data{'ship_contact'} = $escape_function->($cust_main->contact);
$invoice_data{'ship_country'} = ''
if ( $invoice_data{'ship_country'} eq $countrydefault );
-
+
$invoice_data{'cid'} = $params{'cid'}
if $params{'cid'};
$invoice_data{'barcode_cid'} = $params{'barcode_cid'}
if $params{'barcode_cid'};
- 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;
- 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)
+ # re: rt:78190
+ # using owed_on_invoice() instead of owed() here for $balance_due
+ # using _items_previous_total() instead of ->previous() for $pr_total
+ #
+ # owed_on_invoice() is aware of configuration flags that affect how an
+ # invoice is rendered. May not return actual current balance. Will
+ # return balance appropriate for the invoice being rendered, based
+ # on which past due items, current charges, and future payments are
+ # displayed.
+ #
+ # Going forward, usage of owed(), or bypassing cust_bill helper methods
+ # when generating invoice lines may lead to incorrect or misleading
+ # math on invoices.
+ #
+ # Helper methods that are aware of invoicing conf flags:
+ # - owed_on_invoice # use instead of owed()
+ # - _items_previous() # use instead of previous()
+ # - _items_credits() # use instead of cust_credit()
+ # - _items_payments()
+ # - _items_total()
+ # - _items_previous_total() # use instead of previous()
+ # - _items_payments_total()
+ # - _items_credits_total() # use instead of cust_credit()
+
+ my $pr_total = $self->_items_previous_total();
+
+ my $balance_due = $self->owed_on_invoice();
$invoice_data{'balance'} = sprintf("%.2f", $balance_due);
# flag telling this invoice to have a first-page summary
# 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
+ # 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 ) {
- # "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",
- "SELECT -1 * COALESCE( SUM(amount), 0 ) FROM cust_credit",
- "SELECT -1 * COALESCE( SUM(paid), 0 ) FROM cust_pay",
- "SELECT COALESCE( SUM(refund), 0 ) FROM cust_refund",
- );
-
- # the customer's current balance immediately after generating the last
- # bill
-
- my $last_bill_balance = $last_bill->charged;
- foreach (@sql) {
- my $delta = FS::Record->scalar_sql(
- $_,
- $last_bill->_date - 1,
- $self->custnum,
- );
- $last_bill_balance += $delta;
- }
+ # my $last_bill = $self->previous_bill;
+ # if ( $last_bill ) {
- $last_bill_balance = sprintf("%.2f", $last_bill_balance);
-
- warn sprintf("LAST BILL: INVNUM %d, DATE %s, BALANCE %.2f\n\n",
- $last_bill->invnum,
- $self->time2str_local('%D', $last_bill->_date),
- $last_bill_balance
- ) if $DEBUG > 0;
- # ("true_previous_balance" is a terrible name, but at least it's no
- # longer stored in the database)
- $invoice_data{'true_previous_balance'} = $last_bill_balance;
-
- # 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,
- $last_bill->_date,
- $self->custnum,
- );
- $adjustments += $delta;
- }
- $invoice_data{'balance_adjustments'} = sprintf("%.2f", $adjustments);
+ # Populate template stash for previous balance and payments
+ if ($pr_total) {
+ # Used on summary page as "Previous Balance"
+ $invoice_data{'true_previous_balance'} = sprintf("%.2f", $pr_total);
- warn sprintf("BALANCE ADJUSTMENTS: %.2f\n\n",
- $invoice_data{'balance_adjustments'}
- ) if $DEBUG > 0;
+ # Used on summary page as "Payments"
+ $invoice_data{'balance_adjustments'} = sprintf("%.2f",
+ $self->_items_payments_total() + $self->_items_credits_total()
+ );
- # the sum of amount owed on all previous invoices
- # ($pr_total is used elsewhere but not as $previous_balance)
+ # Used in invoice template as "Previous Balance"
$invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
- $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', {
- 'custnum' => $self->custnum,
- '_date' => { op => '>=',
- value => $last_bill->_date }
- } ) )
- {
- next if $cust_pay->_date > $self->_date;
- push @payments, {
- '_date' => $cust_pay->_date,
- 'date' => $self->time2str_local('long', $cust_pay->_date, $format),
- 'payinfo' => $cust_pay->payby_payinfo_pretty,
- 'amount' => sprintf('%.2f', $cust_pay->paid),
- };
- # not concerned about applications
- }
- foreach my $cust_credit ( qsearch('cust_credit', {
- 'custnum' => $self->custnum,
- '_date' => { op => '>=',
- value => $last_bill->_date }
- } ) )
- {
- next if $cust_credit->_date > $self->_date;
- push @credits, {
- '_date' => $cust_credit->_date,
- 'date' => $self->time2str_local('long', $cust_credit->_date, $format),
- 'creditreason'=> $cust_credit->reason,
- 'amount' => sprintf('%.2f', $cust_credit->amount),
- };
+ # $invoice_data{last_bill}{_date}:
+ # Not used in default templates, but may be in use by someone
+ #
+ # ! May be a problem field if they are using it... this field
+ # stores the date of the previous invoice... it is possible to
+ # carry a balance, but have the immediately previous invoice paid off.
+ # In this case, this field might be presenting bad data? Not
+ # altering the problematic behavior, because someone might be
+ # expecting this bad behavior in their templates for some other
+ # purpose, such as a "your last bill was dated %_date%"
+ my $last_bill = $self->previous_bill;
+ $invoice_data{'last_bill'}{'_date'}
+ = ref $last_bill
+ ? $last_bill->_date()
+ : undef;
+
+ # $invoice_data{previous_payments}
+ # Not used in default templates, but may be in use by someone
+ #
+ # Returns an array of hrefs representing payments, each with keys:
+ # - _date: epoch timestamp
+ # - date: text formatted date
+ # - amount: money formatted amount string
+ # - payinfo: string from payby_payinfo_pretty()
+ # - paynum: id for cust_pay
+ # - description: Text description for bill line item
+ #
+ my @payments = $self->_items_payments();
+ $invoice_data{previous_payments} = \@payments;
+
+ # $invoice_data{previous_credits}
+ # Not used in default templates, but may be in use by someone
+ #
+ # Returns an array of hrefs representing credits, each with keys:
+ # - _date: epoch timestamp
+ # - date: text formatted date
+ # - amount: money formatted amount string
+ # - crednum: id for cust_credit
+ # - description: Text description for bill line item
+ # - creditreason: reason() from cust_credit
+ #
+ my @credits = $self->_items_credits();
+ $invoice_data{previous_credits} = \@credits;
+
+ # Populate formatted date field
+ for my $pmt_href (@payments, @credits) {
+ $pmt_href->{date} = $self->time2str_local(
+ 'long',
+ $pmt_href->{_date},
+ $format
+ );
}
- $invoice_data{'previous_payments'} = \@payments;
- $invoice_data{'previous_credits'} = \@credits;
+
} else {
- # there is no $last_bill
+ # There are no outstanding invoices = YAPH
$invoice_data{'true_previous_balance'} =
$invoice_data{'balance_adjustments'} =
$invoice_data{'previous_balance'} = '0.00';
- $invoice_data{'previous_payments'} = [];
- $invoice_data{'previous_credits'} = [];
+ $invoice_data{'previous_payments'} =
+ $invoice_data{'previous_credits'} = [];
+ }
+
+ # Condencing a lot of debug staements here
+ if ($DEBUG) {
+ warn "\$invoice_data{$_}: $invoice_data{$_}"
+ for qw(
+ true_previous_balance
+ balance_adjustments
+ previous_balance
+ previous_payments
+ previous_credits
+ );
}
-
+
if ( $conf->exists('invoice_usesummary', $agentnum) ) {
$invoice_data{'summarypage'} = $summarypage = 1;
}
# 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')
+# $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
# ? $cust_main->payinfo : '';
-# $invoice_data{'po_line'} =
+# $invoice_data{'po_line'} =
# ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
# ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
# : $nbsp;
# default section ('Charges')
my $default_section = { 'description' => '',
- 'subtotal' => '',
+ 'subtotal' => '',
'no_subtotal' => 1,
};
# Previous Charges section
# subtotal is the first return value from $self->previous
my $previous_section;
- # if the invoice has major sections, or if we're summarizing previous
+ # if the invoice has major sections, or if we're summarizing previous
# charges with a single line, or if we've been specifically told to put them
# in a section, create a section for previous charges:
if ( $multisection or
$conf->exists('previous_balance-summary_only') or
$conf->exists('previous_balance-section') ) {
-
+
$previous_section = { 'description' => $self->mt('Previous Charges'),
'subtotal' => $other_money_char.
sprintf('%.2f', $pr_total),
'summarized' => '', #why? $summarypage ? 'Y' : '',
};
- $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
- join(' / ', map { $cust_main->balance_date_range(@$_) }
- $self->_prior_month30s
- )
- if $conf->exists('invoice_include_aging');
+
+ # Include balance aging line and template variables
+ my @aged_balances = $self->_items_aging_balances();
+ ( $invoice_data{aged_balance_current},
+ $invoice_data{aged_balance_30d},
+ $invoice_data{aged_balance_60d},
+ $invoice_data{aged_balance_90d}
+ ) = @aged_balances;
+
+ if ($conf->exists('invoice_include_aging')) {
+ $previous_section->{posttotal} = sprintf(
+ '0 / 30 / 60 / 90 days overdue %.2f / %.2f / %.2f / %.2f',
+ @aged_balances,
+ );
+ }
} else {
# otherwise put them in the main section
push @detail_items, @$extra_lines if $extra_lines;
# the code is written so that both methods can be used together, but
- # we haven't yet changed the template to take advantage of that, so for
+ # we haven't yet changed the template to take advantage of that, so for
# now, treat them as mutually exclusive.
my %section_method = ( by_category => 1 );
if ( $conf->config($tc.'sections_method') eq 'location' ) {
my @finance_charges;
my @charges;
foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
- if ( $invoice_data{finance_section} and
+ if ( $invoice_data{finance_section} and
grep { $_->section eq $invoice_data{finance_section} }
$cust_bill_pkg->cust_bill_pkg_display ) {
# I think these are always setup fees, but just to be sure...
push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
}
}
- $invoice_data{finance_amount} =
+ $invoice_data{finance_amount} =
sprintf('%.2f', sum( @finance_charges ) || 0);
$default_section->{subtotal} = $other_money_char.
sprintf('%.2f', sum( @charges ) || 0);
#quantity => 1, # not really correct
section => $previous_section, # which might be $default_section
description => &$escape_function($line_item->{'description'}),
- ext_description => [ map { &$escape_function($_) }
+ ext_description => [ map { &$escape_function($_) }
@{ $line_item->{'ext_description'} || [] }
],
amount => $money_char . $line_item->{'amount'},
}
- if ( @pr_cust_bill && $self->enable_previous ) {
+ if ( $pr_total && $self->enable_previous ) {
push @buf, ['','-----------'];
push @buf, [ $self->mt('Total Previous Balance'),
$money_char. sprintf("%10.2f", $pr_total) ];
push @buf, ['',''];
}
-
+
if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
warn "$me adding DID summary\n"
if $DEBUG > 1;
my ($didsummary,$minutes) = $self->_did_summary;
my $didsummary_desc = 'DID Activity Summary (since last invoice)';
- push @detail_items,
+ push @detail_items,
{ 'description' => $didsummary_desc,
'ext_description' => [ $didsummary, $minutes ],
};
$line_item->{'unit_amount'} = $money_char.$line_item->{'unit_amount'};
}
$line_item->{'ext_description'} ||= [];
-
+
push @detail_items, $line_item;
}
if ( $section->{'description'} ) {
push @buf, ( ['','-----------'],
[ $section->{'description'}. ' sub-total',
- $section->{'subtotal'} # already formatted this
+ $section->{'subtotal'} # already formatted this
],
[ '', '' ],
[ '', '' ],
);
}
-
+
}
$invoice_data{current_less_finance} =
];
}
-
+
if ( @items_tax ) {
my $total = {};
$total->{'total_item'} = $self->mt('Sub-total');
}
}
-
+
if ( $self->can('_items_total') ) { # should always be true now
# even for multisection, need plain text version
push @buf, [ '', '' ];
# if we're showing previous invoices, also show previous
- # credits and payments
- if ( $self->enable_previous
+ # credits and payments
+ if ( $self->enable_previous
and $self->can('_items_credits')
and $self->can('_items_payments') )
{
-
+
# credits
my $credittotal = 0;
foreach my $credit (
];
}
$invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
-
+
if ( $multisection ) {
$adjust_section->{'subtotal'} = $other_money_char.
sprintf('%.2f', $credittotal + $paymenttotal);
#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
+ # do not summarize; adjustments there are shown according to
# different rules
}
# create Balance Due message
- {
+ {
my $total;
$total->{'total_item'} = &$embolden_function($self->balance_due_msg);
$total->{'total_amount'} =
&$embolden_function(
- $other_money_char. sprintf('%.2f', #why? $summarypage
+ $other_money_char. sprintf('%.2f', #why? $summarypage
# ? $self->charged +
# $self->billing_balance
# :
- $self->owed + $pr_total
+ $balance_due
)
);
if ( $multisection && !$adjust_section->{sort_weight} ) {
push @total_items, $total;
}
push @buf,['','-----------'];
- push @buf,[$self->balance_due_msg, $money_char.
+ push @buf,[$self->balance_due_msg, $money_char.
sprintf("%10.2f", $balance_due ) ];
}
push @total_items, $credit_total;
}
push @buf,['','-----------'];
- push @buf,[$self->credit_balance_msg, $money_char.
+ push @buf,[$self->credit_balance_msg, $money_char.
sprintf("%10.2f", -$cust_main->balance ) ];
}
}
$total->{'total_item'} = &$embolden_function($self->balance_due_msg);
$total->{'total_amount'} =
&$embolden_function(
- $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
+ $other_money_char. sprintf('%.2f', $balance_due)
);
my $last_section = pop @sections;
$last_section->{'posttotal'} = $total->{'total_item'}. ' '.
}
# make a discounts-available section, even without multisection
- if ( $conf->exists('discount-show_available')
+ if ( $conf->exists('discount-show_available')
and my @discounts_avail = $self->_items_discounts_avail ) {
my $discount_section = {
'description' => $self->mt('Discounts Available'),
}
# invoice history "section" (not really a section)
- # not to be included in any subtotals, completely independent of
+ # not to be included in any subtotals, completely independent of
# everything...
if ( $conf->exists('previous_invoice_history') and $cust_main->isa('FS::cust_main') ) {
my %history;
}
$invoice_data{location_info} = \%location_info;
- # debugging hook: call this with 'diag' => 1 to just get a hash of
+ # debugging hook: call this with 'diag' => 1 to just get a hash of
# the invoice variables
return \%invoice_data if ( $params{'diag'} );
@inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
s/--\@\]/$delimiters{$format}[1]/g;
$_;
- }
+ }
&$convert_map( $conf->config($inc_file, $agentnum) );
}
#setup subroutine for the template
$invoice_data{invoice_lines} = sub {
my $lines = shift || scalar(@buf);
- map {
+ map {
scalar(@buf)
? shift @buf
: [ '', '' ];
'invoice_';
}
-# helper routine for generating date ranges
-sub _prior_month30s {
- my $self = shift;
- my @ranges = (
- [ 1, 2592000 ], # 0-30 days ago
- [ 2592000, 5184000 ], # 30-60 days ago
- [ 5184000, 7776000 ], # 60-90 days ago
- [ 7776000, 0 ], # 90+ days ago
- );
-
- map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
- $_->[1] ? $self->_date - $_->[1] - 1 : '',
- ] }
- @ranges;
-}
-
=item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
Returns an postscript invoice, as a scalar.
my $self = shift;
my %params;
if ( ref($_[0]) ) {
- %params = %{ shift() };
+ %params = %{ shift() };
} else {
%params = @_;
}
$params{'format'} = 'html';
-
+
$self->print_generic( %params );
}
# quick subroutine for print_latex
#
# There are ten characters that LaTeX treats as special characters, which
-# means that they do not simply typeset themselves:
+# means that they do not simply typeset themselves:
# # $ % & ~ _ ^ \ { }
#
# TeX ignores blanks following an escaped character; if you want a blank (as
-# in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
+# in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
sub _latex_escape {
my $value = shift;
sub _translate_old_latex_format {
warn "_translate_old_latex_format called\n"
- if $DEBUG;
+ if $DEBUG;
my @template = ();
while ( @_ ) {
my $line = shift;
-
+
if ( $line =~ /^%%Detail\s*$/ ) {
-
+
push @template, q![@--!,
q! foreach my $_tr_line (@detail_items) {!,
q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
- q! $_tr_line->{'description'} .= !,
+ q! $_tr_line->{'description'} .= !,
q! "\\tabularnewline\n~~".!,
q! join( "\\tabularnewline\n~~",!,
q! @{$_tr_line->{'ext_description'}}!,
} else {
$line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
- push @template, $line;
+ push @template, $line;
}
-
+
}
if ($DEBUG) {
#check for an invoice-specific override
return $self->invoice_terms if $self->invoice_terms;
-
+
#check for a customer- specific override
my $cust_main = $self->cust_main;
return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_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
+ # 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 ) {
$duedate;
}
-sub credit_balance_msg {
+sub credit_balance_msg {
my $self = shift;
$self->mt('Credit Balance Remaining')
}
if $DEBUG;
@text = $conf->config($tc.'email_pdf_note');
$html = join('<BR>', @text);
-
+
} # else use the plain text invoice
}
'Filename' => 'logo.png',
'Content-ID' => "<$content_id>",
;
-
+
if ( ref($self) eq 'FS::cust_bill' && $conf->exists('invoice-barcode') ) {
my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
push @related_parts, build MIME::Entity
'Data' => [ '<html>',
' <head>',
' <title>',
- ' '. encode_entities($return{'subject'}),
+ ' '. encode_entities($return{'subject'}),
' </title>',
' </head>',
' <body bgcolor="#e8e8e8">',
;
} else { # } elsif ( $conf->config('voip-cdr_email_attach') eq 'csv' ) {
-
+
push @otherparts, build MIME::Entity
'Type' => 'text/csv',
'Encoding' => '7bit',
my $pages = CAM::PDF->new($file)->numPages;
my $ua = LWP::UserAgent->new(
- 'ssl_opts' => {
+ 'ssl_opts' => {
verify_hostname => 0,
SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE,
SSL_version => 'SSLv3',
Generate section information for all items appearing on this invoice.
This will only be called for multi-section invoices.
-For each line item (L<FS::cust_bill_pkg> record), this will fetch all
-related display records (L<FS::cust_bill_pkg_display>) and organize
-them into two groups ("early" and "late" according to whether they come
-before or after the total), then into sections. A subtotal is calculated
+For each line item (L<FS::cust_bill_pkg> record), this will fetch all
+related display records (L<FS::cust_bill_pkg_display>) and organize
+them into two groups ("early" and "late" according to whether they come
+before or after the total), then into sections. A subtotal is calculated
for each section.
-Section descriptions are returned in sort weight order. Each consists
+Section descriptions are returned in sort weight order. Each consists
of a hash containing:
description: the package category name, escaped
summarized: same as tax_section, for some reason
sort_weight: the package category's sort weight
-If 'condense' is set on the display record, it also contains everything
+If 'condense' is set on the display record, it also contains everything
returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
coderefs to generate parts of the invoice. This is not advised.
OPTIONS may include:
-by_location: a flag to divide the invoice into sections by location.
-Each section hash will have a 'location' element containing a hashref of
+by_location: a flag to divide the invoice into sections by location.
+Each section hash will have a 'location' element containing a hashref of
the location fields (see L<FS::cust_location>). The section description
-will be the location label, but the template can use any of the location
+will be the location label, but the template can use any of the location
fields to create a suitable label.
-by_category: a flag to divide the invoice into sections using display
-records (see L<FS::cust_bill_pkg_display>). This is the "traditional"
+by_category: a flag to divide the invoice into sections using display
+records (see L<FS::cust_bill_pkg_display>). This is the "traditional"
behavior. Each section hash will have a 'category' element containing
-the section name from the display record (which probably equals the
+the section name from the display record (which probably equals the
category name of the package, but may not in some cases).
summary: a flag indicating that this is a summary-format invoice.
Turning this on has the following effects:
- Ignores display items with the 'summary' flag.
- Places all sections in the "early" group even if they have post_total.
-- Creates sections for all non-disabled package categories, even if they
+- Creates sections for all non-disabled package categories, even if they
have no charges on this invoice, as well as a section with no name.
escape: an escape function to use for section titles.
-extra_sections: an arrayref of additional sections to return after the
-sorted list. If there are any of these, section subtotals exclude
+extra_sections: an arrayref of additional sections to return after the
+sorted list. If there are any of these, section subtotals exclude
usage charges.
-format: 'latex', 'html', or 'template' (i.e. text). Not used, but
+format: 'latex', 'html', or 'template' (i.e. text). Not used, but
passed through to C<_condense_section()>.
=cut
sub _items_sections {
my $self = shift;
my %opt = @_;
-
+
my $escape = $opt{escape};
my @extra_sections = @{ $opt{extra_sections} || [] };
my %not_tax = ();
# About tax items + multisection invoices:
- # If either invoice_*summary option is enabled, AND there is a
- # package category with the name of the tax, then there will be
+ # If either invoice_*summary option is enabled, AND there is a
+ # package category with the name of the tax, then there will be
# a display record assigning the tax item to that category.
#
# However, the taxes are always placed in the "Taxes, Surcharges,
- # and Fees" section regardless of that. The only effect of the
+ # and Fees" section regardless of that. The only effect of the
# display record is to create a subtotal for the summary page.
# cache these
if $cust_bill_pkg->pkgnum or $cust_bill_pkg->feepart;
# there's actually a very important piece of logic buried in here:
- # incrementing $late_subtotal{$section} CREATES
- # $late_subtotal{$section}. keys(%late_subtotal) is later used
+ # incrementing $late_subtotal{$section} CREATES
+ # $late_subtotal{$section}. keys(%late_subtotal) is later used
# to define the list of late sections, and likewise keys(%subtotal).
- # When _items_cust_bill_pkg is called to generate line items for
- # real, it will be called with 'section' => $section for each
+ # When _items_cust_bill_pkg is called to generate line items for
+ # real, it will be called with 'section' => $section for each
# of these.
if ( $display->post_total && !$opt{summary} ) {
if (! $type || $type eq 'S') {
if $cust_bill_pkg->recur != 0
|| $cust_bill_pkg->recur_show_zero;
}
-
+
if ($type && $type eq 'U') {
$late_subtotal{$locationnum}{$section} += $usage
unless scalar(@extra_sections);
$section->{'locationnum'} = $locationnum;
my $location = FS::cust_location->by_key($locationnum);
$section->{'description'} = &{ $escape }($location->location_label);
- # Better ideas? This will roughly group them by proximity,
+ # Better ideas? This will roughly group them by proximity,
# which alpha sorting on any of the address fields won't.
# Sorting by locationnum is meaningless.
- # We have to sort on _something_ or the order may change
+ # We have to sort on _something_ or the order may change
# randomly from one invoice to the next, which will confuse
# people.
$section->{'sort_weight'} = sprintf('%012s',$location->zip) .
} # foreach $sectionname
} #foreach $locationnum
push @these, @extra_sections if $post_total == 0;
- # need an alpha sort for location sections, because postal codes can
+ # need an alpha sort for location sections, because postal codes can
# be non-numeric
$sections[ $post_total ] = [ sort {
- $opt{'by_location'} ?
+ $opt{'by_location'} ?
($a->{sort_weight} cmp $b->{sort_weight}) :
($a->{sort_weight} <=> $b->{sort_weight})
} @these ];
=item _items_pkg [ OPTIONS ]
-Return line item hashes for each package item on this invoice. Nearly
-equivalent to
+Return line item hashes for each package item on this invoice. Nearly
+equivalent to
$self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
}
} # otherwise include them all in the main section
# XXX what to do when sectioning by location?
-
+
my @ext_desc;
my %base_invnums; # invnum => invoice date
foreach ($cust_bill_pkg->cust_bill_pkg_fee) {
sub _items_tax {
my $self = shift;
- my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum and ! $_->feepart }
+ my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum and ! $_->feepart }
$self->cust_bill_pkg;
my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
DEPRECATED? (expensive, mostly unused?)
format_function: the function used to format CDRs.
-section: a hashref containing 'category' and/or 'locationnum'; if this
+section: a hashref containing 'category' and/or 'locationnum'; if this
is present, only returns line items that belong to that category and/or
location (whichever is defined).
Returns a list of hashrefs, each of which may contain:
-pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
-ext_description, which is an arrayref of detail lines to show below
+pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
+ext_description, which is an arrayref of detail lines to show below
the package line.
=cut
# this is a location section; skip packages that aren't at this
# service location.
next if $cust_bill_pkg->pkgnum == 0; # skips fees...
- next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum
+ next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum
!= $locationnum;
}
# Consider display records for this item to determine if it belongs
# in this section. Note that if there are no display records, there
- # will be a default pseudo-record that includes all charge types
+ # will be a default pseudo-record that includes all charge types
# and has no section name.
my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
? $cust_bill_pkg->cust_bill_pkg_display
@cust_bill_pkg_display;
} else {
# otherwise, process all display records that aren't usage summaries
- # (I don't think there should be usage summaries if you aren't using
+ # (I don't think there should be usage summaries if you aren't using
# category sections, but this is the historical behavior)
@cust_bill_pkg_display = grep { !$_->summary }
@cust_bill_pkg_display;
warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
if $DEBUG > 1;
-
+
my $cust_pkg = $cust_bill_pkg->cust_pkg;
my $part_pkg = $cust_pkg->part_pkg;
# which pkgpart to show for display purposes?
my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
- # start/end dates for invoice formats that do nonstandard
+ # start/end dates for invoice formats that do nonstandard
# things with them
my %item_dates = ();
%item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
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)
+ # a recur line for the same package (i.e. not a one-time charge)
# XXX localization
my $description = $desc;
$description .= ' Setup'
unless ( $part_pkg->hide_svc_detail ) {
- # still pass the svc_label through to the template, even if
+ # still pass the svc_label through to the template, even if
# not displaying it as an ext_description
@svc_labels = map &{$escape_function}($_),
$cust_pkg->h_labels_short($self->_date,
# or this is a usage summary line
|| $is_summary && $type && $type eq 'U'
# or this is a usage line and there's a recurring line
- # for the package in the same section (which will
+ # for the package in the same section (which will
# have service labels already)
|| ($type eq 'U' and defined($r))
)
# Display of seconds_since_sqlradacct:
# On the invoice, when processing @detail_items, look for a field
- # named 'seconds'. This will contain total seconds for each
- # service, in the same order as @ext_description. For services
+ # named 'seconds'. This will contain total seconds for each
+ # service, in the same order as @ext_description. For services
# that don't support this it will show undef.
- if ( $conf->exists('svc_acct-usage_seconds')
+ if ( $conf->exists('svc_acct-usage_seconds')
and ! $cust_bill_pkg->pkgpart_override ) {
- foreach my $cust_svc (
- $cust_pkg->h_cust_svc(@dates, 'I')
+ foreach my $cust_svc (
+ $cust_pkg->h_cust_svc(@dates, 'I')
) {
- # eval because not having any part_export_usage exports
- # is a fatal error, last_bill/_date because that's how
+ # eval because not having any part_export_usage exports
+ # is a fatal error, last_bill/_date because that's how
# sqlradius_hour billing does it
my $sec = eval {
$cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
warn "$me _items_cust_bill_pkg calculating amount\n"
if $DEBUG > 1;
-
+
my $amount = 0;
if (!$type) {
$amount = $cust_bill_pkg->recur;
} elsif ($type eq 'U') {
$amount = $cust_bill_pkg->usage;
}
-
+
if ( !$type || $type eq 'R' ) {
warn "$me _items_cust_bill_pkg adding recur\n"
# items of this kind should normally not have sdate/edate.
push @b, {
'description' => $desc,
- 'amount' => sprintf('%.2f', $cust_bill_pkg->setup
+ 'amount' => sprintf('%.2f', $cust_bill_pkg->setup
+ $cust_bill_pkg->recur)
};
# 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
+ # 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 )
) {
$_ = &{$escape_function}($_) foreach @{ $d->{ext_description} };
}
- # update the active line (before the discount) to show the
+ # update the active line (before the discount) to show the
# original price (whether this is a hidden line or not)
$s->{amount} -= $item_discount->{setup_amount} if $s;
=item _items_discounts_avail
Returns an array of line item hashrefs representing available term discounts
-for this invoice. This makes the same assumptions that apply to term
-discounts in general: that the package is billed monthly, at a flat rate,
-with no usage charges. A prorated first month will be handled, as will
+for this invoice. This makes the same assumptions that apply to term
+discounts in general: that the package is billed monthly, at a flat rate,
+with no usage charges. A prorated first month will be handled, as will
a setup fee if the discount is allowed to apply to setup fees.
=cut
sub _items_discounts_avail {
my $self = shift;
- #maybe move this method from cust_bill when quotations support discount_plans
+ #maybe move this method from cust_bill when quotations support discount_plans
return () unless $self->can('discount_plans');
my %plans = $self->discount_plans;
my $plan = $plans{$months};
my $term_total = sprintf('%.2f', $plan->discounted_total);
- my $percent = sprintf('%.0f',
+ my $percent = sprintf('%.0f',
100 * (1 - $term_total / $plan->base_total) );
my $permonth = sprintf('%.2f', $term_total / $months);
my $detail = $self->mt('discount on item'). ' '.
+{
description => $self->mt('Save [_1]% by paying for [_2] months',
$percent, $months),
- amount => $self->mt('[_1] ([_2] per month)',
+ amount => $self->mt('[_1] ([_2] per month)',
$term_total, $money_char.$permonth),
ext_description => ($detail || ''),
}
use Cwd;
use List::Util qw(min max sum);
use Date::Format;
+use DateTime;
use File::Temp 0.14;
use HTML::Entities;
use Storable qw( freeze thaw );
=over 4
=item billing_balance - the customer's balance immediately before generating
-this invoice. DEPRECATED. Use the L<FS::cust_main/balance_date> method
+this invoice. DEPRECATED. Use the L<FS::cust_main/balance_date> method
to determine the customer's balance at a specific time.
=item previous_balance - the customer's balance immediately after generating
the invoice before this one. DEPRECATED.
-=item printed - formerly used to track the number of times an invoice had
+=item printed - formerly used to track the number of times an invoice had
been printed; no longer used.
=back
$self->conf->config('notice_name') || 'Invoice'
}
-sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum }
+sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum }
sub cust_unlinked_msg {
my $self = shift;
"WARNING: can't find cust_main.custnum ". $self->custnum.
# internal-only and discourage use
#
# =item delete
-#
+#
# DO NOT USE THIS METHOD. Instead, apply a credit against the invoice, or use
# the B<void> method.
-#
+#
# This is only for internal use by V<void>, which is what you should be using.
-#
+#
# DO NOT USE THIS METHOD. Whatever reason you think you have is almost certainly
# wrong. Use B<void>, that's what it is for. Really. This means you.
-#
+#
# =cut
sub _delete {
$self->get('previous_bill');
}
+=item following_bill
+
+Returns the customer's invoice that follows this one
+
+=cut
+
+sub following_bill {
+ my $self = shift;
+ if (!$self->get('following_bill')) {
+ $self->set('following_bill', qsearchs({
+ table => 'cust_bill',
+ hashref => {
+ custnum => $self->custnum,
+ invnum => { op => '>', value => $self->invnum },
+ },
+ order_by => 'ORDER BY invnum ASC LIMIT 1',
+ }));
+ }
+ $self->get('following_bill');
+}
+
=item previous
-Returns a list consisting of the total previous balance for this customer,
+Returns a list consisting of the total previous balance for this customer,
followed by the previous outstanding invoices (as FS::cust_bill objects also).
=cut
qsearch( 'cust_bill', { 'custnum' => $self->custnum,
#'_date' => { op=>'<', value=>$self->_date },
'invnum' => { op=>'<', value=>$self->invnum },
- } )
+ } )
;
foreach ( @cust_bill ) { $total += $_->owed; }
$self->set('previous', [$total, @cust_bill]);
my $sql =
"SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
" WHERE tablenum = ? AND eventtable = 'cust_bill'";
- my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
+ my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
$sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
$sth->fetchrow_arrayref->[0];
}
sub suspend {
my $self = shift;
- grep { $_->suspend(@_) }
- grep {! $_->getfield('cancel') }
+ grep { $_->suspend(@_) }
+ grep {! $_->getfield('cancel') }
$self->cust_pkg;
}
grep { $_ }
map { $_->cancel(%opt) }
- grep { ! $_->getfield('cancel') }
+ grep { ! $_->getfield('cancel') }
@pkgs;
}
=item discount_plans
-Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
+Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
hash keyed by term length.
=cut
$balance;
}
+=item owed_on_invoice
+
+Returns the amount to be displayed as the "Balance Due" on this
+invoice. Amount returned depends on conf flags for invoicing
+
+See L<FS::cust_bill::owed> for the true amount currently owed
+
+=cut
+
+sub owed_on_invoice {
+ my $self = shift;
+
+ #return $self->owed()
+ # unless $self->conf->exists('previous_balance-payments_since')
+
+ # Add charges from this invoice
+ my $owed = $self->charged();
+
+ # Add carried balances from previous invoices
+ # If previous items aren't to be displayed on the invoice,
+ # _items_previous() is aware of this and responds appropriately.
+ $owed += $_->{amount} for $self->_items_previous();
+
+ # Subtract payments and credits displayed on this invoice
+ $owed -= $_->{amount} for $self->_items_payments(), $self->_items_credits();
+
+ return $owed;
+}
+
sub owed_pkgnum {
my( $self, $pkgnum ) = @_;
$self->select_for_update; #mutex
- my @payments = grep { $_->unapplied > 0 }
+ my @payments = grep { $_->unapplied > 0 }
grep { !$_->no_auto_apply }
$self->cust_main->cust_pay;
my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
);
my $max_credit_weight =
max( map { $_->part_pkg->credit_weight || 0 }
- grep { $_ }
+ grep { $_ }
map { $_->cust_pkg }
@open_lineitems
);
} else {
$app = 'credit';
}
-
+
} elsif ( @payments ) {
$app = 'pay';
} elsif ( @credits ) {
I<amount>: obsolete, does nothing
-I<notice_name> overrides "Invoice" as the name of the sent document
+I<notice_name> overrides "Invoice" as the name of the sent document
(templates from 10/2009 or newer required).
I<lpr> overrides the system 'lpr' option as the command to print a document
$self->set('mode', $opt{mode})
if $opt{mode};
- my %args = map {$_ => $opt{$_}}
+ my %args = map {$_ => $opt{$_}}
grep { $opt{$_} }
qw( from notice_name no_coupon template );
Returns the postscript or plaintext for this invoice as an arrayref.
-Options must be passed as a hashref. Positional parameters are no longer
+Options must be passed as a hashref. Positional parameters are no longer
allowed.
I<template>, if specified, is the name of a suffix for alternate invoices.
=item batch_invoice [ HASHREF ]
-Place this invoice into the open batch (see C<FS::bill_batch>). If there
+Place this invoice into the open batch (see C<FS::bill_batch>). If there
isn't an open batch, one will be created.
HASHREF may contain any options to be passed to C<print_pdf>.
return $batch;
}
-=item ftp_invoice [ TEMPLATENAME ]
+=item ftp_invoice [ TEMPLATENAME ]
Sends this invoice data via FTP.
);
}
-=item spool_invoice [ TEMPLATENAME ]
+=item spool_invoice [ TEMPLATENAME ]
Spools this invoice data (see L<FS::spool_csv>)
# don't localize dates here, they're a defined format
my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
my $file = "$spooldir/$tracctnum.csv";
-
+
my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
open(CSV, ">$file") or die "can't open $file: $!";
}
$file = "$spooldir/$file.csv";
-
+
my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum);
open(CSV, ">>$file") or die "can't open $file: $!";
If I<format> is not specified or "default", the fields of the CSV file are as
follows:
-record_type, invnum, custnum, _date, charged, first, last, company, address1,
+record_type, invnum, custnum, _date, charged, first, last, company, address1,
address2, city, state, zip, country, pkg, setup, recur, sdate, edate
=over 4
9 | Grouping Code | GROUP | CHAR | 2
10 | User Defined | ACCT CODE | CHAR | 15
-If format is 'oneline', there is no detail file. Each invoice has a
+If format is 'oneline', there is no detail file. Each invoice has a
header line only, with the fields:
Agent number, agent name, customer number, first name, last name, address
and then, for each line item, three columns containing the package number,
description, and amount.
-If format is 'bridgestone', there is no detail file. Each invoice has a
+If format is 'bridgestone', there is no detail file. Each invoice has a
header line with the following fields in a fixed-width format:
Customer number (in display format), date, name (first last), company,
address 1, address 2, city, state, zip.
This is a mailing list format, and has no per-invoice fields. To avoid
-sending redundant notices, the spooling event should have a "once" or
+sending redundant notices, the spooling event should have a "once" or
"once_percust_every" condition.
=cut
sub print_csv {
my($self, %opt) = @_;
-
+
eval "use Text::CSV_XS";
die $@ if $@;
my $time = $opt{'time'} || time;
+ $self->set('_template', $opt{template})
+ if exists $opt{template};
+
my $tracctnum = ''; #leaking out from billco-specific sections :/
if ( $format eq 'billco' ) {
);
} elsif ( $format eq 'oneline' ) { #name
-
- my ($previous_balance) = $self->previous;
+
+ my ($previous_balance) = $self->previous;
$previous_balance = sprintf('%.2f', $previous_balance);
my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
my @items = map {
my $classnum = $cust_svc->part_svc->classnum;
my $part_svc_class = FS::part_svc_class->by_key($classnum)
if $classnum;
- $svc_class{$svcpart} = $part_svc_class ?
+ $svc_class{$svcpart} = $part_svc_class ?
$part_svc_class->classname :
'';
}
return join('', $header, @details, "\n");
} else { # default
-
+
$csv->combine(
'cust_bill',
$self->invnum,
my($pkg, $setup, $recur, $sdate, $edate);
if ( $cust_bill_pkg->pkgnum ) {
-
+
($pkg, $setup, $recur, $sdate, $edate) = (
$cust_bill_pkg->part_pkg->pkg,
( $cust_bill_pkg->setup != 0
( $cust_bill_pkg->recur != 0
? sprintf("%.2f", $cust_bill_pkg->recur )
: '' ),
- ( $cust_bill_pkg->sdate
+ ( $cust_bill_pkg->sdate
? time2str("%x", $cust_bill_pkg->sdate)
: '' ),
- ($cust_bill_pkg->edate
+ ($cust_bill_pkg->edate
? time2str("%x", $cust_bill_pkg->edate)
: '' ),
);
-
+
} else { #pkgnum tax
next unless $cust_bill_pkg->setup != 0;
$pkg = $cust_bill_pkg->desc;
$setup = sprintf('%10.2f', $cust_bill_pkg->setup );
( $sdate, $edate ) = ( '', '' );
}
-
+
$csv->combine(
'cust_bill_pkg',
$self->invnum,
my $cust_main = $self->cust_main;
$options{invnum} = $self->invnum;
-
+
$cust_main->batch_card(%options);
}
sub invoice_barcode {
my ($self, $dir) = (shift,shift);
-
+
my $gdbar = new GD::Barcode('Code39',$self->invnum);
die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
my $gd = $gdbar->plot(Height => 30);
foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
my $amount = $detail->amount;
next unless $amount && $amount > 0;
-
+
$sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
$sections{$section}{amount} += $amount; #subtotal
$sections{$section}{calls}++;
$sections{$section}{duration} += $detail->duration;
- my $desc = $detail->regionname;
+ my $desc = $detail->regionname;
my $description = $desc;
$description = substr($desc, 0, $maxlength). '...'
if $format eq 'latex' && length($desc) > $maxlength;
qw( description_generator header_generator total_generator total_line_generator )
)
: ()
- ),
+ ),
};
}
my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
my $phone_deleted;
$phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
-
+
# DID either activated or ported in; cannot be both for same DID simultaneously
if ($inserted >= $start && $inserted <= $end && $phone_inserted
- && (!$phone_inserted->lnp_status
+ && (!$phone_inserted->lnp_status
|| $phone_inserted->lnp_status eq ''
|| $phone_inserted->lnp_status eq 'native')) {
$num_activated++;
else { # this one not so clean, should probably move to (h_)svc_phone
local($FS::Record::qsearch_qualify_columns) = 0;
my $phone_portedin = qsearchs( 'h_svc_phone',
- { 'svcnum' => $h_cust_svc->svcnum,
- 'lnp_status' => 'portedin' },
- FS::h_svc_phone->sql_h_searchs($end),
+ { 'svcnum' => $h_cust_svc->svcnum,
+ 'lnp_status' => 'portedin' },
+ FS::h_svc_phone->sql_h_searchs($end),
);
$num_portedin++ if $phone_portedin;
}
# DID either deactivated or ported out; cannot be both for same DID simultaneously
if($deleted >= $start && $deleted <= $end && $phone_deleted
- && (!$phone_deleted->lnp_status
+ && (!$phone_deleted->lnp_status
|| $phone_deleted->lnp_status ne 'portingout')) {
$num_deactivated++;
- }
- elsif($deleted >= $start && $deleted <= $end && $phone_deleted
- && $phone_deleted->lnp_status
+ }
+ elsif($deleted >= $start && $deleted <= $end && $phone_deleted
+ && $phone_deleted->lnp_status
&& $phone_deleted->lnp_status eq 'portingout') {
$num_portedout++;
}
foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
$section->{'header'} = $detail->formatted('format' => $format)
- if($detail->detail eq $section->{'header'});
-
+ if($detail->detail eq $section->{'header'});
+
my $accountcode = $detail->accountcode;
next unless $accountcode;
$sections{$phonenum}{calls}++;
$sections{$phonenum}{duration} += $detail->duration;
- my $desc = $detail->regionname;
+ my $desc = $detail->regionname;
my $description = $desc;
$description = substr($desc, 0, $maxlength). '...'
if $format eq 'latex' && length($desc) > $maxlength;
total_line_generator
)
)
- ),
+ ),
};
}
push @lines, $l;
}
}
-
- if($conf->exists('phone_usage_class_summary')) {
+
+ if($conf->exists('phone_usage_class_summary')) {
# this only works with Latex
my @newlines;
my @newsections;
};
$calls_detail{'description'} = 'Calls Detail: '
. $section->{'phonenum'};
- push @newsections, \%calls_detail;
+ push @newsections, \%calls_detail;
}
}
foreach my $newsection ( @newsections ) {
if($newsection->{'post_total'}) { # this means Calls Summary
foreach my $section ( @sections ) {
- next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
+ next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
&& !$section->{'post_total'});
my $newdesc = $section->{'description'};
my $tn = $section->{'phonenum'};
=sub _items_usage_class_summary OPTIONS
-Returns a list of detail items summarizing the usage charges on this
+Returns a list of detail items summarizing the usage charges on this
invoice. Each one will have 'amount', 'description' (the usage charge name),
and 'usage_classnum'.
return @l;
}
+=sub _items_previous()
+
+ Returns an array of hashrefs, each hashref representing a line-item on
+ the current bill for previous unpaid invoices.
+
+ keys for each previous_item:
+ - amount (see notes)
+ - pkgnum
+ - description
+ - invnum
+ - _date
+
+ Payments and credits shown on this invoice may vary based on configuraiton.
+
+ when conf flag previous_balance-payments_since is set:
+ This method works backwards to rebuild the invoice as a snapshot in time.
+ The invoice displayed will have the balances owed, and payments made,
+ reflecting the state of the account at the time of invoice generation.
+
+=cut
+
sub _items_previous {
+
my $self = shift;
- my $conf = $self->conf;
- my $cust_main = $self->cust_main;
- my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
- my @b = ();
- foreach ( @pr_cust_bill ) {
- my $date = $conf->exists('invoice_show_prior_due_date')
- ? 'due '. $_->due_date2str('short')
- : $self->time2str_local('short', $_->_date);
- push @b, {
- 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
- #'pkgpart' => 'N/A',
- 'pkgnum' => 'N/A',
- 'amount' => sprintf("%.2f", $_->owed),
- };
+
+ # simple memoize
+ if ($self->get('_items_previous')) {
+ return sort { $a->{_date} <=> $b->{_date} }
+ values %{ $self->get('_items_previous') };
+ }
+
+ # Gets the customer's current balance and outstanding invoices.
+ my ($prev_balance, @open_invoices) = $self->previous;
+
+ my %invoices = map {
+ $_->invnum => $self->__items_previous_map_invoice($_)
+ } @open_invoices;
+
+ # Which credits and payments displayed on the bill will vary based on
+ # conf flag previous_balance-payments_since.
+ my @credits = $self->_items_credits();
+ my @payments = $self->_items_payments();
+
+
+ if ($self->conf->exists('previous_balance-payments_since')) {
+ # For each credit or payment, determine which invoices it was applied to.
+ # Manipulate data displayed so the invoice displayed appears as a
+ # snapshot in time... with previous balances and balance owed displayed
+ # as they were at the time of invoice creation.
+
+ my @credits_postbill = $self->_items_credits_postbill();
+ my @payments_postbill = $self->_items_payments_postbill();
+
+ my %pmnt_dupechk;
+ my %cred_dupechk;
+
+ # Each section below follows this pattern on a payment/credit
+ #
+ # - Dupe check, avoid adjusting for the same item twice
+ # - If invoice being adjusted for isn't in our list, add it
+ # - Adjust the invoice balance to refelct balnace without the
+ # credit or payment applied
+ #
+
+ # Working with payments displayed on this bill
+ for my $pmt_hash (@payments) {
+ my $pmt_obj = qsearchs('cust_pay', {paynum => $pmt_hash->{paynum}});
+ for my $cust_bill_pay ($pmt_obj->cust_bill_pay) {
+ next if exists $pmnt_dupechk{$cust_bill_pay->billpaynum};
+ $pmnt_dupechk{$cust_bill_pay->billpaynum} = 1;
+
+ my $invnum = $cust_bill_pay->invnum;
+
+ $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
+ unless exists $invoices{$invnum};
+
+ $invoices{$invnum}->{amount} += $cust_bill_pay->amount;
+ }
+ }
+
+ # Working with credits displayed on this bill
+ for my $cred_hash (@credits) {
+ my $cred_obj = qsearchs('cust_credit', {crednum => $cred_hash->{crednum}});
+ for my $cust_credit_bill ($cred_obj->cust_credit_bill) {
+ next if exists $cred_dupechk{$cust_credit_bill->creditbillnum};
+ $cred_dupechk{$cust_credit_bill->creditbillnum} = 1;
+
+ my $invnum = $cust_credit_bill->invnum;
+
+ $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
+ unless exists $invoices{$invnum};
+
+ $invoices{$invnum}->{amount} += $cust_credit_bill->amount;
+ }
+ }
+
+ # Working with both credits and payments which are not displayed
+ # on this bill, but which have affected this bill's balances
+ for my $postbill (@payments_postbill, @credits_postbill) {
+
+ if ($postbill->{billpaynum}) {
+ next if exists $pmnt_dupechk{$postbill->{billpaynum}};
+ $pmnt_dupechk{$postbill->{billpaynum}} = 1;
+ } elsif ($postbill->{creditbillnum}) {
+ next if exists $cred_dupechk{$postbill->{creditbillnum}};
+ $cred_dupechk{$postbill->{creditbillnum}} = 1;
+ } else {
+ die "Missing creditbillnum or billpaynum";
+ }
+
+ my $invnum = $postbill->{invnum};
+
+ $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
+ unless exists $invoices{$invnum};
+
+ $invoices{$invnum}->{amount} += $postbill->{amount};
+ }
+
+ # Make sure current invoice doesn't appear in previous items
+ delete $invoices{$self->invnum}
+ if exists $invoices{$self->invnum};
+
}
- @b;
- #{
- # 'description' => 'Previous Balance',
- # #'pkgpart' => 'N/A',
- # 'pkgnum' => 'N/A',
- # 'amount' => sprintf("%10.2f", $pr_total ),
- # 'ext_description' => [ map {
- # "Invoice ". $_->invnum.
- # " (". time2str("%x",$_->_date). ") ".
- # sprintf("%10.2f", $_->owed)
- # } @pr_cust_bill ],
+ # Make sure amount is formatted as a dollar string
+ # (Formatting should happen on the template side, but is not?)
+ $invoices{$_}->{amount} = sprintf('%.2f',$invoices{$_}->{amount})
+ for keys %invoices;
+
+ $self->set('_items_previous', \%invoices);
+ return sort { $a->{_date} <=> $b->{_date} } values %invoices;
- #};
}
+=sub _items_previous_total
+
+ Return sum of amounts from all items returned by _items_previous
+ Results will vary based on invoicing conf flags
+
+=cut
+
+sub _items_previous_total {
+ my $self = shift;
+ my $tot = 0;
+ $tot += $_->{amount} for $self->_items_previous();
+ return $tot;
+}
+
+sub __items_previous_get_invoice {
+ # Helper function for _items_previous
+ #
+ # Read a record from cust_bill, return a hash of it's information
+ my ($self, $invnum) = @_;
+ die "Incorrect usage of __items_previous_get_invoice()" unless $invnum;
+
+ my $cust_bill = qsearchs('cust_bill', {invnum => $invnum});
+ return $self->__items_previous_map_invoice($cust_bill);
+}
+
+sub __items_previous_map_invoice {
+ # Helper function for _items_previous
+ #
+ # Transform a cust_bill object into a simple hash reference of the type
+ # required by _items_previous
+ my ($self, $cust_bill) = @_;
+ die "Incorrect usage of __items_previous_map_invoice" unless ref $cust_bill;
+
+ my $date = $self->conf->exists('invoice_show_prior_due_date')
+ ? 'due '.$cust_bill->due_date2str('short')
+ : $self->time2str_local('short', $cust_bill->_date);
+
+ return {
+ invnum => $cust_bill->invnum,
+ amount => $cust_bill->owed,
+ pkgnum => 'N/A',
+ _date => $cust_bill->_date,
+ description => join(' ',
+ $self->mt('Previous Balance, Invoice #'),
+ $cust_bill->invnum,
+ "($date)"
+ ),
+ }
+}
+
+=sub _items_credits()
+
+ Return array of hashrefs containing credits to be shown as line-items
+ when rendering this bill.
+
+ keys for each credit item:
+ - crednum: id of payment
+ - amount: payment amount
+ - description: line item to be displayed on the bill
+
+ This method has three ways it selects which credits to display on
+ this bill:
+
+ 1) Default Case: No Conf flag for 'previous_balance-payments_since'
+
+ Returns credits that have been applied to this bill only
+
+ 2) Case:
+ Conf flag set for 'previous_balance-payments_since'
+
+ List all credits that have been recorded during the time period
+ between the timestamps of the last invoice and this invoice
+
+ 3) Case:
+ Conf flag set for 'previous_balance-payments_since'
+ $opt{'template'} eq 'statement'
+
+ List all payments that have been recorded between the timestamps
+ of the previous invoice and the following invoice.
+
+ This is used to give the customer a receipt for a payment
+ in the form of their last bill with the payment amended.
+
+ I am concerned with this implementation, but leaving in place as is
+ If this option is selected, while viewing an older bill, the old bill
+ will show ALL future credits for future bills, but no charges for
+ future bills. Somebody could be misled into believing they have a
+ large account credit when they don't. Also, interrupts the chain of
+ invoices as an account history... the customer could have two invoices
+ in their fileing cabinet, for two different dates, both with a line item
+ for the same duplicate credit. The accounting is technically accurate,
+ but somebody could easily become confused and think two credits were
+ made, when really those two line items on two different bills represent
+ only a single credit
+
+=cut
+
sub _items_credits {
- my( $self, %opt ) = @_;
- my $trim_len = $opt{'trim_len'} || 40;
-
- my @b;
- #credits
- my @objects;
- if ( $self->conf->exists('previous_balance-payments_since') ) {
- if ( $opt{'template'} eq 'statement' ) {
- # then the current bill is a "statement" (i.e. an invoice sent as
- # a payment receipt)
- # and in that case we want to see payments on or after THIS invoice
- @objects = qsearch('cust_credit', {
- 'custnum' => $self->custnum,
- '_date' => {op => '>=', value => $self->_date},
- });
+
+ my $self= shift;
+
+ # Simple memoize
+ return @{$self->get('_items_credits')} if $self->get('_items_credits');
+
+ my %opt = @_;
+ my $template = $opt{template} || $self->get('_template');
+ my $trim_len = $opt{template} || $self->get('trim_len') || 40;
+
+ my @return;
+ my @cust_credit_objs;
+
+ if ($self->conf->exists('previous_balance-payments_since')) {
+ if ($template eq 'statement') {
+ # Case 3 (see above)
+ # Return credits timestamped between the previous and following bills
+
+ my $previous_bill = $self->previous_bill;
+ my $following_bill = $self->following_bill;
+
+ my $date_start = ref $previous_bill ? $previous_bill->_date : 0;
+ my $date_end = ref $following_bill ? $following_bill->_date : undef;
+
+ my %query = (
+ table => 'cust_credit',
+ hashref => {
+ custnum => $self->custnum,
+ _date => { op => '>=', value => $date_start },
+ },
+ );
+ $query{extra_sql} = " AND _date <= $date_end " if $date_end;
+
+ @cust_credit_objs = qsearch(\%query);
+
} else {
- my $date = 0;
- $date = $self->previous_bill->_date if $self->previous_bill;
- @objects = qsearch('cust_credit', {
- 'custnum' => $self->custnum,
- '_date' => {op => '>=', value => $date},
+ # Case 2 (see above)
+ # Return credits timestamps between this and the previous bills
+
+ my $date_start = 0;
+ my $date_end = $self->_date;
+
+ my $previous_bill = $self->previous_bill;
+ if (ref $previous_bill) {
+ $date_start = $previous_bill->_date;
+ }
+
+ @cust_credit_objs = qsearch({
+ table => 'cust_credit',
+ hashref => {
+ custnum => $self->custnum,
+ _date => {op => '>=', value => $date_start},
+ },
+ extra_sql => " AND _date <= $date_end ",
});
}
+
} else {
- @objects = $self->cust_credited;
+ # Case 1 (see above)
+ # Return only credits that have been applied to this bill
+
+ @cust_credit_objs = $self->cust_credited;
+
}
- foreach my $obj ( @objects ) {
+ # Translate objects into hashrefs
+ foreach my $obj ( @cust_credit_objs ) {
my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
+ my %r_obj = (
+ amount => sprintf('%.2f',$cust_credit->amount),
+ crednum => $cust_credit->crednum,
+ _date => $cust_credit->_date,
+ creditreason => $cust_credit->reason,
+ );
my $reason = substr($cust_credit->reason, 0, $trim_len);
$reason .= '...' if length($reason) < length($cust_credit->reason);
- $reason = " ($reason) " if $reason;
-
- push @b, {
- #'description' => 'Credit ref\#'. $_->crednum.
- # " (". time2str("%x",$_->cust_credit->_date) .")".
- # $reason,
- 'description' => $self->mt('Credit applied').' '.
- $self->time2str_local('short', $obj->_date). $reason,
- 'amount' => sprintf("%.2f",$obj->amount),
- };
+ $reason = "($reason)" if $reason;
+
+ $r_obj{description} = join(' ',
+ $self->mt('Credit applied'),
+ $self->time2str_local('short', $cust_credit->_date),
+ $reason,
+ );
+
+ push @return, \%r_obj;
}
+ $self->set('_items_credits',\@return);
+ @return;
+}
+
+=sub _items_credits_total
+
+ Return the total of al items from _items_credits
+ Will vary based on invoice display conf flag
- @b;
+=cut
+
+sub _items_credits_total {
+ my $self = shift;
+ my $tot = 0;
+ $tot += $_->{amount} for $self->_items_credits();
+ return $tot;
+}
+
+
+
+=sub _items_credits_postbill()
+
+ Returns an array of hashrefs for credits where
+ - Credit issued after this invoice
+ - Credit applied to an invoice before this invoice
+
+ Returned hashrefs are of the format returned by _items_credits()
+
+=cut
+
+sub _items_credits_postbill {
+ my $self = shift;
+
+ my @cust_credit_bill = qsearch({
+ table => 'cust_credit_bill',
+ select => join(', ',qw(
+ cust_credit_bill.creditbillnum
+ cust_credit_bill._date
+ cust_credit_bill.invnum
+ cust_credit_bill.amount
+ )),
+ addl_from => ' LEFT JOIN cust_credit'.
+ ' ON (cust_credit_bill.crednum = cust_credit.crednum) ',
+ extra_sql => ' WHERE cust_credit.custnum = '.$self->custnum.
+ ' AND cust_credit_bill._date > '.$self->_date.
+ ' AND cust_credit_bill.invnum < '.$self->invnum.' ',
+#! did not investigate why hashref doesn't work for this join query
+# hashref => {
+# 'cust_credit.custnum' => {op => '=', value => $self->custnum},
+# 'cust_credit_bill._date' => {op => '>', value => $self->_date},
+# 'cust_credit_bill.invnum' => {op => '<', value => $self->invnum},
+# },
+ });
+ return map {{
+ _date => $_->_date,
+ invnum => $_->invnum,
+ amount => $_->amount,
+ creditbillnum => $_->creditbillnum,
+ }} @cust_credit_bill;
}
+=sub _items_payments_postbill()
+
+ Returns an array of hashrefs for payments where
+ - Payment occured after this invoice
+ - Payment applied to an invoice before this invoice
+
+ Returned hashrefs are of the format returned by _items_payments()
+
+=cut
+
+sub _items_payments_postbill {
+ my $self = shift;
+
+ my @cust_bill_pay = qsearch({
+ table => 'cust_bill_pay',
+ select => join(', ',qw(
+ cust_bill_pay.billpaynum
+ cust_bill_pay._date
+ cust_bill_pay.invnum
+ cust_bill_pay.amount
+ )),
+ addl_from => ' LEFT JOIN cust_bill'.
+ ' ON (cust_bill_pay.invnum = cust_bill.invnum) ',
+ extra_sql => ' WHERE cust_bill.custnum = '.$self->custnum.
+ ' AND cust_bill_pay._date > '.$self->_date.
+ ' AND cust_bill_pay.invnum < '.$self->invnum.' ',
+ });
+
+ return map {{
+ _date => $_->_date,
+ invnum => $_->invnum,
+ amount => $_->amount,
+ billpaynum => $_->billpaynum,
+ }} @cust_bill_pay;
+}
+
+=sub _items_payments()
+
+ Return array of hashrefs containing payments to be shown as line-items
+ when rendering this bill.
+
+ keys for each payment item:
+ - paynum: id of payment
+ - amount: payment amount
+ - description: line item to be displayed on the bill
+
+ This method has three ways it selects which payments to display on
+ this bill:
+
+ 1) Default Case: No Conf flag for 'previous_balance-payments_since'
+
+ Returns payments that have been applied to this bill only
+
+ 2) Case:
+ Conf flag set for 'previous_balance-payments_since'
+
+ List all payments that have been recorded between the timestamps
+ of the previous invoice and this invoice
+
+ 3) Case:
+ Conf flag set for 'previous_balance-payments_since'
+ $opt{'template'} eq 'statement'
+
+ List all payments that have been recorded between the timestamps
+ of the previous invoice and the following invoice.
+
+ I am concerned with this implementation, but leaving in place as is
+ If this option is selected, while viewing an older bill, the old bill
+ will show ALL future payments for future bills, but no charges for
+ future bills. Somebody could be misled into believing they have a
+ large account credit when they don't. Also, interrupts the chain of
+ invoices as an account history... the customer could have two invoices
+ in their fileing cabinet, for two different dates, both with a line item
+ for the same duplicate payment. The accounting is technically accurate,
+ but somebody could easily become confused and think two payments were
+ made, when really those two line items on two different bills represent
+ only a single payment.
+
+=cut
+
sub _items_payments {
+
my $self = shift;
+
+ # Simple memoize
+ return @{$self->get('_items_payments')} if $self->get('_items_payments');
+
my %opt = @_;
+ my $template = $opt{template} || $self->get('_template');
+
+ my @return;
+ my @cust_pay_objs;
+
+ my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details');
+
+ if ($self->conf->exists('previous_balance-payments_since')) {
+ if ($template eq 'statement') {
+print "\nCASE 3\n";
+ # Case 3 (see above)
+ # Return payments timestamped between the previous and following bills
+
+ my $previous_bill = $self->previous_bill;
+ my $following_bill = $self->following_bill;
+
+ my $date_start = ref $previous_bill ? $previous_bill->_date : 0;
+ my $date_end = ref $following_bill ? $following_bill->_date : undef;
+
+ my %query = (
+ table => 'cust_pay',
+ hashref => {
+ custnum => $self->custnum,
+ _date => { op => '>=', value => $date_start },
+ },
+ );
+ $query{extra_sql} = " AND _date <= $date_end " if $date_end;
+
+ @cust_pay_objs = qsearch(\%query);
- my @b;
- my $detailed = $self->conf->exists('invoice_payment_details');
- my @objects;
- if ( $self->conf->exists('previous_balance-payments_since') ) {
- # then show payments dated on/after the previous bill...
- if ( $opt{'template'} eq 'statement' ) {
- # then the current bill is a "statement" (i.e. an invoice sent as
- # a payment receipt)
- # and in that case we want to see payments on or after THIS invoice
- @objects = qsearch('cust_pay', {
- 'custnum' => $self->custnum,
- '_date' => {op => '>=', value => $self->_date},
- });
} else {
- # the normal case: payments on or after the previous invoice
- my $date = 0;
- $date = $self->previous_bill->_date if $self->previous_bill;
- @objects = qsearch('cust_pay', {
- 'custnum' => $self->custnum,
- '_date' => {op => '>=', value => $date},
+ # Case 2 (see above)
+ # Return payments timestamped between this and the previous bill
+print "\nCASE 2\n";
+ my $date_start = 0;
+ my $date_end = $self->_date;
+
+ my $previous_bill = $self->previous_bill;
+ if (ref $previous_bill) {
+ $date_start = $previous_bill->_date;
+ }
+
+ @cust_pay_objs = qsearch({
+ table => 'cust_pay',
+ hashref => {
+ custnum => $self->custnum,
+ _date => {op => '>=', value => $date_start},
+ },
+ extra_sql => " AND _date <= $date_end ",
});
- # and before the current bill...
- @objects = grep { $_->_date < $self->_date } @objects;
}
+
} else {
- @objects = $self->cust_bill_pay;
+ # Case 1 (see above)
+ # Return payments applied only to this bill
+
+ @cust_pay_objs = $self->cust_bill_pay;
+
}
- foreach my $obj (@objects) {
+ $self->set(
+ '_items_payments',
+ [ $self->__items_payments_make_hashref(@cust_pay_objs) ]
+ );
+ return @{ $self->get('_items_payments') };
+}
+
+=sub _items_payments_total
+
+ Return a total of all records returned by _items_payments
+ Results vary based on invoicing conf flags
+
+=cut
+
+sub _items_payments_total {
+ my $self = shift;
+ my $tot = 0;
+ $tot += $_->{amount} for $self->_items_payments();
+ return $tot;
+}
+
+sub __items_payments_make_hashref {
+ # Transform a FS::cust_pay object into a simple hashref for invoice
+ my ($self, @cust_pay_objs) = @_;
+ my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details');
+ my @return;
+
+ for my $obj (@cust_pay_objs) {
+
+ # In case we're passed FS::cust_bill_pay (or something else?)
+ # Below, we use $obj to render amount rather than $cust_apy.
+ # If we were passed cust_bill_pay objs, then:
+ # $obj->amount represents the amount applied to THIS invoice
+ # $cust_pay->amount represents the total payment, which may have
+ # been applied accross several invoices.
+ # If we were passed cust_bill_pay objects, then the conf flag
+ # previous_balance-payments_since is NOT set, so we should not
+ # present any payments not applied to this invoice.
my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
- my $desc = $self->mt('Payment received').' '.
- $self->time2str_local('short', $cust_pay->_date );
- $desc .= $self->mt(' via ') .
- $cust_pay->payby_payinfo_pretty( $self->cust_main->locale )
- if $detailed;
-
- push @b, {
- 'description' => $desc,
- 'amount' => sprintf("%.2f", $obj->amount )
- };
- }
- @b;
+ my %r_obj = (
+ _date => $cust_pay->_date,
+ amount => sprintf("%.2f", $obj->amount),
+ paynum => $cust_pay->paynum,
+ payinfo => $cust_pay->payby_payinfo_pretty(),
+ description => join(' ',
+ $self->mt('Payment received'),
+ $self->time2str_local('short', $cust_pay->_date),
+ ),
+ );
-}
+ if ($c_invoice_payment_details) {
+ $r_obj{description} = join(' ',
+ $r_obj{description},
+ $self->mt('via'),
+ $cust_pay->payby_payinfo_pretty($self->cust_main->locale),
+ );
+ }
+
+ push @return, \%r_obj;
+ }
+ return @return;
+}
+
+=sub _items_total()
+
+ Generate the line-items to be shown on the bill in the "Totals" section
+
+ Returns a list of hashrefs, each with the keys:
+ - total_item: description field
+ - total_amount: dollar-formatted number amount
+
+ Information presented by this method varies based on Conf
+
+ Conf previous_balance-payments_due
+ - default, flag not set
+ Only transactions that were applied to this bill bill be
+ displayed and calculated intothe total. If items exist in
+ the past-due section, those items will disappear from this
+ invoice if they have been paid off.
+
+ - previous_balance-payments_due flag is set
+ Transactions occuring after the timestsamp of this
+ invoice are not reflected on invoice line items
+
+ Only payments/credits applied between the previous invoice
+ and this one are displayed and calculated into the total
+
+ - previous_balance-payments_due && $opt{template} eq 'statement'
+ Same as above, except payments/credits occuring before the date
+ of the following invoice are also displayed and calculated into
+ the total
+
+ Conf previous_balance-exclude_from_total
+ - default, flag not set
+ The "Totals" section contains a single line item.
+ The dollar amount of this line items is a sum of old and new charges
+ - previous_balance-exclude_from_total flag is set
+ The "Totals" section contains two line items.
+ One for previous balance, one for new charges
+ !NOTE: Avent virtualization flag 'disable_previous_balance' can
+ override the global conf flag previous_balance-exclude_from_total
+
+ Conf invoice_show_prior_due_date
+ - default, flag not set
+ Total line item in the "Totals" section does not mention due date
+ - invoice_show_prior_due_date flag is set
+ Total line item in the "Totals" section includes either the due
+ date of the invoice, or the specified invoice terms
+ ? Not sure why this is called "Prior" due date, since we seem to be
+ displaying THIS due date...
+=cut
sub _items_total {
my $self = shift;
my $conf = $self->conf;
- my @items;
- my ($pr_total) = $self->previous;
- my ($previous_charges_desc, $new_charges_desc, $new_charges_amount);
+ my $c_multi_line_total = 0;
+ $c_multi_line_total = 1
+ if $conf->exists('previous_balance-exclude_from_total')
+ && $self->enable_previous();
- if ( $conf->exists('previous_balance-exclude_from_total') ) {
- # if enabled, specifically add a line for the previous balance total
- $previous_charges_desc = $self->mt(
- $conf->config('previous_balance-text') || 'Previous Balance'
- );
+ my @line_items;
+ my $invoice_charges = $self->charged();
- # then return separate lines for previous balance and total new charges
- if ( $pr_total ) {
- push @items,
- { total_item => $previous_charges_desc,
- total_amount => sprintf('%.2f',$pr_total)
- };
- }
- }
+ # _items_previous() is aware of conf flags
+ my $previous_balance = 0;
+ $previous_balance += $_->{amount} for $self->_items_previous();
+
+ my $total_charges;
+ my $total_descr;
+
+ if ( $previous_balance && $c_multi_line_total ) {
+ # previous balance, new charges on separate lines
- if ( $conf->exists('previous_balance-exclude_from_total')
- or !$self->enable_previous ) {
- # show new charges only
+ push @line_items, {
+ total_amount => sprintf('%.2f',$previous_balance),
+ total_item => $self->mt(
+ $conf->config('previous_balance-text') || 'Previous Balance'
+ ),
+ };
- $new_charges_desc = $self->mt(
+ $total_charges = $invoice_charges;
+ $total_descr = $self->mt(
$conf->config('previous_balance-text-total_new_charges')
- || 'Total New Charges'
+ || 'Total New Charges'
);
- $new_charges_amount = $self->charged;
-
} else {
- # show new charges + previous invoice total
-
- $new_charges_desc = $self->mt('Total Charges');
- if ( $self->enable_previous ) {
- $new_charges_amount = sprintf('%.2f', $self->charged + $pr_total);
- } else {
- $new_charges_amount = sprintf('%.2f', $self->charged);
- }
-
+ # previous balance and new charges combined into a single total line
+ $total_charges = $invoice_charges + $previous_balance;
+ $total_descr = $self->mt('Total Charges');
}
if ( $conf->exists('invoice_show_prior_due_date') ) {
# then the due date should be shown with Total New Charges,
# and should NOT be shown with the Balance Due message.
+
if ( $self->due_date ) {
- # localize the "Please pay by" message and the date itself
- # (grammar issues with this, yeah)
- $new_charges_desc .= ' - ' . $self->mt('Please pay by') . ' ' .
- $self->due_date2str('short');
+ $total_descr = join(' ',
+ $total_descr,
+ '-',
+ $self->mt('Please pay by'),
+ $self->due_date2str('short')
+ );
} elsif ( $self->terms ) {
- # phrases like "due on receipt" should be localized
- $new_charges_desc .= ' - ' . $self->mt($self->terms);
+ $total_descr = join(' ',
+ $total_descr,
+ '-',
+ $self->mt($self->terms)
+ );
}
}
- push @items,
- { total_item => $new_charges_desc,
- total_amount => $new_charges_amount,
- };
+ push @line_items, {
+ total_amount => sprintf('%.2f', $total_charges),
+ total_item => $total_descr,
+ };
- @items;
+ return @line_items;
}
+=item _items_aging_balances
+
+ Returns an array of aged balance amounts from a given epoch timestamp.
+
+ The time of day is ignored for this calculation, so that slight differences
+ on the generation time of an invoice doesn't determine which column an
+ aged balance falls into.
+
+ Will not include any balances dated after the given timestamp in
+ the calculated totals
+ usage:
+ @aged_balances = $b->_items_aging_balances( $b->_date )
+
+ @aged_balances = (
+ under30d,
+ 30d-60d,
+ 60d-90d,
+ over90d
+ )
+
+=cut
+
+sub _items_aging_balances {
+ my ($self, $basetime) = @_;
+ die "Incorrect usage of _items_aging_balances()" unless ref $self;
+
+ $basetime = $self->_date unless $basetime;
+ my @aging_balances = (0, 0, 0, 0);
+ my @open_invoices = $self->_items_previous();
+ my $d30 = 2592000; # 60 * 60 * 24 * 30,
+ my $d60 = 5184000; # 60 * 60 * 24 * 60,
+ my $d90 = 7776000; # 60 * 60 * 24 * 90
+
+ # Move the clock back on our given day to 12:00:01 AM
+ my $dt_basetime = DateTime->from_epoch(epoch => $basetime);
+ my $dt_12am = DateTime->new(
+ year => $dt_basetime->year,
+ month => $dt_basetime->month,
+ day => $dt_basetime->day,
+ hour => 0,
+ minute => 0,
+ second => 1,
+ )->epoch();
+
+ # set our epoch breakpoints
+ $_ = $dt_12am - $_ for $d30, $d60, $d90;
+
+ # grep the aged balances
+ for my $oinv (@open_invoices) {
+ if ($oinv->{_date} <= $basetime && $oinv->{_date} > $d30) {
+ # If post invoice dated less than 30days ago
+ $aging_balances[0] += $oinv->{amount};
+ } elsif ($oinv->{_date} <= $d30 && $oinv->{_date} > $d60) {
+ # If past invoice dated between 30-60 days ago
+ $aging_balances[1] += $oinv->{amount};
+ } elsif ($oinv->{_date} <= $d60 && $oinv->{_date} > $d90) {
+ # If past invoice dated between 60-90 days ago
+ $aging_balances[2] += $oinv->{amount};
+ } else {
+ # If past invoice dated 90+ days ago
+ $aging_balances[3] += $oinv->{amount};
+ }
+ }
+
+ return map{ sprintf('%.2f',$_) } @aging_balances;
+}
=item call_details [ OPTION => VALUE ... ]
my $row = shift;
$row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
-
+
};
}
}
-# this is called from search/cust_bill.html and given all its search
+# this is called from search/cust_bill.html and given all its search
# parameters, so it needs to perform the same search.
sub re_X {
delete $query->{'count_query'};
delete $query->{'count_addl'};
- $query->{debug} = 1; # was in here before, is obviously useful
+ $query->{debug} = 1; # was in here before, is obviously useful
my @cust_bill = qsearch( $query );
sub owed_sql {
my ($class, $start, $end) = @_;
- 'charged - '.
- $class->paid_sql($start, $end). ' - '.
+ 'charged - '.
+ $class->paid_sql($start, $end). ' - '.
$class->credited_sql($start, $end);
}
=cut
1;
-