ZUGFeRD: ZUGFeRD-Controller der minimal ZUGFeRD PDF parst
[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::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