X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=5118f020e061c200eab96503c3ebe7edaf142f3c;hp=1f4943a28037c7697e054e0be01352aa6098d5ac;hb=17a8b72b78ba455b58d53731fe557a471e0f2947;hpb=5214a5560240667a3a914c45df046b420926a5ec diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 1f4943a28..5118f020e 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -1,26 +1,22 @@ package FS::cust_bill; +use base qw( FS::cust_bill::Search FS::Template_Mixin + FS::cust_main_Mixin FS::Record + ); use strict; -use vars qw( @ISA $DEBUG $me - $money_char $date_format $rdate_format $date_format_long ); +use vars qw( $DEBUG $me ); # but NOT $conf -use vars qw( $invoice_lines @buf ); #yuck use Fcntl qw(:flock); #for spool_csv use Cwd; use List::Util qw(min max sum); use Date::Format; -use Date::Language; -use Text::Template 1.20; use File::Temp 0.14; -use String::ShellQuote; use HTML::Entities; -use Locale::Country; use Storable qw( freeze thaw ); use GD::Barcode; use FS::UID qw( datasrc ); -use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print ); +use FS::Misc qw( send_fax do_print ); use FS::Record qw( qsearch qsearchs dbh ); -use FS::cust_main_Mixin; use FS::cust_main; use FS::cust_statement; use FS::cust_bill_pkg; @@ -44,22 +40,12 @@ use FS::cust_bill_batch; use FS::cust_bill_pay_pkg; use FS::cust_credit_bill_pkg; use FS::discount_plan; +use FS::cust_bill_void; use FS::L10N; -@ISA = qw( FS::cust_main_Mixin FS::Record ); - $DEBUG = 0; $me = '[FS::cust_bill]'; -#ask FS::UID to run this stuff for us later -FS::UID->install_callback( sub { - my $conf = new FS::Conf; #global - $money_char = $conf->config('money_char') || '$'; - $date_format = $conf->config('date_format') || '%x'; #/YY - $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY - $date_format_long = $conf->config('date_format_long') || '%b %o, %Y'; -} ); - =head1 NAME FS::cust_bill - Object methods for cust_bill records @@ -90,7 +76,7 @@ FS::cust_bill - Object methods for cust_bill records $tax_amount = $record->tax; @lines = $cust_bill->print_text; - @lines = $cust_bill->print_text $time; + @lines = $cust_bill->print_text('time' => $time); =head1 DESCRIPTION @@ -116,21 +102,19 @@ L and L for conversion functions. =back -Customer info at invoice generation time +Deprecated fields =over 4 -=item previous_balance - -=item billing_balance +=item billing_balance - the customer's balance immediately before generating +this invoice. DEPRECATED. Use the L method +to determine the customer's balance at a specific time. -=back - -Deprecated - -=over 4 +=item previous_balance - the customer's balance immediately after generating +the invoice before this one. DEPRECATED. -=item printed - deprecated +=item printed - formerly used to track the number of times an invoice had +been printed; no longer used. =back @@ -162,6 +146,13 @@ Invoices are normally created by calling the bill method of a customer object sub table { 'cust_bill'; } +# should be the ONLY occurrence of "Invoice" in invoice rendering code. +# (except email_subject and invnum_date_pretty) +sub notice_name { + my $self = shift; + $self->conf->config('notice_name') || 'Invoice' +} + sub cust_linked { $_[0]->cust_main_custnum; } sub cust_unlinked_msg { my $self = shift; @@ -213,10 +204,64 @@ sub insert { } +=item void + +Voids this invoice: deletes the invoice and adds a record of the voided invoice +to the FS::cust_bill_void table (and related tables starting from +FS::cust_bill_pkg_void). + +=cut + +sub void { + my $self = shift; + my $reason = scalar(@_) ? shift : ''; + my $reprocess_cdrs = scalar(@_) ? shift : ''; + + local $SIG{HUP} = 'IGNORE'; + local $SIG{INT} = 'IGNORE'; + local $SIG{QUIT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{TSTP} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + my $cust_bill_void = new FS::cust_bill_void ( { + map { $_ => $self->get($_) } $self->fields + } ); + $cust_bill_void->reason($reason); + my $error = $cust_bill_void->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) { + my $error = $cust_bill_pkg->void($reason, $reprocess_cdrs); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + + $error = $self->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + ''; + +} + =item delete This method now works but you probably shouldn't use it. Instead, apply a -credit against the invoice. +credit against the invoice, or use the new void method. Using this method to delete invoices outright is really, really bad. There would be no record you ever posted this invoice, and there are no check to @@ -246,11 +291,10 @@ sub delete { cust_event cust_credit_bill cust_bill_pay - cust_credit_bill cust_pay_batch cust_bill_pay_batch - cust_bill_pkg cust_bill_batch + cust_bill_pkg )) { foreach my $linked ( $self->$table() ) { @@ -369,14 +413,33 @@ cust_bill-default_agent_invid is set and it has a value, invnum otherwise. sub display_invnum { my $self = shift; - my $conf = $self->conf; - if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){ + if ( $self->agent_invid + && FS::Conf->new->exists('cust_bill-default_agent_invid') ) { return $self->agent_invid; } else { return $self->invnum; } } +=item previous_bill + +Returns the customer's last invoice before this one. + +=cut + +sub previous_bill { + my $self = shift; + if ( !$self->get('previous_bill') ) { + $self->set('previous_bill', qsearchs({ + 'table' => 'cust_bill', + 'hashref' => { 'custnum' => $self->custnum, + '_date' => { op=>'<', value=>$self->_date } }, + 'order_by' => 'ORDER BY _date DESC LIMIT 1', + }) ); + } + $self->get('previous_bill'); +} + =item previous Returns a list consisting of the total previous balance for this customer, @@ -386,13 +449,33 @@ followed by the previous outstanding invoices (as FS::cust_bill objects also). sub previous { 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 } ) - ; - foreach ( @cust_bill ) { $total += $_->owed; } - $total, @cust_bill; + # simple memoize; we use this a lot + if (!$self->get('previous')) { + my $total = 0; + my @cust_bill = sort { $a->_date <=> $b->_date } + grep { $_->owed != 0 } + 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]); + } + return @{ $self->get('previous') }; +} + +=item enable_previous + +Whether to show the 'Previous Charges' section when printing this invoice. +The negation of the 'disable_previous_balance' config setting. + +=cut + +sub enable_previous { + my $self = shift; + my $agentnum = $self->cust_main->agentnum; + !$self->conf->exists('disable_previous_balance', $agentnum); } =item cust_bill_pkg @@ -404,7 +487,13 @@ Returns the line items (see L) for this invoice. sub cust_bill_pkg { my $self = shift; qsearch( - { 'table' => 'cust_bill_pkg', + { + 'select' => 'cust_bill_pkg.*, pkg_category.categoryname', + 'table' => 'cust_bill_pkg', + 'addl_from' => ' LEFT JOIN cust_pkg USING ( pkgnum ) '. + ' LEFT JOIN part_pkg USING ( pkgpart ) '. + ' LEFT JOIN pkg_class USING ( classnum ) '. + ' LEFT JOIN pkg_category USING ( categorynum ) ', 'hashref' => { 'invnum' => $self->invnum }, 'order_by' => 'ORDER BY billpkgnum', } @@ -831,6 +920,7 @@ sub hide { =item apply_payments_and_credits [ OPTION => VALUE ... ] Applies unapplied payments and credits to this invoice. +Payments with the no_auto_apply flag set will not be applied. A hash of optional arguments may be passed. Currently "manual" is supported. If true, a payment receipt is sent instead of a statement when @@ -857,7 +947,9 @@ sub apply_payments_and_credits { $self->select_for_update; #mutex - my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay; + my @payments = grep { $_->unapplied > 0 } + grep { !$_->no_auto_apply } + $self->cust_main->cust_pay; my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit; if ( $conf->exists('pkg-balances') ) { @@ -953,485 +1045,108 @@ sub apply_payments_and_credits { } -=item generate_email OPTION => VALUE ... - -Options: - -=over 4 - -=item from - -sender address, required - -=item tempate - -alternate template name, optional - -=item print_text - -text attachment arrayref, optional - -=item subject - -email subject, optional - -=item notice_name - -notice name instead of "Invoice", optional - -=back - -Returns an argument list to be passed to L. - -=cut - -use MIME::Entity; - -sub generate_email { - - my $self = shift; - my %args = @_; - my $conf = $self->conf; - - my $me = '[FS::cust_bill::generate_email]'; - - my %return = ( - 'from' => $args{'from'}, - 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'), - ); - - my %opt = ( - 'unsquelch_cdr' => $conf->exists('voip-cdr_email'), - 'template' => $args{'template'}, - 'notice_name' => ( $args{'notice_name'} || 'Invoice' ), - 'no_coupon' => $args{'no_coupon'}, - ); - - my $cust_main = $self->cust_main; - - if (ref($args{'to'}) eq 'ARRAY') { - $return{'to'} = $args{'to'}; - } else { - $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ } - $cust_main->invoicing_list - ]; - } - - if ( $conf->exists('invoice_html') ) { - - warn "$me creating HTML/text multipart message" - if $DEBUG; - - $return{'nobody'} = 1; - - my $alternative = build MIME::Entity - 'Type' => 'multipart/alternative', - #'Encoding' => '7bit', - 'Disposition' => 'inline' - ; - - my $data; - if ( $conf->exists('invoice_email_pdf') - and scalar($conf->config('invoice_email_pdf_note')) ) { - - warn "$me using 'invoice_email_pdf_note' in multipart message" - if $DEBUG; - $data = [ map { $_ . "\n" } - $conf->config('invoice_email_pdf_note') - ]; - - } else { - - warn "$me not using 'invoice_email_pdf_note' in multipart message" - if $DEBUG; - if ( ref($args{'print_text'}) eq 'ARRAY' ) { - $data = $args{'print_text'}; - } else { - $data = [ $self->print_text(\%opt) ]; - } - - } - - $alternative->attach( - 'Type' => 'text/plain', - 'Encoding' => 'quoted-printable', - #'Encoding' => '7bit', - 'Data' => $data, - 'Disposition' => 'inline', - ); - - - my $htmldata; - my $image = ''; - my $barcode = ''; - if ( $conf->exists('invoice_email_pdf') - and scalar($conf->config('invoice_email_pdf_note')) ) { - - $htmldata = join('
', $conf->config('invoice_email_pdf_note') ); - - } else { - - $args{'from'} =~ /\@([\w\.\-]+)/; - my $from = $1 || 'example.com'; - my $content_id = join('.', rand()*(2**32), $$, time). "\@$from"; - - my $logo; - my $agentnum = $cust_main->agentnum; - if ( defined($args{'template'}) && length($args{'template'}) - && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum ) - ) - { - $logo = 'logo_'. $args{'template'}. '.png'; - } else { - $logo = "logo.png"; - } - my $image_data = $conf->config_binary( $logo, $agentnum); - - $image = build MIME::Entity - 'Type' => 'image/png', - 'Encoding' => 'base64', - 'Data' => $image_data, - 'Filename' => 'logo.png', - 'Content-ID' => "<$content_id>", - ; - - if ($conf->exists('invoice-barcode')) { - my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from"; - $barcode = build MIME::Entity - 'Type' => 'image/png', - 'Encoding' => 'base64', - 'Data' => $self->invoice_barcode(0), - 'Filename' => 'barcode.png', - 'Content-ID' => "<$barcode_content_id>", - ; - $opt{'barcode_cid'} = $barcode_content_id; - } - - $htmldata = $self->print_html({ 'cid'=>$content_id, %opt }); - } - - $alternative->attach( - 'Type' => 'text/html', - 'Encoding' => 'quoted-printable', - 'Data' => [ '', - ' ', - ' ', - ' '. encode_entities($return{'subject'}), - ' ', - ' ', - ' ', - $htmldata, - ' ', - '', - ], - 'Disposition' => 'inline', - #'Filename' => 'invoice.pdf', - ); - - - my @otherparts = (); - if ( $cust_main->email_csv_cdr ) { - - push @otherparts, build MIME::Entity - 'Type' => 'text/csv', - 'Encoding' => '7bit', - 'Data' => [ map { "$_\n" } - $self->call_details('prepend_billed_number' => 1) - ], - 'Disposition' => 'attachment', - 'Filename' => 'usage-'. $self->invnum. '.csv', - ; - - } - - if ( $conf->exists('invoice_email_pdf') ) { - - #attaching pdf too: - # multipart/mixed - # multipart/related - # multipart/alternative - # text/plain - # text/html - # image/png - # application/pdf - - my $related = build MIME::Entity 'Type' => 'multipart/related', - 'Encoding' => '7bit'; - - #false laziness w/Misc::send_email - $related->head->replace('Content-type', - $related->mime_type. - '; boundary="'. $related->head->multipart_boundary. '"'. - '; type=multipart/alternative' - ); - - $related->add_part($alternative); - - $related->add_part($image) if $image; - - my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt); - - $return{'mimeparts'} = [ $related, $pdf, @otherparts ]; - - } else { - - #no other attachment: - # multipart/related - # multipart/alternative - # text/plain - # text/html - # image/png - - $return{'content-type'} = 'multipart/related'; - if ($conf->exists('invoice-barcode') && $barcode) { - $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ]; - } else { - $return{'mimeparts'} = [ $alternative, $image, @otherparts ]; - } - $return{'type'} = 'multipart/alternative'; #Content-Type of first part... - #$return{'disposition'} = 'inline'; - - } - - } else { - - if ( $conf->exists('invoice_email_pdf') ) { - warn "$me creating PDF attachment" - if $DEBUG; - - #mime parts arguments a la MIME::Entity->build(). - $return{'mimeparts'} = [ - { $self->mimebuild_pdf(\%opt) } - ]; - } - - if ( $conf->exists('invoice_email_pdf') - and scalar($conf->config('invoice_email_pdf_note')) ) { - - warn "$me using 'invoice_email_pdf_note'" - if $DEBUG; - $return{'body'} = [ map { $_ . "\n" } - $conf->config('invoice_email_pdf_note') - ]; - - } else { - - warn "$me not using 'invoice_email_pdf_note'" - if $DEBUG; - if ( ref($args{'print_text'}) eq 'ARRAY' ) { - $return{'body'} = $args{'print_text'}; - } else { - $return{'body'} = [ $self->print_text(\%opt) ]; - } - - } - - } - - %return; - -} - -=item mimebuild_pdf - -Returns a list suitable for passing to MIME::Entity->build(), representing -this invoice as PDF attachment. - -=cut - -sub mimebuild_pdf { - my $self = shift; - ( - 'Type' => 'application/pdf', - 'Encoding' => 'base64', - 'Data' => [ $self->print_pdf(@_) ], - 'Disposition' => 'attachment', - 'Filename' => 'invoice-'. $self->invnum. '.pdf', - ); -} - -=item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ] +=item send HASHREF Sends this invoice to the destinations configured for this customer: sends email, prints and/or faxes. See L. -Options can be passed as a hashref (recommended) or as a list of up to -four values for templatename, agentnum, invoice_from and amount. - -I