From 47d35d063516eaa1a058109cf46cf2eb25c852a2 Mon Sep 17 00:00:00 2001 From: Moritz Bunkus Date: Thu, 13 Jan 2011 13:30:31 +0100 Subject: [PATCH] Hintergrundjob zum Erzeugen periodischer Rechnungen MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Das Erzeugen/Buchen der Rechnungen sowie die E-Mail-Benachrichtigun am Schluss wurden implementiert. Was noch fehlt ist der automatisch Ausdruck (sofern gewünscht). --- SL/BackgroundJob/ALL.pm | 1 + SL/BackgroundJob/CreatePeriodicInvoices.pm | 260 ++++++++++++++++++ SL/DB/PeriodicInvoicesConfig.pm | 67 +++++ config/periodic_invoices.conf.default | 7 +- .../webpages/oe/periodic_invoices_email.txt | 6 +- 5 files changed, 338 insertions(+), 3 deletions(-) create mode 100644 SL/BackgroundJob/CreatePeriodicInvoices.pm diff --git a/SL/BackgroundJob/ALL.pm b/SL/BackgroundJob/ALL.pm index be357a612..5688d2cc8 100644 --- a/SL/BackgroundJob/ALL.pm +++ b/SL/BackgroundJob/ALL.pm @@ -4,6 +4,7 @@ use strict; use SL::BackgroundJob::Base; use SL::BackgroundJob::CleanBackgroundJobHistory; +use SL::BackgroundJob::CreatePeriodicInvoices; 1; diff --git a/SL/BackgroundJob/CreatePeriodicInvoices.pm b/SL/BackgroundJob/CreatePeriodicInvoices.pm new file mode 100644 index 000000000..226883cc3 --- /dev/null +++ b/SL/BackgroundJob/CreatePeriodicInvoices.pm @@ -0,0 +1,260 @@ +package SL::BackgroundJob::CreatePeriodicInvoices; + +use strict; + +use parent qw(SL::BackgroundJob::Base); + +use Config::Std; +use English qw(-no_match_vars); + +use SL::DB::AuthUser; +use SL::DB::Order; +use SL::DB::Invoice; +use SL::DB::PeriodicInvoice; +use SL::DB::PeriodicInvoicesConfig; +use SL::Mailer; + +sub create_job { + $_[0]->create_standard_job('0 3 1 * *'); # first day of month at 3:00 am +} + +sub run { + my $self = shift; + $self->{db_obj} = shift; + + my $configs = SL::DB::Manager::PeriodicInvoicesConfig->get_all(where => [ active => 1 ]); + + foreach my $config (@{ $configs }) { + my $new_end_date = $config->handle_automatic_extension; + _log_msg("Periodic invoice configuration ID " . $config->id . " extended through " . $new_end_date->strftime('%d.%m.%Y') . "\n") if $new_end_date; + } + + my (@new_invoices, @invoices_to_print); + + _log_msg("Number of configs: " . scalar(@{ $configs})); + + foreach my $config (@{ $configs }) { + # A configuration can be set to inactive by + # $config->handle_automatic_extension. Therefore the check in + # ...->get_all() does not suffice. + _log_msg("Config " . $config->id . " active " . $config->active); + next unless $config->active; + + my @dates = _calculate_dates($config); + + _log_msg("Dates: " . join(' ', map { $_->to_lxoffice } @dates)); + + foreach my $date (@dates) { + my $invoice = $self->_create_periodic_invoice($config, $date); + next unless $invoice; + + _log_msg("Invoice " . $invoice->invnumber . " posted for config ID " . $config->id . ", period start date " . $::locale->format_date(\%::myconfig, $date) . "\n"); + push @new_invoices, $invoice; + push @invoices_to_print, $invoice if $config->print; + + # last; + } + } + + map { _print_invoice($_) } @invoices_to_print; + + _send_email(\@new_invoices, \@invoices_to_print) if @new_invoices; + + return 1; +} + +sub _log_msg { + # my $message = join('', @_); + # $message .= "\n" unless $message =~ m/\n$/; + # $::lxdebug->message(0, $message); +} + +sub _generate_time_period_variables { + my $config = shift; + my $period_start_date = shift; + my $period_end_date = $period_start_date->clone->truncate(to => 'month')->add(months => $config->get_period_length)->subtract(days => 1); + + my @month_names = ('', + 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', + 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'); + + my $vars = { current_quarter => $period_start_date->quarter, + previous_quarter => $period_start_date->clone->subtract(months => 3)->quarter, + next_quarter => $period_start_date->clone->add( months => 3)->quarter, + + current_month => $period_start_date->month, + previous_month => $period_start_date->clone->subtract(months => 1)->month, + next_month => $period_start_date->clone->add( months => 1)->month, + + current_year => $period_start_date->year, + previous_year => $period_start_date->year - 1, + next_year => $period_start_date->year + 1, + + period_start_date => $::locale->format_date(\%::myconfig, $period_start_date), + period_end_date => $::locale->format_date(\%::myconfig, $period_end_date), + }; + + map { $vars->{"${_}_month_long"} = $month_names[ $vars->{"${_}_month"} ] } qw(current previous next); + + return $vars; +} + +sub _replace_vars { + my $object = shift; + my $vars = shift; + my $sub = shift; + my $str = $object->$sub; + + my ($key, $value); + $str =~ s|<\%${key}\%>|$value|g while ($key, $value) = each %{ $vars }; + $object->$sub($str); +} + +sub _create_periodic_invoice { + my $self = shift; + my $config = shift; + my $period_start_date = shift; + + my $time_period_vars = _generate_time_period_variables($config, $period_start_date); + + my $invdate = DateTime->today_local; + + my $order = $config->order; + my $invoice; + if (!$self->{db_obj}->db->do_transaction(sub { + 1; # make Emacs happy + + $invoice = SL::DB::Invoice->new_from($order); + + my $intnotes = $invoice->intnotes ? $invoice->intnotes . "\n\n" : ''; + $intnotes .= "Automatisch am " . $invdate->to_lxoffice . " erzeugte Rechnung"; + + $invoice->assign_attributes(deliverydate => $period_start_date, + intnotes => $intnotes, + ); + + map { _replace_vars($invoice, $time_period_vars, $_) } qw(notes intnotes transaction_description); + + foreach my $item (@{ $invoice->items }) { + map { _replace_vars($item, $time_period_vars, $_) } qw(description longdescription); + } + + $invoice->post(ar_id => $config->ar_chart_id) || die; + + $order->link_to_record($invoice); + + SL::DB::PeriodicInvoice->new(config_id => $config->id, + ar_id => $invoice->id, + period_start_date => $period_start_date) + ->save; + + # die $invoice->transaction_description; + })) { + $::lxdebug->message(LXDebug->WARN(), "_create_invoice failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2])); + return undef; + } + + return $invoice; +} + +sub _calculate_dates { + my $config = shift; + + my $cur_date = $config->start_date; + my $start_date = $config->get_previous_invoice_date || DateTime->new(year => 1970, month => 1, day => 1); + my $end_date = $config->end_date || DateTime->new(year => 2100, month => 1, day => 1); + my $tomorrow = DateTime->today_local->add(days => 1); + my $period_len = $config->get_period_length; + + $end_date = $tomorrow if $end_date > $tomorrow; + + my @dates; + + while (1) { + last if $cur_date >= $end_date; + + push @dates, $cur_date->clone if $cur_date > $start_date; + + $cur_date->add(months => $period_len); + } + + return @dates; +} + +sub _send_email { + my ($posted_invoices, $printed_invoices) = @_; + + read_config 'config/periodic_invoices.conf' => my %config; + + return if !$config{periodic_invoices} || !$config{periodic_invoices}->{send_email_to} || !scalar @{ $posted_invoices }; + + my $user = SL::DB::Manager::AuthUser->find_by(login => $config{periodic_invoices}->{send_email_to}); + my $email = $user ? $user->get_config_value('email') : undef; + + return unless $email; + + my $template = Template->new({ 'INTERPOLATE' => 0, + 'EVAL_PERL' => 0, + 'ABSOLUTE' => 1, + 'CACHE_SIZE' => 0, + }); + + return unless $template; + + my $email_template = $config{periodic_invoices}->{email_template}; + my $filename = $email_template || ( ($user->get_config_value('templates') || "templates/webpages") . "/periodic_invoices_email.txt" ); + my %params = ( POSTED_INVOICES => $posted_invoices, + PRINTED_INVOICES => $printed_invoices ); + + my $output; + $template->process($filename, \%params, \$output); + + my $mail = Mailer->new; + $mail->{from} = $config{periodic_invoices}->{email_from}; + $mail->{to} = $email; + $mail->{subject} = $config{periodic_invoices}->{email_subject}; + $mail->{content_type} = $filename =~ m/.html$/ ? 'text/html' : 'text/plain'; + $mail->{message} = $output; + + $mail->send; +} + +1; + +__END__ + +=pod + +=encoding utf8 + +=head1 NAME + +SL::BackgroundJob::CleanBackgroundJobHistory - Create periodic +invoices for orders + +=head1 SYNOPSIS + +Iterate over all periodic invoice configurations, extend them if +applicable, calculate the dates for which invoices have to be posted +and post those invoices by converting the order into an invoice for +each date. + +=head1 TOTO + +=over 4 + +=item * + +Strings like month names are hardcoded to German in this file. + +=item * + +Implement printing the invoices if requested. + +=back + +=head1 AUTHOR + +Moritz Bunkus Em.bunkus@linet-services.deE + +=cut diff --git a/SL/DB/PeriodicInvoicesConfig.pm b/SL/DB/PeriodicInvoicesConfig.pm index 3ed93bcf8..d49595bb5 100644 --- a/SL/DB/PeriodicInvoicesConfig.pm +++ b/SL/DB/PeriodicInvoicesConfig.pm @@ -2,6 +2,8 @@ package SL::DB::PeriodicInvoicesConfig; use strict; +use Readonly; + use SL::DB::MetaSetup::PeriodicInvoicesConfig; __PACKAGE__->meta->add_relationships( @@ -17,4 +19,69 @@ __PACKAGE__->meta->initialize; # Creates get_all, get_all_count, get_all_iterator, delete_all and update_all. __PACKAGE__->meta->make_manager_class; +Readonly our @PERIODICITIES => qw(m q f b y); +Readonly our %PERIOD_LENGTHS => ( m => 1, q => 3, f => 4, b => 6, y => 12 ); + +sub get_period_length { + my $self = shift; + return $PERIOD_LENGTHS{ $self->periodicity } || 1; +} + +sub _log_msg { + $::lxdebug->message(LXDebug->DEBUG1(), join('', @_)); +} + +sub handle_automatic_extension { + my $self = shift; + + _log_msg("HAE for " . $self->id . "\n"); + # Don't extend configs that have been terminated. There's nothing to + # extend if there's no end date. + return if $self->terminated || !$self->end_date; + + my $today = DateTime->now_local; + my $end_date = $self->end_date; + + _log_msg("today $today end_date $end_date\n"); + + # The end date has not been reached yet, therefore no extension is + # needed. + return if $today <= $end_date; + + # The end date has been reached. If no automatic extension has been + # set then terminate the config and return. + if (!$self->extend_automatically_by) { + _log_msg("setting inactive\n"); + $self->active(0); + $self->save; + return; + } + + # Add the automatic extension period to the new end date as long as + # the new end date is in the past. Then save it and get out. + $end_date->add(months => $self->extend_automatically_by) while $today > $end_date; + _log_msg("new end date $end_date\n"); + + $self->end_date($end_date); + $self->save; + + return $end_date; +} + +sub get_previous_invoice_date { + my $self = shift; + + my $query = <dbh->selectrow_array($query, undef, $self->id); + + return undef unless $max_transdate; + return ref $max_transdate ? $max_transdate : $self->db->parse_date($max_transdate); +} + 1; diff --git a/config/periodic_invoices.conf.default b/config/periodic_invoices.conf.default index facc21c1e..0a92a8390 100644 --- a/config/periodic_invoices.conf.default +++ b/config/periodic_invoices.conf.default @@ -1,5 +1,10 @@ [periodic_invoices] -send_email = 1 +# The user name a report about the posted and printed invoices is sent +# to. +send_email_to = login +# The "From:" header for said email. email_from = Lx-Office Daemon +# The subject for said email. email_subject = Benachrichtigung: automatisch erstellte Rechnungen +# The template file used for the email's body. email_template = templates/webpages/oe/periodic_invoices_email.txt diff --git a/templates/webpages/oe/periodic_invoices_email.txt b/templates/webpages/oe/periodic_invoices_email.txt index 56b3e5a78..15d6039a8 100644 --- a/templates/webpages/oe/periodic_invoices_email.txt +++ b/templates/webpages/oe/periodic_invoices_email.txt @@ -2,8 +2,10 @@ Sehr geehrter Benutzer, die folgenden wiederkehrenden Rechnungen wurden automatisch erzeugt: -[% FOREACH inv = NEW_INVNUMBERS %][% inv.number %] [% END %] +[% FOREACH inv = POSTED_INVOICES %][% inv.invnumber %] [% END %] +[% IF PRINTED_INVOICES.size -%] Davon wurden die folgenden Rechnungen automatisch ausgedruckt: -[% FOREACH inv = PRINTED_INVNUMBERS %][% inv.number %] [% END %] +[% FOREACH inv = PRINTED_INVOICES %][% inv.invnumber %] [% END %] +[%- END %] -- 2.20.1