Merge pull request #41 from kivitendo/f-rights-to-view
authorJan Büren <jan@kivitendo-premium.de>
Wed, 9 Mar 2022 08:12:50 +0000 (09:12 +0100)
committerGitHub <noreply@github.com>
Wed, 9 Mar 2022 08:12:50 +0000 (09:12 +0100)
F rights to view

27 files changed:
SL/AP.pm
SL/Common.pm
SL/Controller/DeliveryOrder.pm
SL/Controller/Order.pm
SL/Controller/PayPostingImport.pm
SL/DATEV.pm
SL/DB/ApGl.pm [new file with mode: 0644]
SL/DB/Helper/ALL.pm
SL/DB/Helper/Mappings.pm
SL/DB/Manager/ApGl.pm [new file with mode: 0644]
SL/DB/MetaSetup/ApGl.pm [new file with mode: 0644]
SL/DB/MetaSetup/InvoiceItem.pm
SL/DB/MetaSetup/OrderItem.pm
SL/DB/MetaSetup/Tax.pm
SL/Helper/UNECERecommendation20.pm
bin/mozilla/ap.pl
bin/mozilla/gl.pl
doc/changelog
locale/de/all
locale/en/all
sql/Pg-upgrade2/ap_gl.sql [new file with mode: 0644]
sql/Pg-upgrade2/cape_remove_oids.sql [new file with mode: 0644]
sql/Pg-upgrade2/convert_real_qty.sql [new file with mode: 0644]
sql/Pg-upgrade2/remove_oids.sql [new file with mode: 0644]
sql/Pg-upgrade2/tax_reverse_charge.sql [new file with mode: 0644]
t/pay_posting_import/datev_import.t
templates/webpages/ap/form_header.html

index c2b5e64..8ca5634 100644 (file)
--- a/SL/AP.pm
+++ b/SL/AP.pm
@@ -39,6 +39,7 @@ use SL::DATEV qw(:CONSTANTS);
 use SL::DBUtils;
 use SL::IO;
 use SL::MoreCommon;
+use SL::DB::ApGl;
 use SL::DB::Default;
 use SL::DB::Draft;
 use SL::DB::Order;
@@ -406,6 +407,8 @@ sub _post_transaction {
     SL::DB::Manager::Draft->delete_all(where => [ id => delete($form->{draft_id}) ]);
   }
 
