Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 231ea4d9

Von Tamino Steinert vor etwa 1 Jahr hinzugefügt

  • ID 231ea4d9c7649e9a7c4330594fb6dd5c2b311eaa
  • Vorgänger 26dbf59b
  • Nachfolger b0d95d74

ZUGFeRD: Anpassung nach Rebase: Kreditorenbuchung direkt aus ZUGFeRD-XML

Unterschiede anzeigen:

SL/DB/Helper/ZUGFeRD.pm
use SL::ZUGFeRD qw(:PROFILES);
use SL::Locale::String qw(t8);
use SL::Controller::ZUGFeRD;
use Carp;
use Encode qw(encode);
use List::MoreUtils qw(any pairwise);
......
};
}
sub import_zugferd_xml {
my ($self, $zugferd_xml) = @_;
# 1. check if ZUGFeRD SellerTradeParty has a VAT-ID
my $ustid = $zugferd_xml->findnodes(
'//ram:SellerTradeParty/ram:SpecifiedTaxRegistration'
)->string_value;
die t8("No VAT Info for this Factur-X/ZUGFeRD invoice," .
" please ask your vendor to add this for his Factur-X/ZUGFeRD data.") unless $ustid;
$ustid = SL::VATIDNr->normalize($ustid);
# 1.1 check if we a have a vendor with this VAT-ID (vendor.ustid)
my $vendor_name = $zugferd_xml->findnodes('//ram:SellerTradeParty/ram:Name')->string_value;
my $vendor = SL::DB::Manager::Vendor->find_by(
ustid => $ustid,
or => [
obsolete => undef,
obsolete => 0,
]);
if (!$vendor) {
# 1.2 If no vendor with the exact VAT ID number is found, the
# number might be stored slightly different in the database
# (e.g. with spaces breaking up groups of numbers). Iterate over
# all existing vendors with VAT ID numbers, normalize their
# representation and compare those.
my $vendors = SL::DB::Manager::Vendor->get_all(
where => [
'!ustid' => undef,
'!ustid' => '',
or => [
obsolete => undef,
obsolete => 0,
],
]);
foreach my $other_vendor (@{ $vendors }) {
next unless SL::VATIDNr->normalize($other_vendor->ustid) eq $ustid;
$vendor = $other_vendor;
last;
}
sub import_zugferd_data {
my ($self, $zugferd_data) = @_;
my $parser = $zugferd_data->{'invoice_xml'};
my %metadata = %{$parser->metadata};
my @items = @{$parser->items};
my $notes = t8("ZUGFeRD Import. Type: #1", $metadata{'type'});
my $iban = $metadata{'iban'};
my $invnumber = $metadata{'invnumber'};
if ( ! ($metadata{'ustid'} or $metadata{'taxnumber'}) ) {
die t8("Cannot process this invoice: neither VAT ID nor tax ID present.");
}
die t8("Please add a valid VAT-ID for this vendor: #1", $vendor_name)
unless (ref $vendor eq 'SL::DB::Vendor');
my $vendor = SL::Controller::ZUGFeRD::find_vendor($metadata{'ustid'}, $metadata{'taxnumber'});
# 2. check if we have a ap record template for this vendor (TODO only the oldest template is choosen)
my $template_ap = SL::DB::Manager::RecordTemplate->get_first(where => [vendor_id => $vendor->id]);
die t8("No AP Record Template for vendor #1 found, please add one", $vendor_name)
unless (ref $template_ap eq 'SL::DB::RecordTemplate');
die t8("Vendor with VAT ID (#1) and/or tax ID (#2) not found. Please check if the vendor " .
"#3 exists and whether it has the correct tax ID/VAT ID." ,
$metadata{'ustid'},
$metadata{'taxnumber'},
$metadata{'vendor_name'},
) unless $vendor;
# 3. parse the zugferd data and fill the ap record template
# -> no need to check sign (credit notes will be negative) just record thei ZUGFeRD type in ap.notes
# -> check direct debit (defaults to no)
# -> set amount (net amount) and unset taxincluded
# (template and user cares for tax and if there is more than one booking accno)
# -> date (can be empty)
# -> duedate (may be empty)
# -> compare record iban and generate a warning if this differs from vendor's master data iban
my $total = $zugferd_xml->findnodes(
'//ram:SpecifiedTradeSettlementHeaderMonetarySummation' .
'/ram:TaxBasisTotalAmount'
)->string_value;
# Check IBAN specified on bill matches the one we've got in
# the database for this vendor.
if ($iban) {
$notes .= "\nIBAN: ";
$notes .= $iban ne $vendor->iban ?
t8("Record IBAN #1 doesn't match vendor IBAN #2", $iban, $vendor->iban)
: $iban
}
my $invnumber = $zugferd_xml->findnodes(
'//rsm:ExchangedDocument/ram:ID'
)->string_value;
# Use invoice creation date as due date if there's no due date
$metadata{'duedate'} = $metadata{'transdate'} unless defined $metadata{'duedate'};
# parse dates to kivi if set/valid
my %dates = (
transdate => {
key => '//ram:IssueDateTime',
value => undef,
},
duedate => {
key => '//ram:DueDateDateTime',
value => undef,
},
);
foreach my $date (keys %dates) {
my $string_value = $zugferd_xml->findnodes($dates{$date}->{key})->string_value;
$string_value =~ s/^\s+|\s+$//g;
if ($string_value =~ /^[0-9]{8}$/) {
$dates{$date}->{value} = DateTime->new(
year => substr($string_value,0,4),
month => substr ($string_value,4,2),
day => substr($string_value,6,2)
)->to_kivitendo;
foreach my $key ( qw(transdate duedate) ) {
next unless defined $metadata{$key};
$metadata{$key} =~ s/^\s+|\s+$//g;
if ($metadata{$key} =~ /^([0-9]{4})-?([0-9]{2})-?([0-9]{2})$/) {
$metadata{$key} = DateTime->new(year => $1,
month => $2,
day => $3)->to_kivitendo;
}
}
my $transdate = $dates{transdate}->{value};
my $duedate = $dates{duedate}->{value};
my $type = $zugferd_xml->findnodes(
'//rsm:ExchangedDocument/ram:TypeCode'
)->string_value;
my $dd = $zugferd_xml->findnodes(
'//ram:ApplicableHeaderTradeSettlement' .
'/ram:SpecifiedTradeSettlementPaymentMeans/ram:TypeCode'
)->string_value;
my $direct_debit = $dd == 59 ? 1 : 0;
my $iban = $zugferd_xml->findnodes(
'//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementPaymentMeans' .
'/ram:PayeePartyCreditorFinancialAccount/ram:IBANID'
)->string_value;
my $ibanmessage;
$ibanmessage = $iban ne $vendor->iban ?
"Record IBAN $iban doesn't match vendor IBAN " . $vendor->iban
: $iban if $iban;
# write values to self
my $today = DateTime->today_local;
my $additional_notes = "ZUGFeRD Import. Type: $type\nIBAN: " . $ibanmessage;
# Try to fill in AP account to book against
my $ap_chart_id = $::instance_conf->get_ap_chart_id;
unless ( defined $ap_chart_id ) {
# If no default account is configured, just use the first AP account found.
my $ap_chart = SL::DB::Manager::Chart->get_all(
where => [ link => 'AP' ],
sort_by => [ 'accno' ],
);
$ap_chart_id = ${$ap_chart}[0]->id;
}
my $currency = SL::DB::Manager::Currency->find_by(
name => $metadata{'currency'},
);
my $default_ap_amount_chart = SL::DB::Manager::Chart->find_by(
id => $::instance_conf->get_expense_accno_id
);
# Fallback if there's no default AP amount chart configured
$default_ap_amount_chart ||= SL::DB::Manager::Chart->find_by(charttype => 'A');
my $active_taxkey = $default_ap_amount_chart->taxkey_id;
my $taxes = SL::DB::Manager::Tax->get_all(
where => [ chart_categories => {
like => '%' . $default_ap_amount_chart->category . '%'
}],
sort_by => 'taxkey, rate',
);
die t8(
"No tax found for chart #1", $default_ap_amount_chart->displayable_name
) unless scalar @{$taxes};
my $today = DateTime->today_local;
my %params = (
invoice => 0,
vendor_id => $vendor->id,
vendor_id => $vendor->id,
taxzone_id => $vendor->taxzone_id,
currency_id => $template_ap->currency_id,
direct_debit => $direct_debit,
globalproject_id => $template_ap->project_id,
payment_id => $template_ap->payment_id,
currency_id => $currency->id,
direct_debit => $metadata{'direct_debit'},
# globalproject_id => $template_ap->project_id,
# payment_id => $template_ap->payment_id,
invnumber => $invnumber,
transdate => $transdate || $today->to_kivitendo,
duedate => $duedate || (
$template_ap->vendor->payment ?
$template_ap->vendor->payment->calc_date(reference_date => $today)->to_kivitendo
: $today->to_kivitendo
),
department_id => $template_ap->department_id,
ordnumber => $template_ap->ordnumber,
transdate => $metadata{transdate} || $today->to_kivitendo,
duedate => $metadata{duedate} || $today->to_kivitendo,
# department_id => $template_ap->department_id,
# ordnumber => $template_ap->ordnumber,
taxincluded => 0,
notes => join("\n", $template_ap->notes, $additional_notes),
notes => $notes,
transactions => [],
transaction_description => $template_ap->transaction_description,
# transaction_description => $template_ap->transaction_description,
);
$self->assign_attributes(%params);
foreach my $template_item (@{$template_ap->items}) {
# parse items
foreach my $i (@items) {
my %item = %{$i};
my $net_total = $::form->format_amount(\%::myconfig, $item{'subtotal'}, 2);
my $tax_rate = $item{'tax_rate'};
$tax_rate /= 100 if $tax_rate > 1; # XML data is usually in percent
my $tax = first { $tax_rate == $_->rate } @{ $taxes };
$tax //= first { $active_taxkey->tax_id == $_->id } @{ $taxes };
$tax //= $taxes->[0];
my %line_params = (
amount => $total,
project_id => $template_item->project_id,
tax_id => $template_item->tax_id,
chart => $template_item->chart,
amount => $net_total,
tax_id => $tax->id,
chart => $default_ap_amount_chart->chart,
);
$self->add_ap_amount_row(%line_params);
}
$self->recalculate_amounts();
my $ap_chart = SL::DB::Manager::Chart->get_first(
where => [id => $template_ap->ar_ap_chart_id]
);
$self->create_ap_row(chart => $ap_chart);
$self->create_ap_row(chart => $default_ap_amount_chart);
return $self;
}
SL/DB/PurchaseInvoice.pm
return $reclamation;
}
sub create_from_zugferd_xml {
my ($class, $zugferd_xml) = @_;
sub create_from_zugferd_data {
my ($class, $data) = @_;
my $ap_invoice = $class->new();
$ap_invoice->import_zugferd_xml($zugferd_xml);
$ap_invoice->import_zugferd_data($data);
}
sub create_ap_row {

Auch abrufbar als: Unified diff