Revision 9077dc27
Von Jan Büren vor mehr als 4 Jahren hinzugefügt
SL/Controller/ZUGFeRD.pm | ||
---|---|---|
1 |
package SL::Controller::ZUGFeRD; |
|
2 |
use strict; |
|
3 |
use parent qw(SL::Controller::Base); |
|
4 |
|
|
5 |
use SL::DB::RecordTemplate; |
|
6 |
use SL::Locale::String qw(t8); |
|
7 |
use SL::Helper::DateTime; |
|
8 |
use SL::ZUGFeRD; |
|
9 |
|
|
10 |
use XML::LibXML; |
|
11 |
|
|
12 |
|
|
13 |
__PACKAGE__->run_before('check_auth'); |
|
14 |
|
|
15 |
sub action_upload_zugferd { |
|
16 |
my ($self, %params) = @_; |
|
17 |
|
|
18 |
$self->setup_zugferd_action_bar; |
|
19 |
$self->render('zugferd/form', title => $::locale->text('ZUGFeRD import')); |
|
20 |
} |
|
21 |
|
|
22 |
sub action_import_zugferd { |
|
23 |
my ($self, %params) = @_; |
|
24 |
|
|
25 |
die t8("missing file for action import") unless $::form->{file}; |
|
26 |
die t8("can only parse a pdf file") unless $::form->{file} =~ m/^%PDF/; |
|
27 |
|
|
28 |
my $info = SL::ZUGFeRD->extract_from_pdf($::form->{file}); |
|
29 |
|
|
30 |
if ($info->{result} != SL::ZUGFeRD::RES_OK()) { |
|
31 |
# An error occurred; log message from parser: |
|
32 |
$::lxdebug->message(LXDebug::DEBUG1(), "Could not extract ZUGFeRD data, error message: " . $info->{message}); |
|
33 |
die t8("Could not extract ZUGFeRD data, data and error message:") . $info->{message}; |
|
34 |
} |
|
35 |
# valid ZUGFeRD metadata |
|
36 |
my $dom = XML::LibXML->load_xml(string => $info->{invoice_xml}); |
|
37 |
|
|
38 |
# 1. check if ZUGFeRD SellerTradeParty has a VAT-ID |
|
39 |
my $ustid = $dom->findnodes('//ram:SellerTradeParty/ram:SpecifiedTaxRegistration')->string_value; |
|
40 |
die t8("No VAT Info for this ZUGFeRD invoice," . |
|
41 |
" please ask your vendor to add this for his ZUGFeRD data.") unless $ustid; |
|
42 |
|
|
43 |
$ustid =~ s/^\s+|\s+$//g; |
|
44 |
|
|
45 |
# 1.1 check if we a have a vendor with this VAT-ID (vendor.ustid) |
|
46 |
my $vc = $dom->findnodes('//ram:SellerTradeParty/ram:Name')->string_value; |
|
47 |
my $vendor = SL::DB::Manager::Vendor->find_by(ustid => $ustid); |
|
48 |
die t8("Please add a valid VAT-ID for this vendor: " . $vc) unless (ref $vendor eq 'SL::DB::Vendor'); |
|
49 |
|
|
50 |
# 2. check if we have a ap record template for this vendor (TODO only the oldest template is choosen) |
|
51 |
my $template_ap = SL::DB::Manager::RecordTemplate->get_first(where => [vendor_id => $vendor->id]); |
|
52 |
die t8("No AP Record Template for this vendor found, please add one") unless (ref $template_ap eq 'SL::DB::RecordTemplate'); |
|
53 |
|
|
54 |
|
|
55 |
# 3. parse the zugferd data and fill the ap record template |
|
56 |
# -> no need to check sign (credit notes will be negative) just record thei ZUGFeRD type in ap.notes |
|
57 |
# -> check direct debit (defaults to no) |
|
58 |
# -> set amount (net amount) and unset taxincluded |
|
59 |
# (template and user cares for tax and if there is more than one booking accno) |
|
60 |
# -> date (can be empty) |
|
61 |
# -> duedate (may be empty) |
|
62 |
# -> compare record iban and generate a warning if this differs from vendor's master data iban |
|
63 |
my $total = $dom->findnodes('//ram:SpecifiedTradeSettlementHeaderMonetarySummation' . |
|
64 |
'/ram:TaxBasisTotalAmount')->string_value; |
|
65 |
|
|
66 |
my $invnumber = $dom->findnodes('//rsm:ExchangedDocument/ram:ID')->string_value; |
|
67 |
|
|
68 |
# parse dates to kivi if set/valid |
|
69 |
my ($transdate, $duedate, $dt_to_kivi, $due_dt_to_kivi); |
|
70 |
$transdate = $dom->findnodes('//ram:IssueDateTime')->string_value; |
|
71 |
$duedate = $dom->findnodes('//ram:DueDateDateTime')->string_value; |
|
72 |
$transdate =~ s/^\s+|\s+$//g; |
|
73 |
$duedate =~ s/^\s+|\s+$//g; |
|
74 |
|
|
75 |
if ($transdate =~ /^[0-9]{8}$/) { |
|
76 |
$dt_to_kivi = DateTime->new(year => substr($transdate,0,4), |
|
77 |
month => substr ($transdate,4,2), |
|
78 |
day => substr($transdate,6,2))->to_kivitendo; |
|
79 |
} |
|
80 |
if ($duedate =~ /^[0-9]{8}$/) { |
|
81 |
$due_dt_to_kivi = DateTime->new(year => substr($duedate,0,4), |
|
82 |
month => substr ($duedate,4,2), |
|
83 |
day => substr($duedate,6,2))->to_kivitendo; |
|
84 |
} |
|
85 |
|
|
86 |
my $type = $dom->findnodes('//rsm:ExchangedDocument/ram:TypeCode')->string_value; |
|
87 |
|
|
88 |
my $dd = $dom->findnodes('//ram:ApplicableHeaderTradeSettlement' . |
|
89 |
'/ram:SpecifiedTradeSettlementPaymentMeans/ram:TypeCode')->string_value; |
|
90 |
my $direct_debit = $dd == 59 ? 1 : 0; |
|
91 |
|
|
92 |
my $iban = $dom->findnodes('//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementPaymentMeans' . |
|
93 |
'/ram:PayeePartyCreditorFinancialAccount/ram:IBANID')->string_value; |
|
94 |
my $ibanmessage; |
|
95 |
$ibanmessage = $iban ne $vendor->iban ? "Record IBAN $iban doesn't match vendor IBAN " . $vendor->iban : $iban if $iban; |
|
96 |
|
|
97 |
my $url = $self->url_for( |
|
98 |
controller => 'ap.pl', |
|
99 |
action => 'load_record_template', |
|
100 |
id => $template_ap->id, |
|
101 |
'form_defaults.amount_1' => $::form->format_amount(\%::myconfig, $total, 2), |
|
102 |
'form_defaults.transdate' => $dt_to_kivi, |
|
103 |
'form_defaults.invnumber' => $invnumber, |
|
104 |
'form_defaults.duedate' => $due_dt_to_kivi, |
|
105 |
'form_defaults.no_payment_bookings' => 0, |
|
106 |
'form_defaults.paid_1_suggestion' => $::form->format_amount(\%::myconfig, $total, 2), |
|
107 |
'form_defaults.notes' => "ZUGFeRD Import. Type: $type\nIBAN: " . $ibanmessage, |
|
108 |
'form_defaults.taxincluded' => 0, |
|
109 |
'form_defaults.direct_debit' => $direct_debit, |
|
110 |
); |
|
111 |
|
|
112 |
$self->redirect_to($url); |
|
113 |
|
|
114 |
} |
|
115 |
|
|
116 |
sub check_auth { |
|
117 |
$::auth->assert('ap_transactions'); |
|
118 |
} |
|
119 |
sub setup_zugferd_action_bar { |
|
120 |
my ($self) = @_; |
|
121 |
|
|
122 |
for my $bar ($::request->layout->get('actionbar')) { |
|
123 |
$bar->add( |
|
124 |
action => [ |
|
125 |
$::locale->text('Import'), |
|
126 |
submit => [ '#form', { action => 'ZUGFeRD/import_zugferd' } ], |
|
127 |
accesskey => 'enter', |
|
128 |
], |
|
129 |
); |
|
130 |
} |
|
131 |
} |
|
132 |
|
|
133 |
|
|
134 |
1; |
|
135 |
__END__ |
|
136 |
|
|
137 |
=pod |
|
138 |
|
|
139 |
=encoding utf8 |
|
140 |
|
|
141 |
=head1 NAME |
|
142 |
|
|
143 |
SL::Controller::ZUGFeRD |
|
144 |
Controller for importing ZUGFeRD pdf files to kivitendo |
|
145 |
|
|
146 |
=head1 FUNCTIONS |
|
147 |
|
|
148 |
=over 4 |
|
149 |
|
|
150 |
=item C<action_upload_zugferd> |
|
151 |
|
|
152 |
Creates a web from with a single upload dialog. |
|
153 |
|
|
154 |
=item C<action_import_zugferd $pdf> |
|
155 |
|
|
156 |
Expects a single pdf with ZUGFeRD 2.0 metadata. |
|
157 |
Checks if the param <C$pdf> is set and a valid pdf file. |
|
158 |
Calls helper functions to validate and extract the ZUGFeRD data. |
|
159 |
Needs a valid VAT ID (EU) for this vendor and |
|
160 |
expects one ap template for this vendor in kivitendo. |
|
161 |
|
|
162 |
Parses some basic ZUGFeRD data (invnumber, total net amount, |
|
163 |
transdate, duedate, vendor VAT ID, IBAN) and uses the first |
|
164 |
found ap template for this vendor to fill this template with |
|
165 |
ZUGFeRD data. |
|
166 |
If the vendor's master data contain a IBAN and the |
|
167 |
ZUGFeRD record has a IBAN also these values will be compared. |
|
168 |
If they don't match a warning will be writte in ap.notes. |
|
169 |
Furthermore the ZUGFeRD type code will be written to ap.notes. |
|
170 |
No callback implemented. |
|
171 |
|
|
172 |
=back |
|
173 |
|
|
174 |
=head1 TODO and CAVEAT |
|
175 |
|
|
176 |
This is just a very basic Parser for ZUGFeRD data. |
|
177 |
We assume that the ZUGFeRD generator is a company with a |
|
178 |
valid European VAT ID. Furthermore this vendor needs only |
|
179 |
one and just noe ap template (the first match will be used). |
|
180 |
|
|
181 |
The ZUGFeRD data should also be extracted in the helper package |
|
182 |
and maybe a model should be used for this. |
|
183 |
The user should set one ap template as a default for ZUGFeRD. |
|
184 |
The ZUGFeRD pdf should be written to WebDAV or DMS. |
|
185 |
If the ZUGFeRD data has a payment purpose set, this should |
|
186 |
be the default for the SEPA-XML export. |
|
187 |
|
|
188 |
|
|
189 |
=head1 AUTHOR |
|
190 |
|
|
191 |
Jan Büren E<lt>jan@kivitendo-premium.deE<gt>, |
|
192 |
|
|
193 |
=cut |
locale/de/all | ||
---|---|---|
690 | 690 |
'Correct taxkey' => 'Richtiger Steuerschlüssel', |
691 | 691 |
'Cost Center' => 'Kostenstelle', |
692 | 692 |
'Costs' => 'Kosten', |
693 |
'Could not extract ZUGFeRD data, data and error message:' => 'Konnte keine ZUGFeRD Daten extrahieren, folgende Fehlermeldung und das PDF:', |
|
693 | 694 |
'Could not find an entry for this part in the pricegroup.' => 'Konnte keine Eintrag für diesen Artikel in der Preisgruppe finden.', |
694 | 695 |
'Could not load class #1 (#2): "#3"' => 'Konnte Klasse #1 (#2) nicht laden: "#3"', |
695 | 696 |
'Could not load class #1, #2' => 'Konnte Klasse #1 nicht laden: "#2"', |
... | ... | |
1626 | 1627 |
'Import CSV' => 'CSV-Import', |
1627 | 1628 |
'Import Status' => 'Import Status', |
1628 | 1629 |
'Import a MT940 file:' => 'Laden Sie eine MT940 Datei hoch:', |
1630 |
'Import a ZUGFeRD file:' => 'Eine ZUGFeRD-Datei importieren', |
|
1629 | 1631 |
'Import all' => 'Importiere Alle', |
1630 | 1632 |
'Import documents from #1' => 'Importiere Dateien von Quelle \'#1\'', |
1631 | 1633 |
'Import file' => 'Import-Datei', |
... | ... | |
1990 | 1992 |
'Next run at' => 'Nächste Ausführung um', |
1991 | 1993 |
'No' => 'Nein', |
1992 | 1994 |
'No 1:n or n:1 relation' => 'Keine 1:n oder n:1 Beziehung', |
1995 |
'No AP Record Template for this vendor found, please add one' => 'Konnte keine Kreditorenbuchungsvorlage für diesen Lieferanten finden, bitte legen Sie eine an.', |
|
1993 | 1996 |
'No AP template was found.' => 'Keine Kreditorenbuchungsvorlage gefunden.', |
1994 | 1997 |
'No Company Address given' => 'Keine Firmenadresse hinterlegt!', |
1995 | 1998 |
'No Company Name given' => 'Kein Firmenname hinterlegt!', |
... | ... | |
1998 | 2001 |
'No Journal' => 'Kein Journal', |
1999 | 2002 |
'No Shopdescription' => 'Keine Shop-Artikelbeschreibung', |
2000 | 2003 |
'No Shopimages' => 'Keine Shop-Bilder', |
2004 |
'No VAT Info for this ZUGFeRD invoice, please ask your vendor to add this for his ZUGFeRD data.' => 'Konnte keine UST-ID für diese ZUGFeRD Rechnungen finden, bitte fragen Sie bei Ihren Lieferanten nach, ob dieses Feld im ZUGFeRD Datensatz gesetzt wird.', |
|
2001 | 2005 |
'No Vendor was found matching the search parameters.' => 'Zu dem Suchbegriff wurde kein Händler gefunden', |
2002 | 2006 |
'No action defined.' => 'Keine Aktion definiert.', |
2003 | 2007 |
'No article has been selected yet.' => 'Es wurde noch kein Artikel ausgewählt.', |
... | ... | |
2312 | 2316 |
'Pictures for search parts' => 'Bilder für Warensuche', |
2313 | 2317 |
'Please Check the bank information for each customer:' => 'Bitte überprüfen Sie die Bankinformationen der Kunden:', |
2314 | 2318 |
'Please Check the bank information for each vendor:' => 'Bitte überprüfen Sie die Kontoinformationen der Lieferanten:', |
2319 |
'Please add a valid VAT-ID for this vendor: ' => 'Bitte prüfen Sie ob dieser Lieferant eine valide UST-ID (Großschreibungen und Leerzeichen beachten) besitzt:', |
|
2315 | 2320 |
'Please ask your administrator to create warehouses and bins.' => 'Bitten Sie Ihren Administrator, dass er Lager und Lagerplätze anlegt.', |
2316 | 2321 |
'Please change the partnumber of the following parts and run the update again:' => 'Bitte ändern Sie daher die Artikelnummer folgender Artikel:', |
2317 | 2322 |
'Please choose a part.' => 'Bitte wählen Sie einen Artikel aus.', |
... | ... | |
4003 | 4008 |
'Your download does not exist anymore. Please re-run the DATEV export assistant.' => 'Ihr Download existiert nicht mehr. Bitte starten Sie den DATEV-Exportassistenten erneut.', |
4004 | 4009 |
'Your import is being processed.' => 'Ihr Import wird verarbeitet', |
4005 | 4010 |
'Your target quantity will be added to the stocked quantity.' => 'Ihre gezählte Zielmenge wird zum Lagerbestand hinzugezählt.', |
4011 |
'ZUGFeRD import' => 'ZUGFeRD Import', |
|
4006 | 4012 |
'ZUGFeRD invoice' => 'ZUGFeRD-Rechnung', |
4007 | 4013 |
'ZUGFeRD notes for each invoice' => 'ZUGFeRD-Notizen für jede Rechnung', |
4008 | 4014 |
'Zeitraum' => 'Zeitraum', |
... | ... | |
4011 | 4017 |
'Zip, City' => 'PLZ, Ort', |
4012 | 4018 |
'Zipcode' => 'PLZ', |
4013 | 4019 |
'Zipcode and city' => 'PLZ und Stadt', |
4020 |
'ZugFeRD Import' => 'ZUGFeRD Import', |
|
4014 | 4021 |
'[email]' => '[email]', |
4015 | 4022 |
'absolute' => 'absolut', |
4016 | 4023 |
'account_description' => 'Beschreibung', |
... | ... | |
4045 | 4052 |
'brutto' => 'brutto', |
4046 | 4053 |
'building data' => 'Verarbeite Daten', |
4047 | 4054 |
'building report' => 'Erstelle Bericht', |
4055 |
'can only parse a pdf file' => 'Kann nur eine gültige PDF-Datei verwenden.', |
|
4048 | 4056 |
'cash' => 'Ist-Versteuerung', |
4049 | 4057 |
'chargenumber #1' => 'Chargennummer #1', |
4050 | 4058 |
'chart_of_accounts' => 'kontenuebersicht', |
... | ... | |
4149 | 4157 |
'male' => 'männlich', |
4150 | 4158 |
'max filesize' => 'maximale Dateigröße', |
4151 | 4159 |
'missing' => 'Fehlbestand', |
4160 |
'missing file for action import' => 'Es wurde keine Datei zum Hochladen ausgewählt', |
|
4152 | 4161 |
'missing_br' => 'Fehl.', |
4153 | 4162 |
'month' => 'Monatliche Abgabe', |
4154 | 4163 |
'monthly' => 'monatlich', |
menus/user/00-erp.yaml | ||
---|---|---|
641 | 641 |
access: general_ledger |
642 | 642 |
params: |
643 | 643 |
action: YearEndTransactions/form |
644 |
- parent: general_ledger |
|
645 |
id: zugferd_import |
|
646 |
name: ZugFeRD Import |
|
647 |
icon: cbob |
|
648 |
order: 485 |
|
649 |
access: ap_transactions |
|
650 |
params: |
|
651 |
action: ZUGFeRD/upload_zugferd |
|
644 | 652 |
- parent: general_ledger |
645 | 653 |
id: general_ledger_reports |
646 | 654 |
name: Reports |
templates/webpages/zugferd/form.html | ||
---|---|---|
1 |
[%- USE HTML %] |
|
2 |
[%- USE LxERP %] |
|
3 |
[%- USE L %] |
|
4 |
[%- USE T8 %] |
|
5 |
[%- INCLUDE 'common/flash.html' %] |
|
6 |
<div class="listtop">[% FORM.title %]</div> |
|
7 |
|
|
8 |
<p> |
|
9 |
[% "Import a ZUGFeRD file:" | $T8 %] |
|
10 |
</p> |
|
11 |
|
|
12 |
<form method="post" action="controller.pl" enctype="multipart/form-data" id="form"> |
|
13 |
[% L.input_tag('file', '', type => 'file', accept => '.pdf') %] |
|
14 |
</form> |
|
15 |
|
Auch abrufbar als: Unified diff
ZUGFeRD: ZUGFeRD-Controller der minimal ZUGFeRD PDF parst
Simples Upload Formular für ein PDF. Falls das PDF gültige
ZUGFeRD Daten hat und ein Lieferant mit UST-ID in kivi gefunden wird,
werden die Formulardaten in der ersten gefunden Kreditorenbelegvorlage
des Lieferanten angezeigt. Details und TODO, s.a. perldoc