Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision d472ce56

Von Johannes Grassler vor etwa 1 Jahr hinzugefügt

  • ID d472ce560fa4732ee8b16213a7bdb614703551aa
  • Vorgänger 34e66c3e
  • Nachfolger bb889114

ZUGFeRD-Import auf SL::XMLInvoice umgestellt

Wichtigste Aenderung dieses Commits ist die Umstellung des
ZUGFeRD-Imports in der Finanzbuchhaltung auf das neu
hinzugefuegte Modul SL::XMLInvoice, das auch die Verabeitung
von Rechnungen im XRechnung-Format erlaubt. Darueber hinaus
gibt es einige weitere Aenderungen:

  • Datenformate: Neben ZUGFeRD wird nun auch XRechnung
    unterstuetzt.
  • Fehlertoleranz: Fehlende ZUGFeRD-Metadaten erzeugen nur noch
    Warnungen, saemtliche XML-Anhaenge an PDF-Dateien werden
    automatisch erkannt und verarbeitet (egal ob es sich um
    ZUGFeRD/Faktur-X- oder XRechnung-Daten handelt).
  • Upload von reinen XML-Dateien ist nun auch moeglich (fuer
    Rechnungen im XRechnung-Format wichtig).
  • Die Posten der Rechnung werden nun auch automatisch in das
    Formular eingetragen, nicht mehr nur Rechnungsnummer und
    Datum.
  • Es muss keine Belegvorlage mehr fuer den Lieferanten
    existieren. Eine eigene Belegvorlage wird fuer jede
    importierte Rechnung automatisch angelegt.
  • Es wird automatisch ein Gegenkonto fuer die Buchung
    aus den Verbindlichkeitskonten ausgewaehlt.
  • Das Faelligkeitsdatum der Rechnung wird immer gesetzt.
    Enthaelt die Rechnung keines, wird es auf das Rechnungsdatum
    gesetzt.

Unterschiede anzeigen:

SL/Controller/ZUGFeRD.pm
1 1
package SL::Controller::ZUGFeRD;
2 2
use strict;
3
use warnings;
3 4
use parent qw(SL::Controller::Base);
4 5

  
5 6
use SL::DB::RecordTemplate;
6 7
use SL::Locale::String qw(t8);
7 8
use SL::Helper::DateTime;
9
use SL::XMLInvoice;
8 10
use SL::VATIDNr;
9 11
use SL::ZUGFeRD;
10 12
use SL::SessionFile;
......
21 23
  $self->render('zugferd/form', title => $::locale->text('Factur-X/ZUGFeRD import'));
