my $self = shift;
my $total = 0;
my @cust_bill = sort { $a->_date <=> $b->_date }
- grep { $_->owed != 0 && $_->_date < $self->_date }
- qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
+ grep { $_->owed != 0 }
+ qsearch( 'cust_bill', { 'custnum' => $self->custnum,
+ '_date' => { op=>'<', value=>$self->_date },
+ } )
;
foreach ( @cust_bill ) { $total += $_->owed; }
$total, @cust_bill;
$balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
}
+ my $cust_main = $self->cust_main;
+
return 'N/A' unless ! $agentnums
- or grep { $_ == $self->cust_main->agentnum } @$agentnums;
+ or grep { $_ == $cust_main->agentnum } @$agentnums;
return ''
- unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
+ unless $cust_main->total_owed_date($self->_date) > $balance_over;
$invoice_from ||= $self->_agent_invoice_from || #XXX should go away
- $conf->config('invoice_from', $self->cust_main->agentnum );
+ $conf->config('invoice_from', $cust_main->agentnum );
my %opt = (
'template' => $template,
'notice_name' => ( $notice_name || 'Invoice' ),
);
- my @invoicing_list = $self->cust_main->invoicing_list;
+ my @invoicing_list = $cust_main->invoicing_list;
#$self->email_invoice(\%opt)
$self->email(\%opt)
- if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
+ if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
+ && ! $self->invoice_noemail;
#$self->print_invoice(\%opt)
$self->print(\%opt)
=over 4
-=item format - 'default' or 'billco'
+=item format - any of FS::Misc::::Invoicing::spool_formats
-=item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L<FS::cust_main_invoice>).
+=item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the
+customer has the corresponding invoice destinations set (see
+L<FS::cust_main_invoice>).
-=item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
+=item agent_spools - if set to a true value, will spool to per-agent files
+rather than a single global file
-=item balanceover - if set, only spools the invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount.
+=item ftp_targetnum - if set to an FTP target (see L<FS::ftp_target>), will
+append to that spool. L<FS::Cron::upload> will then send the spool file to
+that destination.
+
+=item balanceover - if set, only spools the invoice if the total amount owed on
+this invoice and all older invoices is greater than the specified amount.
=back
my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
- my $file =
- "$spooldir/".
- ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
- ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
- '.csv';
+ my $file;
+ if ( $opt{'agent_spools'} ) {
+ $file = 'agentnum'.$cust_main->agentnum;
+ } else {
+ $file = 'spool';
+ }
+
+ if ( $opt{'ftp_targetnum'} ) {
+ $spooldir .= '/target'.$opt{'ftp_targetnum'};
+ mkdir $spooldir, 0700 unless -d $spooldir;
+ } # otherwise it just goes into export.xxx/cust_bill
+
+ if ( lc($opt{'format'}) eq 'billco' ) {
+ $file .= '-header';
+ }
+
+ $file = "$spooldir/$file.csv";
my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
flock(CSV, LOCK_UN);
close CSV;
- $file =
- "$spooldir/".
- ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
- '-detail.csv';
+ $file =~ s/-header.csv$/-detail.csv/;
open(CSV,">>$file") or die "can't open $file: $!";
flock(CSV, LOCK_EX);
Options are:
-format - 'default' or 'billco'
+format - 'default', 'billco', 'oneline', 'bridgestone'
Returns a list consisting of two scalars. The first is a single line of CSV
header information for this invoice. The second is one or more lines of CSV
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, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
+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
+header line only, with the fields:
+
+Agent number, agent name, customer number, first name, last name, address
+line 1, address line 2, city, state, zip, invoice date, invoice number,
+amount charged, amount due,
+
+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
+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
+"once_percust_every" condition.
+
=cut
sub print_csv {
'0', # 29 | Other Taxes & Fees*** NUM* 9
);
+ } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
+
+ my ($previous_balance) = $self->previous;
+ my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
+ my @items = map {
+ ($_->{pkgnum} || ''),
+ $_->{description},
+ $_->{amount}
+ } $self->_items_pkg;
+
+ $csv->combine(
+ $cust_main->agentnum,
+ $cust_main->agent->agent,
+ $self->custnum,
+ $cust_main->first,
+ $cust_main->last,
+ $cust_main->address1,
+ $cust_main->address2,
+ $cust_main->city,
+ $cust_main->state,
+ $cust_main->zip,
+
+ # invoice fields
+ time2str("%x", $self->_date),
+ $self->invnum,
+ $self->charged,
+ $totaldue,
+
+ @items,
+ );
+
+ } elsif ( lc($opt{'format'}) eq 'bridgestone' ) {
+
+ # bypass the CSV stuff and just return this
+ my $longdate = time2str('%B %d, %Y', time); #current time, right?
+ my $zip = $cust_main->zip;
+ $zip =~ s/\D//;
+ my $prefix = $self->conf->config('bridgestone-prefix', $cust_main->agentnum)
+ || '';
+ return (
+ sprintf(
+ "%-5s%-15s%-20s%-30s%-30s%-30s%-30s%-20s%-2s%-9s\n",
+ $prefix,
+ $cust_main->display_custnum,
+ $longdate,
+ uc(substr($cust_main->contact_firstlast,0,30)),
+ uc(substr($cust_main->company ,0,30)),
+ uc(substr($cust_main->address1 ,0,30)),
+ uc(substr($cust_main->address2 ,0,30)),
+ uc(substr($cust_main->city ,0,20)),
+ uc($cust_main->state),
+ $zip
+ ),
+ '' #detail
+ );
+
} else {
$csv->combine(
}
+ } elsif ( lc($opt{'format'}) eq 'oneline' ) {
+
+ #do nothing
+
} else {
foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
$invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
my $countrydefault = $conf->config('countrydefault') || 'US';
- my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
- foreach ( qw( contact company address1 address2 city state zip country fax) ){
- my $method = $prefix.$_;
+ foreach ( qw( address1 address2 city state zip country fax) ){
+ my $method = 'ship_'.$_;
$invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
}
+ foreach ( qw( contact company ) ) { #compatibility
+ $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
+ }
$invoice_data{'ship_country'} = ''
if ( $invoice_data{'ship_country'} eq $countrydefault );
my $late_sections = [];
my $extra_sections = [];
my $extra_lines = ();
+
+ my $default_section = { 'description' => '',
+ 'subtotal' => '',
+ 'no_subtotal' => 1,
+ };
+
if ( $multisection ) {
($extra_sections, $extra_lines) =
$self->_items_extra_usage_sections($escape_function_nonbsp, $format)
}
} else {# not multisection
# make a default section
- push @sections, { 'description' => '', 'subtotal' => '',
- 'no_subtotal' => 1 };
+ push @sections, $default_section;
# and calculate the finance charge total, since it won't get done otherwise.
# XXX possibly other totals?
# XXX possibly finance_pkgclass should not be used in this manner?
};
$detail->{'ref'} = $line_item->{'pkgnum'};
$detail->{'quantity'} = 1;
- $detail->{'section'} = $previous_section;
+ $detail->{'section'} = $multisection ? $previous_section
+ : $default_section;
$detail->{'description'} = &$escape_function($line_item->{'description'});
if ( exists $line_item->{'ext_description'} ) {
@{$detail->{'ext_description'}} = map {
if ( $display->post_total && !$summarypage ) {
if (! $type || $type eq 'S') {
$late_subtotal{$section} += $cust_bill_pkg->setup
- if $cust_bill_pkg->setup != 0;
+ if $cust_bill_pkg->setup != 0
+ || $cust_bill_pkg->setup_show_zero;
}
if (! $type) {
$late_subtotal{$section} += $cust_bill_pkg->recur
- if $cust_bill_pkg->recur != 0;
+ if $cust_bill_pkg->recur != 0
+ || $cust_bill_pkg->recur_show_zero;
}
if ($type && $type eq 'R') {
$late_subtotal{$section} += $cust_bill_pkg->recur - $usage
- if $cust_bill_pkg->recur != 0;
+ if $cust_bill_pkg->recur != 0
+ || $cust_bill_pkg->recur_show_zero;
}
if ($type && $type eq 'U') {
if (! $type || $type eq 'S') {
$subtotal{$section} += $cust_bill_pkg->setup
- if $cust_bill_pkg->setup != 0;
+ if $cust_bill_pkg->setup != 0
+ || $cust_bill_pkg->setup_show_zero;
}
if (! $type) {
$subtotal{$section} += $cust_bill_pkg->recur
- if $cust_bill_pkg->recur != 0;
+ if $cust_bill_pkg->recur != 0
+ || $cust_bill_pkg->recur_show_zero;
}
if ($type && $type eq 'R') {
$subtotal{$section} += $cust_bill_pkg->recur - $usage
- if $cust_bill_pkg->recur != 0;
+ if $cust_bill_pkg->recur != 0
+ || $cust_bill_pkg->recur_show_zero;
}
if ($type && $type eq 'U') {
my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
+ my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
+
my @b = ();
my ($s, $r, $u) = ( undef, undef, undef );
foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
}
}
+ my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
+
warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
$cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
if $DEBUG > 1;
}
#grep { !$_->summary || !$summary_page } # bunk!
grep { !$_->summary || $multisection }
- $cust_bill_pkg->cust_bill_pkg_display
+ @cust_bill_pkg_display
)
{
my $description = ($is_summary && $type && $type eq 'U')
? "Usage charges" : $desc;
+ #pry be a bit more efficient to look some of this conf stuff up
+ # outside the loop
unless (
$conf->exists('disable_line_item_date_ranges')
|| $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
) {
my $time_period;
- my $date_style = $conf->config('cust_bill-line_item-date_style');
- if ( $date_style eq 'month_of' ) {
+ my $date_style = $conf->config( 'cust_bill-line_item-date_style',
+ $cust_main->agentnum
+ );
+ if ( defined($date_style) && $date_style eq 'month_of' ) {
$time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
+ } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
+ my $desc = $conf->config( 'cust_bill-line_item-date_description',
+ $cust_main->agentnum
+ );
+ $desc .= ' ' unless $desc =~ /\s$/;
+ $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
} else {
$time_period = time2str($date_format, $cust_bill_pkg->sdate).
" - ". time2str($date_format, $cust_bill_pkg->edate);
join(', ', map { "#$_" } $plan->pkgnums)
if $list_pkgnums;
+ # discounts for non-integer months don't work anyway
+ $months = sprintf("%d", $months);
+
+{
description => $self->mt('Save [_1]% by paying for [_2] months',
$percent, $months),
}
sub re_X {
+ # spool_invoice ftp_invoice fax_invoice print_invoice
my($method, $job, %param ) = @_;
if ( $DEBUG ) {
warn "re_X $method for job $job with param:\n".