]> wagnertech.de Git - mfinanz.git/commitdiff
Berechtigung, Verkaufsrechnungen persönlich zugeordneter Projekte einzusehen
authorMoritz Bunkus <m.bunkus@linet-services.de>
Wed, 13 Feb 2019 16:35:31 +0000 (17:35 +0100)
committerMoritz Bunkus <m.bunkus@linet-services.de>
Thu, 14 Feb 2019 15:40:29 +0000 (16:40 +0100)
Man kann nun Mitarbeiter*innen zu Projekten zuordnen, indem man sie in
den Projektstammdaten hinzufügt.

Ist eine Mitarbeiter*in zu einem Projekt zugeordnet, so darf sie alle
Rechnungen ansehen, die über die Projektnummer der Rechnung (nicht der
Positionen) dem Projekt zugeordnet sind, auch dann, wenn sie nicht das
allgemeine Recht zum Erstellen und Ansehen von Rechnungen hat.

Verändern oder Ausdrucken der Rechnungen ist nicht gestattet.

Die Verwaltung dieser Projektberechtigungen ist über ein neues
Gruppenrecht eingeschränkt.

Betrifft Verkaufsrechnungen, Verkaufsgutschriften und Debitorenbuchungen.

18 files changed:
SL/AR.pm
SL/Controller/Project.pm
SL/DB/Employee.pm
SL/DB/EmployeeProjectInvoices.pm [new file with mode: 0644]
SL/DB/Helper/ALL.pm
SL/DB/Helper/Mappings.pm
SL/DB/Manager/EmployeeProjectInvoices.pm [new file with mode: 0644]
SL/DB/MetaSetup/EmployeeProjectInvoices.pm [new file with mode: 0644]
SL/DB/Project.pm
bin/mozilla/ar.pl
bin/mozilla/io.pl
bin/mozilla/is.pl
locale/de/all
menus/user/00-erp.yaml
sql/Pg-upgrade2-auth/rights_for_viewing_project_specific_invoices.sql [new file with mode: 0644]
sql/Pg-upgrade2/add_emloyee_project_assignment_for_viewing_invoices.sql [new file with mode: 0644]
templates/webpages/project/_invoice_permissions.html [new file with mode: 0644]
templates/webpages/project/form.html

