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_month_long => [ $period_start_date->clone->truncate(to => 'month'), sub { $month_names[ $_[0]->month ] } ],
93 previous_month_long => [ $period_start_date->clone->truncate(to => 'month')->subtract(months => 1), sub { $month_names[ $_[0]->month ] } ],
94 next_month_long => [ $period_start_date->clone->truncate(to => 'month')->add( months => 1), sub { $month_names[ $_[0]->month ] } ],
96 current_year => [ $period_start_date->clone->truncate(to => 'year'), sub { $_[0]->year } ],
97 previous_year => [ $period_start_date->clone->truncate(to => 'year')->subtract(years => 1), sub { $_[0]->year } ],
98 next_year => [ $period_start_date->clone->truncate(to => 'year')->add( years => 1), sub { $_[0]->year } ],
100 period_start_date => [ $period_start_date->clone->truncate(to => 'month'), sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
101 period_end_date => [ $period_end_date ->clone->truncate(to => 'month'), sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
111 my $str = $object->$sub;
113 $str =~ s{ <\% ([a-z0-9_]+) ( \s+ format \s*=\s* (.*?) \s* )? \%>}{
114 my ($key, $format) = ($1, $3);
115 if (!$vars->{$key}) {
119 DateTime::Format::Strptime->new(
122 time_zone => 'local',
123 )->format_datetime($vars->{$key}->[0]);
126 $vars->{$1}->[1]->($vars->{$1}->[0]);
133 sub _create_periodic_invoice {
136 my $period_start_date = shift;
138 my $time_period_vars = _generate_time_period_variables($config, $period_start_date);
140 my $invdate = DateTime->today_local;
142 my $order = $config->order;
144 if (!$self->{db_obj}->db->do_transaction(sub {
145 1; # make Emacs happy
147 $invoice = SL::DB::Invoice->new_from($order);
149 my $intnotes = $invoice->intnotes ? $invoice->intnotes . "\n\n" : '';
150 $intnotes .= "Automatisch am " . $invdate->to_lxoffice . " erzeugte Rechnung";
152 $invoice->assign_attributes(deliverydate => $period_start_date,
153 intnotes => $intnotes,
156 map { _replace_vars($invoice, $time_period_vars, $_) } qw(notes intnotes transaction_description);
158 foreach my $item (@{ $invoice->items }) {
159 map { _replace_vars($item, $time_period_vars, $_) } qw(description longdescription);
162 $invoice->post(ar_id => $config->ar_chart_id) || die;
164 # like $form->add_shipto, but we don't need to check for a manual exception,
165 # because we can already assume this (otherwise no shipto_id from order)
166 if ($order->shipto_id) {
168 my $shipto_oe = SL::DB::Manager::Shipto->find_by(shipto_id => $order->shipto_id);
169 my $shipto_ar = $shipto_oe->clone_and_reset;
171 $shipto_ar->module('AR'); # alter module OE -> AR
172 $shipto_ar->trans_id($invoice->id); # alter trans_id -> new id from invoice
176 $order->link_to_record($invoice);
178 SL::DB::PeriodicInvoice->new(config_id => $config->id,
179 ar_id => $invoice->id,
180 period_start_date => $period_start_date)
183 # die $invoice->transaction_description;
185 $::lxdebug->message(LXDebug->WARN(), "_create_invoice failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2]));
192 sub _calculate_dates {
194 return $config->calculate_invoice_dates(end_date => DateTime->today_local);
198 my ($posted_invoices, $printed_invoices) = @_;
200 my %config = %::lx_office_conf;
202 return if !$config{periodic_invoices} || !$config{periodic_invoices}->{send_email_to} || !scalar @{ $posted_invoices };
204 my $user = SL::DB::Manager::AuthUser->find_by(login => $config{periodic_invoices}->{send_email_to});
205 my $email = $user ? $user->get_config_value('email') : undef;
207 return unless $email;
209 my $template = Template->new({ 'INTERPOLATE' => 0,
215 return unless $template;
217 my $email_template = $config{periodic_invoices}->{email_template};
218 my $filename = $email_template || ( (SL::DB::Default->get->templates || "templates/webpages") . "/oe/periodic_invoices_email.txt" );
219 my %params = ( POSTED_INVOICES => $posted_invoices,
220 PRINTED_INVOICES => $printed_invoices );
223 $template->process($filename, \%params, \$output);
225 my $mail = Mailer->new;
226 $mail->{from} = $config{periodic_invoices}->{email_from};
227 $mail->{to} = $email;
228 $mail->{subject} = $config{periodic_invoices}->{email_subject};
229 $mail->{content_type} = $filename =~ m/.html$/ ? 'text/html' : 'text/plain';
230 $mail->{message} = $output;
236 my ($invoice, $config) = @_;
238 return unless $config->print && $config->printer_id && $config->printer->printer_command;
240 my $form = Form->new;
241 $invoice->flatten_to_form($form, format_amounts => 1);
243 $form->{printer_code} = $config->printer->template_code;
244 $form->{copies} = $config->copies;
245 $form->{formname} = $form->{type};
246 $form->{format} = 'pdf';
247 $form->{media} = 'printer';
248 $form->{OUT} = $config->printer->printer_command;
249 $form->{OUT_MODE} = '|-';
251 $form->prepare_for_printing;
253 $form->throw_on_error(sub {
255 $form->parse_template(\%::myconfig);
257 } || die $EVAL_ERROR->getMessage;
271 SL::BackgroundJob::CleanBackgroundJobHistory - Create periodic
276 Iterate over all periodic invoice configurations, extend them if
277 applicable, calculate the dates for which invoices have to be posted
278 and post those invoices by converting the order into an invoice for
287 Strings like month names are hardcoded to German in this file.
293 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>