1 package SL::Template::OpenDocument;
3 use parent qw(SL::Template::Simple);
13 use SL::Template::OpenDocument::Styles;
15 use SL::Helper::QrBill;
16 use SL::Helper::QrBillFunctions qw(
17 get_ref_number_formatted get_iban_formatted get_amount_formatted
18 get_street_name_from_address_line get_building_number_from_address_line
24 # use File::Temp qw(:mktemp);
29 my %text_markup_replace = (
39 my ($self, $content, %params) = @_;
41 $content = $::locale->quote_special_chars('Template/OpenDocument', $content);
43 # Allow some HTML markup to be converted into the output format's
44 # corresponding markup code, e.g. bold or italic.
45 foreach my $key (keys(%text_markup_replace)) {
46 my $value = $text_markup_replace{$key};
47 $content =~ s|\<${key}\>|<text:span text:style-name=\"TKIVITENDO${value}\">|gi; #"
48 $content =~ s|\</${key}\>|</text:span>|gi;
55 '</ul>' => '</text:list>',
56 '</ol>' => '</text:list>',
57 '</li>' => '</text:p></text:list-item>',
58 '<b>' => '<text:span text:style-name="TKIVITENDOBOLD">',
59 '</b>' => '</text:span>',
60 '<strong>' => '<text:span text:style-name="TKIVITENDOBOLD">',
61 '</strong>' => '</text:span>',
62 '<i>' => '<text:span text:style-name="TKIVITENDOITALIC">',
63 '</i>' => '</text:span>',
64 '<em>' => '<text:span text:style-name="TKIVITENDOITALIC">',
65 '</em>' => '</text:span>',
66 '<u>' => '<text:span text:style-name="TKIVITENDOUNDERLINE">',
67 '</u>' => '</text:span>',
68 '<s>' => '<text:span text:style-name="TKIVITENDOSTRIKETHROUGH">',
69 '</s>' => '</text:span>',
70 '<sub>' => '<text:span text:style-name="TKIVITENDOSUB">',
71 '</sub>' => '</text:span>',
72 '<sup>' => '<text:span text:style-name="TKIVITENDOSUPER">',
73 '</sup>' => '</text:span>',
74 '<br/>' => '<text:line-break/>',
75 '<br>' => '<text:line-break/>',
79 my ($self, $content, %params) = @_;
82 my $p_start_tag = qq|<text:p text:style-name="@{[ $self->{current_text_style} ]}">|;
86 my (@tags_to_open, @tags_to_close);
87 for (my $idx = scalar(@{ $self->{tag_stack} }) - 1; $idx >= 0; --$idx) {
88 my $tag = $self->{tag_stack}->[$idx];
90 next if $tag =~ m{/>$};
91 last if $tag =~ m{^<table};
93 if ($tag =~ m{^<text:p}) {
99 $suffix = "${tag}${suffix}";
101 $prefix .= '</' . substr($tag, 1);
105 $content =~ s{ ^<p> | </p>$ }{}gx if $in_p;
106 $content =~ s{ \r+ }{}gx;
107 $content =~ s{ \n+ }{ }gx;
108 $content =~ s{ (?:\ |\s)+ }{ }gx;
110 my $ul_start_tag = qq|<text:list xml:id="list@{[ int rand(9999999999999999) ]}" text:style-name="LKIVITENDOitemize@{[ $self->{current_text_style} ]}">|;
111 my $ol_start_tag = qq|<text:list xml:id="list@{[ int rand(9999999999999999) ]}" text:style-name="LKIVITENDOenumerate@{[ $self->{current_text_style} ]}">|;
112 my $ul_li_start_tag = qq|<text:list-item><text:p text:style-name="PKIVITENDOitemize@{[ $self->{current_text_style} ]}">|;
113 my $ol_li_start_tag = qq|<text:list-item><text:p text:style-name="PKIVITENDOenumerate@{[ $self->{current_text_style} ]}">|;
116 if (substr($_, 0, 1) eq '<') {
120 $in_p == 0 ? '</text:p>' : '';
122 } elsif ($_ eq '<p>') {
124 $in_p == 1 ? $p_start_tag : '';
126 } elsif ($_ eq '<ul>') {
127 $self->{used_list_styles}->{itemize}->{$self->{current_text_style}} = 1;
128 $html_replace{'<li>'} = $ul_li_start_tag;
131 } elsif ($_ eq '<ol>') {
132 $self->{used_list_styles}->{enumerate}->{$self->{current_text_style}} = 1;
133 $html_replace{'<li>'} = $ol_li_start_tag;
137 $html_replace{$_} || '';
141 $::locale->quote_special_chars('Template/OpenDocument', HTML::Entities::decode_entities($_));
143 } split(m{(<.*?>)}x, $content);
145 my $out = join('', $prefix, @parts, $suffix);
147 # $::lxdebug->dump(0, "prefix parts suffix", [ $prefix, join('', @parts), $suffix ]);
153 html => \&_format_html,
154 text => \&_format_text,
160 my $self = $type->SUPER::new(@_);
162 $self->set_tag_style('<%', '%>');
163 $self->{quot_re} = '"';
169 my ($self, $var, $text, $start_tag, $end_tag, @indices) = @_;
171 my ($form, $new_contents) = ($self->{"form"}, "");
173 my $ary = $self->_get_loop_variable($var, 1, @indices);
175 for (my $i = 0; $i < scalar(@{$ary || []}); $i++) {
176 $form->{"__first__"} = $i == 0;
177 $form->{"__last__"} = ($i + 1) == scalar(@{$ary});
178 $form->{"__odd__"} = (($i + 1) % 2) == 1;
179 $form->{"__counter__"} = $i + 1;
180 my $new_text = $self->parse_block($text, (@indices, $i));
181 return undef unless (defined($new_text));
182 $new_contents .= $start_tag . $new_text . $end_tag;
184 map({ delete($form->{"__${_}__"}); } qw(first last odd counter));
186 return $new_contents;
190 my ($self, $text, $pos, $var, $not) = @_;
193 $pos = 0 unless ($pos);
195 while ($pos < length($text)) {
198 next if (substr($text, $pos - 1, 5) ne '<%');
200 if ((substr($text, $pos + 4, 2) eq 'if') || (substr($text, $pos + 4, 3) eq 'for')) {
203 } elsif ((substr($text, $pos + 4, 4) eq 'else') && (1 == $depth)) {
205 $self->{error} = '<%else%> outside of <%if%> / <%ifnot%>.';
209 my $block = substr($text, 0, $pos - 1);
210 substr($text, 0, $pos - 1) = "";
211 $text =~ s!^\<\%[^\%]+\%\>!!;
212 $text = '<%if' . ($not ? " " : "not ") . $var . '%>' . $text;
214 return ($block, $text);
216 } elsif (substr($text, $pos + 4, 3) eq 'end') {
219 my $block = substr($text, 0, $pos - 1);
220 substr($text, 0, $pos - 1) = "";
221 $text =~ s!^\<\%[^\%]+\%\>!!;
223 return ($block, $text);
232 $main::lxdebug->enter_sub();
234 my ($self, $contents, @indices) = @_;
236 my $new_contents = "";
238 while ($contents ne "") {
239 if (substr($contents, 0, 1) eq "<") {
240 $contents =~ m|^(<[^>]+>)|;
242 substr($contents, 0, length($1)) = "";
244 $self->{current_text_style} = $1 if $tag =~ m|text:style-name\s*=\s*"([^"]+)"|;
246 push @{ $self->{tag_stack} }, $tag;
248 if ($tag =~ m|<table:table-row|) {
249 $contents =~ m|^(.*?)(</table:table-row[^>]*>)|;
253 if ($table_row =~ m|\<\%foreachrow\s+(.*?)\%\>|) {
256 $contents =~ m|^(.*?)(\<\%foreachrow\s+.*?\%\>)|;
257 substr($contents, length($1), length($2)) = "";
259 ($table_row, $contents) = $self->find_end($contents, length($1));
261 $self->{error} = "Unclosed <\%foreachrow\%>." unless ($self->{"error"});
262 $main::lxdebug->leave_sub();
266 $contents =~ m|^(.*?)(</table:table-row[^>]*>)|;
270 substr $contents, 0, length($1) + length($2), '';
272 my $new_text = $self->parse_foreach($var, $table_row, $tag, $end_tag, @indices);
273 if (!defined($new_text)) {
274 $main::lxdebug->leave_sub();
277 $new_contents .= $new_text;
280 substr($contents, 0, length($table_row) + length($end_tag)) = "";
281 my $new_text = $self->parse_block($table_row, @indices);
282 if (!defined($new_text)) {
283 $main::lxdebug->leave_sub();
286 $new_contents .= $tag . $new_text . $end_tag;
290 $new_contents .= $tag;
293 if ($tag =~ m{^</ | />$}x) {
294 # $::lxdebug->message(0, "popping top tag is $tag top " . $self->{tag_stack}->[-1]);
295 pop @{ $self->{tag_stack} };
299 $contents =~ /^([^<]+)/;
302 my $pos_if = index($text, '<%if');
303 my $pos_foreach = index($text, '<%foreach');
305 if ((-1 == $pos_if) && (-1 == $pos_foreach)) {
306 substr($contents, 0, length($text)) = "";
307 $new_contents .= $self->substitute_vars($text, @indices);
311 if ((-1 == $pos_if) || ((-1 != $pos_foreach) && ($pos_if > $pos_foreach))) {
312 $new_contents .= $self->substitute_vars(substr($contents, 0, $pos_foreach), @indices);
313 substr($contents, 0, $pos_foreach) = "";
315 if ($contents !~ m|^(\<\%foreach (.*?)\%\>)|) {
316 $self->{error} = "Malformed <\%foreach\%>.";
317 $main::lxdebug->leave_sub();
323 substr($contents, 0, length($1)) = "";
326 ($block, $contents) = $self->find_end($contents);
328 $self->{error} = "Unclosed <\%foreach\%>." unless ($self->{error});
329 $main::lxdebug->leave_sub();
333 my $new_text = $self->parse_foreach($var, $block, "", "", @indices);
334 if (!defined($new_text)) {
335 $main::lxdebug->leave_sub();
338 $new_contents .= $new_text;
341 if (!$self->_parse_block_if(\$contents, \$new_contents, $pos_if, @indices)) {
342 $main::lxdebug->leave_sub();
349 $main::lxdebug->leave_sub();
351 return $new_contents;
355 $main::lxdebug->enter_sub();
359 my $form = $self->{form};
363 my $is_qr_bill = $::instance_conf->get_create_qrbill_invoices &&
364 ($form->{formname} eq 'invoice' ||
365 $form->{formname} eq 'invoice_for_advance_payment') &&
366 $form->{template_meta}->{printer}->{template_code} =~ m/qr/ ?
371 # the biller account information, biller address and the reference number,
372 # are needed in the template aswell as in the qr-code generation, therefore
373 # assemble these and add to $::form
374 $qr_image_path = $self->generate_qr_code;
378 if ($form->{IN} =~ m|^/|) {
379 $file_name = $form->{IN};
381 $file_name = $form->{templates} . "/" . $form->{IN};
384 my $zip = Archive::Zip->new();
385 if (Archive::Zip->AZ_OK != $zip->read($file_name)) {
386 $self->{error} = "File not found/is not a OpenDocument file.";
387 $main::lxdebug->leave_sub();
391 my $contents = Encode::decode('utf-8-strict', $zip->contents("content.xml"));
393 $self->{error} = "File is not a OpenDocument file.";
394 $main::lxdebug->leave_sub();
398 $self->{current_text_style} = '';
399 $self->{used_list_styles} = {
405 if ($self->{use_template_toolkit}) {
406 my $additional_params = $::form;
408 $::form->template->process(\$contents, $additional_params, \$new_contents) || die $::form->template->error;
410 $self->{tag_stack} = [];
411 $new_contents = $self->parse_block($contents);
413 if (!defined($new_contents)) {
414 $main::lxdebug->leave_sub();
418 my $new_styles = SL::Template::OpenDocument::Styles->get_style('text_basic');
420 foreach my $type (qw(itemize enumerate)) {
421 foreach my $parent (sort { $a cmp $b } keys %{ $self->{used_list_styles}->{$type} }) {
422 $new_styles .= SL::Template::OpenDocument::Styles->get_style('text_list_item', TYPE => $type, PARENT => $parent)
423 . SL::Template::OpenDocument::Styles->get_style("list_${type}", TYPE => $type, PARENT => $parent);
427 # $::lxdebug->dump(0, "new_Styles", $new_styles);
429 $new_contents =~ s|</office:automatic-styles>|${new_styles}</office:automatic-styles>|;
430 $new_contents =~ s|[\n\r]||gm;
432 # $new_contents =~ s|>|>\n|g;
434 $zip->contents("content.xml", Encode::encode('utf-8-strict', $new_contents));
436 my $styles = Encode::decode('utf-8-strict', $zip->contents("styles.xml"));
438 my $new_styles = $self->parse_block($styles);
439 if (!defined($new_contents)) {
440 $main::lxdebug->leave_sub();
443 $zip->contents("styles.xml", Encode::encode('utf-8-strict', $new_styles));
447 # get placeholder path from odt XML
448 my $qr_placeholder_path;
449 my $dom = XML::LibXML->load_xml(string => $contents);
450 my @nodelist = $dom->getElementsByTagName("draw:frame");
451 for my $node (@nodelist) {
452 my $attr = $node->getAttribute('draw:name');
453 if ($attr eq 'QRCodePlaceholder') {
454 my @children = $node->getChildrenByTagName('draw:image');
455 $qr_placeholder_path = $children[0]->getAttribute('xlink:href');
458 if (!defined($qr_placeholder_path)) {
459 $::form->error($::locale->text('QR-Code placeholder image: QRCodePlaceholder not found in template.'));
461 # replace QR-Code Placeholder Image in zip file (odt) with generated one
463 $qr_placeholder_path,
468 $zip->writeToFileNamed($form->{tmpfile}, 1);
471 if ($form->{format} =~ /pdf/) {
472 $res = $self->convert_to_pdf();
475 $main::lxdebug->leave_sub();
479 sub generate_qr_code {
480 $main::lxdebug->enter_sub();
482 my $form = $self->{form};
484 # assemble data for QR-Code
486 if (!$form->{qrbill_iban}) {
487 $::form->error($::locale->text('No bank account flagged for QRBill usage was found.'));
490 my %biller_information = (
491 iban => $form->{qrbill_iban}
494 if (!$form->{qrbill_biller_countrycode}) {
495 $::form->error($::locale->text('Error mapping biller countrycode.'));
499 company => $::instance_conf->get_company(),
500 street => get_street_name_from_address_line($::instance_conf->get_address_street1()),
501 street_no => get_building_number_from_address_line($::instance_conf->get_address_street1()),
502 postalcode => $::instance_conf->get_address_zipcode(),
503 city => $::instance_conf->get_address_city(),
504 countrycode => $form->{qrbill_biller_countrycode},
507 my ($amount, $amount_formatted);
508 if ($form->{qrbill_without_amount}) {
510 $amount_formatted = '';
512 $amount = $form->{qrbill_amount};
514 # format amount for template
515 $amount_formatted = get_amount_formatted($amount);
516 if (!$amount_formatted) {
517 $::form->error($::locale->text('Amount has wrong format.'));
521 my %payment_information = (
523 currency => $form->{currency},
526 # get address data from billing address if given, otherwise from invoice
527 my $street_name = get_street_name_from_address_line($form->{billing_address_id} ?
528 $form->{billing_address_street} :
530 my $building_number = get_building_number_from_address_line($form->{billing_address_id} ?
531 $form->{billing_address_street} :
533 my $postalcode = $form->{billing_address_id} ?
534 $form->{billing_address_zipcode} :
536 my $city = $form->{billing_address_id} ?
537 $form->{billing_address_city} :
540 # validate address data
541 if ($postalcode !~ m/^\d{4,}$/) {
542 $::form->error($::locale->text('Zipcode missing or wrong format.'));
545 $::form->error($::locale->text('No city given.'));
547 if (!$form->{qrbill_customer_countrycode}) {
548 $::form->error($::locale->text('Error mapping customer countrycode.'));
551 my %invoice_recipient_data = (
553 name => $form->{billing_address_id} ?
554 $form->{billing_address_name} :
556 street => $street_name,
557 street_no => $building_number,
558 postalcode => $postalcode,
560 countrycode => $form->{qrbill_customer_countrycode},
564 if ($::instance_conf->get_create_qrbill_invoices == 1) {
565 # fill reference number with zeros when printing preview (before booking)
566 my $reference_number = $form->{id} ? $form->{qr_reference} : '0' x 27;
570 ref_number => $reference_number,
572 # get ref. number/iban formatted with spaces and set into form for template
574 $form->{ref_number} = $reference_number;
575 $form->{ref_number_formatted} = get_ref_number_formatted($reference_number);
576 } elsif ($::instance_conf->get_create_qrbill_invoices == 2) {
582 $::form->error($::locale->text('Error getting QR-Bill type.'));
585 my %additional_information = (
586 unstructured_message => $form->{qr_unstructured_message}
589 # set into form for template processing
590 $form->{biller_information} = \%biller_information;
591 $form->{biller_data} = \%biller_data;
592 $form->{iban_formatted} = get_iban_formatted($form->{qrbill_iban});
593 $form->{amount_formatted} = $amount_formatted;
594 $form->{unstructured_message} = $form->{qr_unstructured_message};
597 my $outfile = $form->{tmpdir} . '/' . 'qr-code.png';
599 # generate QR-Code Image
601 my $qr_image = SL::Helper::QrBill->new(
602 \%biller_information,
604 \%payment_information,
605 \%invoice_recipient_data,
607 \%additional_information
609 $qr_image->generate($outfile);
611 local $_ = $@; chomp; my $error = $_;
612 $::form->error($::locale->text('QR-Image generation failed: ' . $error));
615 $main::lxdebug->leave_sub();
619 sub _run_python_uno {
620 my ($self, @args) = @_;
622 local $ENV{PYTHONPATH};
623 $ENV{PYTHONPATH} = $::lx_office_conf{environment}->{python_uno_path} . ':' . $ENV{PYTHONPATH} if $::lx_office_conf{environment}->{python_uno_path};
624 my $cmd = $::lx_office_conf{applications}->{python_uno} . ' ' . join(' ', @args);
628 sub is_openoffice_running {
631 $main::lxdebug->enter_sub();
633 my $output = $self->_run_python_uno('./scripts/oo-uno-test-conn.py', $::lx_office_conf{print_templates}->{openofficeorg_daemon_port}, ' 2> /dev/null');
636 my $res = ($? == 0) || $output;
637 $main::lxdebug->message(LXDebug->DEBUG2(), " is_openoffice_running(): res $res\n");
639 $main::lxdebug->leave_sub();
644 sub spawn_openoffice {
645 $main::lxdebug->enter_sub();
649 $main::lxdebug->message(LXDebug->DEBUG2(), "spawn_openoffice()\n");
651 my ($try, $spawned_oo, $res);
654 for ($try = 0; $try < 15; $try++) {
655 if ($self->is_openoffice_running()) {
660 if ($::dispatcher->interface_type eq 'FastCGI') {
661 $::dispatcher->{request}->Detach;
667 $main::lxdebug->message(LXDebug->DEBUG2(), " Child daemonizing\n");
669 if ($::dispatcher->interface_type eq 'FastCGI') {
670 $::dispatcher->{request}->Finish;
671 $::dispatcher->{request}->LastCall;
674 open(STDIN, '/dev/null');
675 open(STDOUT, '>/dev/null');
676 my $new_pid = fork();
678 my $ssres = setsid();
679 $main::lxdebug->message(LXDebug->DEBUG2(), " Child execing\n");
680 my @cmdline = ($::lx_office_conf{applications}->{openofficeorg_writer},
681 "--minimized", "--norestore", "--nologo", "--nolockcheck",
683 "--accept=socket,host=localhost,port=" .
684 $::lx_office_conf{print_templates}->{openofficeorg_daemon_port} . ";urp;");
688 if ($::dispatcher->interface_type eq 'FastCGI') {
689 $::dispatcher->{request}->Attach;
693 $main::lxdebug->message(LXDebug->DEBUG2(), " Parent after fork\n");
698 sleep($try >= 5 ? 2 : 1);
702 $self->{error} = "Conversion from OpenDocument to PDF failed because " .
703 "OpenOffice could not be started.";
706 $main::lxdebug->leave_sub();
712 $main::lxdebug->enter_sub();
716 my $form = $self->{form};
718 my $filename = $form->{tmpfile};
719 $filename =~ s/.odt$//;
720 if (substr($filename, 0, 1) ne "/") {
721 $filename = getcwd() . "/${filename}";
724 if (substr($self->{userspath}, 0, 1) eq "/") {
725 $ENV{HOME} = $self->{userspath};
727 $ENV{HOME} = getcwd() . "/" . $self->{userspath};
730 my $outdir = dirname($filename);
732 if (!$::lx_office_conf{print_templates}->{openofficeorg_daemon}) {
733 if (system($::lx_office_conf{applications}->{openofficeorg_writer},
734 "--minimized", "--norestore", "--nologo", "--nolockcheck", "--headless",
735 "--convert-to", "pdf", "--outdir", $outdir,
736 "file:${filename}.odt") == -1) {
737 die "system call to $::lx_office_conf{applications}->{openofficeorg_writer} failed: $!";
740 if (!$self->spawn_openoffice()) {
741 $main::lxdebug->leave_sub();
745 $self->_run_python_uno('./scripts/oo-uno-convert-pdf.py', $::lx_office_conf{print_templates}->{openofficeorg_daemon_port}, "${filename}.odt");
749 if ((0 == $?) || (-f "${filename}.pdf" && -s "${filename}.pdf")) {
750 $form->{tmpfile} =~ s/odt$/pdf/;
752 unlink($filename . ".odt");
754 $main::lxdebug->leave_sub();
759 unlink($filename . ".odt", $filename . ".pdf");
760 $self->{error} = "Conversion from OpenDocument to PDF failed. " .
763 $main::lxdebug->leave_sub();
768 my ($self, $content, $variable) = @_;
771 $formatters{ $self->{variable_content_types}->{$variable} }
772 // $formatters{ $self->{default_content_type} }
773 // $formatters{ text };
775 return $formatter->($self, $content, variable => $variable);
778 sub get_mime_type() {
781 if ($self->{form}->{format} =~ /pdf/) {
782 return "application/pdf";
784 return "application/vnd.oasis.opendocument.text";