index 0c8f842b315d2fe21e3bcbe221f877083d1f5449..188bf9eee9c805750b679cdc080ca84a6702d535 100644 (file)
--- a/SL/AR.pm
+++ b/SL/AR.pm
@@ -516,9 +516,46 @@ sub ar_transactions {
 
   my $where = "1 = 1";
 
-  unless ( $::auth->assert('show_ar_transactions', 1) ) {
-    $where .= " AND NOT invoice = 'f' ";  # remove ar transactions from Sales -> Reports -> Invoices
-  };
+  # Permissions:
+  # - Always return invoices & AR transactions for projects the employee has "view invoices" permissions for, no matter what the other rules say.
+  # - Exclude AR transactions if no permissions for them exist.
+  # - Limit to own invoices unless may edit all invoices.
+  # - If may edit all, allow filtering by employee/salesman.
+  my (@permission_where, @permission_values);
+
+  if ($::auth->assert('invoice_edit', 1)) {
+    if (!$::auth->assert('show_ar_transactions', 1) ) {
+      push @permission_where, "NOT invoice = 'f'";  # remove ar transactions from Sales -> Reports -> Invoices
+    }
+
+    if (!$::auth->assert('sales_all_edit', 1)) {
+      # only show own invoices
+      push @permission_where,  "a.employee_id = ?";
+      push @permission_values, SL::DB::Manager::Employee->current->id;
+
+    } else {
+      if ($form->{employee_id}) {
+        push @permission_where,  "a.employee_id = ?";
+        push @permission_values, conv_i($form->{employee_id});
+      }
+      if ($form->{salesman_id}) {
+        push @permission_where,  "a.salesman_id = ?";
+        push @permission_values, conv_i($form->{salesman_id});
+      }
+    }
+  }
+
+  if (@permission_where || !$::auth->assert('invoice_edit', 1)) {
+    my $permission_where_str = @permission_where ? "OR (" . join(" AND ", map { "($_)" } @permission_where) . ")" : "";
+    $where .= qq|
+      AND (   (a.globalproject_id IN (
+               SELECT epi.project_id
+               FROM employee_project_invoices epi
+               WHERE epi.employee_id = ?))
+           $permission_where_str)
+    |;
+    push @values, SL::DB::Manager::Employee->current->id, @permission_values;
+  }
 
   if ($form->{customer}) {
     $where .= " AND c.name ILIKE ?";
@@ -578,21 +615,6 @@ sub ar_transactions {
     }
   }
 
-  if (!$main::auth->assert('sales_all_edit', 1)) {
-    # only show own invoices
-    $where .= " AND a.employee_id = (select id from employee where login= ?)";
-    push (@values, $::myconfig{login});
-  } else {
-    if ($form->{employee_id}) {
-      $where .= " AND a.employee_id = ?";
-      push @values, conv_i($form->{employee_id});
-    }
-    if ($form->{salesman_id}) {
-      $where .= " AND a.salesman_id = ?";
-      push @values, conv_i($form->{salesman_id});
-    }
-  };
-
   if ($form->{parts_partnumber}) {
     $where .= <<SQL;
       AND EXISTS (
index 601b245b7cd7cf2d004816ceab81b3bb82b2313d..11cdd9c47d8854e6510993f71db588f5b493c195 100644 (file)
@@ -12,6 +12,7 @@ use SL::Controller::Helper::ReportGenerator;
 use SL::CVar;
 use SL::DB::Customer;
 use SL::DB::DeliveryOrder;
+use SL::DB::Employee;
 use SL::DB::Invoice;
 use SL::DB::Order;
 use SL::DB::Project;
@@ -29,11 +30,12 @@ use Rose::DB::Object::Helpers qw(as_tree);
 use Rose::Object::MakeMethods::Generic
 (
  scalar => [ qw(project) ],
- 'scalar --get_set_init' => [ qw(models customers project_types project_statuses projects linked_records) ],
+ 'scalar --get_set_init' => [ qw(models customers project_types project_statuses projects linked_records employees may_edit_invoice_permissions) ],
 );
 
 __PACKAGE__->run_before('check_auth',   except => [ qw(ajax_autocomplete) ]);
 __PACKAGE__->run_before('load_project', only   => [ qw(edit update destroy) ]);
+__PACKAGE__->run_before('use_multiselect_js', only => [ qw(new create edit update) ]);
 
 #
 # actions
@@ -166,6 +168,8 @@ sub check_auth {
 
 sub init_project_statuses { SL::DB::Manager::ProjectStatus->get_all_sorted }
 sub init_project_types    { SL::DB::Manager::ProjectType->get_all_sorted   }
+sub init_employees        { SL::DB::Manager::Employee->get_all_sorted   }
+sub init_may_edit_invoice_permissions { $::auth->assert('project_edit_view_invoices_permission', 1) }
 
 sub init_linked_records {
   my ($self) = @_;
@@ -223,6 +227,10 @@ sub init_customers {
   return SL::DB::Manager::Customer->get_all_sorted(where => [ or => [ obsolete => 0, obsolete => undef, @customer_id ]]);
 }
 
+sub use_multiselect_js {
+  $::request->layout->use_javascript("${_}.js") for qw(jquery.selectboxes jquery.multiselect2side);
+}
+
 sub display_form {
   my ($self, %params) = @_;
 
@@ -246,6 +254,12 @@ sub create_or_update {
   my $is_new = !$self->project->id;
   my $params = delete($::form->{project}) || { };
 
+  if (!$self->may_edit_invoice_permissions) {
+    delete $params->{employee_invoice_permissions};
+  } elsif (!$params->{employee_invoice_permissions}) {
+    $params->{employee_invoice_permissions} = [];
+  }
+
   delete $params->{id};
   $self->project->assign_attributes(%{ $params });
 
index 4cc109a6802a6a8257970fc50a8cda5476bf78c8..8da20c0b9935148259156a2e24aeb2911dce8e5d 100644 (file)
@@ -5,6 +5,13 @@ use strict;
 use SL::DB::MetaSetup::Employee;
 use SL::DB::Manager::Employee;
 
+__PACKAGE__->meta->add_relationship(
+  project_invoice_permissions  => {
+    type       => 'many to many',
+    map_class  => 'SL::DB::EmployeeProjectInvoices',
+  },
+);
+
 __PACKAGE__->meta->initialize;
 
 sub has_right {
diff --git a/SL/DB/EmployeeProjectInvoices.pm b/SL/DB/EmployeeProjectInvoices.pm
new file mode 100644 (file)
index 0000000..e45196a
--- /dev/null
@@ -0,0 +1,10 @@
+package SL::DB::EmployeeProjectInvoices;
+
+use strict;
+
+use SL::DB::MetaSetup::EmployeeProjectInvoices;
+use SL::DB::Manager::EmployeeProjectInvoices;
+
+__PACKAGE__->meta->initialize;
+
+1;
index 0e6ed07394005f6621a12461e9c1962d08a0ee5f..5698036e86902316ef32412a3c57fd7dd1f58ba3 100644 (file)
@@ -52,6 +52,7 @@ use SL::DB::DunningConfig;
 use SL::DB::EmailJournal;
 use SL::DB::EmailJournalAttachment;
 use SL::DB::Employee;
+use SL::DB::EmployeeProjectInvoices;
 use SL::DB::Exchangerate;
 use SL::DB::File;
 use SL::DB::Finanzamt;
index 6290bd3f43f4aa91e33a093866a09de3748b9539..12e56d06080dc689bb24b38c1c8c22668d9a84d4 100644 (file)
@@ -136,6 +136,7 @@ my %kivitendo_package_names = (
   email_journal                  => 'EmailJournal',
   email_journal_attachments      => 'EmailJournalAttachment',
   employee                       => 'employee',
+  employee_project_invoices      => 'EmployeeProjectInvoices',
   exchangerate                   => 'exchangerate',
   files                          => 'file',
   finanzamt                      => 'finanzamt',
diff --git a/SL/DB/Manager/EmployeeProjectInvoices.pm b/SL/DB/Manager/EmployeeProjectInvoices.pm
new file mode 100644 (file)
index 0000000..a176ef4
--- /dev/null
@@ -0,0 +1,11 @@
+package SL::DB::Manager::EmployeeProjectInvoices;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::EmployeeProjectInvoices' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
diff --git a/SL/DB/MetaSetup/EmployeeProjectInvoices.pm b/SL/DB/MetaSetup/EmployeeProjectInvoices.pm
new file mode 100644 (file)
index 0000000..c3ef6de
--- /dev/null
@@ -0,0 +1,31 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::EmployeeProjectInvoices;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('employee_project_invoices');
+
+__PACKAGE__->meta->columns(
+  employee_id => { type => 'integer', not_null => 1 },
+  project_id  => { type => 'integer', not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'employee_id', 'project_id' ]);
+
+__PACKAGE__->meta->foreign_keys(
+  employee => {
+    class       => 'SL::DB::Employee',
+    key_columns => { employee_id => 'id' },
+  },
+
+  project => {
+    class       => 'SL::DB::Project',
+    key_columns => { project_id => 'id' },
+  },
+);
+
+1;
+;
index df5fe09ec2e24d17cce4f335ac8f48db135e8500..54975350c8bc71953b4f77b50caf0d7f03e75f4f 100644 (file)
@@ -12,6 +12,13 @@ use SL::DB::Helper::CustomVariables(
   cvars_alias => 1,
 );
 
+__PACKAGE__->meta->add_relationship(
+  employee_invoice_permissions  => {
+    type       => 'many to many',
+    map_class  => 'SL::DB::EmployeeProjectInvoices',
+  },
+);
+
 __PACKAGE__->meta->initialize;
 
 sub validate {
@@ -84,6 +91,23 @@ sub full_description {
   return $description;
 }
 
+sub may_employee_view_project_invoices {
+  my ($self, $employee) = @_;
+
+  return undef if !$self->id;
+
+  my $employee_id = ref($employee) ? $employee->id : $employee * 1;
+  my $query       = <<EOSQL;
+    SELECT project_id
+    FROM employee_project_invoices
+    WHERE (employee_id = ?)
+      AND (project_id  = ?)
+    LIMIT 1
+EOSQL
+
+  return !!$self->db->dbh->selectrow_arrayref($query, undef, $employee_id, $self->id)->[0];
+}
+
 1;
 
 __END__
index 3706598fb2f9c91e2c1968aa80bd5db7d04e6ade..19f0058d120512bd60554f6f743ab12a7f82f18b 100644 (file)
@@ -89,6 +89,20 @@ use strict;
 # $locale->text('Nov')
 # $locale->text('Dec')
 
+sub _may_view_or_edit_this_invoice {
+  return 1 if  $::auth->assert('ar_transactions', 1); # may edit all invoices
+  return 0 if !$::form->{id};                         # creating new invoices isn't allowed without invoice_edit
+  return 0 if !$::form->{globalproject_id};           # existing records without a project ID are not allowed
+  return SL::DB::Project->new(id => $::form->{globalproject_id})->load->may_employee_view_project_invoices(SL::DB::Manager::Employee->current);
+}
+
+sub _assert_access {
+  my $cache = $::request->cache('ar.pl::_assert_access');
+
+  $cache->{_may_view_or_edit_this_invoice} = _may_view_or_edit_this_invoice()                              if !exists $cache->{_may_view_or_edit_this_invoice};
+  $::form->show_generic_error($::locale->text("You do not have the permissions to access this function.")) if !       $cache->{_may_view_or_edit_this_invoice};
+}
+
 sub load_record_template {
   $::auth->assert('ar_transactions');
 
@@ -249,7 +263,9 @@ sub add {
 sub edit {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('ar_transactions');
+  # Delay access check to after the invoice's been loaded in
+  # "create_links" so that project-specific invoice rights can be
+  # evaluated.
 
   my $form     = $main::form;
 
@@ -268,7 +284,7 @@ sub edit {
 sub display_form {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('ar_transactions');
+  _assert_access();
 
   my $form     = $main::form;
 
@@ -287,7 +303,8 @@ sub _retrieve_invoice_object {
 sub create_links {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('ar_transactions');
+  # Delay access check to after the invoice's been loaded so that
+  # project-specific invoice rights can be evaluated.
 
   my %params   = @_;
   my $form     = $main::form;
@@ -296,6 +313,8 @@ sub create_links {
   $form->create_links("AR", \%myconfig, "customer");
   $form->{invoice_obj} = _retrieve_invoice_object();
 
+  _assert_access();
+
   my %saved;
   if (!$params{dont_save}) {
     %saved = map { ($_ => $form->{$_}) } qw(direct_debit id taxincluded);
@@ -329,7 +348,7 @@ sub create_links {
 sub form_header {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('ar_transactions');
+  _assert_access();
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -528,7 +547,7 @@ sub form_header {
 sub form_footer {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('ar_transactions');
+  _assert_access();
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -885,25 +904,30 @@ sub setup_ar_search_action_bar {
 }
 
 sub setup_ar_transactions_action_bar {
-  my %params = @_;
+  my %params          = @_;
+  my $may_edit_create = $::auth->assert('invoice_edit', 1);
 
   for my $bar ($::request->layout->get('actionbar')) {
     $bar->add(
       action => [
         $::locale->text('Print'),
         call     => [ 'kivi.MassInvoiceCreatePrint.showMassPrintOptionsOrDownloadDirectly' ],
-        disabled => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
+        disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
+                  : !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.')
+                  :                      undef,
       ],
 
       combobox => [
         action => [ $::locale->text('Create new') ],
         action => [
           $::locale->text('AR Transaction'),
-          submit => [ '#create_new_form', { action => 'ar_transaction' } ],
+          submit   => [ '#create_new_form', { action => 'ar_transaction' } ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
         ],
         action => [
           $::locale->text('Sales Invoice'),
-          submit => [ '#create_new_form', { action => 'sales_invoice' } ],
+          submit   => [ '#create_new_form', { action => 'sales_invoice' } ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
         ],
       ], # end of combobox "Create new"
     );
@@ -913,8 +937,6 @@ sub setup_ar_transactions_action_bar {
 sub search {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('invoice_edit');
-
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
@@ -969,8 +991,6 @@ sub create_subtotal_row {
 sub ar_transactions {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('invoice_edit');
-
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
@@ -1254,6 +1274,7 @@ sub setup_ar_form_header_action_bar {
 
   my $is_storno               = IS->is_storno(\%::myconfig, $::form, 'ar', $::form->{id});
   my $has_storno              = IS->has_storno(\%::myconfig, $::form, 'ar');
+  my $may_edit_create         = $::auth->assert('ar_transactions', 1);
 
   for my $bar ($::request->layout->get('actionbar')) {
     $bar->add(
@@ -1262,6 +1283,7 @@ sub setup_ar_form_header_action_bar {
         submit    => [ '#form', { action => "update" } ],
         id        => 'update_button',
         checks    => [ 'kivi.validate_form' ],
+        disabled  => !$may_edit_create ? t8('You must not change this AR transaction.') : undef,
         accesskey => 'enter',
       ],
 
@@ -1270,7 +1292,8 @@ sub setup_ar_form_header_action_bar {
           t8('Post'),
           submit   => [ '#form', { action => "post" } ],
           checks   => [ 'kivi.validate_form', 'kivi.AR.check_fields_before_posting' ],
-          disabled => $is_closed                                  ? t8('The billing period has already been locked.')
+          disabled => !$may_edit_create                           ? t8('You must not change this AR transaction.')
+                    : $is_closed                                  ? t8('The billing period has already been locked.')
                     : $is_storno                                  ? t8('A canceled invoice cannot be posted.')
                     : ($::form->{id} && $change_never)            ? t8('Changing invoices has been disabled in the configuration.')
                     : ($::form->{id} && $change_on_same_day_only) ? t8('Invoices can only be changed on the day they are posted.')
@@ -1279,12 +1302,16 @@ sub setup_ar_form_header_action_bar {
         action => [
           t8('Post Payment'),
           submit   => [ '#form', { action => "post_payment" } ],
-          disabled => !$::form->{id} ? t8('This invoice has not been posted yet.') : undef,
+          disabled => !$may_edit_create ? t8('You must not change this AR transaction.')
+                    : !$::form->{id}    ? t8('This invoice has not been posted yet.')
+                    :                     undef,
         ],
         action => [ t8('Mark as paid'),
           submit   => [ '#form', { action => "mark_as_paid" } ],
           confirm  => t8('This will remove the invoice from showing as unpaid even if the unpaid amount does not match the amount. Proceed?'),
-          disabled => !$::form->{id} ? t8('This invoice has not been posted yet.') : undef,
+          disabled => !$may_edit_create ? t8('You must not change this AR transaction.')
+                    : !$::form->{id}    ? t8('This invoice has not been posted yet.')
+                    :                     undef,
           only_if  => $::instance_conf->get_is_show_mark_as_paid,
         ],
       ], # end of combobox "Post"
@@ -1294,16 +1321,18 @@ sub setup_ar_form_header_action_bar {
           submit   => [ '#form', { action => "storno" } ],
           checks   => [ 'kivi.validate_form', 'kivi.AR.check_fields_before_posting' ],
           confirm  => t8('Do you really want to cancel this invoice?'),
-          disabled => !$::form->{id}         ? t8('This invoice has not been posted yet.')
-                      : $has_storno          ? t8('This invoice has been canceled already.')
-                      : $is_storno           ? t8('Reversal invoices cannot be canceled.')
-                      : $::form->{totalpaid} ? t8('Invoices with payments cannot be canceled.')
-                      :                        undef,
+          disabled => !$may_edit_create    ? t8('You must not change this AR transaction.')
+                    : !$::form->{id}       ? t8('This invoice has not been posted yet.')
+                    : $has_storno          ? t8('This invoice has been canceled already.')
+                    : $is_storno           ? t8('Reversal invoices cannot be canceled.')
+                    : $::form->{totalpaid} ? t8('Invoices with payments cannot be canceled.')
+                    :                        undef,
         ],
         action => [ t8('Delete'),
           submit   => [ '#form', { action => "delete" } ],
           confirm  => t8('Do you really want to delete this object?'),
-          disabled => !$::form->{id}           ? t8('This invoice has not been posted yet.')
+          disabled => !$may_edit_create        ? t8('You must not change this AR 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.')
                     : $is_closed               ? t8('The billing period has already been locked.')
@@ -1319,7 +1348,9 @@ sub setup_ar_form_header_action_bar {
           t8('Use As New'),
           submit   => [ '#form', { action => "use_as_new" } ],
           checks   => [ 'kivi.validate_form' ],
-          disabled => !$::form->{id} ? t8('This invoice has not been posted yet.') : undef,
+          disabled => !$may_edit_create ? t8('You must not change this AR transaction.')
+                    : !$::form->{id} ? t8('This invoice has not been posted yet.')
+                    :                  undef,
         ],
       ], # end of combobox "Workflow"
 
@@ -1337,14 +1368,16 @@ sub setup_ar_form_header_action_bar {
         ],
         action => [
           t8('Record templates'),
-          call => [ 'kivi.RecordTemplate.popup', 'ar_transaction' ],
+          call     => [ 'kivi.RecordTemplate.popup', 'ar_transaction' ],
+          disabled => !$may_edit_create ? t8('You must not change this AR transaction.') : undef,
         ],
         action => [
           t8('Drafts'),
           call     => [ 'kivi.Draft.popup', 'ar', 'invoice', $::form->{draft_id}, $::form->{draft_description} ],
-          disabled => $::form->{id} ? t8('This invoice has already been posted.')
-                    : $is_closed    ? t8('The billing period has already been locked.')
-                    :                 undef,
+          disabled => !$may_edit_create ? t8('You must not change this AR transaction.')
+                    : $::form->{id}     ? t8('This invoice has already been posted.')
+                    : $is_closed        ? t8('The billing period has already been locked.')
+                    :                     undef,
         ],
       ], # end of combobox "more"
     );
index 9f5cadf24d201f8e904a2a3b3bf67dd4697af9f8..1c8145c5b1aa6db4d790f1275530993341ea6f3e 100644 (file)
@@ -126,8 +126,6 @@ sub _check_io_auth {
 sub display_row {
   $main::lxdebug->enter_sub();
 
-  _check_io_auth();
-
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
@@ -1655,8 +1653,6 @@ sub relink_accounts {
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  _check_io_auth();
-
   $form->{"taxaccounts"} =~ s/\s*$//;
   $form->{"taxaccounts"} =~ s/^\s*//;
   foreach my $accno (split(/\s*/, $form->{"taxaccounts"})) {
index a9e2963de9e87008e9c5cbb5660797f531931017..abbcef771e8a65dc1aad6e959c0ce7a7b132c5b8 100644 (file)
@@ -58,6 +58,20 @@ use strict;
 
 # end of main
 
+sub _may_view_or_edit_this_invoice {
+  return 1 if  $::auth->assert('invoice_edit', 1); # may edit all invoices
+  return 0 if !$::form->{id};                      # creating new invoices isn't allowed without invoice_edit
+  return 0 if !$::form->{globalproject_id};        # existing records without a project ID are not allowed
+  return SL::DB::Project->new(id => $::form->{globalproject_id})->load->may_employee_view_project_invoices(SL::DB::Manager::Employee->current);
+}
+
+sub _assert_access {
+  my $cache = $::request->cache('is.pl::_assert_access');
+
+  $cache->{_may_view_or_edit_this_invoice} = _may_view_or_edit_this_invoice()                              if !exists $cache->{_may_view_or_edit_this_invoice};
+  $::form->show_generic_error($::locale->text("You do not have the permissions to access this function.")) if !       $cache->{_may_view_or_edit_this_invoice};
+}
+
 sub add {
   $main::lxdebug->enter_sub();
 
@@ -92,11 +106,13 @@ sub add {
 sub edit {
   $main::lxdebug->enter_sub();
 
+  # Delay access check to after the invoice's been loaded in
+  # "invoice_links" so that project-specific invoice rights can be
+  # evaluated.
+
   my $form     = $main::form;
   my $locale   = $main::locale;
 
-  $main::auth->assert('invoice_edit');
-
   $form->{show_details}                = $::myconfig{show_form_details};
   $form->{taxincluded_changed_by_user} = 1;
 
@@ -134,16 +150,19 @@ sub edit {
 sub invoice_links {
   $main::lxdebug->enter_sub();
 
+  # Delay access check to after the invoice's been loaded so that
+  # project-specific invoice rights can be evaluated.
+
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  $main::auth->assert('invoice_edit');
-
   $form->{vc} = 'customer';
 
   # create links
   $form->create_links("AR", \%myconfig, "customer");
 
+  _assert_access();
+
   my $editing = $form->{id};
 
   $form->backup_vars(qw(payment_id language_id taxzone_id salesman_id
@@ -206,11 +225,11 @@ sub invoice_links {
 sub prepare_invoice {
   $main::lxdebug->enter_sub();
 
+  _assert_access();
+
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  $main::auth->assert('invoice_edit');
-
   if ($form->{type} eq "credit_note") {
     $form->{type}     = "credit_note";
     $form->{formname} = "credit_note";
@@ -258,13 +277,16 @@ sub setup_is_action_bar {
   my $change_on_same_day_only = $::instance_conf->get_is_changeable == 2 && ($form->current_date(\%::myconfig) ne $form->{gldate});
   my $payments_balanced       = ($::form->{oldtotalpaid} == 0);
   my $has_storno              = ($::form->{storno} && !$::form->{storno_id});
+  my $may_edit_create         = $::auth->assert('invoice_edit', 1);
 
   for my $bar ($::request->layout->get('actionbar')) {
     $bar->add(
       action => [
         t8('Update'),
         submit    => [ '#form', { action => "update" } ],
-        disabled  => $form->{locked} ? t8('The billing period has already been locked.') : undef,
+        disabled  => !$may_edit_create ? t8('You must not change this invoice.')
+                   : $form->{locked}   ? t8('The billing period has already been locked.')
+                   :                     undef,
         id        => 'update_button',
         checks    => [ 'kivi.validate_form' ],
         accesskey => 'enter',
@@ -275,7 +297,8 @@ sub setup_is_action_bar {
           t8('Post'),
           submit   => [ '#form', { action => "post" } ],
           checks   => [ 'kivi.validate_form' ],
-          disabled => $form->{locked}                           ? t8('The billing period has already been locked.')
+          disabled => !$may_edit_create                         ? t8('You must not change this invoice.')
+                    : $form->{locked}                           ? t8('The billing period has already been locked.')
                     : $form->{storno}                           ? t8('A canceled invoice cannot be posted.')
                     : ($form->{id} && $change_never)            ? t8('Changing invoices has been disabled in the configuration.')
                     : ($form->{id} && $change_on_same_day_only) ? t8('Invoices can only be changed on the day they are posted.')
@@ -285,12 +308,16 @@ sub setup_is_action_bar {
           t8('Post Payment'),
           submit   => [ '#form', { action => "post_payment" } ],
           checks   => [ 'kivi.validate_form' ],
-          disabled => !$form->{id} ? t8('This invoice has not been posted yet.') : undef,
+          disabled => !$may_edit_create ? t8('You must not change this invoice.')
+                    : !$form->{id}      ? t8('This invoice has not been posted yet.')
+                    :                     undef,
         ],
         action => [ t8('Mark as paid'),
           submit   => [ '#form', { action => "mark_as_paid" } ],
           confirm  => t8('This will remove the invoice from showing as unpaid even if the unpaid amount does not match the amount. Proceed?'),
-          disabled => !$form->{id} ? t8('This invoice has not been posted yet.') : undef,
+          disabled => !$may_edit_create ? t8('You must not change this invoice.')
+                    : !$form->{id}      ? t8('This invoice has not been posted yet.')
+                    :                     undef,
           only_if  => $::instance_conf->get_is_show_mark_as_paid,
         ],
       ], # end of combobox "Post"
@@ -300,7 +327,8 @@ sub setup_is_action_bar {
           submit   => [ '#form', { action => "storno" } ],
           confirm  => t8('Do you really want to cancel this invoice?'),
           checks   => [ 'kivi.validate_form' ],
-          disabled => !$form->{id}        ? t8('This invoice has not been posted yet.')
+          disabled => !$may_edit_create   ? t8('You must not change this invoice.')
+                    : !$form->{id}        ? t8('This invoice has not been posted yet.')
                     : !$payments_balanced ? t8('Cancelling is disallowed. Either undo or balance the current payments until the open amount matches the invoice amount')
                     : undef,
         ],
@@ -308,7 +336,8 @@ sub setup_is_action_bar {
           submit   => [ '#form', { action => "delete" } ],
           confirm  => t8('Do you really want to delete this object?'),
           checks   => [ 'kivi.validate_form' ],
-          disabled => !$form->{id}             ? t8('This invoice has not been posted yet.')
+          disabled => !$may_edit_create        ? t8('You must not change this invoice.')
+                    : !$form->{id}             ? t8('This invoice has not been posted yet.')
                     : $form->{locked}          ? t8('The billing period has already been locked.')
                     : $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.')
@@ -325,13 +354,16 @@ sub setup_is_action_bar {
           t8('Use As New'),
           submit   => [ '#form', { action => "use_as_new" } ],
           checks   => [ 'kivi.validate_form' ],
-          disabled => !$form->{id} ? t8('This invoice has not been posted yet.') : undef,
+          disabled => !$may_edit_create ? t8('You must not change this invoice.')
+                    : !$form->{id}      ? t8('This invoice has not been posted yet.')
+                    :                     undef,
         ],
         action => [
           t8('Credit Note'),
           submit   => [ '#form', { action => "credit_note" } ],
           checks   => [ 'kivi.validate_form' ],
-          disabled => $form->{type} eq "credit_note" ? t8('Credit notes cannot be converted into other credit notes.')
+          disabled => !$may_edit_create              ? t8('You must not change this invoice.')
+                    : $form->{type} eq "credit_note" ? t8('Credit notes cannot be converted into other credit notes.')
                     : !$form->{id}                   ? t8('This invoice has not been posted yet.')
                     :                                  undef,
         ],
@@ -349,17 +381,23 @@ sub setup_is_action_bar {
           ($form->{id} ? t8('Print') : t8('Preview')),
           call     => [ 'kivi.SalesPurchase.show_print_dialog', $form->{id} ? 'print' : 'preview' ],
           checks   => [ 'kivi.validate_form' ],
-          disabled => !$form->{id} && $form->{locked} ? t8('The billing period has already been locked.') : undef,
+          disabled => !$may_edit_create               ? t8('You must not print this invoice.')
+                    : !$form->{id} && $form->{locked} ? t8('The billing period has already been locked.')
+                    :                                   undef,
         ],
         action => [ t8('Print and Post'),
           call     => [ 'kivi.SalesPurchase.show_print_dialog', $form->{id} ? 'print' : 'print_and_post' ],
           checks   => [ 'kivi.validate_form' ],
-          disabled => $form->{id} ? t8('This invoice has already been posted.') : undef,,
+          disabled => !$may_edit_create ? t8('You must not print this invoice.')
+                    : $form->{id}       ? t8('This invoice has already been posted.')
+                    :                     undef,,
         ],
         action => [ t8('E Mail'),
           call     => [ 'kivi.SalesPurchase.show_email_dialog' ],
           checks   => [ 'kivi.validate_form' ],
-          disabled => !$form->{id} ? t8('This invoice has not been posted yet.') : undef,
+          disabled => !$may_edit_create ? t8('You must not print this invoice.')
+                    : !$form->{id}      ? t8('This invoice has not been posted yet.')
+                    :                     undef,
         ],
       ], # end of combobox "Export"
 
@@ -378,9 +416,10 @@ sub setup_is_action_bar {
         action => [
           t8('Drafts'),
           call     => [ 'kivi.Draft.popup', 'is', 'invoice', $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,
+          disabled => !$may_edit_create ? t8('You must not change this invoice.')
+                    :  $form->{id}      ? t8('This invoice has already been posted.')
+                    : $form->{locked}   ? t8('The billing period has already been locked.')
+                    :                     undef,
         ],
       ], # end of combobox "more"
     );
@@ -391,13 +430,13 @@ sub setup_is_action_bar {
 sub form_header {
   $main::lxdebug->enter_sub();
 
+  _assert_access();
+
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
   my $cgi      = $::request->{cgi};
 
-  $main::auth->assert('invoice_edit');
-
   my %TMPL_VAR = ();
   my @custom_hiddens;
 
@@ -526,12 +565,12 @@ sub _sort_payments {
 sub form_footer {
   $main::lxdebug->enter_sub();
 
+  _assert_access();
+
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
 
-  $main::auth->assert('invoice_edit');
-
   $form->{invtotal}    = $form->{invsubtotal};
 
   # note rows
@@ -658,11 +697,11 @@ sub show_draft {
 sub update {
   $main::lxdebug->enter_sub();
 
+  _assert_access();
+
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  $main::auth->assert('invoice_edit');
-
   my ($recursive_call) = @_;
 
   $form->{print_and_post} = 0         if $form->{second_run};
@@ -1180,7 +1219,7 @@ sub credit_note {
 sub display_form {
   $::lxdebug->enter_sub;
 
-  $::auth->assert('invoice_edit');
+  _assert_access();
 
   relink_accounts();
 
index ece3072e69a32ce1428e3badde6fa86098bc53e9..175a8054877cf3a9a127c6f240f99af1d9fa5847 100755 (executable)
@@ -250,6 +250,7 @@ $self->{texts} = {
   'All as list'                 => 'Alle als Liste',
   'All changes in that file have been reverted.' => 'Alle Änderungen in dieser Datei wurden rückgängig gemacht.',
   'All clients'                 => 'Alle Mandanten',
+  'All employees'               => 'Alle Angestellten',
   'All general ledger entries'  => 'Alle Hauptbucheinträge',
   'All groups'                  => 'Alle Gruppen',
   'All modules'                 => 'Alle Module',
@@ -1233,6 +1234,7 @@ $self->{texts} = {
   'Employee (database ID)'      => 'Bearbeiter (Datenbank-ID)',
   'Employee from the original invoice' => 'Mitarbeiter der Ursprungs-Rechnung',
   'Employees'                   => 'Benutzer',
+  'Employees with read access to the project\'s invoices' => 'Angestellte mit Leserechten auf die Projektrechnungen',
   'Empty selection for warehouse will not be added, even if the old bin is still visible (use back and forth to edit again).' => 'Leere Lager-Auswahl wird ignoriert, selbst wenn noch ein Lagerplatz ausgewählt ist. Alle Daten können durch zurück und vorwärts korrigiert werden.',
   'Empty transaction!'          => 'Buchung ist leer!',
   'Enabled Quick Searched'      => 'Aktivierte Schnellsuchen',
@@ -2267,6 +2269,7 @@ $self->{texts} = {
   'Periodic inventory'          => 'Aufwandsmethode',
   'Periodic invoices active'    => 'Wiederkehrende Rechnungen aktiv',
   'Periodic invoices inactive'  => 'Wiederkehrende Rechnungen inaktiv',
+  'Permissions for invoices'    => 'Ansehrechte für Rechnungen',
   'Perpetual inventory'         => 'Bestandsmethode',
   'Personal settings'           => 'Persönliche Einstellungen',
   'Phone'                       => 'Telefon',
@@ -2451,6 +2454,7 @@ $self->{texts} = {
   'Project type'                => 'Projekttyp',
   'Project types'               => 'Projekttypen',
   'Projects'                    => 'Projekte',
+  'Projects: edit the list of employees allowed to view invoices' => 'Projekte: Liste der Angestellten bearbeiten, die Projektrechnungen ansehen dürfen',
   'Projecttransactions'         => 'Projektbuchungen',
   'Proposal'                    => 'Vorschlag',
   'Proposals'                   => 'Vorschläge',
@@ -3922,6 +3926,9 @@ $self->{texts} = {
   'You have to specify an execution date for each antry.' => 'Sie müssen für jeden zu buchenden Eintrag ein Ausführungsdatum angeben.',
   'You must chose a user.'      => 'Sie müssen einen Benutzer auswählen.',
   'You must enter a name for your new print templates.' => 'Sie müssen einen Namen für die neuen Druckvorlagen angeben.',
+  'You must not change this AR transaction.' => 'Sie dürfen diese Debitorenbuchung nicht verändern.',
+  'You must not change this invoice.' => 'Sie dürfen diese Rechnung nicht verändern.',
+  'You must not print this invoice.' => 'Sie dürfen diese Rechnung nicht drucken.',
   'You must select existing print templates or create a new set.' => 'Sie müssen vorhandene Druckvorlagen auswählen oder einen neuen Satz anlegen.',
   'You should create a backup of the database before proceeding because the backup might not be reversible.' => 'Sie sollten eine Sicherungskopie der Datenbank erstellen, bevor Sie fortfahren, da die Aktualisierung unter Umständen nicht umkehrbar ist.',
   'You\'re not editing a file.' => 'Sie bearbeiten momentan keine Datei.',
index d3c006cbbcc8c7548ea859aff620294ec18a6be8..91cbb244986e355ec41f23b0f35df3a10c8026e4 100644 (file)
   name: Invoices, Credit Notes & AR Transactions
   icon: invoices_report
   order: 500
-  access: invoice_edit
   module: ar.pl
   params:
     action: search
diff --git a/sql/Pg-upgrade2-auth/rights_for_viewing_project_specific_invoices.sql b/sql/Pg-upgrade2-auth/rights_for_viewing_project_specific_invoices.sql
new file mode 100644 (file)
index 0000000..c08eab1
--- /dev/null
@@ -0,0 +1,18 @@
+-- @tag: rights_for_viewing_project_specific_invoices
+-- @description: Rechte zum Anzeigen von Rechnungen, die zu Projekten gehören
+-- @depends: release_3_5_3
+-- @locales: Projects: edit the list of employees allowed to view invoices
+INSERT INTO auth.master_rights (position, name, description, category)
+VALUES (
+  (SELECT position + 2
+   FROM auth.master_rights
+   WHERE name = 'project_edit'),
+  'project_edit_view_invoices_permission',
+  'Projects: edit the list of employees allowed to view invoices',
+  false
+);
+
+INSERT INTO auth.group_rights (group_id, "right", granted)
+SELECT id, 'project_edit_view_invoices_permission', true
+FROM auth.group
+WHERE name = 'Vollzugriff';
diff --git a/sql/Pg-upgrade2/add_emloyee_project_assignment_for_viewing_invoices.sql b/sql/Pg-upgrade2/add_emloyee_project_assignment_for_viewing_invoices.sql
new file mode 100644 (file)
index 0000000..2f26968
--- /dev/null
@@ -0,0 +1,11 @@
+-- @tag: add_emloyee_project_assignment_for_viewing_invoices
+-- @description: Mitarbeiter*innen Projekten zuweisen können, damit diese Projektrechnungen anschauen dürfen
+-- @depends: release_3_5_3
+CREATE TABLE employee_project_invoices (
+  employee_id INTEGER NOT NULL,
+  project_id  INTEGER NOT NULL,
+
+  CONSTRAINT employee_project_invoices_pkey             PRIMARY KEY (employee_id, project_id),
+  CONSTRAINT employee_project_invoices_employee_id_fkey FOREIGN KEY (employee_id) REFERENCES employee (id) ON DELETE CASCADE,
+  CONSTRAINT employee_project_invoices_project_id_fkey  FOREIGN KEY (project_id)  REFERENCES project  (id) ON DELETE CASCADE
+);
diff --git a/templates/webpages/project/_invoice_permissions.html b/templates/webpages/project/_invoice_permissions.html
new file mode 100644 (file)
index 0000000..6419748
--- /dev/null
@@ -0,0 +1,4 @@
+[%- USE LxERP -%][%- USE L -%]<div class="clearfix">
+ [% L.select_tag("project.employee_invoice_permissions[]", SELF.employees, id="employee_invoice_permissions", title_key="safe_name", default=SELF.project.employee_invoice_permissions, default_value_key='id', multiple=1) %]
+ [% L.multiselect2side("employee_invoice_permissions", labelsx => LxERP.t8("All employees"), labeldx => LxERP.t8("Employees with read access to the project's invoices")) %]
+</div>
index b8343fb3925805d06ae8cdc586793f8cd5f870d8..67df852e4ef8cbe7fe373356bcb4c2912fb5acb1 100644 (file)
@@ -17,6 +17,9 @@
     [%- IF CUSTOM_VARIABLES.size %]
     <li><a href="#custom_variables">[% 'Custom Variables' | $T8 %]</a></li>
     [%- END %]
+    [%- IF SELF.may_edit_invoice_permissions %]
+     <li><a href="#invoice_permissions">[% 'Permissions for invoices' | $T8 %]</a></li>
+    [%- END %]
     [%- IF SELF.project.id and AUTH.assert('record_links', 1) %]
     <li><a href="#linked_records">[% 'Linked Records' | $T8 %]</a></li>
     [%- END %]
    </div>
    [%- END %]
 
+   [%- IF SELF.may_edit_invoice_permissions %]
+    <div id="invoice_permissions">
+     [%- PROCESS 'project/_invoice_permissions.html' %]
+    </div>
+   [%- END %]
+
    [%- IF SELF.project.id and AUTH.assert('record_links', 1) %]
    <div id="linked_records">
    [%- PROCESS 'project/_linked_records.html' records=SELF.linked_records %]