Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision f58e9600

Von Tamino Steinert vor 12 Monaten hinzugefügt

  • ID f58e9600508e0b4641f6238a1f5b42b83ae4b8f8
  • Vorgänger 05d7e914
  • Nachfolger ffdea081

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

Unterschiede anzeigen:

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
}
SL/DB/PurchaseInvoice.pm
160 160
  return $reclamation;
161 161
}
162 162

  
163
sub create_from_zugferd_xml {
164
  my ($class, $zugferd_xml) = @_;
163
sub create_from_zugferd_data {
164
  my ($class, $data) = @_;
165 165

  
166 166
  my $ap_invoice = $class->new();
167 167

  
168
  $ap_invoice->import_zugferd_xml($zugferd_xml);
168
  $ap_invoice->import_zugferd_data($data);
169 169
}
170 170

  
171 171
sub create_ap_row {

Auch abrufbar als: Unified diff