+  # hook for taxkey 94
+  $self->_reverse_charge($myconfig, $form);
   # safety check datev export
   if ($::instance_conf->get_datev_check_on_ap_transaction) {
     my $datev = SL::DATEV->new(
@@ -422,12 +425,86 @@ sub _post_transaction {
   return 1;
 }
 
+sub _reverse_charge {
+  my ($self, $myconfig, $form) = @_;
+
+  # check taxkey settings or return
+  my $tax = SL::DB::Manager::Tax->get_first( where => [taxkey => 94 ]);
+  return unless ref $tax eq 'SL::DB::Tax';
+
+  # delete previous bookings, if they exists (repost)
+  my $ap_gl = SL::DB::Manager::ApGl->get_first(where => [ ap_id => $form->{id} ]);
+  my $gl_id = ref $ap_gl eq 'SL::DB::ApGl' ? $ap_gl->gl_id : undef;
+
+  SL::DB::Manager::GLTransaction->delete_all(where => [ id    => $gl_id ])       if $gl_id;
+  SL::DB::Manager::ApGl->         delete_all(where => [ ap_id => $form->{id} ])  if $gl_id;
+  SL::DB::Manager::RecordLink->   delete_all(where => [ from_table => 'ap', to_table => 'gl', from_id => $form->{id} ]);
+
+  # gl booking
+  my ($credit, $debit);
+  $credit   = SL::DB::Manager::Chart->find_by(id => $tax->chart_id);
+  $debit    = SL::DB::Manager::Chart->find_by(id => $tax->reverse_charge_chart_id);
+
+  croak("No such Chart ID" . $tax->chart_id)          unless ref $credit eq 'SL::DB::Chart';
+  croak("No such Chart ID" . $tax->reverse_chart_id)  unless ref $debit  eq 'SL::DB::Chart';
+
+  my ($i, $current_transaction);
+
+  for $i (1 .. $form->{rowcount}) {
+    next unless $form->{"taxkey_$i"} == 94;
+
+    my ($tmpnetamount, $tmptaxamount) = $form->calculate_tax($form->{"amount_$i"}, 0.19, $form->{taxincluded}, 2);
+    $current_transaction = SL::DB::GLTransaction->new(
+          employee_id    => $form->{employee_id},
+          transdate      => $form->{transdate},
+          description    => $form->{notes} || $form->{invnumber},
+          reference      => $form->{invnumber},
+          department_id  => $form->{department_id} ? $form->{department_id} : undef,
+          imported       => 0, # not imported
+          taxincluded    => 0,
+        )->add_chart_booking(
+          chart  => $tmptaxamount < 0 ? $credit : $debit,
+          credit => abs($tmptaxamount),
+          source => "Reverse Charge for " . $form->{invnumber},
+        )->add_chart_booking(
+          chart  => $tmptaxamount < 0 ? $debit : $credit,
+          debit  => abs($tmptaxamount),
+          source => "Reverse Charge for " . $form->{invnumber},
+      )->post;
+    # add a stable link from ap to gl
+    my %props_gl = (
+        ap_id => $form->{id},
+        gl_id => $current_transaction->id,
+      );
+    SL::DB::ApGl->new(%props_gl)->save;
+    # Record a record link from ap to gl
+    my %props_rl = (
+        from_table => 'ap',
+        from_id    => $form->{id},
+        to_table   => 'gl',
+        to_id      => $current_transaction->id,
+      );
+    SL::DB::RecordLink->new(%props_rl)->save;
+  }
+}
+
 sub delete_transaction {
   $main::lxdebug->enter_sub();
 
   my ($self, $myconfig, $form) = @_;
 
   SL::DB->client->with_transaction(sub {
+
+    # if tax 94 reverse charge, clear all GL bookings and links
+    my $ap_gl = SL::DB::Manager::ApGl->get_first(where => [ ap_id => $form->{id} ]);
+    my $gl_id = ref $ap_gl eq 'SL::DB::ApGl' ? $ap_gl->gl_id : undef;
+
+    SL::DB::Manager::GLTransaction->delete_all(where => [ id    => $gl_id ])       if $gl_id;
+    SL::DB::Manager::ApGl->         delete_all(where => [ ap_id => $form->{id} ])  if $gl_id;
+    SL::DB::Manager::RecordLink->   delete_all(where => [ from_table => 'ap', to_table => 'gl', from_id => $form->{id} ]);
+    # done gl delete for tax 94 case
+
+    # begin ap delete
     my $query = qq|DELETE FROM ap WHERE id = ?|;
     do_query($form, SL::DB->client->dbh, $query, $form->{id});
     1;
index 36635fd..126a30e 100644 (file)
@@ -388,6 +388,8 @@ sub save_email_status {
 
   my ($self, $myconfig, $form) = @_;
 
+  return unless ($::instance_conf->get_email_journal);
+
   my ($table, $query, $dbh);
 
   if ($form->{script} eq 'oe.pl') {
@@ -646,6 +648,11 @@ C<full> (replace consecutive line feed/carriage return characters in
 the middle by a single space and remove tailing line feed/carriage
 return characters).
 
+=item C<save_email_status>
+
+Adds sending information to internal notes.
+Does nothing if the client config email_journal is enabled.
+
 =back
 
 =head1 BUGS
index c8075a6..db31844 100644 (file)
@@ -483,18 +483,21 @@ sub action_send_email {
   $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
   $::form->send_email(\%::myconfig, 'pdf');
 
-  # internal notes
-  my $intnotes = $self->order->intnotes;
-  $intnotes   .= "\n\n" if $self->order->intnotes;
-  $intnotes   .= t8('[email]')                                                                                        . "\n";
-  $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
-  $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
-  $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
-  $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
-  $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
-  $intnotes   .= t8('Message')    . ": " . $::form->{message};
-
-  $self->order->update_attributes(intnotes => $intnotes);
+  # internal notes unless no email journal
+  unless ($::instance_conf->get_email_journal) {
+
+    my $intnotes = $self->order->intnotes;
+    $intnotes   .= "\n\n" if $self->order->intnotes;
+    $intnotes   .= t8('[email]')                                                                                        . "\n";
+    $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
+    $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
+    $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
+    $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
+    $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
+    $intnotes   .= t8('Message')    . ": " . $::form->{message};
+
+    $self->order->update_attributes(intnotes => $intnotes);
+  }
 
   $self->save_history('MAILED');
 
index 67402c0..b7d38cd 100644 (file)
@@ -519,18 +519,20 @@ sub action_send_email {
   $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
   $::form->send_email(\%::myconfig, $::form->{print_options}->{format});
 
-  # internal notes
-  my $intnotes = $self->order->intnotes;
-  $intnotes   .= "\n\n" if $self->order->intnotes;
-  $intnotes   .= t8('[email]')                                                                                        . "\n";
-  $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
-  $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
-  $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
-  $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
-  $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
-  $intnotes   .= t8('Message')    . ": " . SL::HTML::Util->strip($::form->{message});
-
-  $self->order->update_attributes(intnotes => $intnotes);
+  # internal notes unless no email journal
+  unless ($::instance_conf->get_email_journal) {
+    my $intnotes = $self->order->intnotes;
+    $intnotes   .= "\n\n" if $self->order->intnotes;
+    $intnotes   .= t8('[email]')                                                                                        . "\n";
+    $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
+    $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
+    $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
+    $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
+    $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
+    $intnotes   .= t8('Message')    . ": " . SL::HTML::Util->strip($::form->{message});
+
+    $self->order->update_attributes(intnotes => $intnotes);
+  }
 
   $self->save_history('MAILED');
 
index a2d103f..c8d3909 100644 (file)
@@ -82,7 +82,7 @@ sub parse_and_import {
       # optional KOST1 - KOST2 ?
       $department_name = $row->[36];
       if ($department_name) {
-        $department    = SL::DB::Manager::Department->get_first(description => { like =>  $department_name . '%' });
+        $department    = SL::DB::Manager::Department->get_first(where => [ description => { ilike =>  $department_name . '%' } ]);
       }
 
       my $amount = $::form->parse_amount({ numberformat => '1000,00' }, $row->[0]);
index 1db417c..86cb729 100644 (file)
@@ -543,6 +543,7 @@ sub generate_datev_data {
          $gl_itime_filter
          $gl_department_id_filter
          $gl_imported
+         AND NOT EXISTS (SELECT gl_id from ap_gl where gl_id = gl.id)
          $filter
 
        ORDER BY trans_id, acc_trans_id|;
@@ -835,6 +836,7 @@ sub generate_datev_lines {
       }
       if ($transaction->[$i]->{'taxkey'}) {
         $taxkey = $transaction->[$i]->{'taxkey'};
+        # $taxkey = 0 if $taxkey == 94; # taxbookings are in gl
       }
       if ($transaction->[$i]->{'charttax'}) {
         $charttax = $transaction->[$i]->{'charttax'};
diff --git a/SL/DB/ApGl.pm b/SL/DB/ApGl.pm
new file mode 100644 (file)
index 0000000..77368a3
--- /dev/null
@@ -0,0 +1,13 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::ApGl;
+
+use strict;
+
+use SL::DB::MetaSetup::ApGl;
+use SL::DB::Manager::ApGl;
+
+__PACKAGE__->meta->initialize;
+
+1;
index bb0c3b4..633373b 100644 (file)
@@ -4,6 +4,7 @@ use strict;
 
 use SL::DB::AccTransaction;
 use SL::DB::AdditionalBillingAddress;
+use SL::DB::ApGl;
 use SL::DB::Assembly;
 use SL::DB::AssortmentItem;
 use SL::DB::AuthClient;
index 0e9f230..8d0ed7e 100644 (file)
@@ -99,6 +99,7 @@ my %kivitendo_package_names = (
   'auth.user_group'              => 'auth_user_group',
   ar                             => 'invoice',
   ap                             => 'purchase_invoice',
+  ap_gl                          => 'ap_gl',
   assembly                       => 'assembly',
   assortment_items               => 'assortment_item',
   background_jobs                => 'background_job',
diff --git a/SL/DB/Manager/ApGl.pm b/SL/DB/Manager/ApGl.pm
new file mode 100644 (file)
index 0000000..3cc0396
--- /dev/null
@@ -0,0 +1,14 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::ApGl;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::ApGl' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
diff --git a/SL/DB/MetaSetup/ApGl.pm b/SL/DB/MetaSetup/ApGl.pm
new file mode 100644 (file)
index 0000000..9f3f80a
--- /dev/null
@@ -0,0 +1,35 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::ApGl;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('ap_gl');
+
+__PACKAGE__->meta->columns(
+  ap_id => { type => 'integer', not_null => 1 },
+  gl_id => { type => 'integer', not_null => 1 },
+  itime => { type => 'timestamp', default => 'now()' },
+  mtime => { type => 'timestamp' },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'ap_id', 'gl_id' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+__PACKAGE__->meta->foreign_keys(
+  ap => {
+    class       => 'SL::DB::PurchaseInvoice',
+    key_columns => { ap_id => 'id' },
+  },
+
+  gl => {
+    class       => 'SL::DB::GLTransaction',
+    key_columns => { gl_id => 'id' },
+  },
+);
+
+1;
+;
index 85b8876..8a67022 100644 (file)
@@ -35,7 +35,7 @@ __PACKAGE__->meta->columns(
   price_factor_id        => { type => 'integer' },
   pricegroup_id          => { type => 'integer' },
   project_id             => { type => 'integer' },
-  qty                    => { type => 'float', precision => 4, scale => 4 },
+  qty                    => { type => 'numeric', precision => 25, scale => 5 },
   sellprice              => { type => 'numeric', precision => 15, scale => 5 },
   serialnumber           => { type => 'text' },
   subtotal               => { type => 'boolean', default => 'false' },
index afa64d8..e83b65b 100644 (file)
@@ -31,7 +31,7 @@ __PACKAGE__->meta->columns(
   price_factor_id        => { type => 'integer' },
   pricegroup_id          => { type => 'integer' },
   project_id             => { type => 'integer' },
-  qty                    => { type => 'float', precision => 4, scale => 4 },
+  qty                    => { type => 'numeric', precision => 25, scale => 5 },
   reqdate                => { type => 'date' },
   sellprice              => { type => 'numeric', precision => 15, scale => 5 },
   serialnumber           => { type => 'text' },
index 55aba86..c29a455 100644 (file)
@@ -15,6 +15,7 @@ __PACKAGE__->meta->columns(
   itime                    => { type => 'timestamp', default => 'now()' },
   mtime                    => { type => 'timestamp' },
   rate                     => { type => 'numeric', default => '0', not_null => 1, precision => 15, scale => 5 },
+  reverse_charge_chart_id  => { type => 'integer' },
   skonto_purchase_chart_id => { type => 'integer' },
   skonto_sales_chart_id    => { type => 'integer' },
   taxdescription           => { type => 'text', not_null => 1 },
index 56ea12e..e79991c 100644 (file)
@@ -39,7 +39,7 @@ my @mappings = (
   [ 'LTR', qr{^(?:l|liter|litre)$}i ],
 
   # miscellaneous
-  [ 'C62', qr{^(?:stck|stück|pieces?|pc|psch|pauschal)$}i ],
+  [ 'C62', qr{^(?:stck|stück|pieces?|pc|psch|pauschal|licenses?|lizenz(?:en)?)$}i ],
 );
 
 sub map_name_to_code {
index e7918d1..7fcfef1 100644 (file)
@@ -493,6 +493,15 @@ sub form_header {
     $form->{"selected_taxchart_$i"}  = $selected_taxchart;
     $form->{"AP_amount_chart_id_$i"} = $amount_chart_id;
     $form->{"taxcharts_$i"}          = \@taxcharts;
+
+    # reverse charge hack for template, display two taxes
+    if ($taxchart_to_use->taxkey == 94) {
+      my $tmpnetamount;
+      ($tmpnetamount, $form->{"tax_reverse_$i"}) = $form->calculate_tax($form->parse_amount(\%myconfig, $form->{"amount_$i"}), 0.19, $form->{taxincluded},2);
+      $form->{"tax_charge_$i"}  = $form->{"tax_reverse_$i"} * -1;
+      $form->{"tax_reverse_$i"} = $form->format_amount(\%myconfig, $form->{"tax_reverse_$i"}, 2);
+      $form->{"tax_charge_$i"}  = $form->format_amount(\%myconfig, $form->{"tax_charge_$i"}, 2);
+    }
   }
 
   $form->{taxchart_value_title_sub} = sub {
@@ -797,10 +806,16 @@ sub post {
   $form->error($locale->text('Cannot post transaction for a closed period!')) if ($form->date_closed($form->{"transdate"}, \%myconfig));
 
   my $zero_amount_posting = 1;
+  # no taxincluded for 94
+  my $tax = SL::DB::Manager::Tax->get_first( where => [taxkey => 94 ]);
+  my $tax_id = ref $tax eq 'SL::DB::Tax' ? $tax->id : undef;
   for my $i (1 .. $form->{rowcount}) {
+    # no taxincluded for 94
+    if ($tax_id && $form->{"taxchart_$i"} =~ m/^$tax_id--/ && $form->{taxincluded}) {
+      $form->error($locale->text('Cannot Post AP transaction with tax included!'));
+    }
     if ($form->parse_amount(\%myconfig, $form->{"amount_$i"})) {
       $zero_amount_posting = 0;
-      last;
     }
   }
 
@@ -1348,6 +1363,10 @@ sub setup_ap_display_form_action_bar {
 
     $is_linked_bank_transaction = 1;
   }
+  my $is_linked_gl_transaction;
+  if ($::form->{id} && SL::DB::Manager::ApGl->find_by(ap_id => $::form->{id})) {
+    $is_linked_gl_transaction = 1;
+  }
 
   my $create_post_action = sub {
     # $_[0]: description
@@ -1419,6 +1438,7 @@ sub setup_ap_display_form_action_bar {
                     : $is_storno           ? t8('Reversal invoices cannot be canceled.')
                     : $::form->{totalpaid} ? t8('Invoices with payments cannot be canceled.')
                     : $has_sepa_exports    ? t8('This invoice has been linked with a sepa export, undo this first.')
+                    : $is_linked_gl_transaction ? t8('This transaction is linked with a gl transaction. Please delete the ap transaction booking if needed.')
                     :                        undef,
         ],
         action => [ t8('Delete'),
@@ -1426,12 +1446,13 @@ sub setup_ap_display_form_action_bar {
           confirm  => t8('Do you really want to delete this object?'),
           disabled => !$may_edit_create           ? t8('You must not change this AP transaction.')
                     : !$::form->{id}              ? t8('This invoice has not been posted yet.')
-                    : $change_never               ? t8('Changing invoices has been disabled in the configuration.')
-                    : $change_on_same_day_only    ? t8('Invoices can only be changed on the day they are posted.')
-                    : $has_storno                 ? t8('This invoice has been canceled already.')
                     : $is_closed                  ? t8('The billing period has already been locked.')
                     : $has_sepa_exports           ? t8('This invoice has been linked with a sepa export, undo this first.')
                     : $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
+                    : $is_linked_gl_transaction   ? undef # linked transactions can be deleted, if period is not closed
+                    : $change_never               ? t8('Changing invoices has been disabled in the configuration.')
+                    : $change_on_same_day_only    ? t8('Invoices can only be changed on the day they are posted.')
+                    : $has_storno                 ? t8('This invoice has been canceled already.')
                     :                               undef,
         ],
       ], # end of combobox "Storno"
index 0d39d47..551937b 100644 (file)
@@ -38,6 +38,7 @@ use strict;
 use POSIX qw(strftime);
 use List::Util qw(first sum);
 
+use SL::DB::ApGl;
 use SL::DB::RecordTemplate;
 use SL::DB::BankTransactionAccTrans;
 use SL::DB::Tax;
@@ -969,11 +970,15 @@ sub setup_gl_action_bar {
   my $form   = $::form;
   my $change_never            = $::instance_conf->get_gl_changeable == 0;
   my $change_on_same_day_only = $::instance_conf->get_gl_changeable == 2 && ($form->current_date(\%::myconfig) ne $form->{gldate});
-  my $is_linked_bank_transaction;
+  my ($is_linked_bank_transaction, $is_linked_ap_transaction);
 
   if ($form->{id} && SL::DB::Manager::BankTransactionAccTrans->find_by(gl_id => $form->{id})) {
     $is_linked_bank_transaction = 1;
   }
+  if ($form->{id} && SL::DB::Manager::ApGl->find_by(gl_id => $form->{id})) {
+    $is_linked_ap_transaction = 1;
+  }
+
 
   my $create_post_action = sub {
     # $_[0]: description
@@ -986,7 +991,8 @@ sub setup_gl_action_bar {
                 : ($form->{id} && $change_never)            ? t8('Changing general ledger transaction has been disabled in the configuration.')
                 : ($form->{id} && $change_on_same_day_only) ? t8('General ledger transactions can only be changed on the day they are posted.')
                 : $is_linked_bank_transaction               ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
-                :                                             undef,
+                : $is_linked_ap_transaction                 ? t8('This transaction is linked with a AP transaction. Please undo and redo the AP transaction booking if needed.')
+                : undef,
     ],
   };
 
@@ -1017,6 +1023,7 @@ sub setup_gl_action_bar {
           disabled => !$form->{id}                ? t8('This general ledger transaction has not been posted yet.')
                     : $form->{storno}             ? t8('A canceled general ledger transaction cannot be canceled again.')
                     : $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
+                    : $is_linked_ap_transaction   ? t8('This transaction is linked with a AP transaction. Please undo and redo the AP transaction booking if needed.')
                     : undef,
         ],
         action => [ t8('Delete'),
@@ -1027,8 +1034,9 @@ sub setup_gl_action_bar {
                     : $change_never            ? t8('Changing invoices has been disabled in the configuration.')
                     : $change_on_same_day_only ? t8('Invoices can only be changed on the day they are posted.')
                     : $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
+                    : $is_linked_ap_transaction   ? t8('This transaction is linked with a AP transaction. Please undo and redo the AP transaction booking if needed.')
                     : $form->{storno}             ? t8('A canceled general ledger transaction cannot be deleted.')
-                    :                            undef,
+                    : undef,
         ],
       ], # end of combobox "Storno"
 
@@ -1053,7 +1061,7 @@ sub setup_gl_action_bar {
           call     => [ 'kivi.Draft.popup', 'gl', 'unknown', $form->{draft_id}, $form->{draft_description} ],
           disabled => $form->{id}     ? t8('This invoice has already been posted.')
                     : $form->{locked} ? t8('The billing period has already been locked.')
-                    :                   undef,
+                    : undef,
         ],
       ], # end of combobox "more"
     );
index 9f87cec..3edc09b 100644 (file)
@@ -2,6 +2,20 @@
 # Veränderungen von kivitendo #
 ###############################
 
+2022-0x-xx - Release 3.6.1
+
+Größere neue Features:
+
+Mittelgroße neue Features:
+
+Kleinere neue Features und Detailverbesserungen:
+
+ - Die Protokollierung von E-Mails in interne Bemerkungen ist deaktiviert,
+   falls der Mandant sowieso das E-Mail-Journal aktiviert hat.
+ - Steuerschlüssel 94, Reverse Charge gleichzeitige Vor- und Mehrwertsteuer
+   kann in einem netto verbuchten Kreditorenbeleg verbucht werden, die Steuer
+   wird dann mit einer verknüpften Dialogbuchung verbucht.
+
 2022-03-02 - Release 3.6.0
 
 Größere neue Features:
index 0c734bb..bc21518 100755 (executable)
@@ -568,6 +568,7 @@ $self->{texts} = {
   'Cancel Accounts Payables Transaction' => 'Kreditorenbuchung stornieren',
   'Cancel Accounts Receivables Transaction' => 'Debitorenbuchung stornieren',
   'Cancelling is disallowed. Either undo or balance the current payments until the open amount matches the invoice amount' => 'Storno verboten, da Zahlungen zum Beleg vorhanden sind. Entweder die Zahlungen löschen oder mit umgekehrten Vorzeichen ausbuchen, sodass der offene Betrag dem Rechnungsbetrag entspricht.',
+  'Cannot Post AP transaction with tax included!' => 'Kann diesen kreditorischen Beleg nicht mit "Steuer im Preis inbegriffen" verbuchen!',
   'Cannot add Booking, reason: #1 DB: #2 ' => 'Kann die Buchung nicht hinzufügen, Grund: #1 DB: #2',
   'Cannot allocate parts.'      => 'Es sind nicht genügend Artikel vorhanden',
   'Cannot change transaction in a closed period!' => 'In einem bereits abgeschlossenen Zeitraum kann keine Buchung verändert werden!',
@@ -3922,7 +3923,9 @@ $self->{texts} = {
   'This sales order has an active configuration for periodic invoices. If you save then all subsequently created invoices will contain those changes as well, but not those that have already been created. Do you want to continue?' => 'Dieser Auftrag besitzt eine aktive Konfiguration für wiederkehrende Rechnungen. Wenn Sie jetzt speichern, so werden alle zukünftig hieraus erzeugten Rechnungen die Änderungen enthalten, nicht aber die bereits erzeugten Rechnungen. Möchten Sie speichern?',
   'This status output will be refreshed every five seconds.' => 'Diese Statusausgabe wird alle fünf Sekunden aktualisiert.',
   'This transaction has to be split into several transactions manually.' => 'Diese Buchung muss manuell in mehrere Buchungen aufgeteilt werden.',
+  'This transaction is linked with a AP transaction. Please undo and redo the AP transaction booking if needed.' => 'Diese Buchung ist mit einer Kreditorenbuchung verknüpft. Bitte Löschen oder Ändern Sie die Kreditorenbuchung nötigenfalls.',
   'This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.' => 'Ein oder mehrere Zahlungen des Belegs sind über das Verbuchen von Kontoauszüge erstellt worden, falls notwendig kann eine Neuverbuchung über Zahlungsverkehr -> Bericht Bankbewegung möglich gemacht werden.',
+  'This transaction is linked with a gl transaction. Please delete the ap transaction booking if needed.' => 'Diese Buchung ist mit einer Dialogbuchung verknüpft. Bitte Löschen oder Ändern Sie diese Kreditorenbuchung nötigenfalls.',
   'This update will change the nature the onhand of goods is tracked.' => 'Dieses update ändert die Art und Weise wie Lagermengen gezält werden.',
   'This user is a member in the following groups' => 'Dieser Benutzer ist Mitglied in den folgenden Gruppen',
   'This user will have access to the following clients' => 'Dieser Benutzer wird Zugriff auf die folgenden Mandanten haben',
index 2699e2e..b33cba2 100644 (file)
@@ -568,6 +568,7 @@ $self->{texts} = {
   'Cancel Accounts Payables Transaction' => '',
   'Cancel Accounts Receivables Transaction' => '',
   'Cancelling is disallowed. Either undo or balance the current payments until the open amount matches the invoice amount' => '',
+  'Cannot Post AP transaction with tax included!' => '',
   'Cannot add Booking, reason: #1 DB: #2 ' => '',
   'Cannot allocate parts.'      => '',
   'Cannot change transaction in a closed period!' => '',
@@ -3921,7 +3922,9 @@ $self->{texts} = {
   'This sales order has an active configuration for periodic invoices. If you save then all subsequently created invoices will contain those changes as well, but not those that have already been created. Do you want to continue?' => '',
   'This status output will be refreshed every five seconds.' => '',
   'This transaction has to be split into several transactions manually.' => '',
+  'This transaction is linked with a AP transaction. Please undo and redo the AP transaction booking if needed.' => '',
   'This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.' => '',
+  'This transaction is linked with a gl transaction. Please delete the ap transaction booking if needed.' => '',
   'This update will change the nature the onhand of goods is tracked.' => '',
   'This user is a member in the following groups' => '',
   'This user will have access to the following clients' => '',
diff --git a/sql/Pg-upgrade2/ap_gl.sql b/sql/Pg-upgrade2/ap_gl.sql
new file mode 100644 (file)
index 0000000..cb9e01b
--- /dev/null
@@ -0,0 +1,16 @@
+-- @tag: ap_gl
+-- @description: Hilfstabelle für automatische GL-Buchung nach Kreditorenbuchung
+-- @depends: release_3_5_0
+-- @ignore: 0
+      CREATE TABLE ap_gl (
+        ap_id                   integer,
+        gl_id                   integer,
+        itime                   TIMESTAMP      DEFAULT now(),
+        mtime                   TIMESTAMP,
+        PRIMARY KEY (ap_id, gl_id),
+        FOREIGN KEY (ap_id)                    REFERENCES ap (id),
+        FOREIGN KEY (gl_id)                    REFERENCES gl (id) ON DELETE CASCADE);
+
+
+
+
diff --git a/sql/Pg-upgrade2/cape_remove_oids.sql b/sql/Pg-upgrade2/cape_remove_oids.sql
new file mode 100644 (file)
index 0000000..b30d286
--- /dev/null
@@ -0,0 +1,9 @@
+-- @tag: cape_remove_oids
+-- @description: OIDs von Tabellen entfernen
+-- @depends: release_3_5_6
+ALTER TABLE assembly             SET WITHOUT OIDS;
+ALTER TABLE delivery_order_items SET WITHOUT OIDS;
+ALTER TABLE invoice              SET WITHOUT OIDS;
+ALTER TABLE orderitems           SET WITHOUT OIDS;
+ALTER TABLE parts                SET WITHOUT OIDS;
+ALTER TABLE partsgroup           SET WITHOUT OIDS;
diff --git a/sql/Pg-upgrade2/convert_real_qty.sql b/sql/Pg-upgrade2/convert_real_qty.sql
new file mode 100644 (file)
index 0000000..5e0ea65
--- /dev/null
@@ -0,0 +1,7 @@
+-- @tag: convert_real_qty
+-- @description: Spaltentyp auf Numeric anstelle von Real für qty
+-- @depends: release_3_6_0
+ALTER TABLE orderitems ALTER column qty type numeric(25,5);
+ALTER TABLE invoice    ALTER column qty type numeric(25,5);
+
+
diff --git a/sql/Pg-upgrade2/remove_oids.sql b/sql/Pg-upgrade2/remove_oids.sql
new file mode 100644 (file)
index 0000000..9e9f439
--- /dev/null
@@ -0,0 +1,9 @@
+-- @tag: remove_oids
+-- @description: OIDs von Tabellen entfernen
+-- @depends: release_3_6_0
+ALTER TABLE assembly             SET WITHOUT OIDS;
+ALTER TABLE delivery_order_items SET WITHOUT OIDS;
+ALTER TABLE invoice              SET WITHOUT OIDS;
+ALTER TABLE orderitems           SET WITHOUT OIDS;
+ALTER TABLE parts                SET WITHOUT OIDS;
+ALTER TABLE partsgroup           SET WITHOUT OIDS;
diff --git a/sql/Pg-upgrade2/tax_reverse_charge.sql b/sql/Pg-upgrade2/tax_reverse_charge.sql
new file mode 100644 (file)
index 0000000..fd1e755
--- /dev/null
@@ -0,0 +1,108 @@
+-- @tag: tax_reverse_charge
+-- @description: Reverse Charge für Kreditorenbelege
+-- @depends: release_3_6_0
+-- @ignore: 0
+
+ALTER TABLE tax add column reverse_charge_chart_id integer;
+
+INSERT INTO chart (
+  accno, description,
+  charttype,   category,  link,
+  taxkey_id
+  )
+SELECT
+  '1577','Abziehbare Vorst. nach §13b UstG 19%',
+  'A',         'E',       'AP_tax:IC_taxpart:IC_taxservice',
+  0
+WHERE EXISTS ( -- update only for SKR03
+    SELECT coa FROM defaults
+    WHERE defaults.coa='Germany-DATEV-SKR03EU' AND NOT EXISTS (SELECT id from chart where accno='1577')
+);
+
+INSERT INTO chart (
+  accno, description,
+  charttype,   category,  link,
+  taxkey_id
+  )
+SELECT
+  '1787','Umsatzsteuer nach §13b UStG 19%',
+  'A',         'I',       'AR_tax:IC_taxpart:IC_taxservice',
+  0
+WHERE EXISTS ( -- update only for SKR03
+    SELECT coa FROM defaults
+    WHERE defaults.coa='Germany-DATEV-SKR03EU' AND NOT EXISTS (SELECT id from chart where accno='1787')
+);
+
+
+INSERT INTO chart (
+  accno, description,
+  charttype,   category,  link,
+  taxkey_id
+  )
+SELECT
+  '1407','Abziehbare Vorst. nach §13b UstG 19%',
+  'A',         'E',       'AP_tax:IC_taxpart:IC_taxservice',
+  0
+WHERE EXISTS ( -- update only for SKR04
+    SELECT coa FROM defaults
+    WHERE defaults.coa='Germany-DATEV-SKR04EU' AND NOT EXISTS (SELECT id from chart where accno='1407')
+);
+
+INSERT INTO chart (
+  accno, description,
+  charttype,   category,  link,
+  taxkey_id
+  )
+SELECT
+  '3837','Umsatzsteuer nach §13b UStG 19%',
+  'A',         'I',       'AR_tax:IC_taxpart:IC_taxservice',
+  0
+WHERE EXISTS ( -- update only for SKR04
+    SELECT coa FROM defaults
+    WHERE defaults.coa='Germany-DATEV-SKR04EU' AND NOT EXISTS (SELECT id from chart where accno='3837')
+);
+
+
+
+INSERT INTO tax (
+  chart_id,
+  reverse_charge_chart_id,
+  rate,
+  taxkey,
+  taxdescription,
+  chart_categories
+  )
+  SELECT
+  (SELECT id FROM chart WHERE accno = '1577'),
+  (SELECT id FROM chart WHERE accno = '1787'), 0,
+  '94', '19% Vorsteuer und 19% Umsatzsteuer', 'EI'
+WHERE EXISTS ( -- update only for SKR03
+    SELECT coa FROM defaults
+    WHERE defaults.coa='Germany-DATEV-SKR03EU'
+);
+
+
+INSERT INTO tax (
+  chart_id,
+  reverse_charge_chart_id,
+  rate,
+  taxkey,
+  taxdescription,
+  chart_categories
+  )
+  SELECT
+  (SELECT id FROM chart WHERE accno = '1407'),
+  (SELECT id FROM chart WHERE accno = '3837'), 0,
+  '94', '19% Vorsteuer und 19% Umsatzsteuer', 'EI'
+WHERE EXISTS ( -- update only for SKR03
+    SELECT coa FROM defaults
+    WHERE defaults.coa='Germany-DATEV-SKR04EU'
+);
+
+-- if not defined
+insert into taxkeys(chart_id,tax_id,taxkey_id,startdate) SELECT (SELECT chart_id FROM tax WHERE taxkey = '94'),0,0,'1970-01-01' WHERE NOT EXISTS
+  (SELECT chart_id from taxkeys where chart_id = ( SELECT chart_id FROM tax WHERE taxkey = '94'));
+
+insert into taxkeys(chart_id,tax_id,taxkey_id,startdate) SELECT (SELECT reverse_charge_chart_id FROM tax WHERE taxkey = '94'),0,0,'1970-01-01' WHERE NOT EXISTS
+  (SELECT chart_id from taxkeys where chart_id = ( SELECT reverse_charge_chart_id FROM tax WHERE taxkey = '94'));
+
index e11415c..c164046 100644 (file)
@@ -50,6 +50,23 @@ foreach my $accno (@charts) {
 }
 
 # and add department (KOST1 description)
+  SL::DB::Department->new(
+    description => 'Total falsche Abteilung, niemals zuordnen!'
+  )->save;
+
+  SL::DB::Department->new(
+    description => '2. Total falsche Abteilung, niemals zuordnen!'
+  )->save;
+
+  SL::DB::Department->new(
+    description => '3. Total falsche Abteilung, niemals zuordnen!'
+  )->save;
+
+  SL::DB::Department->new(
+    description => 'annahme stelle. Total falsche Abteilung, niemals zuordnen!'
+  )->save;
+
+
   SL::DB::Department->new(
     description => 'Wisavis'
   )->save;
@@ -73,7 +90,8 @@ foreach my $booking (@{ $gl_bookings }) {
 
   # gl
   is ($current_row->[13], $booking->reference, "Buchungstext correct");
-  if (ref $booking->department eq 'SL::DB::Department') {
+  if ($current_row->[36] eq 'wisavis') {
+    is(ref $booking->department eq 'SL::DB::Department', 1, "Department assigned");
     is ($current_row->[36], 'wisavis', "Department correctly assigned");                # lowercase
     is ('Wisavis', $booking->department->description, "Department correctly assigned"); # upper case
   } else {
index 05b7a7a..c4902bb 100644 (file)
               <input name="amount_[% i %]" size="10" value="[% temp = "amount_"_ i %][% $temp | html %]">
             </td>
             <td>
-              [% temp = "tax_"_ i %][% $temp | html %]
+              [% IF "tax_reverse_"_ i  %]
+                [% temp_r = "tax_reverse_"_ i %][% $temp_r | html %]
+                &nbsp;&nbsp;&nbsp;
+                [% temp_c = "tax_charge_"_ i %][% $temp_c | html %]
+              [% ELSE %]
+                [% temp = "tax_"_ i %][% $temp | html %]
+              [% END %]
             </td>
             <td>
               [% temp = 'selected_taxchart_'_ i %]