+=item apply_payments_and_credits
+
+=cut
+
+sub apply_payments_and_credits {
+ my $self = 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;
+
+ $self->select_for_update; #mutex
+
+ my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
+ my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
+
+ while ( $self->owed > 0 and ( @payments || @credits ) ) {
+
+ my $app = '';
+ if ( @payments && @credits ) {
+
+ #decide which goes first by weight of top (unapplied) line item
+
+ my @open_lineitems = $self->open_cust_bill_pkg;
+
+ my $max_pay_weight =
+ max( map { $_->part_pkg->pay_weight || 0 }
+ grep { $_ }
+ map { $_->cust_pkg }
+ @open_lineitems
+ );
+ my $max_credit_weight =
+ max( map { $_->part_pkg->credit_weight || 0 }
+ grep { $_ }
+ map { $_->cust_pkg }
+ @open_lineitems
+ );
+
+ #if both are the same... payments first? it has to be something
+ if ( $max_pay_weight >= $max_credit_weight ) {
+ $app = 'pay';
+ } else {
+ $app = 'credit';
+ }
+
+ } elsif ( @payments ) {
+ $app = 'pay';
+ } elsif ( @credits ) {
+ $app = 'credit';
+ } else {
+ die "guru meditation #12 and 35";
+ }
+
+ 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 ) ),
+ };
+
+ } 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 ) ),
+ };
+
+ } else {
+ die "guru meditation #12 and 35";
+ }
+
+ $app->invnum( $self->invnum );
+
+ my $error = $app->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error inserting ". $app->table. " record: $error";
+ }
+ die $error if $error;
+
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ ''; #no error
+
+}
+
+=item generate_email PARAMHASH
+
+PARAMHASH can contain the following:
+
+=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
+
+=back
+
+Returns an argument list to be passed to L<FS::Misc::send_email>.
+
+=cut
+
+use MIME::Entity;
+
+sub generate_email {
+
+ my $self = shift;
+ my %args = @_;
+
+ my $me = '[FS::cust_bill::generate_email]';
+
+ my %return = (
+ 'from' => $args{'from'},
+ 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
+ );
+
+ if (ref($args{'to'}) eq 'ARRAY') {
+ $return{'to'} = $args{'to'};
+ } else {
+ $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
+ $self->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('', $args{'template'}) ];
+ }
+
+ }
+
+ $alternative->attach(
+ 'Type' => 'text/plain',
+ #'Encoding' => 'quoted-printable',
+ 'Encoding' => '7bit',
+ 'Data' => $data,
+ 'Disposition' => 'inline',
+ );
+
+ $args{'from'} =~ /\@([\w\.\-]+)/;
+ 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;
+ if ( defined($args{'template'}) && length($args{'template'})
+ && -e "$path/logo_". $args{'template'}. ".png"
+ )
+ {
+ $file = "$path/logo_". $args{'template'}. ".png";
+ } else {
+ $file = "$path/logo.png";
+ }
+
+ my $image = build MIME::Entity
+ 'Type' => 'image/png',
+ 'Encoding' => 'base64',
+ 'Path' => $file,
+ 'Filename' => 'logo.png',
+ 'Content-ID' => "<$content_id>",
+ ;
+
+ $alternative->attach(
+ 'Type' => 'text/html',
+ 'Encoding' => 'quoted-printable',
+ 'Data' => [ '<html>',
+ ' <head>',
+ ' <title>',
+ ' '. encode_entities($return{'subject'}),
+ ' </title>',
+ ' </head>',
+ ' <body bgcolor="#e8e8e8">',
+ $self->print_html('', $args{'template'}, $content_id),
+ ' </body>',
+ '</html>',
+ ],
+ 'Disposition' => 'inline',
+ #'Filename' => 'invoice.pdf',
+ );
+
+ 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);
+
+ my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
+
+ $return{'mimeparts'} = [ $related, $pdf ];
+
+ } else {
+
+ #no other attachment:
+ # multipart/related
+ # multipart/alternative
+ # text/plain
+ # text/html
+ # image/png
+
+ $return{'content-type'} = 'multipart/related';
+ $return{'mimeparts'} = [ $alternative, $image ];
+ $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('', $args{'template'}) }
+ ];
+ }
+
+ 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('', $args{'template'}) ];
+ }
+
+ }
+
+ }
+
+ %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.pdf',
+ );
+}
+