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 _print_invoice(@{ $_ }) for @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]) } ],
109 my $sub = $params{attribute};
110 my $str = $params{object}->$sub;
111 my $sub_fmt = lc($params{attribute_format} // 'text');
113 my ($start_tag, $end_tag) = $sub_fmt eq 'html' ? ('<%', '%>') : ('<%', '%>');
115 $str =~ s{ ${start_tag} ([a-z0-9_]+) ( \s+ format \s*=\s* (.*?) \s* )? ${end_tag} }{
116 my ($key, $format) = ($1, $3);
117 $key = $::locale->unquote_special_chars('html', $key) if $sub_fmt eq 'html';
120 if (!$params{vars}->{$key}) {
124 $format = $::locale->unquote_special_chars('html', $format) if $sub_fmt eq 'html';
126 $new_value = DateTime::Format::Strptime->new(
129 time_zone => 'local',
130 )->format_datetime($params{vars}->{$key}->[0]);
133 $new_value = $params{vars}->{$1}->[1]->($params{vars}->{$1}->[0]);
136 $new_value = $::locale->quote_special_chars('html', $new_value) if $sub_fmt eq 'html';
142 $params{object}->$sub($str);
145 sub _create_periodic_invoice {
148 my $period_start_date = shift;
150 my $time_period_vars = _generate_time_period_variables($config, $period_start_date);
152 my $invdate = DateTime->today_local;
154 my $order = $config->order;
156 if (!$self->{db_obj}->db->do_transaction(sub {
157 1; # make Emacs happy
159 $invoice = SL::DB::Invoice->new_from($order);
161 my $intnotes = $invoice->intnotes ? $invoice->intnotes . "\n\n" : '';
162 $intnotes .= "Automatisch am " . $invdate->to_lxoffice . " erzeugte Rechnung";
164 $invoice->assign_attributes(deliverydate => $period_start_date,
165 intnotes => $intnotes,
166 employee => $order->employee, # new_from sets employee to import user
169 _replace_vars(object => $invoice, vars => $time_period_vars, attribute => $_) for qw(notes intnotes transaction_description);
171 foreach my $item (@{ $invoice->items }) {
172 _replace_vars(object => $item, vars => $time_period_vars, attribute => $_, attribute_format => ($_ eq 'longdescription' ? 'html' : 'text')) for qw(description longdescription);
175 $invoice->post(ar_id => $config->ar_chart_id) || die;
177 # like $form->add_shipto, but we don't need to check for a manual exception,
178 # because we can already assume this (otherwise no shipto_id from order)
179 if ($order->shipto_id) {
181 my $shipto_oe = SL::DB::Manager::Shipto->find_by(shipto_id => $order->shipto_id);
182 my $shipto_ar = $shipto_oe->clone_and_reset;
184 $shipto_ar->module('AR'); # alter module OE -> AR
185 $shipto_ar->trans_id($invoice->id); # alter trans_id -> new id from invoice
189 $order->link_to_record($invoice);
191 SL::DB::PeriodicInvoice->new(config_id => $config->id,
192 ar_id => $invoice->id,
193 period_start_date => $period_start_date)
196 # die $invoice->transaction_description;
198 $::lxdebug->message(LXDebug->WARN(), "_create_invoice failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2]));
205 sub _calculate_dates {
207 return $config->calculate_invoice_dates(end_date => DateTime->today_local);
211 my ($posted_invoices, $printed_invoices) = @_;
213 my %config = %::lx_office_conf;
215 return if !$config{periodic_invoices} || !$config{periodic_invoices}->{send_email_to} || !scalar @{ $posted_invoices };
217 my $user = SL::DB::Manager::AuthUser->find_by(login => $config{periodic_invoices}->{send_email_to});
218 my $email = $user ? $user->get_config_value('email') : undef;
220 return unless $email;
222 my $template = Template->new({ 'INTERPOLATE' => 0,
228 return unless $template;
230 my $email_template = $config{periodic_invoices}->{email_template};
231 my $filename = $email_template || ( (SL::DB::Default->get->templates || "templates/webpages") . "/oe/periodic_invoices_email.txt" );
232 my %params = ( POSTED_INVOICES => $posted_invoices,
233 PRINTED_INVOICES => $printed_invoices );
236 $template->process($filename, \%params, \$output);
238 my $mail = Mailer->new;
239 $mail->{from} = $config{periodic_invoices}->{email_from};
240 $mail->{to} = $email;
241 $mail->{subject} = $config{periodic_invoices}->{email_subject};
242 $mail->{content_type} = $filename =~ m/.html$/ ? 'text/html' : 'text/plain';
243 $mail->{message} = $output;
249 my ($invoice, $config) = @_;
251 return unless $config->print && $config->printer_id && $config->printer->printer_command;
253 my $form = Form->new;
254 $invoice->flatten_to_form($form, format_amounts => 1);
256 $form->{printer_code} = $config->printer->template_code;
257 $form->{copies} = $config->copies;
258 $form->{formname} = $form->{type};
259 $form->{format} = 'pdf';
260 $form->{media} = 'printer';
261 $form->{OUT} = $config->printer->printer_command;
262 $form->{OUT_MODE} = '|-';
264 $form->prepare_for_printing;
266 $form->throw_on_error(sub {
268 $form->parse_template(\%::myconfig);
270 } || die $EVAL_ERROR->getMessage;
284 SL::BackgroundJob::CleanBackgroundJobHistory - Create periodic
289 Iterate over all periodic invoice configurations, extend them if
290 applicable, calculate the dates for which invoices have to be posted
291 and post those invoices by converting the order into an invoice for
300 Strings like month names are hardcoded to German in this file.
306 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>