25 |
25 |
use SL::ZUGFeRD qw(:PROFILES);
|
26 |
26 |
use SL::Locale::String qw(t8);
|
27 |
27 |
|
|
28 |
use SL::Controller::ZUGFeRD;
|
|
29 |
|
28 |
30 |
use Carp;
|
29 |
31 |
use Encode qw(encode);
|
30 |
32 |
use List::MoreUtils qw(any pairwise);
|
... | ... | |
709 |
711 |
};
|
710 |
712 |
}
|
711 |
713 |
|
712 |
|
sub import_zugferd_xml {
|
713 |
|
my ($self, $zugferd_xml) = @_;
|
714 |
|
|
715 |
|
# 1. check if ZUGFeRD SellerTradeParty has a VAT-ID
|
716 |
|
my $ustid = $zugferd_xml->findnodes(
|
717 |
|
'//ram:SellerTradeParty/ram:SpecifiedTaxRegistration'
|
718 |
|
)->string_value;
|
719 |
|
die t8("No VAT Info for this Factur-X/ZUGFeRD invoice," .
|
720 |
|
" please ask your vendor to add this for his Factur-X/ZUGFeRD data.") unless $ustid;
|
721 |
|
|
722 |
|
$ustid = SL::VATIDNr->normalize($ustid);
|
723 |
|
|
724 |
|
# 1.1 check if we a have a vendor with this VAT-ID (vendor.ustid)
|
725 |
|
my $vendor_name = $zugferd_xml->findnodes('//ram:SellerTradeParty/ram:Name')->string_value;
|
726 |
|
my $vendor = SL::DB::Manager::Vendor->find_by(
|
727 |
|
ustid => $ustid,
|
728 |
|
or => [
|
729 |
|
obsolete => undef,
|
730 |
|
obsolete => 0,
|
731 |
|
]);
|
732 |
|
|
733 |
|
if (!$vendor) {
|
734 |
|
# 1.2 If no vendor with the exact VAT ID number is found, the
|
735 |
|
# number might be stored slightly different in the database
|
736 |
|
# (e.g. with spaces breaking up groups of numbers). Iterate over
|
737 |
|
# all existing vendors with VAT ID numbers, normalize their
|
738 |
|
# representation and compare those.
|
739 |
|
|
740 |
|
my $vendors = SL::DB::Manager::Vendor->get_all(
|
741 |
|
where => [
|
742 |
|
'!ustid' => undef,
|
743 |
|
'!ustid' => '',
|
744 |
|
or => [
|
745 |
|
obsolete => undef,
|
746 |
|
obsolete => 0,
|
747 |
|
],
|
748 |
|
]);
|
749 |
|
|
750 |
|
foreach my $other_vendor (@{ $vendors }) {
|
751 |
|
next unless SL::VATIDNr->normalize($other_vendor->ustid) eq $ustid;
|
752 |
|
|
753 |
|
$vendor = $other_vendor;
|
754 |
|
last;
|
755 |
|
}
|
|
714 |
sub import_zugferd_data {
|
|
715 |
my ($self, $zugferd_data) = @_;
|
|
716 |
|
|
717 |
my $parser = $zugferd_data->{'invoice_xml'};
|
|
718 |
|
|
719 |
my %metadata = %{$parser->metadata};
|
|
720 |
my @items = @{$parser->items};
|
|
721 |
|
|
722 |
my $notes = t8("ZUGFeRD Import. Type: #1", $metadata{'type'});
|
|
723 |
my $iban = $metadata{'iban'};
|
|
724 |
my $invnumber = $metadata{'invnumber'};
|
|
725 |
|
|
726 |
if ( ! ($metadata{'ustid'} or $metadata{'taxnumber'}) ) {
|
|
727 |
die t8("Cannot process this invoice: neither VAT ID nor tax ID present.");
|
756 |
728 |
}
|
757 |
729 |
|
758 |
|
die t8("Please add a valid VAT-ID for this vendor: #1", $vendor_name)
|
759 |
|
unless (ref $vendor eq 'SL::DB::Vendor');
|
|
730 |
my $vendor = SL::Controller::ZUGFeRD::find_vendor($metadata{'ustid'}, $metadata{'taxnumber'});
|
760 |
731 |
|
761 |
|
# 2. check if we have a ap record template for this vendor (TODO only the oldest template is choosen)
|
762 |
|
my $template_ap = SL::DB::Manager::RecordTemplate->get_first(where => [vendor_id => $vendor->id]);
|
763 |
|
die t8("No AP Record Template for vendor #1 found, please add one", $vendor_name)
|
764 |
|
unless (ref $template_ap eq 'SL::DB::RecordTemplate');
|
|
732 |
die t8("Vendor with VAT ID (#1) and/or tax ID (#2) not found. Please check if the vendor " .
|
|
733 |
"#3 exists and whether it has the correct tax ID/VAT ID." ,
|
|
734 |
$metadata{'ustid'},
|
|
735 |
$metadata{'taxnumber'},
|
|
736 |
$metadata{'vendor_name'},
|
|
737 |
) unless $vendor;
|
765 |
738 |
|
766 |
739 |
|
767 |
|
# 3. parse the zugferd data and fill the ap record template
|
768 |
|
# -> no need to check sign (credit notes will be negative) just record thei ZUGFeRD type in ap.notes
|
769 |
|
# -> check direct debit (defaults to no)
|
770 |
|
# -> set amount (net amount) and unset taxincluded
|
771 |
|
# (template and user cares for tax and if there is more than one booking accno)
|
772 |
|
# -> date (can be empty)
|
773 |
|
# -> duedate (may be empty)
|
774 |
|
# -> compare record iban and generate a warning if this differs from vendor's master data iban
|
775 |
|
my $total = $zugferd_xml->findnodes(
|
776 |
|
'//ram:SpecifiedTradeSettlementHeaderMonetarySummation' .
|
777 |
|
'/ram:TaxBasisTotalAmount'
|
778 |
|
)->string_value;
|
|
740 |
# Check IBAN specified on bill matches the one we've got in
|
|
741 |
# the database for this vendor.
|
|
742 |
if ($iban) {
|
|
743 |
$notes .= "\nIBAN: ";
|
|
744 |
$notes .= $iban ne $vendor->iban ?
|
|
745 |
t8("Record IBAN #1 doesn't match vendor IBAN #2", $iban, $vendor->iban)
|
|
746 |
: $iban
|
|
747 |
}
|
779 |
748 |
|
780 |
|
my $invnumber = $zugferd_xml->findnodes(
|
781 |
|
'//rsm:ExchangedDocument/ram:ID'
|
782 |
|
)->string_value;
|
|
749 |
# Use invoice creation date as due date if there's no due date
|
|
750 |
$metadata{'duedate'} = $metadata{'transdate'} unless defined $metadata{'duedate'};
|
783 |
751 |
|
784 |
752 |
# parse dates to kivi if set/valid
|
785 |
|
my %dates = (
|
786 |
|
transdate => {
|
787 |
|
key => '//ram:IssueDateTime',
|
788 |
|
value => undef,
|
789 |
|
},
|
790 |
|
duedate => {
|
791 |
|
key => '//ram:DueDateDateTime',
|
792 |
|
value => undef,
|
793 |
|
},
|
794 |
|
);
|
795 |
|
foreach my $date (keys %dates) {
|
796 |
|
my $string_value = $zugferd_xml->findnodes($dates{$date}->{key})->string_value;
|
797 |
|
$string_value =~ s/^\s+|\s+$//g;
|
798 |
|
if ($string_value =~ /^[0-9]{8}$/) {
|
799 |
|
$dates{$date}->{value} = DateTime->new(
|
800 |
|
year => substr($string_value,0,4),
|
801 |
|
month => substr ($string_value,4,2),
|
802 |
|
day => substr($string_value,6,2)
|
803 |
|
)->to_kivitendo;
|
|
753 |
foreach my $key ( qw(transdate duedate) ) {
|
|
754 |
next unless defined $metadata{$key};
|
|
755 |
$metadata{$key} =~ s/^\s+|\s+$//g;
|
|
756 |
|
|
757 |
if ($metadata{$key} =~ /^([0-9]{4})-?([0-9]{2})-?([0-9]{2})$/) {
|
|
758 |
$metadata{$key} = DateTime->new(year => $1,
|
|
759 |
month => $2,
|
|
760 |
day => $3)->to_kivitendo;
|
804 |
761 |
}
|
805 |
762 |
}
|
806 |
|
my $transdate = $dates{transdate}->{value};
|
807 |
|
my $duedate = $dates{duedate}->{value};
|
808 |
|
|
809 |
|
my $type = $zugferd_xml->findnodes(
|
810 |
|
'//rsm:ExchangedDocument/ram:TypeCode'
|
811 |
|
)->string_value;
|
812 |
|
|
813 |
|
my $dd = $zugferd_xml->findnodes(
|
814 |
|
'//ram:ApplicableHeaderTradeSettlement' .
|
815 |
|
'/ram:SpecifiedTradeSettlementPaymentMeans/ram:TypeCode'
|
816 |
|
)->string_value;
|
817 |
|
my $direct_debit = $dd == 59 ? 1 : 0;
|
818 |
|
|
819 |
|
my $iban = $zugferd_xml->findnodes(
|
820 |
|
'//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementPaymentMeans' .
|
821 |
|
'/ram:PayeePartyCreditorFinancialAccount/ram:IBANID'
|
822 |
|
)->string_value;
|
823 |
|
|
824 |
|
my $ibanmessage;
|
825 |
|
$ibanmessage = $iban ne $vendor->iban ?
|
826 |
|
"Record IBAN $iban doesn't match vendor IBAN " . $vendor->iban
|
827 |
|
: $iban if $iban;
|
828 |
|
|
829 |
|
# write values to self
|
830 |
|
my $today = DateTime->today_local;
|
831 |
763 |
|
832 |
|
my $additional_notes = "ZUGFeRD Import. Type: $type\nIBAN: " . $ibanmessage;
|
|
764 |
# Try to fill in AP account to book against
|
|
765 |
my $ap_chart_id = $::instance_conf->get_ap_chart_id;
|
|
766 |
|
|
767 |
unless ( defined $ap_chart_id ) {
|
|
768 |
# If no default account is configured, just use the first AP account found.
|
|
769 |
my $ap_chart = SL::DB::Manager::Chart->get_all(
|
|
770 |
where => [ link => 'AP' ],
|
|
771 |
sort_by => [ 'accno' ],
|
|
772 |
);
|
|
773 |
$ap_chart_id = ${$ap_chart}[0]->id;
|
|
774 |
}
|
833 |
775 |
|
|
776 |
my $currency = SL::DB::Manager::Currency->find_by(
|
|
777 |
name => $metadata{'currency'},
|
|
778 |
);
|
|
779 |
|
|
780 |
my $default_ap_amount_chart = SL::DB::Manager::Chart->find_by(
|
|
781 |
id => $::instance_conf->get_expense_accno_id
|
|
782 |
);
|
|
783 |
# Fallback if there's no default AP amount chart configured
|
|
784 |
$default_ap_amount_chart ||= SL::DB::Manager::Chart->find_by(charttype => 'A');
|
|
785 |
|
|
786 |
my $active_taxkey = $default_ap_amount_chart->taxkey_id;
|
|
787 |
my $taxes = SL::DB::Manager::Tax->get_all(
|
|
788 |
where => [ chart_categories => {
|
|
789 |
like => '%' . $default_ap_amount_chart->category . '%'
|
|
790 |
}],
|
|
791 |
sort_by => 'taxkey, rate',
|
|
792 |
);
|
|
793 |
die t8(
|
|
794 |
"No tax found for chart #1", $default_ap_amount_chart->displayable_name
|
|
795 |
) unless scalar @{$taxes};
|
|
796 |
|
|
797 |
|
|
798 |
my $today = DateTime->today_local;
|
834 |
799 |
my %params = (
|
835 |
800 |
invoice => 0,
|
836 |
|
vendor_id => $vendor->id,
|
|
801 |
vendor_id => $vendor->id,
|
837 |
802 |
taxzone_id => $vendor->taxzone_id,
|
838 |
|
currency_id => $template_ap->currency_id,
|
839 |
|
direct_debit => $direct_debit,
|
840 |
|
globalproject_id => $template_ap->project_id,
|
841 |
|
payment_id => $template_ap->payment_id,
|
|
803 |
currency_id => $currency->id,
|
|
804 |
direct_debit => $metadata{'direct_debit'},
|
|
805 |
# globalproject_id => $template_ap->project_id,
|
|
806 |
# payment_id => $template_ap->payment_id,
|
842 |
807 |
invnumber => $invnumber,
|
843 |
|
transdate => $transdate || $today->to_kivitendo,
|
844 |
|
duedate => $duedate || (
|
845 |
|
$template_ap->vendor->payment ?
|
846 |
|
$template_ap->vendor->payment->calc_date(reference_date => $today)->to_kivitendo
|
847 |
|
: $today->to_kivitendo
|
848 |
|
),
|
849 |
|
department_id => $template_ap->department_id,
|
850 |
|
ordnumber => $template_ap->ordnumber,
|
|
808 |
transdate => $metadata{transdate} || $today->to_kivitendo,
|
|
809 |
duedate => $metadata{duedate} || $today->to_kivitendo,
|
|
810 |
# department_id => $template_ap->department_id,
|
|
811 |
# ordnumber => $template_ap->ordnumber,
|
851 |
812 |
taxincluded => 0,
|
852 |
|
notes => join("\n", $template_ap->notes, $additional_notes),
|
|
813 |
notes => $notes,
|
853 |
814 |
transactions => [],
|
854 |
|
transaction_description => $template_ap->transaction_description,
|
|
815 |
# transaction_description => $template_ap->transaction_description,
|
855 |
816 |
);
|
856 |
817 |
|
857 |
818 |
$self->assign_attributes(%params);
|
858 |
819 |
|
859 |
|
foreach my $template_item (@{$template_ap->items}) {
|
|
820 |
# parse items
|
|
821 |
foreach my $i (@items) {
|
|
822 |
my %item = %{$i};
|
|
823 |
|
|
824 |
my $net_total = $::form->format_amount(\%::myconfig, $item{'subtotal'}, 2);
|
|
825 |
|
|
826 |
my $tax_rate = $item{'tax_rate'};
|
|
827 |
$tax_rate /= 100 if $tax_rate > 1; # XML data is usually in percent
|
|
828 |
|
|
829 |
my $tax = first { $tax_rate == $_->rate } @{ $taxes };
|
|
830 |
$tax //= first { $active_taxkey->tax_id == $_->id } @{ $taxes };
|
|
831 |
$tax //= $taxes->[0];
|
|
832 |
|
860 |
833 |
my %line_params = (
|
861 |
|
amount => $total,
|
862 |
|
project_id => $template_item->project_id,
|
863 |
|
tax_id => $template_item->tax_id,
|
864 |
|
chart => $template_item->chart,
|
|
834 |
amount => $net_total,
|
|
835 |
tax_id => $tax->id,
|
|
836 |
chart => $default_ap_amount_chart->chart,
|
865 |
837 |
);
|
866 |
838 |
|
867 |
839 |
$self->add_ap_amount_row(%line_params);
|
868 |
840 |
}
|
869 |
841 |
$self->recalculate_amounts();
|
870 |
842 |
|
871 |
|
my $ap_chart = SL::DB::Manager::Chart->get_first(
|
872 |
|
where => [id => $template_ap->ar_ap_chart_id]
|
873 |
|
);
|
874 |
|
$self->create_ap_row(chart => $ap_chart);
|
|
843 |
$self->create_ap_row(chart => $default_ap_amount_chart);
|
875 |
844 |
|
876 |
845 |
return $self;
|
877 |
846 |
}
|
ZUGFeRD: Anpassung nach Rebase: Kreditorenbuchung direkt aus ZUGFeRD-XML