1 package SL::Controller::ZUGFeRD;
3 use parent qw(SL::Controller::Base);
5 use SL::DB::RecordTemplate;
6 use SL::Locale::String qw(t8);
7 use SL::Helper::DateTime;
13 __PACKAGE__->run_before('check_auth');
15 sub action_upload_zugferd {
16 my ($self, %params) = @_;
18 $self->setup_zugferd_action_bar;
19 $self->render('zugferd/form', title => $::locale->text('ZUGFeRD import'));
22 sub action_import_zugferd {
23 my ($self, %params) = @_;
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/;
28 my $info = SL::ZUGFeRD->extract_from_pdf($::form->{file});
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};
35 # valid ZUGFeRD metadata
36 my $dom = XML::LibXML->load_xml(string => $info->{invoice_xml});
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;
43 $ustid =~ s/^\s+|\s+$//g;
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');
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');
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;
66 my $invnumber = $dom->findnodes('//rsm:ExchangedDocument/ram:ID')->string_value;
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;
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;
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;
86 my $type = $dom->findnodes('//rsm:ExchangedDocument/ram:TypeCode')->string_value;
88 my $dd = $dom->findnodes('//ram:ApplicableHeaderTradeSettlement' .
89 '/ram:SpecifiedTradeSettlementPaymentMeans/ram:TypeCode')->string_value;
90 my $direct_debit = $dd == 59 ? 1 : 0;
92 my $iban = $dom->findnodes('//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementPaymentMeans' .
93 '/ram:PayeePartyCreditorFinancialAccount/ram:IBANID')->string_value;
95 $ibanmessage = $iban ne $vendor->iban ? "Record IBAN $iban doesn't match vendor IBAN " . $vendor->iban : $iban if $iban;
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,
112 $self->redirect_to($url);
117 $::auth->assert('ap_transactions');
119 sub setup_zugferd_action_bar {
122 for my $bar ($::request->layout->get('actionbar')) {
125 $::locale->text('Import'),
126 submit => [ '#form', { action => 'ZUGFeRD/import_zugferd' } ],
127 accesskey => 'enter',
143 SL::Controller::ZUGFeRD
144 Controller for importing ZUGFeRD pdf files to kivitendo
150 =item C<action_upload_zugferd>
152 Creates a web from with a single upload dialog.
154 =item C<action_import_zugferd $pdf>
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.
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
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.
174 =head1 TODO and CAVEAT
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).
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.
191 Jan Büren E<lt>jan@kivitendo-premium.deE<gt>,