Revision f58e9600
Von Tamino Steinert vor 11 Monaten hinzugefügt
SL/DB/Helper/ZUGFeRD.pm | ||
---|---|---|
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 |
} |
Auch abrufbar als: Unified diff
ZUGFeRD: Anpassung nach Rebase: Kreditorenbuchung direkt aus ZUGFeRD-XML