Revision 3818b392
Von Tamino Steinert vor 12 Monaten hinzugefügt
SL/Controller/ZUGFeRD.pm | ||
---|---|---|
12 | 12 |
use SL::SessionFile; |
13 | 13 |
|
14 | 14 |
use XML::LibXML; |
15 |
use List::Util qw(first); |
|
15 | 16 |
|
16 | 17 |
|
17 | 18 |
__PACKAGE__->run_before('check_auth'); |
... | ... | |
128 | 129 |
|
129 | 130 |
my %res; # result data structure returned by SL::ZUGFeRD->extract_from_{pdf,xml}() |
130 | 131 |
my $parser; # SL::XMLInvoice object created by SL::ZUGFeRD->extract_from_{pdf,xml}() |
131 |
my $dom; # DOM object for parsed XML data |
|
132 | 132 |
my $vendor; # SL::DB::Vendor object |
133 | 133 |
|
134 |
my $ibanmessage; # Message to display if vendor's database and invoice IBANs don't match up |
|
135 |
|
|
136 |
die t8("missing file for action import") unless $file; |
|
137 |
die t8("can only parse a pdf or xml file") unless $file =~ m/^%PDF|<\?xml/; |
|
134 |
die t8("missing file for action import") unless $file; |
|
135 |
die t8("can only parse a pdf or xml file") unless $file =~ m/^%PDF|<\?xml/; |
|
138 | 136 |
|
139 | 137 |
if ( $::form->{file} =~ m/^%PDF/ ) { |
140 | 138 |
%res = %{SL::ZUGFeRD->extract_from_pdf($file)}; |
... | ... | |
149 | 147 |
|
150 | 148 |
$parser = $res{'invoice_xml'}; |
151 | 149 |
|
152 |
# Shouldn't be neccessary with SL::XMLInvoice doing the heavy lifting, but |
|
153 |
# let's grab it, just in case. |
|
154 |
$dom = $parser->{dom}; |
|
155 |
|
|
156 | 150 |
my %metadata = %{$parser->metadata}; |
157 | 151 |
my @items = @{$parser->items}; |
158 | 152 |
|
153 |
my $notes = t8("ZUGFeRD Import. Type: #1", $metadata{'type'}); |
|
159 | 154 |
my $iban = $metadata{'iban'}; |
160 | 155 |
my $invnumber = $metadata{'invnumber'}; |
161 | 156 |
|
... | ... | |
175 | 170 |
|
176 | 171 |
# Check IBAN specified on bill matches the one we've got in |
177 | 172 |
# the database for this vendor. |
178 |
$ibanmessage = $iban ne $vendor->iban ? "Record IBAN $iban doesn't match vendor IBAN " . $vendor->iban : $iban if $iban; |
|
173 |
if ($iban) { |
|
174 |
$notes .= "\nIBAN: "; |
|
175 |
$notes .= $iban ne $vendor->iban ? |
|
176 |
t8("Record IBAN #1 doesn't match vendor IBAN #2", $iban, $vendor->iban) |
|
177 |
: $iban |
|
178 |
} |
|
179 | 179 |
|
180 | 180 |
# save the zugferd file to session file for reuse in ap.pl |
181 | 181 |
my $session_file = SL::SessionFile->new($file_name, mode => 'w'); |
... | ... | |
213 | 213 |
name => $metadata{'currency'}, |
214 | 214 |
); |
215 | 215 |
|
216 |
my $default_ap_amount_chart = SL::DB::Manager::Chart->find_by( |
|
217 |
id => $::instance_conf->get_expense_accno_id |
|
218 |
); |
|
219 |
# Fallback if there's no default AP amount chart configured |
|
220 |
$default_ap_amount_chart ||= SL::DB::Manager::Chart->find_by(charttype => 'A'); |
|
221 |
|
|
222 |
my $active_taxkey = $default_ap_amount_chart->taxkey_id; |
|
223 |
my $taxes = SL::DB::Manager::Tax->get_all( |
|
224 |
where => [ chart_categories => { |
|
225 |
like => '%' . $default_ap_amount_chart->category . '%' |
|
226 |
}], |
|
227 |
sort_by => 'taxkey, rate', |
|
228 |
); |
|
229 |
die t8( |
|
230 |
"No tax found for chart #1", $default_ap_amount_chart->displayable_name |
|
231 |
) unless scalar @{$taxes}; |
|
232 |
|
|
233 |
# parse items |
|
234 |
my $row = 0; |
|
235 |
my %item_form = (); |
|
236 |
foreach my $i (@items) { |
|
237 |
$row++; |
|
238 |
|
|
239 |
my %item = %{$i}; |
|
240 |
|
|
241 |
my $net_total = $::form->format_amount(\%::myconfig, $item{'subtotal'}, 2); |
|
242 |
|
|
243 |
my $tax_rate = $item{'tax_rate'}; |
|
244 |
$tax_rate /= 100 if $tax_rate > 1; # XML data is usually in percent |
|
245 |
|
|
246 |
my $tax = first { $tax_rate == $_->rate } @{ $taxes }; |
|
247 |
$tax //= first { $active_taxkey->tax_id == $_->id } @{ $taxes }; |
|
248 |
$tax //= $taxes->[0]; |
|
249 |
|
|
250 |
$item_form{"AP_amount_chart_id_${row}"} = $default_ap_amount_chart->id; |
|
251 |
$item_form{"previous_AP_amount_chart_id_${row}"} = $default_ap_amount_chart->id; |
|
252 |
$item_form{"amount_${row}"} = $net_total; |
|
253 |
$item_form{"taxchart_${row}"} = $tax->id . '--' . $tax->rate; |
|
254 |
} |
|
255 |
$item_form{rowcount} = $row; |
|
256 |
|
|
216 | 257 |
$self->redirect_to( |
217 |
controller => 'ap.pl', |
|
218 |
action => 'load_zugferd', |
|
219 |
'form_defaults.no_payment_bookings' => 0, |
|
220 |
'form_defaults.paid_1_suggestion' => $::form->format_amount(\%::myconfig, $metadata{'total'}, 2), |
|
221 |
'form_defaults.invnumber' => $invnumber, |
|
222 |
'form_defaults.AP_chart_id' => $ap_chart_id, |
|
223 |
'form_defaults.currency' => $currency->name, |
|
224 |
'form_defaults.duedate' => $metadata{'duedate'}, |
|
225 |
'form_defaults.transdate' => $metadata{'transdate'}, |
|
226 |
'form_defaults.notes' => "ZUGFeRD Import. Type: $metadata{'type'}\nIBAN: " . $ibanmessage, |
|
227 |
'form_defaults.taxincluded' => 0, |
|
228 |
'form_defaults.direct_debit' => $metadata{'direct_debit'}, |
|
229 |
'form_defaults.vendor' => $vendor->name, |
|
230 |
'form_defaults.vendor_id' => $vendor->id, |
|
231 |
'form_defaults.zugferd_session_file' => $file_name, |
|
258 |
controller => 'ap.pl', |
|
259 |
action => 'load_zugferd', |
|
260 |
no_payment_bookings => 0, |
|
261 |
paid_1_suggestion => $::form->format_amount(\%::myconfig, $metadata{'total'}, 2), |
|
262 |
invnumber => $invnumber, |
|
263 |
AP_chart_id => $ap_chart_id, |
|
264 |
currency => $currency->name, |
|
265 |
duedate => $metadata{'duedate'}, |
|
266 |
transdate => $metadata{'transdate'}, |
|
267 |
notes => $notes, |
|
268 |
taxincluded => 0, |
|
269 |
direct_debit => $metadata{'direct_debit'}, |
|
270 |
vendor => $vendor->name, |
|
271 |
vendor_id => $vendor->id, |
|
272 |
zugferd_session_file => $file_name, |
|
273 |
%item_form, |
|
232 | 274 |
); |
233 | 275 |
|
234 | 276 |
} |
bin/mozilla/ap.pl | ||
---|---|---|
113 | 113 |
sub load_zugferd { |
114 | 114 |
$::auth->assert('ap_transactions'); |
115 | 115 |
|
116 |
my $data; # buffer for holding file contents |
|
117 |
|
|
118 |
my $form_defaults = $::form->{form_defaults}; |
|
119 |
my $file = SL::SessionFile->new($form_defaults->{zugferd_session_file}, mode => '<'); |
|
120 |
my $file_name = $file->file_name; |
|
121 |
|
|
122 |
$::form->{$_} = $form_defaults->{$_} for keys %{ $form_defaults // {} }; |
|
116 |
my $file_name = $::form->{zugferd_session_file}; |
|
123 | 117 |
|
124 | 118 |
# Defaults |
125 |
$::form->{title} = "Add"; |
|
126 |
$::form->{paidaccounts} = 1; |
|
127 |
|
|
128 |
$file->open('<'); |
|
129 |
if ( ! defined($file->fh->read($data, -s $file->fh)) ) { |
|
130 |
SL::Helper::Flash::flash_later('error', |
|
131 |
t8('Could not open ZUGFeRD file for reading: #1', $!)); |
|
132 |
} else { |
|
133 |
|
|
134 |
my %res; # result data structure returned by SL::ZUGFeRD->extract_from_{pdf,xml}() |
|
135 |
my $parser; # SL::XMLInvoice object created by SL::ZUGFeRD->extract_from_{pdf,xml}() |
|
136 |
my $template_ap; # SL::DB::RecordTemplate object |
|
137 |
my $vendor; # SL::DB::Vendor object |
|
138 |
my %metadata; # structured data extracted from XML payload |
|
139 |
my @items; # list of invoice items |
|
140 |
my $default_ap_amount_chart; |
|
141 |
|
|
142 |
if ( $data =~ m/^%PDF/ ) { |
|
143 |
%res = %{SL::ZUGFeRD->extract_from_pdf($data)}; |
|
144 |
} else { |
|
145 |
%res = %{SL::ZUGFeRD->extract_from_xml($data)}; |
|
146 |
} |
|
147 |
|
|
148 |
|
|
149 |
$parser = $res{'invoice_xml'}; |
|
150 |
%metadata = %{$parser->metadata}; |
|
151 |
@items = @{$parser->items}; |
|
152 |
|
|
153 |
$default_ap_amount_chart = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_expense_accno_id); |
|
154 |
|
|
155 |
# Fallback if there's no default AP amount chart configured |
|
156 |
unless ( $default_ap_amount_chart ) { |
|
157 |
$default_ap_amount_chart = SL::DB::Manager::Chart->find_by(charttype => 'A'); |
|
158 |
} |
|
159 |
|
|
160 |
my $row = 0; |
|
161 |
foreach my $i (@items) { |
|
162 |
$row++; |
|
163 |
|
|
164 |
my %item = %{$i}; |
|
119 |
$::form->{title} ||= "Add"; |
|
120 |
$::form->{paidaccounts} = 1 if undef $::form->{paidaccounts}; |
|
165 | 121 |
|
166 |
my $net_total = $::form->format_amount(\%::myconfig, $item{'subtotal'}, 2); |
|
167 |
my $desc = $item{'description'}; |
|
168 |
my $tax_rate = $item{'tax_rate'} / 100; # XML data is usually in percent |
|
169 |
|
|
170 |
my $active_taxkey = $default_ap_amount_chart->taxkey_id; |
|
171 |
my $taxes = SL::DB::Manager::Tax->get_all( |
|
172 |
where => [ chart_categories => { like => '%' . $default_ap_amount_chart->category . '%' }], |
|
173 |
sort_by => 'taxkey, rate', |
|
174 |
); |
|
175 |
|
|
176 |
my $tax = first { $tax_rate == $_->rate } @{ $taxes }; |
|
177 |
$tax //= first { $active_taxkey->tax_id == $_->id } @{ $taxes }; |
|
178 |
$tax //= $taxes->[0]; |
|
179 |
|
|
180 |
# If we really can't find any tax definition (a simple rounding error may |
|
181 |
# be sufficient for that to happen), grab the first tax fitting the default |
|
182 |
# AP amount chart, just like the AP form would do it for manual entry. |
|
183 |
if ( scalar @{$taxes} == 0 ) { |
|
184 |
$taxes = SL::DB::Manager::Tax->get_all( |
|
185 |
where => [ chart_categories => { like => '%' . $default_ap_amount_chart->category . '%' } ], |
|
186 |
); |
|
187 |
} |
|
188 |
|
|
189 |
if (!$tax) { |
|
190 |
$row--; |
|
191 |
next; |
|
192 |
} |
|
193 |
|
|
194 |
$::form->{"AP_amount_chart_id_${row}"} = $default_ap_amount_chart->id; |
|
195 |
$::form->{"previous_AP_amount_chart_id_${row}"} = $default_ap_amount_chart->id; |
|
196 |
$::form->{"amount_${row}"} = $net_total; |
|
197 |
$::form->{"taxchart_${row}"} = $tax->id . '--' . $tax->rate; |
|
198 |
} |
|
199 |
|
|
200 |
flash('info', $::locale->text("The ZUGFeRD/Factur-X invoice '#1' has been loaded.", $file_name)); |
|
201 |
|
|
202 |
$::form->{form_validity_token} = SL::DB::ValidityToken->create(scope => SL::DB::ValidityToken::SCOPE_PURCHASE_INVOICE_POST())->token; |
|
203 |
$::form->{rowcount} = $row; |
|
204 |
|
|
205 |
update( |
|
206 |
keep_rows_without_amount => 1, |
|
207 |
dont_add_new_row => 1, |
|
208 |
); |
|
209 |
} |
|
122 |
$::form->{form_validity_token} = SL::DB::ValidityToken->create(scope => SL::DB::ValidityToken::SCOPE_PURCHASE_INVOICE_POST())->token; |
|
123 |
flash('info', $::locale->text( |
|
124 |
"The ZUGFeRD/Factur-X invoice '#1' has been loaded.", $file_name)); |
|
125 |
update( |
|
126 |
keep_rows_without_amount => 1, |
|
127 |
dont_add_new_row => 1, |
|
128 |
); |
|
210 | 129 |
} |
211 | 130 |
|
212 | 131 |
sub load_record_template { |
locale/de/all | ||
---|---|---|
2444 | 2444 |
'No Order items fetched' => 'Keine Auftragspositionen gefunden', |
2445 | 2445 |
'No Shopdescription' => 'Keine Shop-Artikelbeschreibung', |
2446 | 2446 |
'No Shopimages' => 'Keine Shop-Bilder', |
2447 |
'No VAT Info for this Factur-X/ZUGFeRD invoice, please ask your vendor to add this for his Factur-X/ZUGFeRD data.' => 'Keine USt-Info für diese Factur-X/ZUGFeRD Rechnung, bitte fragen Sie Ihren Lieferanten, diese für seine Factur-X/ZUGFeRD Daten hinzuzufügen.', |
|
2447 | 2448 |
'No Vendor' => 'Kein Lieferant', |
2448 | 2449 |
'No Vendor was found matching the search parameters.' => 'Zu dem Suchbegriff wurde kein Händler gefunden', |
2449 | 2450 |
'No account selected. Please select an account.' => 'Kein Konto ausgewählt. Bitte Konto auswählen.', |
... | ... | |
2523 | 2524 |
'No such job #1 in the database.' => 'Hintergrund-Job #1 existiert nicht mehr.', |
2524 | 2525 |
'No summary account' => 'Kein Sammelkonto', |
2525 | 2526 |
'No superuser credentials were entered.' => 'Es wurden keine Super-Benutzer-Anmeldedaten eingegeben.', |
2527 |
'No tax found for chart #1' => 'Keine Steuer für Konto #1 gefunden', |
|
2526 | 2528 |
'No template has been selected yet.' => 'Es wurde noch keine Vorlage ausgewählt.', |
2527 | 2529 |
'No text blocks have been created for this position.' => 'Für diese Position wurden noch keine Textblöcke angelegt.', |
2528 | 2530 |
'No text has been entered yet.' => 'Es wurde noch kein Text eingegeben.', |
... | ... | |
2834 | 2836 |
'Pictures for search parts' => 'Bilder für Warensuche', |
2835 | 2837 |
'Please Check the bank information for each customer:' => 'Bitte überprüfen Sie die Bankinformationen der Kunden:', |
2836 | 2838 |
'Please Check the bank information for each vendor:' => 'Bitte überprüfen Sie die Kontoinformationen der Lieferanten:', |
2839 |
'Please add a valid VAT-ID for this vendor: #1' => 'Bitte fügen Sie eine gültige USt-ID für diesen Lieferanten hinzu: #1', |
|
2837 | 2840 |
'Please ask your administrator to create warehouses and bins.' => 'Bitten Sie Ihren Administrator, dass er Lager und Lagerplätze anlegt.', |
2838 | 2841 |
'Please change the partnumber of the following parts and run the update again:' => 'Bitte ändern Sie daher die Artikelnummer folgender Artikel:', |
2839 | 2842 |
'Please choose a part.' => 'Bitte wählen Sie einen Artikel aus.', |
... | ... | |
3154 | 3157 |
'Reconcile' => 'Abgleichen', |
3155 | 3158 |
'Reconciliation' => 'Kontenabgleich', |
3156 | 3159 |
'Reconciliation with bank' => 'Kontenabgleich mit Bank', |
3160 |
'Record IBAN #1 doesn\'t match vendor IBAN #2' => 'Beleg IBAN #1 stimmt nicht mit Lieferanten IBAN #2 überein', |
|
3157 | 3161 |
'Record Type' => 'Belegtyp', |
3158 | 3162 |
'Record Vendor Invoice' => 'Einkaufsrechnung erfassen', |
3159 | 3163 |
'Record in' => 'Buchen auf', |
... | ... | |
4846 | 4850 |
'Your import is being processed.' => 'Ihr Import wird verarbeitet', |
4847 | 4851 |
'Your target quantity will be added to the stocked quantity.' => 'Ihre gezählte Zielmenge wird zum Lagerbestand hinzugezählt.', |
4848 | 4852 |
'ZIPcode' => 'PLZ', |
4853 |
'ZUGFeRD Import. Type: #1' => 'ZUGFeRD Import. Typ: #1', |
|
4849 | 4854 |
'Zeitraum' => 'Zeitraum', |
4850 | 4855 |
'Zero amount posting!' => 'Buchung ohne Wert', |
4851 | 4856 |
'Zip' => 'PLZ', |
locale/en/all | ||
---|---|---|
2432 | 2432 |
'Next run at' => '', |
2433 | 2433 |
'No' => '', |
2434 | 2434 |
'No 1:n or n:1 relation' => '', |
2435 |
'No AP Record Template for vendor #1 found, please add one' => '', |
|
2435 | 2436 |
'No AP template was found.' => '', |
2436 | 2437 |
'No Billing and ship to address, for Order Number #1 with ID Billing #2 and ID Shipping #3' => '', |
2437 | 2438 |
'No Company Address given' => '', |
... | ... | |
2522 | 2523 |
'No such job #1 in the database.' => '', |
2523 | 2524 |
'No summary account' => '', |
2524 | 2525 |
'No superuser credentials were entered.' => '', |
2526 |
'No tax found for chart #1' => '', |
|
2525 | 2527 |
'No template has been selected yet.' => '', |
2526 | 2528 |
'No text blocks have been created for this position.' => '', |
2527 | 2529 |
'No text has been entered yet.' => '', |
... | ... | |
3153 | 3155 |
'Reconcile' => '', |
3154 | 3156 |
'Reconciliation' => '', |
3155 | 3157 |
'Reconciliation with bank' => '', |
3158 |
'Record IBAN #1 doesn\'t match vendor IBAN #2' => '', |
|
3156 | 3159 |
'Record Type' => '', |
3157 | 3160 |
'Record Vendor Invoice' => '', |
3158 | 3161 |
'Record in' => '', |
... | ... | |
4844 | 4847 |
'Your import is being processed.' => '', |
4845 | 4848 |
'Your target quantity will be added to the stocked quantity.' => '', |
4846 | 4849 |
'ZIPcode' => '', |
4850 |
'ZUGFeRD Import. Type: #1' => '', |
|
4847 | 4851 |
'Zeitraum' => '', |
4848 | 4852 |
'Zero amount posting!' => '', |
4849 | 4853 |
'Zip' => '', |
Auch abrufbar als: Unified diff
ZUGFeRD: Verschiebe komplettes Parsen aus ap.pl nach S:C:ZUGFeRD