22 24
}
23 25

  
24
sub action_import_zugferd {
25
  my ($self, %params) = @_;
26
  my $file = $::form->{file};
27
  my $file_name = $::form->{file_name};
26
sub find_vendor_by_taxnumber {
27
  my $taxnumber = shift @_;
28 28

  
29
  die t8("missing file for action import") unless $file;
30
  die t8("can only parse a pdf file")      unless $file =~ m/^%PDF/;
29
  # 1.1 check if we a have a vendor with this tax number (vendor.taxnumber)
30
  my $vendor = SL::DB::Manager::Vendor->find_by(
31
    taxnumber => $taxnumber,
32
    or    => [
33
      obsolete => undef,
34
      obsolete => 0,
35
    ]);
36

  
37
  if (!$vendor) {
38
    # 1.2 If no vendor with the exact VAT ID number is found, the
39
    # number might be stored slightly different in the database
40
    # (e.g. with spaces breaking up groups of numbers). Iterate over
41
    # all existing vendors with VAT ID numbers, normalize their
42
    # representation and compare those.
31 43

  
32
  my $info = SL::ZUGFeRD->extract_from_pdf($file);
44
    my $vendors = SL::DB::Manager::Vendor->get_all(
45
      where => [
46
        '!taxnumber' => undef,
47
        '!taxnumber' => '',
48
        or       => [
49
          obsolete => undef,
50
          obsolete => 0,
51
        ],
52
      ]);
33 53

  
34
  if ($info->{result} != SL::ZUGFeRD::RES_OK()) {
35
    # An error occurred; log message from parser:
36
    $::lxdebug->message(LXDebug::DEBUG1(), "Could not extract ZUGFeRD data, error message: " . $info->{message});
37
    die t8("Could not extract Factur-X/ZUGFeRD data, data and error message:") . $info->{message};
54
    foreach my $other_vendor (@{ $vendors }) {
55
      next unless $other_vendor->taxnumber eq $taxnumber;
56

  
57
      $vendor = $other_vendor;
58
      last;
59
    }
38 60
  }
39
  # valid ZUGFeRD metadata
40
  my $dom   = XML::LibXML->load_xml(string => $info->{invoice_xml});
61
}
41 62

  
42
  # 1. check if ZUGFeRD SellerTradeParty has a VAT-ID
43
  my $ustid = $dom->findnodes('//ram:SellerTradeParty/ram:SpecifiedTaxRegistration')->string_value;
44
  die t8("No VAT Info for this Factur-X/ZUGFeRD invoice," .
45
         " please ask your vendor to add this for his Factur-X/ZUGFeRD data.") unless $ustid;
63
sub find_vendor_by_ustid {
64
  my $ustid = shift @_;
46 65

  
47 66
  $ustid = SL::VATIDNr->normalize($ustid);
48 67

  
49 68
  # 1.1 check if we a have a vendor with this VAT-ID (vendor.ustid)
50
  my $vc     = $dom->findnodes('//ram:SellerTradeParty/ram:Name')->string_value;
51 69
  my $vendor = SL::DB::Manager::Vendor->find_by(
52 70
    ustid => $ustid,
53 71
    or    => [
......
80 98
    }
81 99
  }
82 100

  
83
  die t8("Please add a valid VAT-ID for this vendor: #1", $vc) unless (ref $vendor eq 'SL::DB::Vendor');
101
  return $vendor;
102
}
84 103

  
85
  # 2. check if we have a ap record template for this vendor (TODO only the oldest template is choosen)
86
  my $template_ap = SL::DB::Manager::RecordTemplate->get_first(where => [vendor_id => $vendor->id]);
87
  die t8("No AP Record Template for this vendor found, please add one") unless (ref $template_ap eq 'SL::DB::RecordTemplate');
104
sub find_vendor {
105
  my ($ustid, $taxnumber) = @_;
106
  my $vendor;
88 107

  
108
  if ( $ustid ) {
109
    $vendor = find_vendor_by_ustid($ustid);
110
  }
89 111

  
90
  # 3. parse the zugferd data and fill the ap record template
91
  # -> no need to check sign (credit notes will be negative) just record thei ZUGFeRD type in ap.notes
92
  # -> check direct debit (defaults to no)
93
  # -> set amount (net amount) and unset taxincluded
94
  #    (template and user cares for tax and if there is more than one booking accno)
95
  # -> date (can be empty)
96
  # -> duedate (may be empty)
97
  # -> compare record iban and generate a warning if this differs from vendor's master data iban
98
  my $total     = $dom->findnodes('//ram:SpecifiedTradeSettlementHeaderMonetarySummation' .
99
                                  '/ram:TaxBasisTotalAmount')->string_value;
112
  if (ref $vendor eq 'SL::DB::Vendor') { return $vendor; }
100 113

  
101
  my $invnumber = $dom->findnodes('//rsm:ExchangedDocument/ram:ID')->string_value;
114
  if ( $taxnumber ) {
115
    $vendor = find_vendor_by_taxnumber($taxnumber);
116
  }
102 117

  
103
  # parse dates to kivi if set/valid
104
  my ($transdate, $duedate, $dt_to_kivi, $due_dt_to_kivi);
105
  $transdate = $dom->findnodes('//ram:IssueDateTime')->string_value;
106
  $duedate   = $dom->findnodes('//ram:DueDateDateTime')->string_value;
107
  $transdate =~ s/^\s+|\s+$//g;
108
  $duedate   =~ s/^\s+|\s+$//g;
109

  
110
  if ($transdate =~ /^[0-9]{8}$/) {
111
    $dt_to_kivi = DateTime->new(year  => substr($transdate,0,4),
112
                                month => substr ($transdate,4,2),
113
                                day   => substr($transdate,6,2))->to_kivitendo;
118
  if (ref $vendor eq 'SL::DB::Vendor') { return $vendor; }
119

  
120
  return undef;
121
}
122

  
123
sub action_import_zugferd {
124
  my ($self, %params) = @_;
125

  
126
  my $file = $::form->{file};
127
  my $file_name = $::form->{file_name};
128

  
129
  my %res;          # result data structure returned by SL::ZUGFeRD->extract_from_{pdf,xml}()
130
  my $parser;       # SL::XMLInvoice object created by SL::ZUGFeRD->extract_from_{pdf,xml}()
131
  my $dom;          # DOM object for parsed XML data
132
  my $template_ap;  # SL::DB::RecordTemplate object
133
  my $vendor;       # SL::DB::Vendor object
134

  
135
  my $ibanmessage;  # Message to display if vendor's database and invoice IBANs don't match up
136

  
137
  die t8("missing file for action import") unless $file;
138
  die t8("can only parse a pdf or xml file")      unless $file =~ m/^%PDF|<\?xml/;
139

  
140
  if ( $::form->{file} =~ m/^%PDF/ ) {
141
    %res = %{SL::ZUGFeRD->extract_from_pdf($::form->{file})}
142
  } else {
143
    %res = %{SL::ZUGFeRD->extract_from_xml($::form->{file})};
114 144
  }
115
  if ($duedate =~ /^[0-9]{8}$/) {
116
    $due_dt_to_kivi = DateTime->new(year  => substr($duedate,0,4),
117
                                    month => substr ($duedate,4,2),
118
                                    day   => substr($duedate,6,2))->to_kivitendo;
145

  
146
  if ($res{'result'} != SL::ZUGFeRD::RES_OK()) {
147
    # An error occurred; log message from parser:
148
    $::lxdebug->message(LXDebug::DEBUG1(), "Could not extract ZUGFeRD data, error message: " . $res{'message'});
149
    die(t8("Could not extract Factur-X/ZUGFeRD data, data and error message:") . " $res{'message'}");
119 150
  }
120 151

  
121
  my $type = $dom->findnodes('//rsm:ExchangedDocument/ram:TypeCode')->string_value;
152
  $parser = $res{'invoice_xml'};
153

  
154
  # Shouldn't be neccessary with SL::XMLInvoice doing the heavy lifting, but
155
  # let's grab it, just in case.
156
  $dom  = $parser->{dom};
157

  
158
  my %metadata = %{$parser->metadata};
159
  my @items = @{$parser->items};
122 160

  
123
  my $dd   = $dom->findnodes('//ram:ApplicableHeaderTradeSettlement' .
124
                             '/ram:SpecifiedTradeSettlementPaymentMeans/ram:TypeCode')->string_value;
125
  my $direct_debit = $dd == 59 ? 1 : 0;
161
  my $iban = $metadata{'iban'};
162
  my $invnumber = $metadata{'invnumber'};
126 163

  
127
  my $iban = $dom->findnodes('//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementPaymentMeans' .
128
                             '/ram:PayeePartyCreditorFinancialAccount/ram:IBANID')->string_value;
129
  my $ibanmessage;
164
  if ( ! ($metadata{'ustid'} or $metadata{'taxnumber'}) ) {
165
    die t8("Cannot process this invoice: neither VAT ID nor tax ID present.");
166
    }
167

  
168
  $vendor = find_vendor($metadata{'ustid'}, $metadata{'taxnumber'});
169

  
170
  die t8("Please add a valid VAT ID or tax number for this vendor: #1", $metadata{'vendor_name'}) unless $vendor;
171

  
172

  
173
  # Create a record template for this imported invoice
174
  $template_ap = SL::DB::RecordTemplate->new(
175
      vendor_id=>$vendor->id,
176
  );
177

  
178
  # Check IBAN specified on bill matches the one we've got in
179
  # the database for this vendor.
130 180
  $ibanmessage = $iban ne $vendor->iban ? "Record IBAN $iban doesn't match vendor IBAN " . $vendor->iban : $iban if $iban;
131 181

  
132 182
  # save the zugferd file to session file for reuse in ap.pl
......
134 184
  $session_file->fh->print($file);
135 185
  $session_file->fh->close;
136 186

  
187
  # Use invoice creation date as due date if there's no due date
188
  $metadata{'duedate'} = $metadata{'transdate'} unless defined $metadata{'duedate'};
189

  
190
  # parse dates to kivi if set/valid
191
  foreach my $key ( qw(transdate duedate) ) {
192
    next unless defined $metadata{$key};
193
    $metadata{$key} =~ s/^\s+|\s+$//g;
194

  
195
    if ($metadata{$key} =~ /^([0-9]{4})-?([0-9]{2})-?([0-9]{2})$/) {
196
    $metadata{$key} = DateTime->new(year  => $1,
197
                                    month => $2,
198
                                    day   => $3)->to_kivitendo;
199
    }
200
  }
201

  
202
  # Try to fill in AP account to book against
203
  my $ap_chart_id = $::instance_conf->get_ap_chart_id;
204

  
205
  unless ( defined $ap_chart_id ) {
206
    # If no default account is configured, just use the first AP account found.
207
    my $ap_chart = SL::DB::Manager::Chart->get_all(
208
      where   => [ link => 'AP' ],
209
      sort_by => [ 'accno' ],
210
    );
211
    $ap_chart_id = ${$ap_chart}[0]->id;
212
  }
213

  
214
  my $currency = SL::DB::Manager::Currency->find_by(
215
    name => $metadata{'currency'},
216
    );
217

  
218
  $template_ap->assign_attributes(
219
    template_name       => "Faktur-X/ZUGFeRD/XRechnung Import $vendor->name, $invnumber",
220
    template_type       => 'ap_transaction',
221
    direct_debit        => $metadata{'direct_debit'},
222
    notes               => "Faktur-X/ZUGFeRD/XRechnung Import. Type: $metadata{'type'}\nIBAN: " . $ibanmessage,
223
    taxincluded         => 0,
224
    currency_id         => $currency->id,
225
    ar_ap_chart_id      => $ap_chart_id,
226
    );
227

  
228
  $template_ap->save;
229

  
230
  my $default_ap_amount_chart = SL::DB::Manager::Chart->find_by(charttype => 'A');
231

  
232
  foreach my $i ( @items )
233
    {
234
    my %item = %{$i};
235

  
236
    my $net_total = $item{'subtotal'};
237
    my $desc = $item{'description'};
238
    my $tax_rate = $item{'tax_rate'} / 100; # XML data is usually in percent
239

  
240
    my $taxes = SL::DB::Manager::Tax->get_all(
241
      where   => [ chart_categories => { like => '%' . $default_ap_amount_chart->category . '%' },
242
                   rate => $tax_rate,
243
                 ],
244
    );
245

  
246
    # If we really can't find any tax definition (a simple rounding error may
247
    # be sufficient for that to happen), grab the first tax fitting the default
248
    # category, just like the AP form would do it for manual entry.
249
    if ( scalar @{$taxes} == 0 ) {
250
      $taxes = SL::D::ManagerTax->get_all(
251
        where   => [ chart_categories => { like => '%' . $default_ap_amount_chart->category . '%' } ],
252
      );
253
    }
254

  
255
    my $tax = ${$taxes}[0];
256

  
257
    my $item_obj = SL::DB::RecordTemplateItem
258
      ->new(amount1 => $net_total,
259
            record_template_id => $template_ap->id,
260
            chart_id      => $default_ap_amount_chart->id,
261
            tax_id      => $tax->id,
262
        );
263
    $item_obj->save;
264
    }
265

  
137 266
  $self->redirect_to(
138 267
    controller                           => 'ap.pl',
139 268
    action                               => 'load_record_template',
140 269
    id                                   => $template_ap->id,
141
    'form_defaults.amount_1'             => $::form->format_amount(\%::myconfig, $total, 2),
142
    'form_defaults.transdate'            => $dt_to_kivi,
143
    'form_defaults.invnumber'            => $invnumber,
144
    'form_defaults.duedate'              => $due_dt_to_kivi,
145 270
    'form_defaults.no_payment_bookings'  => 0,
146
    'form_defaults.paid_1_suggestion'    => $::form->format_amount(\%::myconfig, $total, 2),
147
    'form_defaults.notes'                => "ZUGFeRD Import. Type: $type\nIBAN: " . $ibanmessage,
271
    'form_defaults.paid_1_suggestion'    => $::form->format_amount(\%::myconfig, $metadata{'total'}, 2),
272
    'form_defaults.invnumber'            => $invnumber,
273
    'form_defaults.duedate'              => $metadata{'duedate'},
274
    'form_defaults.transdate'            => $metadata{'transdate'},
275
    'form_defaults.notes'                => "ZUGFeRD Import. Type: $metadata{'type'}\nIBAN: " . $ibanmessage,
148 276
    'form_defaults.taxincluded'          => 0,
149
    'form_defaults.direct_debit'         => $direct_debit,
277
    'form_defaults.direct_debit'         => $metadata{'direct_debit'},
150 278
    'form_defaults.zugferd_session_file' => $file_name,
151 279
  );
152 280

  
......
189 317

  
190 318
=head1 NAME
191 319

  
192
SL::Controller::ZUGFeRD
193
Controller for importing ZUGFeRD pdf files to kivitendo
320
SL::Controller::ZUGFeRD - Controller for importing ZUGFeRD PDF files or XML invoices to kivitendo
194 321

  
195 322
=head1 FUNCTIONS
196 323

  
......
200 327

  
201 328
Creates a web from with a single upload dialog.
202 329

  
203
=item C<action_import_zugferd $pdf>
330
=item C<action_import_zugferd $file>
204 331

  
205
Expects a single pdf with ZUGFeRD 2.0 metadata.
206
Checks if the param <C$pdf> is set and a valid pdf file.
207
Calls helper functions to validate and extract the ZUGFeRD data.
208
Needs a valid VAT ID (EU) for this vendor and
209
expects one ap template for this vendor in kivitendo.
332
Expects a single PDF with ZUGFeRD, Factur-X or XRechnung
333
metadata. Alternatively, it can also process said data as a
334
standalone XML file.
210 335

  
211
Parses some basic ZUGFeRD data (invnumber, total net amount,
212
transdate, duedate, vendor VAT ID, IBAN) and uses the first
213
found ap template for this vendor to fill this template with
214
ZUGFeRD data.
215
If the vendor's master data contain a IBAN and the
216
ZUGFeRD record has a IBAN also these values will be compared.
217
If they  don't match a warning will be writte in ap.notes.
218
Furthermore the ZUGFeRD type code will be written to ap.notes.
219
No callback implemented.
336
Checks if the param <C$pdf> is set and a valid PDF or XML
337
file. Calls helper functions to validate and extract the
338
ZUGFeRD/Factur-X/XRechnung data. The invoice needs to have a
339
valid VAT ID (EU) or tax number (Germany) and a vendor with
340
the same VAT ID or tax number enrolled in Kivitendo.
341

  
342
It parses some basic ZUGFeRD data (invnumber, total net amount,
343
transdate, duedate, vendor VAT ID, IBAN, etc.) and also
344
extracts the invoice's items.
345

  
346
If the invoice has a IBAN also, it will be be compared to the
347
IBAN saved for the vendor (if any). If they  don't match a
348
warning will be writte in ap.notes. Furthermore the ZUGFeRD
349
type code will be written to ap.notes. No callback
350
implemented.
220 351

  
221 352
=back
222 353

  
223
=head1 TODO and CAVEAT
354
=head1 CAVEAT
355

  
356
This is just a very basic Parser for ZUGFeRD/Factur-X/XRechnung invoices.
357
We assume that the invoice's creator is a company with a valid
358
European VAT ID or German tax number and enrolled in
359
Kivitendo. Currently, implementation is a bit hacky because
360
invoice import uses AP record templates as a vessel for
361
generating the AP record form with the imported data filled
362
in.
363

  
364
=head1 TODO
365

  
366
This implementation could be improved as follows:
367

  
368
=over 4
369

  
370
=item Direct creation of the filled in AP record form
371

  
372
Creating an AP record template in the database is not
373
very elegant, since it will spam the database with record
374
templates that become redundant once the invoice has been
375
booked. It would be preferable to fill in the form directly.
376

  
377
=item Automatic upload of invoice
378

  
379
Right now, one has to use the "Book and upload" button to
380
upload the raw invoice document to WebDAV or DMS and attach it
381
to the invoice. This should be a simple matter of setting a
382
check box when uploading.
383

  
384
=item Handling of vendor invoices
385

  
386
There is no reason this functionality could not be used to
387
import vendor invoices as well. Since these tend to be very
388
lengthy, the ability to import them would be very beneficial.
224 389

  
225
This is just a very basic Parser for ZUGFeRD data.
226
We assume that the ZUGFeRD generator is a company with a
227
valid European VAT ID. Furthermore this vendor needs only
228
one and just noe ap template (the first match will be used).
390
=item Automatic handling of payment purpose
229 391

  
230
The ZUGFeRD data should also be extracted in the helper package
231
and maybe a model should be used for this.
232
The user should set one ap template as a default for ZUGFeRD.
233
The ZUGFeRD pdf should be written to WebDAV or DMS.
234 392
If the ZUGFeRD data has a payment purpose set, this should
235 393
be the default for the SEPA-XML export.
236 394

  
395
=back
396

  
397
=head1 AUTHORS
398

  
399
=over 4
400

  
401
=item Jan Büren E<lt>jan@kivitendo-premium.deE<gt>,
237 402

  
238
=head1 AUTHOR
403
=item Johannes Graßler E<lt>info@computer-grassler.deE<gt>,
239 404

  
240
Jan Büren E<lt>jan@kivitendo-premium.deE<gt>,
405
=back
241 406

  
242 407
=cut

Auch abrufbar als: Unified diff