X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=769839b30a50dbda17e91a5048af3a8e0df53aaf;hb=bfd85806a865e8428c99aadbe4fd11a7af19f731;hp=c23e85626ac8a8f5c4ba2e14167a019eb2d4a7f3;hpb=dad604abef4fbebc421e753ad5a141879faf67b9;p=freeside.git diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index c23e85626..769839b30 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -17,6 +17,7 @@ use FS::Record qw( qsearch qsearchs dbh ); use FS::cust_main_Mixin; use FS::cust_main; use FS::cust_bill_pkg; +use FS::cust_bill_pkg_display; use FS::cust_credit; use FS::cust_pay; use FS::cust_pkg; @@ -226,7 +227,12 @@ Returns the line items (see L) for this invoice. sub cust_bill_pkg { my $self = shift; - qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } ); + qsearch( + { 'table' => 'cust_bill_pkg', + 'hashref' => { 'invnum' => $self->invnum }, + 'order_by' => 'ORDER BY billpkgnum', + } + ); } =item cust_pkg @@ -557,19 +563,27 @@ sub apply_payments_and_credits { } -=item generate_email PARAMHASH +=item generate_email OPTION => VALUE ... -PARAMHASH can contain the following: +Options: =over 4 -=item from => sender address, required +=item from + +sender address, required + +=item tempate -=item tempate => alternate template name, optional +alternate template name, optional -=item print_text => text attachment arrayref, optional +=item print_text -=item subject => email subject, optional +text attachment arrayref, optional + +=item subject + +email subject, optional =back @@ -829,7 +843,9 @@ sub send { my $invoice_from = scalar(@_) ? shift - : ( $self->_agent_invoice_from || $conf->config('invoice_from') ); + : ( $self->_agent_invoice_from || #XXX should go away + $conf->config('invoice_from', $self->cust_main->agentnum ) + ); my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0; @@ -885,7 +901,10 @@ sub email { my $invoice_from = scalar(@_) ? shift - : ( $self->_agent_invoice_from || $conf->config('invoice_from') ); + : ( $self->_agent_invoice_from || #XXX should go away + $conf->config('invoice_from', $self->cust_main->agentnum ) + ); + my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } $self->cust_main->invoicing_list; @@ -893,10 +912,13 @@ sub email { #better to notify this person than silence @invoicing_list = ($invoice_from) unless @invoicing_list; + my $subject = $self->email_subject($template); + my $error = send_email( $self->generate_email( 'from' => $invoice_from, 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ], + 'subject' => $subject, 'template' => $template, ) ); @@ -905,6 +927,24 @@ sub email { } +sub email_subject { + my $self = shift; + + #my $template = scalar(@_) ? shift : ''; + #per-template? + + my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum) + || 'Invoice'; + + my $cust_main = $self->cust_main; + my $name = $cust_main->name; + my $name_short = $cust_main->name_short; + my $invoice_number = $self->invnum; + my $invoice_date = $self->_date_pretty; + + eval qq("$subject"); +} + =item lpr_data [ TEMPLATENAME ] Returns the postscript or plaintext for this invoice as an arrayref. @@ -961,6 +1001,46 @@ sub fax_invoice { } +=item ftp_invoice [ TEMPLATENAME ] + +Sends this invoice data via FTP. + +TEMPLATENAME is unused? + +=cut + +sub ftp_invoice { + my $self = shift; + my $template = scalar(@_) ? shift : ''; + + $self->send_csv( + 'protocol' => 'ftp', + 'server' => $conf->config('cust_bill-ftpserver'), + 'username' => $conf->config('cust_bill-ftpusername'), + 'password' => $conf->config('cust_bill-ftppassword'), + 'dir' => $conf->config('cust_bill-ftpdir'), + 'format' => $conf->config('cust_bill-ftpformat'), + ); +} + +=item spool_invoice [ TEMPLATENAME ] + +Spools this invoice data (see L) + +TEMPLATENAME is unused? + +=cut + +sub spool_invoice { + my $self = shift; + my $template = scalar(@_) ? shift : ''; + + $self->spool_csv( + 'format' => $conf->config('cust_bill-spoolformat'), + 'agent_spools' => $conf->exists('cust_bill-spoolagent'), + ); +} + =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ] Like B, but only sends the invoice if it is the newest open invoice for @@ -1618,6 +1698,8 @@ L and L for conversion functions. cid - +unsquelch_cdr - overrides any per customer cdr squelching when true + =cut sub print_generic { @@ -1633,8 +1715,8 @@ sub print_generic { my $cust_main = $self->cust_main; $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') ) - unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/; - + unless $cust_main->payname + && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/; my %delimiters = ( 'latex' => [ '[@--', '--@]' ], 'html' => [ '<%=', '%>' ], @@ -1704,6 +1786,7 @@ sub print_generic { s/~/ /g; s/\\\\\*?\s*$/
/; s/\\hyphenation\{[\w\s\-]+}//; + s/\\([&])/$1/g; $_; } @_ }, @@ -1798,7 +1881,7 @@ sub print_generic { ) ) ); - } elsif ( grep /\S/, $conf->config('company_address') ) { + } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) { my $convert_map = $convert_maps{$format}{'returnaddress'}; $returnaddress = join( "\n", &$convert_map( @@ -1806,8 +1889,8 @@ sub print_generic { s/$/\\\\\*/; $_ } - ( $conf->config('company_name'), - $conf->config('company_address'), + ( $conf->config('company_name', $self->cust_main->agentnum), + $conf->config('company_address', $self->cust_main->agentnum), ) ) ); @@ -1823,9 +1906,9 @@ sub print_generic { } my %invoice_data = ( - 'company_name' => scalar( $conf->config('company_name') ), - 'company_address' => join("\n", $conf->config('company_address') ). "\n", - 'custnum' => $self->custnum, + 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ), + 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n", + 'custnum' => $cust_main->display_custnum, 'invnum' => $self->invnum, 'date' => time2str($date_format, $self->_date), 'today' => time2str('%b %o, %Y', $today), @@ -1838,10 +1921,11 @@ sub print_generic { 'city' => &$escape_function($cust_main->city), 'state' => &$escape_function($cust_main->state), 'zip' => &$escape_function($cust_main->zip), + 'fax' => &$escape_function($cust_main->fax), 'returnaddress' => $returnaddress, #'quantity' => 1, 'terms' => $self->terms, - 'template' => $params{'template'}, + 'template' => $template, #params{'template'}, #'notes' => join("\n", $conf->config('invoice_latexnotes') ), # better hang on to conf_dir for a while 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc", @@ -1853,16 +1937,18 @@ sub print_generic { 'unitprices' => $conf->exists('invoice-unitprice'), ); + 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.$_; $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method); } + $invoice_data{'ship_country'} = '' + if ( $invoice_data{'ship_country'} eq $countrydefault ); $invoice_data{'cid'} = $params{'cid'} if $params{'cid'}; - my $countrydefault = $conf->config('countrydefault') || 'US'; if ( $cust_main->country eq $countrydefault ) { $invoice_data{'country'} = ''; } else { @@ -1982,9 +2068,11 @@ sub print_generic { my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments', 'subtotal' => 0 }; # adjusted below + my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y'; my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum); + my $late_sections = []; if ( $multisection ) { - push @sections, $self->_items_sections; + push @sections, $self->_items_sections( $late_sections ); }else{ push @sections, { 'description' => '', 'subtotal' => '' }; } @@ -2023,7 +2111,7 @@ sub print_generic { push @buf, ['','']; } - foreach my $section (@sections) { + foreach my $section (@sections, @$late_sections) { $section->{'subtotal'} = $other_money_char. sprintf('%.2f', $section->{'subtotal'}) @@ -2039,6 +2127,8 @@ sub print_generic { $options{'section'} = $section if $multisection; $options{'format'} = $format; $options{'escape_function'} = $escape_function; + $options{'format_function'} = sub { () } unless $unsquelched; + $options{'unsquelched'} = $unsquelched; foreach my $line_item ( $self->_items_pkg(%options) ) { my $detail = { @@ -2146,8 +2236,8 @@ sub print_generic { ) ); if ( $multisection ) { - $adjust_section->{'pretotal'} = 'New charges total '. - $total->{'total_amount'}; + $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char. + sprintf('%.2f', $self->charged ); }else{ push @total_items, $total; } @@ -2259,6 +2349,11 @@ sub print_generic { } } + if ( $multisection ) { + push @sections, @$late_sections + if $unsquelched; + } + $invoice_lines = 0; my $wasfunc = 0; foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy @@ -2366,12 +2461,17 @@ when emailing the invoice as part of a multipart/related MIME email. =cut sub print_html { - my( $self, $today, $template, $cid ) = @_; + my $self = shift; + my %params; + if ( ref $_[0] ) { + %params = %{ shift() }; + }else{ + $params{'time'} = shift; + $params{'template'} = shift; + $params{'cid'} = shift; + } - my %params = ( 'format' => 'html' ); - $params{'time'} = $today if $today; - $params{'template'} = $template if $template; - $params{'cid'} = $cid if $cid; + $params{'format'} = 'html'; $self->print_generic( %params ); } @@ -2513,29 +2613,90 @@ Returns a string with the invoice number and date, for example: sub invnum_date_pretty { my $self = shift; - 'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')'; + 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')'; +} + +=item _date_pretty + +Returns a string with the date, for example: "3/20/2008" + +=cut + +sub _date_pretty { + my $self = shift; + time2str('%x', $self->_date); } sub _items_sections { my $self = shift; + my $late = shift; my %s = (); - foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) { + my %l = (); - if ( $cust_bill_pkg->pkgnum > 0 ) { + foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) + { - my $desc = $cust_bill_pkg->part_pkg->categoryname; + if ( $cust_bill_pkg->pkgnum > 0 ) { + my $usage = $cust_bill_pkg->usage; + + foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) { + my $desc = $display->section; + my $type = $display->type; + + if ( $display->post_total ) { + if (! $type || $type eq 'S') { + $l{$desc} += $cust_bill_pkg->setup + if ( $cust_bill_pkg->setup != 0 ); + } + + if (! $type) { + $l{$desc} += $cust_bill_pkg->recur + if ( $cust_bill_pkg->recur != 0 ); + } + + if ($type && $type eq 'R') { + $l{$desc} += $cust_bill_pkg->recur - $usage + if ( $cust_bill_pkg->recur != 0 ); + } + + if ($type && $type eq 'U') { + $l{$desc} += $usage; + } + + } else { + if (! $type || $type eq 'S') { + $s{$desc} += $cust_bill_pkg->setup + if ( $cust_bill_pkg->setup != 0 ); + } + + if (! $type) { + $s{$desc} += $cust_bill_pkg->recur + if ( $cust_bill_pkg->recur != 0 ); + } + + if ($type && $type eq 'R') { + $s{$desc} += $cust_bill_pkg->recur - $usage + if ( $cust_bill_pkg->recur != 0 ); + } + + if ($type && $type eq 'U') { + $s{$desc} += $usage; + } - $s{$desc} += $cust_bill_pkg->setup - if ( $cust_bill_pkg->setup != 0 ); + } - $s{$desc} += $cust_bill_pkg->recur - if ( $cust_bill_pkg->recur != 0 ); + } } } + push @$late, map { { 'description' => $_, + 'subtotal' => $l{$_}, + 'post_total' => 1, + } } sort keys %l; + map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s; } @@ -2589,16 +2750,8 @@ sub _items_previous { sub _items_pkg { my $self = shift; - my %options = @_; - my $section = delete $options{'section'}; - my @cust_bill_pkg = - grep { $_->pkgnum && - ( defined($section) - ? $_->part_pkg->categoryname eq $section->{'description'} - : 1 - ) - } $self->cust_bill_pkg; - $self->_items_cust_bill_pkg(\@cust_bill_pkg, %options); + my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg; + $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_); } sub _taxsort { @@ -2623,84 +2776,122 @@ sub _items_cust_bill_pkg { my $format = $opt{format} || ''; my $escape_function = $opt{escape_function} || sub { shift }; + my $format_function = $opt{format_function} || ''; + my $unsquelched = $opt{unsquelched} || ''; + my $section = $opt{section}->{description} if $opt{section}; my @b = (); - foreach my $cust_bill_pkg ( @$cust_bill_pkg ) { + foreach my $cust_bill_pkg ( @$cust_bill_pkg ) + { + foreach my $display ( grep { defined($section) + ? $_->section eq $section + : 1 + } + $cust_bill_pkg->cust_bill_pkg_display + ) + { - my $cust_pkg = $cust_bill_pkg->cust_pkg; + my $type = $display->type; - my $desc = $cust_bill_pkg->desc; + my $cust_pkg = $cust_bill_pkg->cust_pkg; - my %details_opt = ( 'format' => $format, - 'escape_function' => $escape_function, - ); + my $desc = $cust_bill_pkg->desc; + $desc = substr($desc, 0, 50). '...' + if $format eq 'latex' && length($desc) > 50; - if ( $cust_bill_pkg->pkgnum > 0 ) { + my %details_opt = ( 'format' => $format, + 'escape_function' => $escape_function, + 'format_function' => $format_function, + ); - if ( $cust_bill_pkg->setup != 0 ) { + if ( $cust_bill_pkg->pkgnum > 0 ) { - my $description = $desc; - $description .= ' Setup' if $cust_bill_pkg->recur != 0; + if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) { - my @d = map &{$escape_function}($_), - $cust_pkg->h_labels_short($self->_date); - push @d, $cust_bill_pkg->details(%details_opt) - if $cust_bill_pkg->recur == 0; + my $description = $desc; + $description .= ' Setup' 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, - }; - } + my @d = map &{$escape_function}($_), + $cust_pkg->h_labels_short($self->_date); + push @d, $cust_bill_pkg->details(%details_opt) + if $cust_bill_pkg->recur == 0; - 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, + }; - my $description = $desc; - unless ( $conf->exists('disable_line_item_date_ranges') ) { - $desc .= " (" . time2str("%x", $cust_bill_pkg->sdate). - " - ". time2str("%x", $cust_bill_pkg->edate). ")"; } - #at least until cust_bill_pkg has "past" ranges in addition to - #the "future" sdate/edate ones... see #3032 - my @d = map &{$escape_function}($_), - $cust_pkg->h_labels_short($self->_date); - #$cust_bill_pkg->edate, - #$cust_bill_pkg->sdate), - push @d, $cust_bill_pkg->details(%details_opt); - - push @b, { - description => $description, - #pkgpart => $part_pkg->pkgpart, - pkgnum => $cust_bill_pkg->pkgnum, - amount => sprintf("%.2f", $cust_bill_pkg->recur), - unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur), - quantity => $cust_bill_pkg->quantity, - ext_description => \@d, - }; + if ( $cust_bill_pkg->recur != 0 && + ( !$type || $type eq 'R' || $type eq 'U' ) + ) + { + + my $is_summary = $display->summary; + my $description = $is_summary ? "Usage charges" : $desc; + + unless ( $conf->exists('disable_line_item_date_ranges') ) { + $description .= " (" . time2str("%x", $cust_bill_pkg->sdate). + " - ". time2str("%x", $cust_bill_pkg->edate). ")"; + } + + #at least until cust_bill_pkg has "past" ranges in addition to + #the "future" sdate/edate ones... see #3032 + my @d = (); + 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); + push @d, $cust_bill_pkg->details(%details_opt) + unless ($is_summary || $type && $type eq 'R'); + + my $amount = 0; + if (!$type) { + $amount = $cust_bill_pkg->recur; + }elsif($type eq 'R') { + $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage; + }elsif($type eq 'U') { + $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 ); - } + } - } else { #pkgnum tax or one-shot line item (??) + } else { #pkgnum tax or one-shot line item (??) + + if ( $cust_bill_pkg->setup != 0 ) { + push @b, { + 'description' => $desc, + 'amount' => sprintf("%.2f", $cust_bill_pkg->setup), + }; + } + if ( $cust_bill_pkg->recur != 0 ) { + push @b, { + 'description' => "$desc (". + time2str("%x", $cust_bill_pkg->sdate). ' - '. + time2str("%x", $cust_bill_pkg->edate). ')', + 'amount' => sprintf("%.2f", $cust_bill_pkg->recur), + }; + } - if ( $cust_bill_pkg->setup != 0 ) { - push @b, { - 'description' => $desc, - 'amount' => sprintf("%.2f", $cust_bill_pkg->setup), - }; - } - if ( $cust_bill_pkg->recur != 0 ) { - push @b, { - 'description' => "$desc (". - time2str("%x", $cust_bill_pkg->sdate). ' - '. - time2str("%x", $cust_bill_pkg->edate). ')', - 'amount' => sprintf("%.2f", $cust_bill_pkg->recur), - }; } } @@ -2771,7 +2962,7 @@ sub _items_payments { =over 4 -=item reprint +=item process_reprint =cut @@ -2779,7 +2970,7 @@ sub process_reprint { process_re_X('print', @_); } -=item reemail +=item process_reemail =cut @@ -2787,7 +2978,7 @@ sub process_reemail { process_re_X('email', @_); } -=item refax +=item process_refax =cut @@ -2795,6 +2986,22 @@ sub process_refax { process_re_X('fax', @_); } +=item process_reftp + +=cut + +sub process_reftp { + process_re_X('ftp', @_); +} + +=item respool + +=cut + +sub process_respool { + process_re_X('spool', @_); +} + use Storable qw(thaw); use Data::Dumper; use MIME::Base64; @@ -2838,6 +3045,8 @@ sub re_X { 'debug' => 1, } ); + $method .= '_invoice' unless $method eq 'email' || $method eq 'print'; + warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n" if $DEBUG;