4 |
4 |
use utf8;
|
5 |
5 |
|
6 |
6 |
use parent qw(Exporter);
|
7 |
|
our @EXPORT = qw(create_zugferd_data create_zugferd_xmp_data);
|
|
7 |
our @EXPORT_CREATE = qw(create_zugferd_data create_zugferd_xmp_data);
|
|
8 |
our @EXPORT_IMPORT = qw(import_zugferd_xml);
|
|
9 |
our @EXPORT_OK = (@EXPORT_CREATE, @EXPORT_IMPORT);
|
|
10 |
our %EXPORT_TAGS = (
|
|
11 |
ALL => (@EXPORT_CREATE, @EXPORT_IMPORT),
|
|
12 |
CREATE => \@EXPORT_CREATE,
|
|
13 |
IMPORT => \@EXPORT_IMPORT,
|
|
14 |
);
|
8 |
15 |
|
9 |
16 |
use SL::DB::BankAccount;
|
10 |
17 |
use SL::DB::GenericTranslation;
|
11 |
18 |
use SL::DB::Tax;
|
12 |
19 |
use SL::DB::TaxKey;
|
|
20 |
use SL::DB::RecordTemplate;
|
13 |
21 |
use SL::Helper::ISO3166;
|
14 |
22 |
use SL::Helper::ISO4217;
|
15 |
23 |
use SL::Helper::UNECERecommendation20;
|
... | ... | |
700 |
708 |
};
|
701 |
709 |
}
|
702 |
710 |
|
|
711 |
sub import_zugferd_xml {
|
|
712 |
my ($self, $zugferd_xml) = @_;
|
|
713 |
|
|
714 |
# 1. check if ZUGFeRD SellerTradeParty has a VAT-ID
|
|
715 |
my $ustid = $zugferd_xml->findnodes(
|
|
716 |
'//ram:SellerTradeParty/ram:SpecifiedTaxRegistration'
|
|
717 |
)->string_value;
|
|
718 |
die t8("No VAT Info for this Factur-X/ZUGFeRD invoice," .
|
|
719 |
" please ask your vendor to add this for his Factur-X/ZUGFeRD data.") unless $ustid;
|
|
720 |
|
|
721 |
$ustid = SL::VATIDNr->normalize($ustid);
|
|
722 |
|
|
723 |
# 1.1 check if we a have a vendor with this VAT-ID (vendor.ustid)
|
|
724 |
my $vendor_name = $zugferd_xml->findnodes('//ram:SellerTradeParty/ram:Name')->string_value;
|
|
725 |
my $vendor = SL::DB::Manager::Vendor->find_by(
|
|
726 |
ustid => $ustid,
|
|
727 |
or => [
|
|
728 |
obsolete => undef,
|
|
729 |
obsolete => 0,
|
|
730 |
]);
|
|
731 |
|
|
732 |
if (!$vendor) {
|
|
733 |
# 1.2 If no vendor with the exact VAT ID number is found, the
|
|
734 |
# number might be stored slightly different in the database
|
|
735 |
# (e.g. with spaces breaking up groups of numbers). Iterate over
|
|
736 |
# all existing vendors with VAT ID numbers, normalize their
|
|
737 |
# representation and compare those.
|
|
738 |
|
|
739 |
my $vendors = SL::DB::Manager::Vendor->get_all(
|
|
740 |
where => [
|
|
741 |
'!ustid' => undef,
|
|
742 |
'!ustid' => '',
|
|
743 |
or => [
|
|
744 |
obsolete => undef,
|
|
745 |
obsolete => 0,
|
|
746 |
],
|
|
747 |
]);
|
|
748 |
|
|
749 |
foreach my $other_vendor (@{ $vendors }) {
|
|
750 |
next unless SL::VATIDNr->normalize($other_vendor->ustid) eq $ustid;
|
|
751 |
|
|
752 |
$vendor = $other_vendor;
|
|
753 |
last;
|
|
754 |
}
|
|
755 |
}
|
|
756 |
|
|
757 |
die t8("Please add a valid VAT-ID for this vendor: #1", $vendor_name)
|
|
758 |
unless (ref $vendor eq 'SL::DB::Vendor');
|
|
759 |
|
|
760 |
# 2. check if we have a ap record template for this vendor (TODO only the oldest template is choosen)
|
|
761 |
my $template_ap = SL::DB::Manager::RecordTemplate->get_first(where => [vendor_id => $vendor->id]);
|
|
762 |
die t8("No AP Record Template for vendor #1 found, please add one", $vendor_name)
|
|
763 |
unless (ref $template_ap eq 'SL::DB::RecordTemplate');
|
|
764 |
|
|
765 |
|
|
766 |
# 3. parse the zugferd data and fill the ap record template
|
|
767 |
# -> no need to check sign (credit notes will be negative) just record thei ZUGFeRD type in ap.notes
|
|
768 |
# -> check direct debit (defaults to no)
|
|
769 |
# -> set amount (net amount) and unset taxincluded
|
|
770 |
# (template and user cares for tax and if there is more than one booking accno)
|
|
771 |
# -> date (can be empty)
|
|
772 |
# -> duedate (may be empty)
|
|
773 |
# -> compare record iban and generate a warning if this differs from vendor's master data iban
|
|
774 |
my $total = $zugferd_xml->findnodes(
|
|
775 |
'//ram:SpecifiedTradeSettlementHeaderMonetarySummation' .
|
|
776 |
'/ram:TaxBasisTotalAmount'
|
|
777 |
)->string_value;
|
|
778 |
|
|
779 |
my $invnumber = $zugferd_xml->findnodes(
|
|
780 |
'//rsm:ExchangedDocument/ram:ID'
|
|
781 |
)->string_value;
|
|
782 |
|
|
783 |
# parse dates to kivi if set/valid
|
|
784 |
my %dates = (
|
|
785 |
transdate => {
|
|
786 |
key => '//ram:IssueDateTime',
|
|
787 |
value => undef,
|
|
788 |
},
|
|
789 |
duedate => {
|
|
790 |
key => '//ram:DueDateDateTime',
|
|
791 |
value => undef,
|
|
792 |
},
|
|
793 |
);
|
|
794 |
foreach my $date (keys %dates) {
|
|
795 |
my $string_value = $zugferd_xml->findnodes($dates{$date}->{key})->string_value;
|
|
796 |
$string_value =~ s/^\s+|\s+$//g;
|
|
797 |
if ($string_value =~ /^[0-9]{8}$/) {
|
|
798 |
$dates{$date}->{value} = DateTime->new(
|
|
799 |
year => substr($string_value,0,4),
|
|
800 |
month => substr ($string_value,4,2),
|
|
801 |
day => substr($string_value,6,2)
|
|
802 |
)->to_kivitendo;
|
|
803 |
}
|
|
804 |
}
|
|
805 |
my $transdate = $dates{transdate}->{value};
|
|
806 |
my $duedate = $dates{duedate}->{value};
|
|
807 |
|
|
808 |
my $type = $zugferd_xml->findnodes(
|
|
809 |
'//rsm:ExchangedDocument/ram:TypeCode'
|
|
810 |
)->string_value;
|
|
811 |
|
|
812 |
my $dd = $zugferd_xml->findnodes(
|
|
813 |
'//ram:ApplicableHeaderTradeSettlement' .
|
|
814 |
'/ram:SpecifiedTradeSettlementPaymentMeans/ram:TypeCode'
|
|
815 |
)->string_value;
|
|
816 |
my $direct_debit = $dd == 59 ? 1 : 0;
|
|
817 |
|
|
818 |
my $iban = $zugferd_xml->findnodes(
|
|
819 |
'//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementPaymentMeans' .
|
|
820 |
'/ram:PayeePartyCreditorFinancialAccount/ram:IBANID'
|
|
821 |
)->string_value;
|
|
822 |
|
|
823 |
my $ibanmessage;
|
|
824 |
$ibanmessage = $iban ne $vendor->iban ?
|
|
825 |
"Record IBAN $iban doesn't match vendor IBAN " . $vendor->iban
|
|
826 |
: $iban if $iban;
|
|
827 |
|
|
828 |
# write values to self
|
|
829 |
my $today = DateTime->today_local;
|
|
830 |
|
|
831 |
my $additional_notes = "ZUGFeRD Import. Type: $type\nIBAN: " . $ibanmessage;
|
|
832 |
|
|
833 |
my %params = (
|
|
834 |
invoice => 0,
|
|
835 |
vendor_id => $vendor->id,
|
|
836 |
taxzone_id => $vendor->taxzone_id,
|
|
837 |
currency_id => $template_ap->currency_id,
|
|
838 |
direct_debit => $direct_debit,
|
|
839 |
globalproject_id => $template_ap->project_id,
|
|
840 |
payment_id => $template_ap->payment_id,
|
|
841 |
invnumber => $invnumber,
|
|
842 |
transdate => $transdate || $today->to_kivitendo,
|
|
843 |
duedate => $duedate || (
|
|
844 |
$template_ap->vendor->payment ?
|
|
845 |
$template_ap->vendor->payment->calc_date(reference_date => $today)->to_kivitendo
|
|
846 |
: $today->to_kivitendo
|
|
847 |
),
|
|
848 |
department_id => $template_ap->department_id,
|
|
849 |
ordnumber => $template_ap->ordnumber,
|
|
850 |
taxincluded => 0,
|
|
851 |
notes => join("\n", $template_ap->notes, $additional_notes),
|
|
852 |
transactions => [],
|
|
853 |
transaction_description => $template_ap->transaction_description,
|
|
854 |
);
|
|
855 |
|
|
856 |
$self->assign_attributes(%params);
|
|
857 |
|
|
858 |
foreach my $template_item (@{$template_ap->items}) {
|
|
859 |
my %line_params = (
|
|
860 |
amount => $total,
|
|
861 |
project_id => $template_item->project_id,
|
|
862 |
tax_id => $template_item->tax_id,
|
|
863 |
chart => $template_item->chart,
|
|
864 |
);
|
|
865 |
|
|
866 |
$self->add_ap_amount_row(%line_params);
|
|
867 |
}
|
|
868 |
$self->recalculate_amounts();
|
|
869 |
|
|
870 |
my $ap_chart = SL::DB::Manager::Chart->get_first(
|
|
871 |
where => [id => $template_ap->ar_ap_chart_id]
|
|
872 |
);
|
|
873 |
$self->create_ap_row(chart => $ap_chart);
|
|
874 |
|
|
875 |
return $self;
|
|
876 |
}
|
|
877 |
|
703 |
878 |
1;
|
ZUGFeRD: Kreditorenbuchung direkt aus ZUGFeRD-XML erstellen