1 package SL::BackgroundJob::CreatePeriodicInvoices;
5 use parent qw(SL::BackgroundJob::Base);
8 use DateTime::Format::Strptime;
9 use English qw(-no_match_vars);
15 use SL::DB::PeriodicInvoice;
16 use SL::DB::PeriodicInvoicesConfig;
20 $_[0]->create_standard_job('0 3 1 * *'); # first day of month at 3:00 am
25 $self->{db_obj} = shift;
27 my $configs = SL::DB::Manager::PeriodicInvoicesConfig->get_all(query => [ active => 1 ]);
29 foreach my $config (@{ $configs }) {
30 my $new_end_date = $config->handle_automatic_extension;
31 _log_msg("Periodic invoice configuration ID " . $config->id . " extended through " . $new_end_date->strftime('%d.%m.%Y') . "\n") if $new_end_date;
34 my (@new_invoices, @invoices_to_print);
36 _log_msg("Number of configs: " . scalar(@{ $configs}));
38 foreach my $config (@{ $configs }) {
39 # A configuration can be set to inactive by
40 # $config->handle_automatic_extension. Therefore the check in
41 # ...->get_all() does not suffice.
42 _log_msg("Config " . $config->id . " active " . $config->active);
43 next unless $config->active;
45 my @dates = _calculate_dates($config);
47 _log_msg("Dates: " . join(' ', map { $_->to_lxoffice } @dates));
49 foreach my $date (@dates) {
50 my $invoice = $self->_create_periodic_invoice($config, $date);
53 _log_msg("Invoice " . $invoice->invnumber . " posted for config ID " . $config->id . ", period start date " . $::locale->format_date(\%::myconfig, $date) . "\n");
54 push @new_invoices, $invoice;
55 push @invoices_to_print, [ $invoice, $config ] if $config->print;
61 map { _print_invoice(@{ $_ }) } @invoices_to_print;
63 _send_email(\@new_invoices, [ map { $_->[0] } @invoices_to_print ]) if @new_invoices;
69 my $message = join('', @_);
70 $message .= "\n" unless $message =~ m/\n$/;
71 $::lxdebug->message(LXDebug::DEBUG1(), $message);
74 sub _generate_time_period_variables {
76 my $period_start_date = shift;
77 my $period_end_date = $period_start_date->clone->truncate(to => 'month')->add(months => $config->get_period_length)->subtract(days => 1);
79 my @month_names = ('',
80 $::locale->text('January'), $::locale->text('February'), $::locale->text('March'), $::locale->text('April'), $::locale->text('May'), $::locale->text('June'),
81 $::locale->text('July'), $::locale->text('August'), $::locale->text('September'), $::locale->text('October'), $::locale->text('November'), $::locale->text('December'));
84 current_quarter => [ $period_start_date->clone->truncate(to => 'month'), sub { $_[0]->quarter } ],
85 previous_quarter => [ $period_start_date->clone->truncate(to => 'month')->subtract(months => 3), sub { $_[0]->quarter } ],
86 next_quarter => [ $period_start_date->clone->truncate(to => 'month')->add( months => 3), sub { $_[0]->quarter } ],
88 current_month => [ $period_start_date->clone->truncate(to => 'month'), sub { $_[0]->month } ],
89 previous_month => [ $period_start_date->clone->truncate(to => 'month')->subtract(months => 1), sub { $_[0]->month } ],
90 next_month => [ $period_start_date->clone->truncate(to => 'month')->add( months => 1), sub { $_[0]->month } ],
92 current_year => [ $period_start_date->clone->truncate(to => 'year'), sub { $_[0]->year } ],
93 previous_year => [ $period_start_date->clone->truncate(to => 'year')->subtract(years => 1), sub { $_[0]->year } ],
94 next_year => [ $period_start_date->clone->truncate(to => 'year')->add( years => 1), sub { $_[0]->year } ],
96 period_start_date => [ $period_start_date->clone->truncate(to => 'month'), sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
97 period_end_date => [ $period_end_date ->clone->truncate(to => 'month'), sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
100 map { $vars->{"${_}_month_long"} = $month_names[ $vars->{"${_}_month"} ] } qw(current previous next);
109 my $str = $object->$sub;
111 $str =~ s{ <\% ([a-z0-9_]+) ( \s+ format \s*=\s* (.*?) \s* )? \%>}{
112 my ($key, $format) = ($1, $3);
113 if (!$vars->{$key}) {
117 DateTime::Format::Strptime->new(
120 time_zone => 'local',
121 )->format_datetime($vars->{$key}->[0]);
124 $vars->{$1}->[1]->($vars->{$1}->[0]);
131 sub _create_periodic_invoice {
134 my $period_start_date = shift;
136 my $time_period_vars = _generate_time_period_variables($config, $period_start_date);
138 my $invdate = DateTime->today_local;
140 my $order = $config->order;
142 if (!$self->{db_obj}->db->do_transaction(sub {
143 1; # make Emacs happy
145 $invoice = SL::DB::Invoice->new_from($order);
147 my $intnotes = $invoice->intnotes ? $invoice->intnotes . "\n\n" : '';
148 $intnotes .= "Automatisch am " . $invdate->to_lxoffice . " erzeugte Rechnung";
150 $invoice->assign_attributes(deliverydate => $period_start_date,
151 intnotes => $intnotes,
154 map { _replace_vars($invoice, $time_period_vars, $_) } qw(notes intnotes transaction_description);
156 foreach my $item (@{ $invoice->items }) {
157 map { _replace_vars($item, $time_period_vars, $_) } qw(description longdescription);
160 $invoice->post(ar_id => $config->ar_chart_id) || die;
162 # like $form->add_shipto, but we don't need to check for a manual exception,
163 # because we can already assume this (otherwise no shipto_id from order)
164 if ($order->shipto_id) {
166 my $shipto_oe = SL::DB::Manager::Shipto->find_by(shipto_id => $order->shipto_id);
167 my $shipto_ar = $shipto_oe->clone_and_reset;
169 $shipto_ar->module('AR'); # alter module OE -> AR
170 $shipto_ar->trans_id($invoice->id); # alter trans_id -> new id from invoice
174 $order->link_to_record($invoice);
176 SL::DB::PeriodicInvoice->new(config_id => $config->id,
177 ar_id => $invoice->id,
178 period_start_date => $period_start_date)
181 # die $invoice->transaction_description;
183 $::lxdebug->message(LXDebug->WARN(), "_create_invoice failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2]));
190 sub _calculate_dates {
193 my $cur_date = $config->first_billing_date || $config->start_date;
194 my $start_date = $config->get_previous_invoice_date || DateTime->new(year => 1970, month => 1, day => 1);
195 my $end_date = $config->end_date || DateTime->new(year => 2100, month => 1, day => 1);
196 my $tomorrow = DateTime->today_local->add(days => 1);
197 my $period_len = $config->get_period_length;
199 $end_date = $tomorrow if $end_date > $tomorrow;
204 last if $cur_date >= $end_date;
206 push @dates, $cur_date->clone if $cur_date > $start_date;
208 $cur_date->add(months => $period_len);
215 my ($posted_invoices, $printed_invoices) = @_;
217 my %config = %::lx_office_conf;
219 return if !$config{periodic_invoices} || !$config{periodic_invoices}->{send_email_to} || !scalar @{ $posted_invoices };
221 my $user = SL::DB::Manager::AuthUser->find_by(login => $config{periodic_invoices}->{send_email_to});
222 my $email = $user ? $user->get_config_value('email') : undef;
224 return unless $email;
226 my $template = Template->new({ 'INTERPOLATE' => 0,
232 return unless $template;
234 my $email_template = $config{periodic_invoices}->{email_template};
235 my $filename = $email_template || ( (SL::DB::Default->get->templates || "templates/webpages") . "/periodic_invoices_email.txt" );
236 my %params = ( POSTED_INVOICES => $posted_invoices,
237 PRINTED_INVOICES => $printed_invoices );
240 $template->process($filename, \%params, \$output);
242 my $mail = Mailer->new;
243 $mail->{from} = $config{periodic_invoices}->{email_from};
244 $mail->{to} = $email;
245 $mail->{subject} = $config{periodic_invoices}->{email_subject};
246 $mail->{content_type} = $filename =~ m/.html$/ ? 'text/html' : 'text/plain';
247 $mail->{message} = $output;
253 my ($invoice, $config) = @_;
255 return unless $config->print && $config->printer_id && $config->printer->printer_command;
257 my $form = Form->new;
258 $invoice->flatten_to_form($form, format_amounts => 1);
260 $form->{printer_code} = $config->printer->template_code;
261 $form->{copies} = $config->copies;
262 $form->{formname} = $form->{type};
263 $form->{format} = 'pdf';
264 $form->{media} = 'printer';
265 $form->{OUT} = $config->printer->printer_command;
266 $form->{OUT_MODE} = '|-';
268 $form->prepare_for_printing;
270 $form->throw_on_error(sub {
272 $form->parse_template(\%::myconfig);
274 } || die $EVAL_ERROR->getMessage;
288 SL::BackgroundJob::CleanBackgroundJobHistory - Create periodic
293 Iterate over all periodic invoice configurations, extend them if
294 applicable, calculate the dates for which invoices have to be posted
295 and post those invoices by converting the order into an invoice for
304 Strings like month names are hardcoded to German in this file.
310 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>