);
}
+=item cust_bill_pkg_pkgnum PKGNUM
+
+Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
+specified pkgnum.
+
+=cut
+
+sub cust_bill_pkg_pkgnum {
+ my( $self, $pkgnum ) = @_;
+ qsearch(
+ { 'table' => 'cust_bill_pkg',
+ 'hashref' => { 'invnum' => $self->invnum,
+ 'pkgnum' => $pkgnum,
+ },
+ 'order_by' => 'ORDER BY billpkgnum',
+ }
+ );
+}
+
=item cust_pkg
Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
;
}
+=item cust_bill_pay_pkgnum
+
+Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
+with matching pkgnum.
+
+=cut
+
+sub cust_bill_pay_pkgnum {
+ my( $self, $pkgnum ) = @_;
+ sort { $a->_date <=> $b->_date }
+ qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
+ 'pkgnum' => $pkgnum,
+ }
+ );
+}
+
+=item cust_credited_pkgnum
+
+Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
+with matching pkgnum.
+
+=cut
+
+sub cust_credited_pkgnum {
+ my( $self, $pkgnum ) = @_;
+ sort { $a->_date <=> $b->_date }
+ qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
+ 'pkgnum' => $pkgnum,
+ }
+ );
+}
+
=item tax
Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
$balance;
}
+sub owed_pkgnum {
+ my( $self, $pkgnum ) = @_;
+
+ #my $balance = $self->charged;
+ my $balance = 0;
+ $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
+
+ $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum);
+ $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum);
+
+ $balance = sprintf( "%.2f", $balance);
+ $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
+ $balance;
+}
+
=item apply_payments_and_credits
=cut
my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
+ if ( $conf->exists('pkg-balances') ) {
+ # limit @payments & @credits to those w/ a pkgnum grepped from $self
+ my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
+ @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
+ @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
+ }
+
while ( $self->owed > 0 and ( @payments || @credits ) ) {
my $app = '';
die "guru meditation #12 and 35";
}
+ my $unapp_amount;
if ( $app eq 'pay' ) {
my $payment = shift @payments;
-
- $app = new FS::cust_bill_pay {
- 'paynum' => $payment->paynum,
- 'amount' => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
- };
+ $unapp_amount = $payment->unapplied;
+ $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
+ $app->pkgnum( $payment->pkgnum )
+ if $conf->exists('pkg-balances') && $payment->pkgnum;
} elsif ( $app eq 'credit' ) {
my $credit = shift @credits;
-
- $app = new FS::cust_credit_bill {
- 'crednum' => $credit->crednum,
- 'amount' => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
- };
+ $unapp_amount = $credit->credited;
+ $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
+ $app->pkgnum( $credit->pkgnum )
+ if $conf->exists('pkg-balances') && $credit->pkgnum;
} else {
die "guru meditation #12 and 35";
}
+ my $owed;
+ if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
+ warn "owed_pkgnum ". $app->pkgnum;
+ $owed = $self->owed_pkgnum($app->pkgnum);
+ } else {
+ $owed = $self->owed;
+ }
+ next unless $owed > 0;
+
+ warn "min ( $unapp_amount, $owed )\n";
+ $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
+
$app->invnum( $self->invnum );
my $error = $app->insert;
'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
);
+ my %cdrs = ( 'unsquelch_cdr' => $conf->exists('voip-cdr_email') );
+
if (ref($args{'to'}) eq 'ARRAY') {
$return{'to'} = $args{'to'};
} else {
if ( ref($args{'print_text'}) eq 'ARRAY' ) {
$data = $args{'print_text'};
} else {
- $data = [ $self->print_text('', $args{'template'}) ];
+ $data = [ $self->print_text('', $args{'template'}, %cdrs) ];
}
}
my $from = $1 || 'example.com';
my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
- my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
- my $file;
+ my $logo;
+ my $agentnum = $self->cust_main->agentnum;
if ( defined($args{'template'}) && length($args{'template'})
- && -e "$path/logo_". $args{'template'}. ".png"
+ && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
)
{
- $file = "$path/logo_". $args{'template'}. ".png";
+ $logo = 'logo_'. $args{'template'}. '.png';
} else {
- $file = "$path/logo.png";
+ $logo = "logo.png";
}
+ my $image_data = $conf->config_binary( $logo, $agentnum);
my $image = build MIME::Entity
'Type' => 'image/png',
'Encoding' => 'base64',
- 'Path' => $file,
+ 'Data' => $image_data,
'Filename' => 'logo.png',
'Content-ID' => "<$content_id>",
;
' </title>',
' </head>',
' <body bgcolor="#e8e8e8">',
- $self->print_html('', $args{'template'}, $content_id),
+ $self->print_html({ time => '',
+ template => $args{'template'},
+ cid => $content_id,
+ %cdrs,
+ }),
' </body>',
'</html>',
],
#'Filename' => 'invoice.pdf',
);
+ my @otherparts = ();
+ if ( $self->cust_main->email_csv_cdr ) {
+
+ push @otherparts, build MIME::Entity
+ 'Type' => 'text/csv',
+ 'Encoding' => '7bit',
+ 'Data' => [ map { "$_\n" } $self->call_details ],
+ 'Disposition' => 'attachment',
+ ;
+
+ }
+
if ( $conf->exists('invoice_email_pdf') ) {
#attaching pdf too:
$related->add_part($image);
- my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
+ my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'}, %cdrs);
- $return{'mimeparts'} = [ $related, $pdf ];
+ $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
} else {
# image/png
$return{'content-type'} = 'multipart/related';
- $return{'mimeparts'} = [ $alternative, $image ];
+ $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
$return{'type'} = 'multipart/alternative'; #Content-Type of first part...
#$return{'disposition'} = 'inline';
#mime parts arguments a la MIME::Entity->build().
$return{'mimeparts'} = [
- { $self->mimebuild_pdf('', $args{'template'}) }
+ { $self->mimebuild_pdf('', $args{'template'}, %cdrs) }
];
}
if ( ref($args{'print_text'}) eq 'ARRAY' ) {
$return{'body'} = $args{'print_text'};
} else {
- $return{'body'} = [ $self->print_text('', $args{'template'}) ];
+ $return{'body'} = [ $self->print_text('', $args{'template'}, %cdrs) ];
}
}
'Encoding' => 'base64',
'Data' => [ $self->print_pdf(@_) ],
'Disposition' => 'attachment',
- 'Filename' => 'invoice.pdf',
+ 'Filename' => 'invoice-'. $self->invnum. '.pdf',
);
}
} else { #pkgnum tax
next unless $cust_bill_pkg->setup != 0;
- my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
- ? ( $cust_bill_pkg->itemdesc || 'Tax' )
- : 'Tax';
- ($pkg, $setup, $recur, $sdate, $edate) =
- ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
+ $pkg = $cust_bill_pkg->desc;
+ $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
+ ( $sdate, $edate ) = ( '', '' );
}
$csv->combine(
=cut
sub print_text {
- my( $self, $today, $template ) = @_;
+ my( $self, $today, $template, %opt ) = @_;
my %params = ( 'format' => 'template' );
$params{'time'} = $today if $today;
$params{'template'} = $template if $template;
+ $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
$self->print_generic( %params );
}
=cut
sub print_latex {
-
- my( $self, $today, $template ) = @_;
+ my( $self, $today, $template, %opt ) = @_;
my %params = ( 'format' => 'latex' );
$params{'time'} = $today if $today;
$params{'template'} = $template if $template;
+ $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
$template ||= $self->_agent_template;
UNLINK => 0,
) or die "can't open temp file: $!\n";
- if ($template && $conf->exists("logo_${template}.eps")) {
- print $lh $conf->config_binary("logo_${template}.eps")
+ my $agentnum = $self->cust_main->agentnum;
+
+ if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
+ print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
or die "can't write temp file: $!\n";
- }else{
- print $lh $conf->config_binary('logo.eps')
+ } else {
+ print $lh $conf->config_binary('logo.eps', $agentnum)
or die "can't write temp file: $!\n";
}
close $lh;
=cut
+#what's with all the sprintf('%10.2f')'s in here? will it cause any
+# (alignment?) problems to change them all to '%.2f' ?
sub print_generic {
my( $self, %params ) = @_;
$invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
$invoice_data{'balance'} = sprintf("%.2f", $balance_due);
+ my $agentnum = $self->cust_main->agentnum;
+
#do variable substitution in notes, footer, smallfooter
foreach my $include (qw( notes footer smallfooter coupon )) {
my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
my @inc_src;
- if ( $conf->exists($inc_file) && length( $conf->config($inc_file) ) ) {
+ if ( $conf->exists($inc_file, $agentnum)
+ && length( $conf->config($inc_file, $agentnum) ) ) {
- @inc_src = $conf->config($inc_file);
+ @inc_src = $conf->config($inc_file, $agentnum);
} else {
s/--\@\]/$delimiters{$format}[1]/g;
$_;
}
- &$convert_map( $conf->config($inc_file) );
+ &$convert_map( $conf->config($inc_file, $agentnum) );
}
);
my $money_char = $money_chars{$format};
- my %other_money_chars = ( 'latex' => '\dollar ',
+ my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
'html' => $conf->config('money_char') || '$',
'template' => '',
);
push @sections, { 'description' => '', 'subtotal' => '' };
}
- foreach my $line_item ( $conf->exists('disable_previous_balance')
- ? ()
- : $self->_items_previous
- )
+ unless ( $conf->exists('disable_previous_balance')
+ || $conf->exists('previous_balance-summary_only')
+ )
{
- my $detail = {
- ext_description => [],
- };
- $detail->{'ref'} = $line_item->{'pkgnum'};
- $detail->{'quantity'} = 1;
- $detail->{'section'} = $previous_section;
- $detail->{'description'} = &$escape_function($line_item->{'description'});
- if ( exists $line_item->{'ext_description'} ) {
- @{$detail->{'ext_description'}} = map {
- &$escape_function($_);
- } @{$line_item->{'ext_description'}};
+
+ foreach my $line_item ( $self->_items_previous ) {
+
+ my $detail = {
+ ext_description => [],
+ };
+ $detail->{'ref'} = $line_item->{'pkgnum'};
+ $detail->{'quantity'} = 1;
+ $detail->{'section'} = $previous_section;
+ $detail->{'description'} = &$escape_function($line_item->{'description'});
+ if ( exists $line_item->{'ext_description'} ) {
+ @{$detail->{'ext_description'}} = map {
+ &$escape_function($_);
+ } @{$line_item->{'ext_description'}};
+ }
+ $detail->{'amount'} = ( $old_latex ? '' : $money_char).
+ $line_item->{'amount'};
+ $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
+
+ push @detail_items, $detail;
+ push @buf, [ $detail->{'description'},
+ $money_char. sprintf("%10.2f", $line_item->{'amount'}),
+ ];
}
- $detail->{'amount'} = ( $old_latex ? '' : $money_char).
- $line_item->{'amount'};
- $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
-
- push @detail_items, $detail;
- push @buf, [ $detail->{'description'},
- $money_char. sprintf("%10.2f", $line_item->{'amount'}),
- ];
+
}
-
+
if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
push @buf, ['','-----------'];
push @buf, [ 'Total Previous Balance',
}
foreach my $tax ( $self->_items_tax ) {
- my $total = {};
- $total->{'total_item'} = &$escape_function($tax->{'description'});
+
$taxtotal += $tax->{'amount'};
- $total->{'total_amount'} = $other_money_char. $tax->{'amount'};
+
+ my $description = &$escape_function( $tax->{'description'} );
+ my $amount = sprintf( '%.2f', $tax->{'amount'} );
+
if ( $multisection ) {
+
my $money = $old_latex ? '' : $money_char;
push @detail_items, {
ext_description => [],
ref => '',
quantity => '',
- description => &$escape_function($tax->{'description'}),
- amount => $money. $tax->{'amount'},
+ description => $description,
+ amount => $money. $amount,
product_code => '',
section => $tax_section,
};
- }else{
- push @total_items, $total;
+
+ } else {
+
+ push @total_items, {
+ 'total_item' => $description,
+ 'total_amount' => $other_money_char. $amount,
+ };
+
}
- push @buf,[ $total->{'total_item'},
- $money_char. sprintf("%10.2f", $total->{'total_amount'}),
+
+ push @buf,[ $description,
+ $money_char. $amount,
];
}
# credits
my $credittotal = 0;
- foreach my $credit ( $self->_items_credits ) {
+ foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
+
my $total;
$total->{'total_item'} = &$escape_function($credit->{'description'});
$credittotal += $credit->{'amount'};
product_code => '',
section => $adjust_section,
};
- }else{
+ } else {
push @total_items, $total;
}
+
}
$invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
-
- # credits (again)
- foreach ( $self->cust_credited ) {
-
- #something more elaborate if $_->amount ne $_->cust_credit->credited ?
-
- my $reason = substr($_->cust_credit->reason,0,32);
- $reason .= '...' if length($reason) < length($_->cust_credit->reason);
- $reason = " ($reason) " if $reason;
- push @buf,[
- "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")". $reason,
- $money_char. sprintf("%10.2f",$_->amount)
- ];
- }
+ #credits (again)
+ foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
+ push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
+ }
+
# payments
my $paymenttotal = 0;
foreach my $payment ( $self->_items_payments ) {
return $self->cust_main->invoice_terms
if $self->cust_main->invoice_terms;
- #use configured default or default default
- $conf->config('invoice_default_terms') || 'Payable upon receipt';
+ #use configured default
+ $conf->config('invoice_default_terms') || '';
}
sub due_date {
my $section = $opt{section}->{description} if $opt{section};
my @b = ();
+ my ($s, $r, $u) = ( undef, undef, undef );
foreach my $cust_bill_pkg ( @$cust_bill_pkg )
{
+
+ foreach ( $s, $r, $u ) {
+ if ( $_ && !$cust_bill_pkg->hidden ) {
+ $_->{amount} = sprintf( "%.2f", $_->{amount} ),
+ $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
+ push @b, { %$_ };
+ $_ = undef;
+ }
+ }
+
foreach my $display ( grep { defined($section)
? $_->section eq $section
: 1
my $description = $desc;
$description .= ' Setup' if $cust_bill_pkg->recur != 0;
- my @d = map &{$escape_function}($_),
- $cust_pkg->h_labels_short($self->_date);
+ my @d = ();
+ push @d, map &{$escape_function}($_),
+ $cust_pkg->h_labels_short($self->_date)
+ unless $cust_pkg->part_pkg->hide_svc_detail
+ || $cust_bill_pkg->hidden;
push @d, $cust_bill_pkg->details(%details_opt)
if $cust_bill_pkg->recur == 0;
- push @b, {
- description => $description,
- #pkgpart => $part_pkg->pkgpart,
- pkgnum => $cust_bill_pkg->pkgnum,
- amount => sprintf("%.2f", $cust_bill_pkg->setup),
- unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup),
- quantity => $cust_bill_pkg->quantity,
- ext_description => \@d,
+ if ( $cust_bill_pkg->hidden ) {
+ $s->{amount} += $cust_bill_pkg->setup;
+ $s->{unit_amount} += $cust_bill_pkg->unitsetup;
+ push @{ $s->{ext_description} }, @d;
+ } else {
+ $s = {
+ description => $description,
+ #pkgpart => $part_pkg->pkgpart,
+ pkgnum => $cust_bill_pkg->pkgnum,
+ amount => $cust_bill_pkg->setup,
+ unit_amount => $cust_bill_pkg->unitsetup,
+ quantity => $cust_bill_pkg->quantity,
+ ext_description => \@d,
+ };
};
}
" - ". time2str("%x", $cust_bill_pkg->edate). ")";
}
+ my @d = ();
+
#at least until cust_bill_pkg has "past" ranges in addition to
#the "future" sdate/edate ones... see #3032
- my @d = ();
+ my @dates = ( $self->_date );
+ my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
+ push @dates, $prev->sdate if $prev;
+
push @d, map &{$escape_function}($_),
- $cust_pkg->h_labels_short($self->_date)
- #$cust_bill_pkg->edate,
- #$cust_bill_pkg->sdate),
- ;
-
- @d = () if ($cust_bill_pkg->itemdesc || $is_summary);
+ $cust_pkg->h_labels_short(@dates)
+ #$cust_bill_pkg->edate,
+ #$cust_bill_pkg->sdate)
+ unless $cust_pkg->part_pkg->hide_svc_detail
+ || $cust_bill_pkg->itemdesc
+ || $cust_bill_pkg->hidden
+ || $is_summary;
+
push @d, $cust_bill_pkg->details(%details_opt)
unless ($is_summary || $type && $type eq 'R');
$amount = $cust_bill_pkg->usage;
}
- push @b, {
- description => $description,
- #pkgpart => $part_pkg->pkgpart,
- pkgnum => $cust_bill_pkg->pkgnum,
- amount => sprintf("%.2f", $amount),
- unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur),
- quantity => $cust_bill_pkg->quantity,
- ext_description => \@d,
- } unless ( $type eq 'U' && ! $amount );
+ if ( !$type || $type eq 'R' ) {
+
+ if ( $cust_bill_pkg->hidden ) {
+ $r->{amount} += $amount;
+ $r->{unit_amount} += $cust_bill_pkg->unitrecur;
+ push @{ $r->{ext_description} }, @d;
+ } else {
+ $r = {
+ description => $description,
+ #pkgpart => $part_pkg->pkgpart,
+ pkgnum => $cust_bill_pkg->pkgnum,
+ amount => $amount,
+ unit_amount => $cust_bill_pkg->unitrecur,
+ quantity => $cust_bill_pkg->quantity,
+ ext_description => \@d,
+ };
+ }
+
+ } elsif ( $amount ) { # && $type eq 'U'
+
+ if ( $cust_bill_pkg->hidden ) {
+ $u->{amount} += $amount;
+ $u->{unit_amount} += $cust_bill_pkg->unitrecur;
+ push @{ $u->{ext_description} }, @d;
+ } else {
+ $u = {
+ description => $description,
+ #pkgpart => $part_pkg->pkgpart,
+ pkgnum => $cust_bill_pkg->pkgnum,
+ amount => $amount,
+ unit_amount => $cust_bill_pkg->unitrecur,
+ quantity => $cust_bill_pkg->quantity,
+ ext_description => \@d,
+ };
+ }
- }
+ }
+
+ } # recurring or usage with recurring charge
} else { #pkgnum tax or one-shot line item (??)
}
+ foreach ( $s, $r, $u ) {
+ if ( $_ ) {
+ $_->{amount} = sprintf( "%.2f", $_->{amount} ),
+ $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
+ push @b, { %$_ };
+ }
+ }
+
@b;
}
sub _items_credits {
- my $self = shift;
+ my( $self, %opt ) = @_;
+ my $trim_len = $opt{'trim_len'} || 60;
my @b;
#credits
#something more elaborate if $_->amount ne $_->cust_credit->credited ?
- my $reason = $_->cust_credit->reason;
- #my $reason = substr($_->cust_credit->reason,0,32);
- #$reason .= '...' if length($reason) < length($_->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) .")".
'amount' => sprintf("%.2f",$_->amount),
};
}
- #foreach ( @cr_cust_credit ) {
- # push @buf,[
- # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
- # $money_char. sprintf("%10.2f",$_->credited)
- # ];
- #}
@b;
}
+=item call_details
+
+Returns an array of CSV strings representing the call details for this invoice
+
+=cut
+
+sub call_details {
+ my $self = shift;
+ map { $_->details( 'format_function' => sub{ shift },
+ 'escape_function' => sub{ return() },
+ )
+ }
+ grep { $_->pkgnum }
+ $self->cust_bill_pkg;
+}
+
=back