699e5fe5ef7ac3e294c4a2bcde17219fff82419d
[kivitendo-erp.git] / 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::VATIDNr;
9 use SL::ZUGFeRD;
10
11 use XML::LibXML;
12
13
14 __PACKAGE__->run_before('check_auth');
15
16 sub action_upload_zugferd {
17   my ($self, %params) = @_;
18
19   $self->setup_zugferd_action_bar;
20   $self->render('zugferd/form', title => $::locale->text('Factur-X/ZUGFeRD import'));
21 }
22
23 sub action_import_zugferd {
24   my ($self, %params) = @_;
25
26   die t8("missing file for action import") unless $::form->{file};
27   die t8("can only parse a pdf file")      unless $::form->{file} =~ m/^%PDF/;
28
29   my $info = SL::ZUGFeRD->extract_from_pdf($::form->{file});
30
31   if ($info->{result} != SL::ZUGFeRD::RES_OK()) {
32     # An error occurred; log message from parser:
33     $::lxdebug->message(LXDebug::DEBUG1(), "Could not extract ZUGFeRD data, error message: " . $info->{message});
34     die t8("Could not extract Factur-X/ZUGFeRD data, data and error message:") . $info->{message};
35   }
36   # valid ZUGFeRD metadata
37   my $dom   = XML::LibXML->load_xml(string => $info->{invoice_xml});
38
39   # 1. check if ZUGFeRD SellerTradeParty has a VAT-ID
40   my $ustid = $dom->findnodes('//ram:SellerTradeParty/ram:SpecifiedTaxRegistration')->string_value;
41   die t8("No VAT Info for this Factur-X/ZUGFeRD invoice," .
42          " please ask your vendor to add this for his Factur-X/ZUGFeRD data.") unless $ustid;
43
44   $ustid = SL::VATIDNr->normalize($ustid);
45
46   # 1.1 check if we a have a vendor with this VAT-ID (vendor.ustid)
47   my $vc     = $dom->findnodes('//ram:SellerTradeParty/ram:Name')->string_value;
48   my $vendor = SL::DB::Manager::Vendor->find_by(
49     ustid => $ustid,
50     or    => [
51       obsolete => undef,
52       obsolete => 0,
53     ]);
54
55   if (!$vendor) {
56     # 1.2 If no vendor with the exact VAT ID number is found, the
57     # number might be stored slightly different in the database
58     # (e.g. with spaces breaking up groups of numbers). Iterate over
59     # all existing vendors with VAT ID numbers, normalize their
60     # representation and compare those.
61
62     my $vendors = SL::DB::Manager::Vendor->get_all(
63       where => [
64         '!ustid' => undef,
65         '!ustid' => '',
66         or       => [
67           obsolete => undef,
68           obsolete => 0,
69         ],
70       ]);
71
72     foreach my $other_vendor (@{ $vendors }) {
73       next unless SL::VATIDNr->normalize($other_vendor->ustid) eq $ustid;
74
75       $vendor = $other_vendor;
76       last;
77     }
78   }
79
80   die t8("Please add a valid VAT-ID for this vendor: #1", $vc) unless (ref $vendor eq 'SL::DB::Vendor');
81
82   # 2. check if we have a ap record template for this vendor (TODO only the oldest template is choosen)
83   my $template_ap = SL::DB::Manager::RecordTemplate->get_first(where => [vendor_id => $vendor->id]);
84   die t8("No AP Record Template for this vendor found, please add one") unless (ref $template_ap eq 'SL::DB::RecordTemplate');
85
86
87   # 3. parse the zugferd data and fill the ap record template
88   # -> no need to check sign (credit notes will be negative) just record thei ZUGFeRD type in ap.notes
89   # -> check direct debit (defaults to no)
90   # -> set amount (net amount) and unset taxincluded
91   #    (template and user cares for tax and if there is more than one booking accno)
92   # -> date (can be empty)
93   # -> duedate (may be empty)
94   # -> compare record iban and generate a warning if this differs from vendor's master data iban
95   my $total     = $dom->findnodes('//ram:SpecifiedTradeSettlementHeaderMonetarySummation' .
96                                   '/ram:TaxBasisTotalAmount')->string_value;
97
98   my $invnumber = $dom->findnodes('//rsm:ExchangedDocument/ram:ID')->string_value;
99
100   # parse dates to kivi if set/valid
101   my ($transdate, $duedate, $dt_to_kivi, $due_dt_to_kivi);
102   $transdate = $dom->findnodes('//ram:IssueDateTime')->string_value;
103   $duedate   = $dom->findnodes('//ram:DueDateDateTime')->string_value;
104   $transdate =~ s/^\s+|\s+$//g;
105   $duedate   =~ s/^\s+|\s+$//g;
106
107   if ($transdate =~ /^[0-9]{8}$/) {
108     $dt_to_kivi = DateTime->new(year  => substr($transdate,0,4),
109                                 month => substr ($transdate,4,2),
110                                 day   => substr($transdate,6,2))->to_kivitendo;
111   }
112   if ($duedate =~ /^[0-9]{8}$/) {
113     $due_dt_to_kivi = DateTime->new(year  => substr($duedate,0,4),
114                                     month => substr ($duedate,4,2),
115                                     day   => substr($duedate,6,2))->to_kivitendo;
116   }
117
118   my $type = $dom->findnodes('//rsm:ExchangedDocument/ram:TypeCode')->string_value;
119
120   my $dd   = $dom->findnodes('//ram:ApplicableHeaderTradeSettlement' .
121                              '/ram:SpecifiedTradeSettlementPaymentMeans/ram:TypeCode')->string_value;
122   my $direct_debit = $dd == 59 ? 1 : 0;
123
124   my $iban = $dom->findnodes('//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementPaymentMeans' .
125                              '/ram:PayeePartyCreditorFinancialAccount/ram:IBANID')->string_value;
126   my $ibanmessage;
127   $ibanmessage = $iban ne $vendor->iban ? "Record IBAN $iban doesn't match vendor IBAN " . $vendor->iban : $iban if $iban;
128
129   my $url = $self->url_for(
130     controller                           => 'ap.pl',
131     action                               => 'load_record_template',
132     id                                   => $template_ap->id,
133     'form_defaults.amount_1'             => $::form->format_amount(\%::myconfig, $total, 2),
134     'form_defaults.transdate'            => $dt_to_kivi,
135     'form_defaults.invnumber'            => $invnumber,
136     'form_defaults.duedate'              => $due_dt_to_kivi,
137     'form_defaults.no_payment_bookings'  => 0,
138     'form_defaults.paid_1_suggestion'    => $::form->format_amount(\%::myconfig, $total, 2),
139     'form_defaults.notes'                => "ZUGFeRD Import. Type: $type\nIBAN: " . $ibanmessage,
140     'form_defaults.taxincluded'          => 0,
141     'form_defaults.direct_debit'          => $direct_debit,
142   );
143
144   $self->redirect_to($url);
145
146 }
147
148 sub check_auth {
149   $::auth->assert('ap_transactions');
150 }
151 sub setup_zugferd_action_bar {
152   my ($self) = @_;
153
154   for my $bar ($::request->layout->get('actionbar')) {
155     $bar->add(
156       action => [
157         $::locale->text('Import'),
158         submit    => [ '#form', { action => 'ZUGFeRD/import_zugferd' } ],
159         accesskey => 'enter',
160       ],
161     );
162   }
163 }
164
165
166 1;
167 __END__
168
169 =pod
170
171 =encoding utf8
172
173 =head1 NAME
174
175 SL::Controller::ZUGFeRD
176 Controller for importing ZUGFeRD pdf files to kivitendo
177
178 =head1 FUNCTIONS
179
180 =over 4
181
182 =item C<action_upload_zugferd>
183
184 Creates a web from with a single upload dialog.
185
186 =item C<action_import_zugferd $pdf>
187
188 Expects a single pdf with ZUGFeRD 2.0 metadata.
189 Checks if the param <C$pdf> is set and a valid pdf file.
190 Calls helper functions to validate and extract the ZUGFeRD data.
191 Needs a valid VAT ID (EU) for this vendor and
192 expects one ap template for this vendor in kivitendo.
193
194 Parses some basic ZUGFeRD data (invnumber, total net amount,
195 transdate, duedate, vendor VAT ID, IBAN) and uses the first
196 found ap template for this vendor to fill this template with
197 ZUGFeRD data.
198 If the vendor's master data contain a IBAN and the
199 ZUGFeRD record has a IBAN also these values will be compared.
200 If they  don't match a warning will be writte in ap.notes.
201 Furthermore the ZUGFeRD type code will be written to ap.notes.
202 No callback implemented.
203
204 =back
205
206 =head1 TODO and CAVEAT
207
208 This is just a very basic Parser for ZUGFeRD data.
209 We assume that the ZUGFeRD generator is a company with a
210 valid European VAT ID. Furthermore this vendor needs only
211 one and just noe ap template (the first match will be used).
212
213 The ZUGFeRD data should also be extracted in the helper package
214 and maybe a model should be used for this.
215 The user should set one ap template as a default for ZUGFeRD.
216 The ZUGFeRD pdf should be written to WebDAV or DMS.
217 If the ZUGFeRD data has a payment purpose set, this should
218 be the default for the SEPA-XML export.
219
220
221 =head1 AUTHOR
222
223 Jan Büren E<lt>jan@kivitendo-premium.deE<gt>,
224
225 =cut