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, 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,
154 employee => $order->employee, # new_from sets employee to import user
157 map { _replace_vars($invoice, $time_period_vars, $_) } qw(notes intnotes transaction_description);
159 foreach my $item (@{ $invoice->items }) {
160 map { _replace_vars($item, $time_period_vars, $_) } qw(description longdescription);
163 $invoice->post(ar_id => $config->ar_chart_id) || die;
165 # like $form->add_shipto, but we don't need to check for a manual exception,
166 # because we can already assume this (otherwise no shipto_id from order)
167 if ($order->shipto_id) {
169 my $shipto_oe = SL::DB::Manager::Shipto->find_by(shipto_id => $order->shipto_id);
170 my $shipto_ar = $shipto_oe->clone_and_reset;
172 $shipto_ar->module('AR'); # alter module OE -> AR
173 $shipto_ar->trans_id($invoice->id); # alter trans_id -> new id from invoice
177 $order->link_to_record($invoice);
179 SL::DB::PeriodicInvoice->new(config_id => $config->id,
180 ar_id => $invoice->id,
181 period_start_date => $period_start_date)
184 # die $invoice->transaction_description;
186 $::lxdebug->message(LXDebug->WARN(), "_create_invoice failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2]));
193 sub _calculate_dates {
195 return $config->calculate_invoice_dates(end_date => DateTime->today_local);
199 my ($posted_invoices, $printed_invoices) = @_;
201 my %config = %::lx_office_conf;
203 return if !$config{periodic_invoices} || !$config{periodic_invoices}->{send_email_to} || !scalar @{ $posted_invoices };
205 my $user = SL::DB::Manager::AuthUser->find_by(login => $config{periodic_invoices}->{send_email_to});
206 my $email = $user ? $user->get_config_value('email') : undef;
208 return unless $email;
210 my $template = Template->new({ 'INTERPOLATE' => 0,
216 return unless $template;
218 my $email_template = $config{periodic_invoices}->{email_template};
219 my $filename = $email_template || ( (SL::DB::Default->get->templates || "templates/webpages") . "/oe/periodic_invoices_email.txt" );
220 my %params = ( POSTED_INVOICES => $posted_invoices,
221 PRINTED_INVOICES => $printed_invoices );
224 $template->process($filename, \%params, \$output);
226 my $mail = Mailer->new;
227 $mail->{from} = $config{periodic_invoices}->{email_from};
228 $mail->{to} = $email;
229 $mail->{subject} = $config{periodic_invoices}->{email_subject};
230 $mail->{content_type} = $filename =~ m/.html$/ ? 'text/html' : 'text/plain';
231 $mail->{message} = $output;
237 my ($invoice, $config) = @_;
239 return unless $config->print && $config->printer_id && $config->printer->printer_command;
241 my $form = Form->new;
242 $invoice->flatten_to_form($form, format_amounts => 1);
244 $form->{printer_code} = $config->printer->template_code;
245 $form->{copies} = $config->copies;
246 $form->{formname} = $form->{type};
247 $form->{format} = 'pdf';
248 $form->{media} = 'printer';
249 $form->{OUT} = $config->printer->printer_command;
250 $form->{OUT_MODE} = '|-';
252 $form->prepare_for_printing;
254 $form->throw_on_error(sub {
256 $form->parse_template(\%::myconfig);
258 } || die $EVAL_ERROR->getMessage;
272 SL::BackgroundJob::CleanBackgroundJobHistory - Create periodic
277 Iterate over all periodic invoice configurations, extend them if
278 applicable, calculate the dates for which invoices have to be posted
279 and post those invoices by converting the order into an invoice for
288 Strings like month names are hardcoded to German in this file.
294 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>