Swiss QR-Bill: Fix: Referenznummer nur bei gewählter Variante erzeugen
[kivitendo-erp.git] / SL / Template / OpenDocument.pm
1 package SL::Template::OpenDocument;
2
3 use parent qw(SL::Template::Simple);
4
5 use Archive::Zip;
6 use Encode;
7 use HTML::Entities;
8 use POSIX 'setsid';
9 use XML::LibXML;
10
11 use SL::Iconv;
12 use SL::Template::OpenDocument::Styles;
13
14 use SL::DB::BankAccount;
15 use SL::Helper::QrBill;
16 use SL::Helper::ISO3166;
17
18 use Cwd;
19 # use File::Copy;
20 # use File::Spec;
21 # use File::Temp qw(:mktemp);
22 use IO::File;
23 use List::Util qw(first);
24
25 use strict;
26
27 my %text_markup_replace = (
28   b   => "BOLD",
29   i   => "ITALIC",
30   s   => "STRIKETHROUGH",
31   u   => "UNDERLINE",
32   sup => "SUPER",
33   sub => "SUB",
34 );
35
36 sub _format_text {
37   my ($self, $content, %params) = @_;
38
39   $content = $::locale->quote_special_chars('Template/OpenDocument', $content);
40
41   # Allow some HTML markup to be converted into the output format's
42   # corresponding markup code, e.g. bold or italic.
43   foreach my $key (keys(%text_markup_replace)) {
44     my $value = $text_markup_replace{$key};
45     $content =~ s|\&lt;${key}\&gt;|<text:span text:style-name=\"TKIVITENDO${value}\">|gi; #"
46     $content =~ s|\&lt;/${key}\&gt;|</text:span>|gi;
47   }
48
49   return $content;
50 }
51
52 my %html_replace = (
53   '</ul>'     => '</text:list>',
54   '</ol>'     => '</text:list>',
55   '</li>'     => '</text:p></text:list-item>',
56   '<b>'       => '<text:span text:style-name="TKIVITENDOBOLD">',
57   '</b>'      => '</text:span>',
58   '<strong>'  => '<text:span text:style-name="TKIVITENDOBOLD">',
59   '</strong>' => '</text:span>',
60   '<i>'       => '<text:span text:style-name="TKIVITENDOITALIC">',
61   '</i>'      => '</text:span>',
62   '<em>'      => '<text:span text:style-name="TKIVITENDOITALIC">',
63   '</em>'     => '</text:span>',
64   '<u>'       => '<text:span text:style-name="TKIVITENDOUNDERLINE">',
65   '</u>'      => '</text:span>',
66   '<s>'       => '<text:span text:style-name="TKIVITENDOSTRIKETHROUGH">',
67   '</s>'      => '</text:span>',
68   '<sub>'     => '<text:span text:style-name="TKIVITENDOSUB">',
69   '</sub>'    => '</text:span>',
70   '<sup>'     => '<text:span text:style-name="TKIVITENDOSUPER">',
71   '</sup>'    => '</text:span>',
72   '<br/>'     => '<text:line-break/>',
73   '<br>'      => '<text:line-break/>',
74 );
75
76 sub _format_html {
77   my ($self, $content, %params) = @_;
78
79   my $in_p        = 0;
80   my $p_start_tag = qq|<text:p text:style-name="@{[ $self->{current_text_style} ]}">|;
81   my $prefix      = '';
82   my $suffix      = '';
83
84   my (@tags_to_open, @tags_to_close);
85   for (my $idx = scalar(@{ $self->{tag_stack} }) - 1; $idx >= 0; --$idx) {
86     my $tag = $self->{tag_stack}->[$idx];
87
88     next if $tag =~ m{/>$};
89     last if $tag =~ m{^<table};
90
91     if ($tag =~ m{^<text:p}) {
92       $in_p        = 1;
93       $p_start_tag = $tag;
94       last;
95
96     } else {
97       $suffix  =  "${tag}${suffix}";
98       $tag     =~ s{ .*>}{>};
99       $prefix .=  '</' . substr($tag, 1);
100     }
101   }
102
103   $content            =~ s{ ^<p> | </p>$ }{}gx if $in_p;
104   $content            =~ s{ \r+ }{}gx;
105   $content            =~ s{ \n+ }{ }gx;
106   $content            =~ s{ (?:\&nbsp;|\s)+ }{ }gx;
107
108   my $ul_start_tag    = qq|<text:list xml:id="list@{[ int rand(9999999999999999) ]}" text:style-name="LKIVITENDOitemize@{[ $self->{current_text_style} ]}">|;
109   my $ol_start_tag    = qq|<text:list xml:id="list@{[ int rand(9999999999999999) ]}" text:style-name="LKIVITENDOenumerate@{[ $self->{current_text_style} ]}">|;
110   my $ul_li_start_tag = qq|<text:list-item><text:p text:style-name="PKIVITENDOitemize@{[ $self->{current_text_style} ]}">|;
111   my $ol_li_start_tag = qq|<text:list-item><text:p text:style-name="PKIVITENDOenumerate@{[ $self->{current_text_style} ]}">|;
112
113   my @parts = map {
114     if (substr($_, 0, 1) eq '<') {
115       s{ +}{}g;
116       if ($_ eq '</p>') {
117         $in_p--;
118         $in_p == 0 ? '</text:p>' : '';
119
120       } elsif ($_ eq '<p>') {
121         $in_p++;
122         $in_p == 1 ? $p_start_tag : '';
123
124       } elsif ($_ eq '<ul>') {
125         $self->{used_list_styles}->{itemize}->{$self->{current_text_style}}   = 1;
126         $html_replace{'<li>'}                                                 = $ul_li_start_tag;
127         $ul_start_tag;
128
129       } elsif ($_ eq '<ol>') {
130         $self->{used_list_styles}->{enumerate}->{$self->{current_text_style}} = 1;
131         $html_replace{'<li>'}                                                 = $ol_li_start_tag;
132         $ol_start_tag;
133
134       } else {
135         $html_replace{$_} || '';
136       }
137
138     } else {
139       $::locale->quote_special_chars('Template/OpenDocument', HTML::Entities::decode_entities($_));
140     }
141   } split(m{(<.*?>)}x, $content);
142
143   my $out  = join('', $prefix, @parts, $suffix);
144
145   # $::lxdebug->dump(0, "prefix parts suffix", [ $prefix, join('', @parts), $suffix ]);
146
147   return $out;
148 }
149
150 my %formatters = (
151   html => \&_format_html,
152   text => \&_format_text,
153 );
154
155 sub new {
156   my $type = shift;
157
158   my $self = $type->SUPER::new(@_);
159
160   $self->set_tag_style('&lt;%', '%&gt;');
161   $self->{quot_re} = '&quot;';
162
163   return $self;
164 }
165
166 sub parse_foreach {
167   my ($self, $var, $text, $start_tag, $end_tag, @indices) = @_;
168
169   my ($form, $new_contents) = ($self->{"form"}, "");
170
171   my $ary = $self->_get_loop_variable($var, 1, @indices);
172
173   for (my $i = 0; $i < scalar(@{$ary || []}); $i++) {
174     $form->{"__first__"} = $i == 0;
175     $form->{"__last__"} = ($i + 1) == scalar(@{$ary});
176     $form->{"__odd__"} = (($i + 1) % 2) == 1;
177     $form->{"__counter__"} = $i + 1;
178     my $new_text = $self->parse_block($text, (@indices, $i));
179     return undef unless (defined($new_text));
180     $new_contents .= $start_tag . $new_text . $end_tag;
181   }
182   map({ delete($form->{"__${_}__"}); } qw(first last odd counter));
183
184   return $new_contents;
185 }
186
187 sub find_end {
188   my ($self, $text, $pos, $var, $not) = @_;
189
190   my $depth = 1;
191   $pos = 0 unless ($pos);
192
193   while ($pos < length($text)) {
194     $pos++;
195
196     next if (substr($text, $pos - 1, 5) ne '&lt;%');
197
198     if ((substr($text, $pos + 4, 2) eq 'if') || (substr($text, $pos + 4, 3) eq 'for')) {
199       $depth++;
200
201     } elsif ((substr($text, $pos + 4, 4) eq 'else') && (1 == $depth)) {
202       if (!$var) {
203         $self->{"error"} = '<%else%> outside of <%if%> / <%ifnot%>.';
204         return undef;
205       }
206
207       my $block = substr($text, 0, $pos - 1);
208       substr($text, 0, $pos - 1) = "";
209       $text =~ s!^\&lt;\%[^\%]+\%\&gt;!!;
210       $text = '&lt;%if' . ($not ?  " " : "not ") . $var . '%&gt;' . $text;
211
212       return ($block, $text);
213
214     } elsif (substr($text, $pos + 4, 3) eq 'end') {
215       $depth--;
216       if ($depth == 0) {
217         my $block = substr($text, 0, $pos - 1);
218         substr($text, 0, $pos - 1) = "";
219         $text =~ s!^\&lt;\%[^\%]+\%\&gt;!!;
220
221         return ($block, $text);
222       }
223     }
224   }
225
226   return undef;
227 }
228
229 sub parse_block {
230   $main::lxdebug->enter_sub();
231
232   my ($self, $contents, @indices) = @_;
233
234   my $new_contents = "";
235
236   while ($contents ne "") {
237     if (substr($contents, 0, 1) eq "<") {
238       $contents =~ m|^(<[^>]+>)|;
239       my $tag = $1;
240       substr($contents, 0, length($1)) = "";
241
242       $self->{current_text_style} = $1 if $tag =~ m|text:style-name\s*=\s*"([^"]+)"|;
243
244       push @{ $self->{tag_stack} }, $tag;
245
246       if ($tag =~ m|<table:table-row|) {
247         $contents =~ m|^(.*?)(</table:table-row[^>]*>)|;
248         my $table_row = $1;
249         my $end_tag = $2;
250
251         if ($table_row =~ m|\&lt;\%foreachrow\s+(.*?)\%\&gt;|) {
252           my $var = $1;
253
254           $contents =~ m|^(.*?)(\&lt;\%foreachrow\s+.*?\%\&gt;)|;
255           substr($contents, length($1), length($2)) = "";
256
257           ($table_row, $contents) = $self->find_end($contents, length($1));
258           if (!$table_row) {
259             $self->{"error"} = "Unclosed <\%foreachrow\%>." unless ($self->{"error"});
260             $main::lxdebug->leave_sub();
261             return undef;
262           }
263
264           $contents   =~ m|^(.*?)(</table:table-row[^>]*>)|;
265           $table_row .=  $1;
266           $end_tag    =  $2;
267
268           substr $contents, 0, length($1) + length($2), '';
269
270           my $new_text = $self->parse_foreach($var, $table_row, $tag, $end_tag, @indices);
271           if (!defined($new_text)) {
272             $main::lxdebug->leave_sub();
273             return undef;
274           }
275           $new_contents .= $new_text;
276
277         } else {
278           substr($contents, 0, length($table_row) + length($end_tag)) = "";
279           my $new_text = $self->parse_block($table_row, @indices);
280           if (!defined($new_text)) {
281             $main::lxdebug->leave_sub();
282             return undef;
283           }
284           $new_contents .= $tag . $new_text . $end_tag;
285         }
286
287       } else {
288         $new_contents .= $tag;
289       }
290
291       if ($tag =~ m{^</ | />$}x) {
292         # $::lxdebug->message(0, "popping top tag is $tag top " . $self->{tag_stack}->[-1]);
293         pop @{ $self->{tag_stack} };
294       }
295
296     } else {
297       $contents =~ /^([^<]+)/;
298       my $text = $1;
299
300       my $pos_if = index($text, '&lt;%if');
301       my $pos_foreach = index($text, '&lt;%foreach');
302
303       if ((-1 == $pos_if) && (-1 == $pos_foreach)) {
304         substr($contents, 0, length($text)) = "";
305         $new_contents .= $self->substitute_vars($text, @indices);
306         next;
307       }
308
309       if ((-1 == $pos_if) || ((-1 != $pos_foreach) && ($pos_if > $pos_foreach))) {
310         $new_contents .= $self->substitute_vars(substr($contents, 0, $pos_foreach), @indices);
311         substr($contents, 0, $pos_foreach) = "";
312
313         if ($contents !~ m|^(\&lt;\%foreach (.*?)\%\&gt;)|) {
314           $self->{"error"} = "Malformed <\%foreach\%>.";
315           $main::lxdebug->leave_sub();
316           return undef;
317         }
318
319         my $var = $2;
320
321         substr($contents, 0, length($1)) = "";
322
323         my $block;
324         ($block, $contents) = $self->find_end($contents);
325         if (!$block) {
326           $self->{"error"} = "Unclosed <\%foreach\%>." unless ($self->{"error"});
327           $main::lxdebug->leave_sub();
328           return undef;
329         }
330
331         my $new_text = $self->parse_foreach($var, $block, "", "", @indices);
332         if (!defined($new_text)) {
333           $main::lxdebug->leave_sub();
334           return undef;
335         }
336         $new_contents .= $new_text;
337
338       } else {
339         if (!$self->_parse_block_if(\$contents, \$new_contents, $pos_if, @indices)) {
340           $main::lxdebug->leave_sub();
341           return undef;
342         }
343       }
344     }
345   }
346
347   $main::lxdebug->leave_sub();
348
349   return $new_contents;
350 }
351
352 sub parse {
353   $main::lxdebug->enter_sub();
354   my $self = $_[0];
355
356   local *OUT = $_[1];
357   my $form = $self->{"form"};
358
359   close(OUT);
360
361   my $qr_image_path;
362   if ($::instance_conf->get_create_qrbill_invoices && $form->{formname} eq 'invoice') {
363     # the biller account information, biller address and the reference number,
364     # are needed in the template aswell as in the qr-code generation, therefore
365     # assemble these and add to $::form
366     $qr_image_path = $self->generate_qr_code;
367   }
368
369   my $file_name;
370   if ($form->{"IN"} =~ m|^/|) {
371     $file_name = $form->{"IN"};
372   } else {
373     $file_name = $form->{"templates"} . "/" . $form->{"IN"};
374   }
375
376   my $zip = Archive::Zip->new();
377   if (Archive::Zip->AZ_OK != $zip->read($file_name)) {
378     $self->{"error"} = "File not found/is not a OpenDocument file.";
379     $main::lxdebug->leave_sub();
380     return 0;
381   }
382
383   my $contents = Encode::decode('utf-8-strict', $zip->contents("content.xml"));
384   if (!$contents) {
385     $self->{"error"} = "File is not a OpenDocument file.";
386     $main::lxdebug->leave_sub();
387     return 0;
388   }
389
390   $self->{current_text_style} =  '';
391   $self->{used_list_styles}   =  {
392     itemize                   => {},
393     enumerate                 => {},
394   };
395
396   my $new_contents;
397   if ($self->{use_template_toolkit}) {
398     my $additional_params = $::form;
399
400     $::form->template->process(\$contents, $additional_params, \$new_contents) || die $::form->template->error;
401   } else {
402     $self->{tag_stack} = [];
403     $new_contents = $self->parse_block($contents);
404   }
405   if (!defined($new_contents)) {
406     $main::lxdebug->leave_sub();
407     return 0;
408   }
409
410   my $new_styles = SL::Template::OpenDocument::Styles->get_style('text_basic');
411
412   foreach my $type (qw(itemize enumerate)) {
413     foreach my $parent (sort { $a cmp $b } keys %{ $self->{used_list_styles}->{$type} }) {
414       $new_styles .= SL::Template::OpenDocument::Styles->get_style('text_list_item', TYPE => $type, PARENT => $parent)
415                    .  SL::Template::OpenDocument::Styles->get_style("list_${type}",  TYPE => $type, PARENT => $parent);
416     }
417   }
418
419   # $::lxdebug->dump(0, "new_Styles", $new_styles);
420
421   $new_contents =~ s|</office:automatic-styles>|${new_styles}</office:automatic-styles>|;
422   $new_contents =~ s|[\n\r]||gm;
423
424 #   $new_contents =~ s|>|>\n|g;
425
426   $zip->contents("content.xml", Encode::encode('utf-8-strict', $new_contents));
427
428   my $styles = Encode::decode('utf-8-strict', $zip->contents("styles.xml"));
429   if ($contents) {
430     my $new_styles = $self->parse_block($styles);
431     if (!defined($new_contents)) {
432       $main::lxdebug->leave_sub();
433       return 0;
434     }
435     $zip->contents("styles.xml", Encode::encode('utf-8-strict', $new_styles));
436   }
437
438   if ($::instance_conf->get_create_qrbill_invoices && $form->{formname} eq 'invoice') {
439     # get placeholder path from odt XML
440     my $qr_placeholder_path;
441     my $dom = XML::LibXML->load_xml(string => $contents);
442     my @nodelist = $dom->getElementsByTagName("draw:frame");
443     for my $node (@nodelist) {
444       my $attr = $node->getAttribute('draw:name');
445       if ($attr eq 'QRCodePlaceholder') {
446         my @children = $node->getChildrenByTagName('draw:image');
447         $qr_placeholder_path = $children[0]->getAttribute('xlink:href');
448       }
449     }
450     if (!defined($qr_placeholder_path)) {
451       $::form->error($::locale->text('QR-Code placeholder image: QRCodePlaceholder not found in template.'));
452     }
453     # replace QR-Code Placeholder Image in zip file (odt) with generated one
454     $zip->updateMember(
455      $qr_placeholder_path,
456      $qr_image_path
457     );
458   }
459
460   $zip->writeToFileNamed($form->{"tmpfile"}, 1);
461
462   my $res = 1;
463   if ($form->{"format"} =~ /pdf/) {
464     $res = $self->convert_to_pdf();
465   }
466
467   $main::lxdebug->leave_sub();
468   return $res;
469 }
470
471 sub get_qrbill_account {
472   $main::lxdebug->enter_sub();
473   my ($self) = @_;
474
475   my $qr_account;
476
477   my $bank_accounts     = SL::DB::Manager::BankAccount->get_all;
478   $qr_account = scalar(@{ $bank_accounts }) == 1 ?
479     $bank_accounts->[0] :
480     first { $_->use_for_qrbill } @{ $bank_accounts };
481
482   if (!$qr_account) {
483     $::form->error($::locale->text('No bank account flagged for QRBill usage was found.'));
484   }
485
486   $main::lxdebug->leave_sub();
487   return $qr_account;
488 }
489
490 sub remove_letters_prefix {
491   my $s = $_[0];
492   $s =~ s/^[a-zA-Z]+//;
493   return $s;
494 }
495
496 sub check_digits_and_max_length {
497   my $s = $_[0];
498   my $length = $_[1];
499
500   return 0 if (!($s =~ /^\d*$/) || length($s) > $length);
501   return 1;
502 }
503
504 sub calculate_check_digit {
505   # calculate ESR check digit using algorithm: "modulo 10, recursive"
506   my $ref_number_str = $_[0];
507
508   my @m = (0, 9, 4, 6, 8, 2, 7, 1, 3, 5);
509   my $carry = 0;
510
511   my @ref_number_split = map int($_), split(//, $ref_number_str);
512
513   for my $v (@ref_number_split) {
514     $carry = @m[($carry + $v) % 10];
515   }
516
517   return (10 - $carry) % 10;
518 }
519
520 sub assemble_ref_number {
521   $main::lxdebug->enter_sub();
522
523   my $bank_id = $_[0];
524   my $customer_number = $_[1];
525   my $order_number = $_[2] // "0";
526   my $invoice_number = $_[3] // "0";
527
528   # check values (analog to checks in makro)
529   # - bank_id
530   #     input: 6 digits, only numbers
531   #     output: 6 digits, only numbers
532   if (!($bank_id =~ /^\d*$/) || length($bank_id) != 6) {
533     $::form->error($::locale->text('Bank account id number invalid. Must be 6 digits.'));
534   }
535
536   # - customer_number
537   #     input: prefix (letters) + up to 6 digits (numbers)
538   #     output: prefix removed, 6 digits, filled with leading zeros
539   $customer_number = remove_letters_prefix($customer_number);
540   if (!check_digits_and_max_length($customer_number, 6)) {
541     $::form->error($::locale->text('Customer number invalid. Must be less then or equal to 6 digits after prefix.'));
542   }
543   # fill with zeros
544   $customer_number = sprintf "%06d", $customer_number;
545
546   # - order_number
547   #     input: prefix (letters) + up to 7 digits, may be zero
548   #     output: prefix removed, 7 digits, filled with leading zeros
549   $order_number = remove_letters_prefix($order_number);
550   if (!check_digits_and_max_length($order_number, 7)) {
551     $::form->error($::locale->text('Order number invalid. Must be less then or equal to 7 digits after prefix.'));
552   }
553   # fill with zeros
554   $order_number = sprintf "%07d", $order_number;
555
556   # - invoice_number
557   #     input: prefix (letters) + up to 7 digits, may be zero
558   #     output: prefix removed, 7 digits, filled with leading zeros
559   $invoice_number = remove_letters_prefix($invoice_number);
560   if (!check_digits_and_max_length($invoice_number, 7)) {
561     $::form->error($::locale->text('Invoice number invalid. Must be less then or equal to 7 digits after prefix.'));
562   }
563   # fill with zeros
564   $invoice_number = sprintf "%07d", $invoice_number;
565
566   # assemble ref. number
567   my $ref_number = $bank_id . $customer_number . $order_number . $invoice_number;
568
569   # calculate check digit
570   my $ref_number_cpl = $ref_number . calculate_check_digit($ref_number);
571
572   $main::lxdebug->leave_sub();
573   return $ref_number_cpl;
574 }
575
576 sub get_ref_number_formatted {
577   $main::lxdebug->enter_sub();
578
579   my $ref_number = $_[0];
580
581   # create ref. number in format:
582   # 'XX XXXXX XXXXX XXXXX XXXXX XXXXX' (2 digits + 5 x 5 digits)
583   my $ref_number_spaced = substr($ref_number, 0, 2) . ' ' .
584                           substr($ref_number, 2, 5) . ' ' .
585                           substr($ref_number, 7, 5) . ' ' .
586                           substr($ref_number, 12, 5) . ' ' .
587                           substr($ref_number, 17, 5) . ' ' .
588                           substr($ref_number, 22, 5);
589
590   $main::lxdebug->leave_sub();
591   return $ref_number_spaced;
592 }
593
594 sub get_iban_formatted {
595   $main::lxdebug->enter_sub();
596
597   my $iban = $_[0];
598
599   # create iban number in format:
600   # 'XXXX XXXX XXXX XXXX XXXX X' (5 x 4 + 1digits)
601   my $iban_spaced = substr($iban, 0, 4) . ' ' .
602                     substr($iban, 4, 4) . ' ' .
603                     substr($iban, 8, 4) . ' ' .
604                     substr($iban, 12, 4) . ' ' .
605                     substr($iban, 16, 4) . ' ' .
606                     substr($iban, 20, 1);
607
608   $main::lxdebug->leave_sub();
609   return $iban_spaced;
610 }
611
612 sub get_amount_formatted {
613   $main::lxdebug->enter_sub();
614
615   unless ($_[0] =~ /^\d+\.\d{2}$/) {
616     $::form->error($::locale->text('Amount has wrong format.'));
617   }
618
619   local $_ = shift;
620   $_ = reverse split //;
621   m/^\d{2}\./g;
622   s/\G(\d{3})(?=\d)/$1 /g;
623
624   $main::lxdebug->leave_sub();
625   return scalar reverse split //;
626 }
627
628 sub generate_qr_code {
629   $main::lxdebug->enter_sub();
630   my $self = $_[0];
631   my $form = $self->{"form"};
632
633   # assemble data for QR-Code
634
635   # get qr-account data
636   my $qr_account = $self->get_qrbill_account();
637
638   my %biller_information = (
639     'iban' => $qr_account->{'iban'}
640   );
641
642   my $biller_countrycode = SL::Helper::ISO3166::map_name_to_alpha_2_code(
643     $::instance_conf->get_address_country()
644   );
645   if (!$biller_countrycode) {
646     $::form->error($::locale->text('Error mapping biller countrycode.'));
647   }
648   my %biller_data = (
649     'address_type' => 'K',
650     'company' => $::instance_conf->get_company(),
651     'address_row1' => $::instance_conf->get_address_street1(),
652     'address_row2' => $::instance_conf->get_address_zipcode() . ' ' . $::instance_conf->get_address_city(),
653     'countrycode' => $biller_countrycode,
654   );
655
656   my %payment_information = (
657     'amount' => sprintf("%.2f", $form->parse_amount(\%::myconfig, $form->{'total'})),
658     'currency' => $form->{'currency'},
659   );
660
661   my $customer_countrycode = SL::Helper::ISO3166::map_name_to_alpha_2_code($form->{'country'});
662   if (!$customer_countrycode) {
663     $::form->error($::locale->text('Error mapping customer countrycode.'));
664   }
665   my %invoice_recipient_data = (
666     'address_type' => 'K',
667     'name' => $form->{'name'},
668     'address_row1' => $form->{'street'},
669     'address_row2' => $form->{'zipcode'} . ' ' . $form->{'city'},
670     'countrycode' => $customer_countrycode,
671   );
672
673   my %ref_nr_data;
674   if ($::instance_conf->get_create_qrbill_invoices == 1) {
675     # generate ref.-no. with check digit
676     my $ref_number = assemble_ref_number(
677       $qr_account->{'bank_account_id'},
678       $form->{'customernumber'},
679       $form->{'ordnumber'},
680       $form->{'invnumber'},
681     );
682     %ref_nr_data = (
683       'type' => 'QRR',
684       'ref_number' => $ref_number,
685     );
686     # get ref. number/iban formatted with spaces and set into form for template
687     # processing
688     $form->{'ref_number'} = $ref_number;
689     $form->{'ref_number_formatted'} = get_ref_number_formatted($ref_number);
690   } elsif ($::instance_conf->get_create_qrbill_invoices == 2) {
691     %ref_nr_data = (
692       'type' => 'NON',
693       'ref_number' => '',
694     );
695   } else {
696     $::form->error($::locale->text('Error getting QR-Bill type.'));
697   }
698
699   # set into form for template processing
700   $form->{'biller_information'} = \%biller_information;
701   $form->{'biller_data'} = \%biller_data;
702   $form->{'iban_formatted'} = get_iban_formatted($qr_account->{'iban'});
703
704   # format amount for template
705   $form->{'amount_formatted'} = get_amount_formatted(
706     sprintf(
707       "%.2f",
708       $form->parse_amount(\%::myconfig, $form->{'total'})
709     )
710   );
711
712   # set outfile
713   my $outfile = $form->{"tmpdir"} . '/' . 'qr-code.png';
714
715   # generate QR-Code Image
716   eval {
717    my $qr_image = SL::Helper::QrBill->new(
718      \%biller_information,
719      \%biller_data,
720      \%payment_information,
721      \%invoice_recipient_data,
722      \%ref_nr_data,
723    );
724    $qr_image->generate($outfile);
725   } or do {
726    local $_ = $@; chomp; my $error = $_;
727    $::form->error($::locale->text('QR-Image generation failed: ' . $error));
728   };
729
730   $main::lxdebug->leave_sub();
731   return $outfile;
732 }
733
734 sub is_xvfb_running {
735   $main::lxdebug->enter_sub();
736
737   my ($self) = @_;
738
739   local *IN;
740   my $dfname = $self->{"userspath"} . "/xvfb_display";
741   my $display;
742
743   $main::lxdebug->message(LXDebug->DEBUG2(), "    Looking for $dfname\n");
744   if ((-f $dfname) && open(IN, $dfname)) {
745     my $pid = <IN>;
746     chomp($pid);
747     $display = <IN>;
748     chomp($display);
749     my $xauthority = <IN>;
750     chomp($xauthority);
751     close(IN);
752
753     $main::lxdebug->message(LXDebug->DEBUG2(), "      found with $pid and $display\n");
754
755     if ((! -d "/proc/$pid") || !open(IN, "/proc/$pid/cmdline")) {
756       $main::lxdebug->message(LXDebug->DEBUG2(), "  no/wrong process #1\n");
757       unlink($dfname, $xauthority);
758       $main::lxdebug->leave_sub();
759       return undef;
760     }
761     my $line = <IN>;
762     close(IN);
763     if ($line !~ /xvfb/i) {
764       $main::lxdebug->message(LXDebug->DEBUG2(), "      no/wrong process #2\n");
765       unlink($dfname, $xauthority);
766       $main::lxdebug->leave_sub();
767       return undef;
768     }
769
770     $ENV{"XAUTHORITY"} = $xauthority;
771     $ENV{"DISPLAY"} = $display;
772   } else {
773     $main::lxdebug->message(LXDebug->DEBUG2(), "      not found\n");
774   }
775
776   $main::lxdebug->leave_sub();
777
778   return $display;
779 }
780
781 sub spawn_xvfb {
782   $main::lxdebug->enter_sub();
783
784   my ($self) = @_;
785
786   $main::lxdebug->message(LXDebug->DEBUG2, "spawn_xvfb()\n");
787
788   my $display = $self->is_xvfb_running();
789
790   if ($display) {
791     $main::lxdebug->leave_sub();
792     return $display;
793   }
794
795   $display = 99;
796   while ( -f "/tmp/.X${display}-lock") {
797     $display++;
798   }
799   $display = ":${display}";
800   $main::lxdebug->message(LXDebug->DEBUG2(), "  display $display\n");
801
802   my $mcookie = `mcookie`;
803   die("Installation error: mcookie not found.") if ($? != 0);
804   chomp($mcookie);
805
806   $main::lxdebug->message(LXDebug->DEBUG2(), "  mcookie $mcookie\n");
807
808   my $xauthority = "/tmp/.Xauthority-" . $$ . "-" . time() . "-" . int(rand(9999999));
809   $ENV{"XAUTHORITY"} = $xauthority;
810
811   $main::lxdebug->message(LXDebug->DEBUG2(), "  xauthority $xauthority\n");
812
813   if (system("xauth add \"${display}\" . \"${mcookie}\"") == -1) {
814     die "system call to xauth failed: $!";
815   }
816   if ($? != 0) {
817     $self->{"error"} = "Conversion to PDF failed because OpenOffice could not be started (xauth: $!)";
818     $main::lxdebug->leave_sub();
819     return undef;
820   }
821
822   $main::lxdebug->message(LXDebug->DEBUG2(), "  about to fork()\n");
823
824   my $pid = fork();
825   if (0 == $pid) {
826     $main::lxdebug->message(LXDebug->DEBUG2(), "  Child execing\n");
827     exec($::lx_office_conf{applications}->{xvfb}, $display, "-screen", "0", "640x480x8", "-nolisten", "tcp");
828   }
829   sleep(3);
830   $main::lxdebug->message(LXDebug->DEBUG2(), "  parent dont sleeping\n");
831
832   local *OUT;
833   my $dfname = $self->{"userspath"} . "/xvfb_display";
834   if (!open(OUT, ">", $dfname)) {
835     $self->{"error"} = "Conversion to PDF failed because OpenOffice could not be started ($dfname: $!)";
836     unlink($xauthority);
837     kill($pid);
838     $main::lxdebug->leave_sub();
839     return undef;
840   }
841   print(OUT "$pid\n$display\n$xauthority\n");
842   close(OUT);
843
844   $main::lxdebug->message(LXDebug->DEBUG2(), "  parent re-testing\n");
845
846   if (!$self->is_xvfb_running()) {
847     $self->{"error"} = "Conversion to PDF failed because OpenOffice could not be started.";
848     unlink($xauthority, $dfname);
849     kill($pid);
850     $main::lxdebug->leave_sub();
851     return undef;
852   }
853
854   $main::lxdebug->message(LXDebug->DEBUG2(), "  spawn OK\n");
855
856   $main::lxdebug->leave_sub();
857
858   return $display;
859 }
860
861 sub _run_python_uno {
862   my ($self, @args) = @_;
863
864   local $ENV{PYTHONPATH};
865   $ENV{PYTHONPATH} = $::lx_office_conf{environment}->{python_uno_path} . ':' . $ENV{PYTHONPATH} if $::lx_office_conf{environment}->{python_uno_path};
866   my $cmd          = $::lx_office_conf{applications}->{python_uno} . ' ' . join(' ', @args);
867   return `$cmd`;
868 }
869
870 sub is_openoffice_running {
871   my ($self) = @_;
872
873   $main::lxdebug->enter_sub();
874
875   my $output = $self->_run_python_uno('./scripts/oo-uno-test-conn.py', $::lx_office_conf{print_templates}->{openofficeorg_daemon_port}, ' 2> /dev/null');
876   chomp $output;
877
878   my $res = ($? == 0) || $output;
879   $main::lxdebug->message(LXDebug->DEBUG2(), "  is_openoffice_running(): res $res\n");
880
881   $main::lxdebug->leave_sub();
882
883   return $res;
884 }
885
886 sub spawn_openoffice {
887   $main::lxdebug->enter_sub();
888
889   my ($self) = @_;
890
891   $main::lxdebug->message(LXDebug->DEBUG2(), "spawn_openoffice()\n");
892
893   my ($try, $spawned_oo, $res);
894
895   $res = 0;
896   for ($try = 0; $try < 15; $try++) {
897     if ($self->is_openoffice_running()) {
898       $res = 1;
899       last;
900     }
901
902     if ($::dispatcher->interface_type eq 'FastCGI') {
903       $::dispatcher->{request}->Detach;
904     }
905
906     if (!$spawned_oo) {
907       my $pid = fork();
908       if (0 == $pid) {
909         $main::lxdebug->message(LXDebug->DEBUG2(), "  Child daemonizing\n");
910
911         if ($::dispatcher->interface_type eq 'FastCGI') {
912           $::dispatcher->{request}->Finish;
913           $::dispatcher->{request}->LastCall;
914         }
915         chdir('/');
916         open(STDIN, '/dev/null');
917         open(STDOUT, '>/dev/null');
918         my $new_pid = fork();
919         exit if ($new_pid);
920         my $ssres = setsid();
921         $main::lxdebug->message(LXDebug->DEBUG2(), "  Child execing\n");
922         my @cmdline = ($::lx_office_conf{applications}->{openofficeorg_writer},
923                        "-minimized", "-norestore", "-nologo", "-nolockcheck",
924                        "-headless",
925                        "-accept=socket,host=localhost,port=" .
926                        $::lx_office_conf{print_templates}->{openofficeorg_daemon_port} . ";urp;");
927         exec(@cmdline);
928       } else {
929         # parent
930         if ($::dispatcher->interface_type eq 'FastCGI') {
931           $::dispatcher->{request}->Attach;
932         }
933       }
934
935       $main::lxdebug->message(LXDebug->DEBUG2(), "  Parent after fork\n");
936       $spawned_oo = 1;
937       sleep(3);
938     }
939
940     sleep($try >= 5 ? 2 : 1);
941   }
942
943   if (!$res) {
944     $self->{"error"} = "Conversion from OpenDocument to PDF failed because " .
945       "OpenOffice could not be started.";
946   }
947
948   $main::lxdebug->leave_sub();
949
950   return $res;
951 }
952
953 sub convert_to_pdf {
954   $main::lxdebug->enter_sub();
955
956   my ($self) = @_;
957
958   my $form = $self->{"form"};
959
960   my $filename = $form->{"tmpfile"};
961   $filename =~ s/.odt$//;
962   if (substr($filename, 0, 1) ne "/") {
963     $filename = getcwd() . "/${filename}";
964   }
965
966   if (substr($self->{"userspath"}, 0, 1) eq "/") {
967     $ENV{'HOME'} = $self->{"userspath"};
968   } else {
969     $ENV{'HOME'} = getcwd() . "/" . $self->{"userspath"};
970   }
971
972   if (!$self->spawn_xvfb()) {
973     $main::lxdebug->leave_sub();
974     return 0;
975   }
976
977   if (!$::lx_office_conf{print_templates}->{openofficeorg_daemon}) {
978     if (system($::lx_office_conf{applications}->{openofficeorg_writer},
979                "-minimized", "-norestore", "-nologo", "-nolockcheck", "-headless",
980                "file:${filename}.odt",
981                "macro://" . (split('/', $filename))[-1] . "/Standard.Conversion.ConvertSelfToPDF()") == -1) {
982       die "system call to $::lx_office_conf{applications}->{openofficeorg_writer} failed: $!";
983     }
984   } else {
985     if (!$self->spawn_openoffice()) {
986       $main::lxdebug->leave_sub();
987       return 0;
988     }
989
990     $self->_run_python_uno('./scripts/oo-uno-convert-pdf.py', $::lx_office_conf{print_templates}->{openofficeorg_daemon_port}, "${filename}.odt");
991   }
992
993   my $res = $?;
994   if ((0 == $?) || (-f "${filename}.pdf" && -s "${filename}.pdf")) {
995     $form->{"tmpfile"} =~ s/odt$/pdf/;
996
997     unlink($filename . ".odt");
998
999     $main::lxdebug->leave_sub();
1000     return 1;
1001
1002   }
1003
1004   unlink($filename . ".odt", $filename . ".pdf");
1005   $self->{"error"} = "Conversion from OpenDocument to PDF failed. " .
1006     "Exit code: $res";
1007
1008   $main::lxdebug->leave_sub();
1009   return 0;
1010 }
1011
1012 sub format_string {
1013   my ($self, $content, $variable) = @_;
1014
1015   my $formatter =
1016        $formatters{ $self->{variable_content_types}->{$variable} }
1017     // $formatters{ $self->{default_content_type} }
1018     // $formatters{ text };
1019
1020   return $formatter->($self, $content, variable => $variable);
1021 }
1022
1023 sub get_mime_type() {
1024   my ($self) = @_;
1025
1026   if ($self->{"form"}->{"format"} =~ /pdf/) {
1027     return "application/pdf";
1028   } else {
1029     return "application/vnd.oasis.opendocument.text";
1030   }
1031 }
1032
1033 sub uses_temp_file {
1034   return 1;
1035 }
1036
1037 1;