]> wagnertech.de Git - mfinanz.git/blob - SL/Controller/ZUGFeRD.pm
restart apache2 in postinst
[mfinanz.git] / SL / Controller / ZUGFeRD.pm
1 package SL::Controller::ZUGFeRD;
2 use strict;
3 use warnings;
4 use parent qw(SL::Controller::Base);
5
6 use SL::DB::RecordTemplate;
7 use SL::Locale::String qw(t8);
8 use SL::Helper::DateTime;
9 use SL::XMLInvoice;
10 use SL::VATIDNr;
11 use SL::ZUGFeRD;
12 use SL::SessionFile;
13
14 use XML::LibXML;
15 use List::Util qw(first);
16
17
18 __PACKAGE__->run_before('check_auth');
19
20 sub action_upload_zugferd {
21   my ($self, %params) = @_;
22
23   $self->pre_render();
24   $self->render('zugferd/form', title => $::locale->text('Factur-X/ZUGFeRD import'));
25 }
26
27 sub find_vendor_by_taxnumber {
28   my $taxnumber = shift @_;
29
30   # 1.1 check if we a have a vendor with this tax number (vendor.taxnumber)
31   my $vendor = SL::DB::Manager::Vendor->find_by(
32     taxnumber => $taxnumber,
33     or    => [
34       obsolete => undef,
35       obsolete => 0,
36     ]);
37
38   if (!$vendor) {
39     # 1.2 If no vendor with the exact VAT ID number is found, the
40     # number might be stored slightly different in the database
41     # (e.g. with spaces breaking up groups of numbers). Iterate over
42     # all existing vendors with VAT ID numbers, normalize their
43     # representation and compare those.
44
45     my $vendors = SL::DB::Manager::Vendor->get_all(
46       where => [
47         '!taxnumber' => undef,
48         '!taxnumber' => '',
49         or       => [
50           obsolete => undef,
51           obsolete => 0,
52         ],
53       ]);
54
55     foreach my $other_vendor (@{ $vendors }) {
56       next unless $other_vendor->taxnumber eq $taxnumber;
57
58       $vendor = $other_vendor;
59       last;
60     }
61   }
62 }
63
64 sub find_vendor_by_ustid {
65   my $ustid = shift @_;
66
67   $ustid = SL::VATIDNr->normalize($ustid);
68
69   # 1.1 check if we a have a vendor with this VAT-ID (vendor.ustid)
70   my $vendor = SL::DB::Manager::Vendor->find_by(
71     ustid => $ustid,
72     or    => [
73       obsolete => undef,
74       obsolete => 0,
75     ]);
76
77   if (!$vendor) {
78     # 1.2 If no vendor with the exact VAT ID number is found, the
79     # number might be stored slightly different in the database
80     # (e.g. with spaces breaking up groups of numbers). Iterate over
81     # all existing vendors with VAT ID numbers, normalize their
82     # representation and compare those.
83
84     my $vendors = SL::DB::Manager::Vendor->get_all(
85       where => [
86         '!ustid' => undef,
87         '!ustid' => '',
88         or       => [
89           obsolete => undef,
90           obsolete => 0,
91         ],
92       ]);
93
94     foreach my $other_vendor (@{ $vendors }) {
95       next unless SL::VATIDNr->normalize($other_vendor->ustid) eq $ustid;
96
97       $vendor = $other_vendor;
98       last;
99     }
100   }
101
102   return $vendor;
103 }
104
105 sub find_vendor {
106   my ($ustid, $taxnumber) = @_;
107   my $vendor;
108
109   if ( $ustid ) {
110     $vendor = find_vendor_by_ustid($ustid);
111   }
112
113   if (ref $vendor eq 'SL::DB::Vendor') { return $vendor; }
114
115   if ( $taxnumber ) {
116     $vendor = find_vendor_by_taxnumber($taxnumber);
117   }
118
119   if (ref $vendor eq 'SL::DB::Vendor') { return $vendor; }
120
121   return undef;
122 }
123
124 sub action_import_zugferd {
125   my ($self, %params) = @_;
126
127   my $file = $::form->{file};
128   my $file_name = $::form->{file_name};
129
130   my %res; # result data structure returned by SL::ZUGFeRD->extract_from_{pdf,xml}()
131
132   die t8("missing file for action import")   unless $file;
133   die t8("can only parse a pdf or xml file") unless $file =~ m/^%PDF|<\?xml/;
134
135   if ( $::form->{file} =~ m/^%PDF/ ) {
136     %res = %{SL::ZUGFeRD->extract_from_pdf($file)};
137   } else {
138     %res = %{SL::ZUGFeRD->extract_from_xml($file)};
139   }
140
141   if ($res{'result'} != SL::ZUGFeRD::RES_OK()) {
142     # An error occurred; log message from parser:
143     die(t8("Could not extract Factur-X/ZUGFeRD data, data and error message:") . " $res{'message'}");
144   }
145
146   my $form_defaults = $self->build_ap_transaction_form_defaults(\%res);
147
148   # save the zugferd file to session file for reuse in ap.pl
149   my $session_file = SL::SessionFile->new($file_name, mode => 'w');
150   $session_file->fh->print($file);
151   $session_file->fh->close;
152   $form_defaults->{zugferd_session_file} = $file_name;
153
154   $form_defaults->{callback} = $self->url_for(action => 'upload_zugferd');
155
156   $self->redirect_to(
157     controller    => 'ap.pl',
158     action        => 'load_zugferd',
159     form_defaults => $form_defaults,
160   );
161 }
162
163 sub build_ap_transaction_form_defaults {
164   my ($self, $data, %params) = @_;
165   my $vendor = $params{vendor};
166
167   my $parser = $data->{'invoice_xml'};
168
169   my %metadata = %{$parser->metadata};
170   my @items = @{$parser->items};
171
172   my $intnotes = t8("ZUGFeRD Import. Type: #1", $metadata{'type'})->translated;
173   my $iban = $metadata{'iban'};
174   my $invnumber = $metadata{'invnumber'};
175
176   if ($vendor) {
177     if ($metadata{'ustid'} && $vendor->ustid && ($metadata{'ustid'} ne $vendor->ustid)) {
178       $intnotes .= "\n" . t8('USt-IdNr.') . ': '
179       . t8("Record VAT ID #1 doesn't match vendor VAT ID #2", $metadata{'ustid'}, $vendor->ustid);
180     }
181     if ($metadata{'taxnumber'} && $vendor->taxnumber && ($metadata{'taxnumber'} ne $vendor->taxnumber)) {
182       $intnotes .= "\n" . t8("Tax Number") . ': '
183       . t8("Record tax ID #1 doesn't match vendor tax ID #2", $metadata{'taxnumber'}, $vendor->taxnumber);
184     }
185   } else {
186     if ( ! ($metadata{'ustid'} or $metadata{'taxnumber'}) ) {
187       die t8("Cannot process this invoice: neither VAT ID nor tax ID present.");
188     }
189
190     $vendor = find_vendor($metadata{'ustid'}, $metadata{'taxnumber'});
191
192     die t8("Vendor with VAT ID (#1) and/or tax ID (#2) not found. Please check if the vendor " .
193             "#3 exists and whether it has the correct tax ID/VAT ID." ,
194              $metadata{'ustid'},
195              $metadata{'taxnumber'},
196              $metadata{'vendor_name'},
197     ) unless $vendor;
198   }
199
200
201   # Check IBAN specified on bill matches the one we've got in
202   # the database for this vendor.
203   if ($iban) {
204     $intnotes .= "\nIBAN: ";
205     $intnotes .= $iban ne $vendor->iban ?
206           t8("Record IBAN #1 doesn't match vendor IBAN #2", $iban, $vendor->iban)
207         : $iban
208   }
209
210   # Use invoice creation date as due date if there's no due date
211   $metadata{'duedate'} = $metadata{'transdate'} unless defined $metadata{'duedate'};
212
213   # parse dates to kivi if set/valid
214   foreach my $key ( qw(transdate duedate) ) {
215     next unless defined $metadata{$key};
216     $metadata{$key} =~ s/^\s+|\s+$//g;
217
218     if ($metadata{$key} =~ /^([0-9]{4})-?([0-9]{2})-?([0-9]{2})$/) {
219     $metadata{$key} = DateTime->new(year  => $1,
220                                     month => $2,
221                                     day   => $3)->to_kivitendo;
222     }
223   }
224
225   # Try to fill in AP account to book against
226   my $ap_chart_id = $::instance_conf->get_ap_chart_id;
227
228   unless ( defined $ap_chart_id ) {
229     # If no default account is configured, just use the first AP account found.
230     my ($ap_chart) = @{SL::DB::Manager::Chart->get_all(
231       where   => [ link => 'AP' ],
232       sort_by => [ 'accno' ],
233     )};
234     $ap_chart_id = $ap_chart->id;
235   }
236
237   my $currency = SL::DB::Manager::Currency->find_by(
238     name => $metadata{'currency'},
239     );
240
241   my $default_ap_amount_chart = SL::DB::Manager::Chart->find_by(
242     id => $::instance_conf->get_expense_accno_id
243   );
244   # Fallback if there's no default AP amount chart configured
245   $default_ap_amount_chart ||= SL::DB::Manager::Chart->find_by(charttype => 'A');
246
247   my $active_taxkey = $default_ap_amount_chart->get_active_taxkey;
248   my $taxes = SL::DB::Manager::Tax->get_all(
249     where   => [ chart_categories => {
250         like => '%' . $default_ap_amount_chart->category . '%'
251       }],
252     sort_by => 'taxkey, rate',
253   );
254   die t8(
255     "No tax found for chart #1", $default_ap_amount_chart->displayable_name
256   ) unless scalar @{$taxes};
257
258   # parse items
259   my $row = 0;
260   my %item_form = ();
261   foreach my $i (@items) {
262     $row++;
263
264     my %item = %{$i};
265
266     my $net_total = $::form->format_amount(\%::myconfig, $item{'subtotal'}, 2);
267
268     my $tax_rate = $item{'tax_rate'};
269     $tax_rate /= 100 if $tax_rate > 1; # XML data is usually in percent
270
271     my $tax   = first { $tax_rate              == $_->rate } @{ $taxes };
272     $tax    //= first { $active_taxkey->tax_id == $_->id }   @{ $taxes };
273     $tax    //= $taxes->[0];
274
275     $item_form{"AP_amount_chart_id_${row}"}          = $default_ap_amount_chart->id;
276     $item_form{"previous_AP_amount_chart_id_${row}"} = $default_ap_amount_chart->id;
277     $item_form{"amount_${row}"}                      = $net_total;
278     $item_form{"taxchart_${row}"}                    = $tax->id . '--' . $tax->rate;
279   }
280   $item_form{rowcount} = $row;
281
282   return {
283     vendor_id            => $vendor->id,
284     vendor               => $vendor->name,
285     invnumber            => $invnumber,
286     transdate            => $metadata{'transdate'},
287     duedate              => $metadata{'duedate'},
288     no_payment_bookings  => 0,
289     intnotes             => $intnotes,
290     taxincluded          => 0,
291     direct_debit         => $metadata{'direct_debit'},
292     currency             => $currency->name,
293     AP_chart_id          => $ap_chart_id,
294     paid_1_suggestion    => $::form->format_amount(\%::myconfig, $metadata{'total'}, 2),
295     %item_form,
296   },
297 }
298
299 sub check_auth {
300   $::auth->assert('ap_transactions');
301 }
302 sub setup_zugferd_action_bar {
303   my ($self) = @_;
304
305   for my $bar ($::request->layout->get('actionbar')) {
306     $bar->add(
307       action => [
308         $::locale->text('Import'),
309         submit    => [ '#form', { action => 'ZUGFeRD/import_zugferd' } ],
310         accesskey => 'enter',
311       ],
312     );
313   }
314 }
315
316 sub pre_render {
317   my ($self) = @_;
318
319   $::request->{layout}->use_javascript("${_}.js") for qw(
320     kivi.ZUGFeRD
321   );
322
323   $self->setup_zugferd_action_bar;
324 }
325
326
327 1;
328 __END__
329
330 =pod
331
332 =encoding utf8
333
334 =head1 NAME
335
336 SL::Controller::ZUGFeRD - Controller for importing ZUGFeRD PDF files or XML invoices to kivitendo
337
338 =head1 FUNCTIONS
339
340 =over 4
341
342 =item C<action_upload_zugferd>
343
344 Creates a web from with a single upload dialog.
345
346 =item C<action_import_zugferd $file>
347
348 Expects a single PDF with ZUGFeRD, Factur-X or XRechnung
349 metadata. Alternatively, it can also process said data as a
350 standalone XML file.
351
352 Checks if the param <C$pdf> is set and a valid PDF or XML
353 file. Calls helper functions to validate and extract the
354 ZUGFeRD/Factur-X/XRechnung data. The invoice needs to have a
355 valid VAT ID (EU) or tax number (Germany) and a vendor with
356 the same VAT ID or tax number enrolled in Kivitendo.
357
358 It parses some basic ZUGFeRD data (invnumber, total net amount,
359 transdate, duedate, vendor VAT ID, IBAN, etc.) and also
360 extracts the invoice's items.
361
362 If the invoice has a IBAN also, it will be be compared to the
363 IBAN saved for the vendor (if any). If they  don't match a
364 warning will be writte in ap.intnotes. Furthermore the ZUGFeRD
365 type code will be written to ap.intnotes. No callback
366 implemented.
367
368 =back
369
370 =head1 CAVEAT
371
372 This is just a very basic Parser for ZUGFeRD/Factur-X/XRechnung invoices.
373 We assume that the invoice's creator is a company with a valid
374 European VAT ID or German tax number and enrolled in
375 Kivitendo.
376
377 =head1 TODO
378
379 This implementation could be improved as follows:
380
381 =over 4
382
383 =item Automatic upload of invoice
384
385 Right now, one has to use the "Book and upload" button to
386 upload the raw invoice document to WebDAV or DMS and attach it
387 to the invoice. This should be a simple matter of setting a
388 check box when uploading.
389
390 =item Handling of vendor invoices
391
392 There is no reason this functionality could not be used to
393 import vendor invoices as well. Since these tend to be very
394 lengthy, the ability to import them would be very beneficial.
395
396 =item Automatic handling of payment purpose
397
398 If the ZUGFeRD data has a payment purpose set, this should
399 be the default for the SEPA-XML export.
400
401 =back
402
403 =head1 AUTHORS
404
405 =over 4
406
407 =item Jan Büren E<lt>jan@kivitendo-premium.deE<gt>,
408
409 =item Johannes Graßler E<lt>info@computer-grassler.deE<gt>,
410
411 =back
412
413 =cut