4. Überarbeitung Prüfen beim Speichern, ob Dokument geändert ist
[kivitendo-erp.git] / SL / Form.pm
index 0aad56e..cc98814 100644 (file)
@@ -66,6 +66,7 @@ use SL::IC;
 use SL::IS;
 use SL::Layout::Dispatcher;
 use SL::Locale;
+use SL::Locale::String;
 use SL::Mailer;
 use SL::Menu;
 use SL::MoreCommon qw(uri_encode uri_decode);
@@ -91,7 +92,8 @@ END {
 
 sub disconnect_standard_dbh {
   return unless $standard_dbh;
-  $standard_dbh->disconnect();
+
+  $standard_dbh->rollback();
   undef $standard_dbh;
 }
 
@@ -467,16 +469,18 @@ sub header {
   # standard css for all
   # this should gradually move to the layouts that need it
   $layout->use_stylesheet("$_.css") for qw(
-    main menu common list_accounts jquery.autocomplete
+    common main menu list_accounts jquery.autocomplete
     jquery.multiselect2side
     ui-lightness/jquery-ui
     jquery-ui.custom
+    tooltipster themes/tooltipster-light
   );
 
   $layout->use_javascript("$_.js") for (qw(
     jquery jquery-ui jquery.cookie jquery.checkall jquery.download
     jquery/jquery.form jquery/fixes client_js
-    common part_selection switchmenuframe
+    jquery/jquery.tooltipster.min
+    common part_selection
   ), "jquery/ui/i18n/jquery.ui.datepicker-$::myconfig{countrycode}");
 
   $self->{favicon} ||= "favicon.ico";
@@ -536,7 +540,7 @@ sub footer {
   print $::request->{layout}->post_content;
 
   if (my @inline_scripts = $::request->{layout}->javascripts_inline) {
-    print "<script type='text/javascript'>@inline_scripts</script>\n";
+    print "<script type='text/javascript'>" . join("; ", @inline_scripts) . "</script>\n";
   }
 
   print <<EOL
@@ -581,6 +585,17 @@ sub set_standard_title {
   $::lxdebug->leave_sub;
 }
 
+sub prepare_global_vars {
+  my ($self) = @_;
+
+  $self->{AUTH}            = $::auth;
+  $self->{INSTANCE_CONF}   = $::instance_conf;
+  $self->{LOCALE}          = $::locale;
+  $self->{LXCONFIG}        = $::lx_office_conf;
+  $self->{LXDEBUG}         = $::lxdebug;
+  $self->{MYCONFIG}        = \%::myconfig;
+}
+
 sub _prepare_html_template {
   $main::lxdebug->enter_sub();
 
@@ -606,36 +621,12 @@ sub _prepare_html_template {
     ::end_of_request();
   }
 
-  if ($self->{"DEBUG"}) {
-    $additional_params->{"DEBUG"} = $self->{"DEBUG"};
-  }
-
-  if ($additional_params->{"DEBUG"}) {
-    $additional_params->{"DEBUG"} =
-      "<br><em>DEBUG INFORMATION:</em><pre>" . $additional_params->{"DEBUG"} . "</pre>";
-  }
-
-  if (%main::myconfig) {
-    $::myconfig{jsc_dateformat} = apply {
-      s/d+/\%d/gi;
-      s/m+/\%m/gi;
-      s/y+/\%Y/gi;
-    } $::myconfig{"dateformat"};
-    $additional_params->{"myconfig"} ||= \%::myconfig;
-    map { $additional_params->{"myconfig_${_}"} = $main::myconfig{$_}; } keys %::myconfig;
-  }
-
+  $additional_params->{AUTH}          = $::auth;
   $additional_params->{INSTANCE_CONF} = $::instance_conf;
-
-  if (my $debug_options = $::lx_office_conf{debug}{options}) {
-    map { $additional_params->{'DEBUG_' . uc($_)} = $debug_options->{$_} } keys %$debug_options;
-  }
-
-  if ($main::auth && $main::auth->{RIGHTS} && $main::auth->{RIGHTS}->{$self->{login}}) {
-    while (my ($key, $value) = each %{ $main::auth->{RIGHTS}->{$self->{login}} }) {
-      $additional_params->{"AUTH_RIGHTS_" . uc($key)} = $value;
-    }
-  }
+  $additional_params->{LOCALE}        = $::locale;
+  $additional_params->{LXCONFIG}      = \%::lx_office_conf;
+  $additional_params->{LXDEBUG}       = $::lxdebug;
+  $additional_params->{MYCONFIG}      = \%::myconfig;
 
   $main::lxdebug->leave_sub();
 
@@ -807,6 +798,7 @@ sub format_amount {
   my $force_places = defined $places && $places >= 0;
 
   $amount = $self->round_amount($amount, abs $places) if $force_places;
+  $neg    = 0 if $amount == 0; # don't show negative zero
   $amount = sprintf "%.*f", ($force_places ? $places : 10), abs $amount; # 6 is default for %fa
 
   # before the sprintf amount was a number, afterwards it's a string. because of the dynamic nature of perl
@@ -823,7 +815,7 @@ sub format_amount {
   if ($places || $p[1]) {
     $amount .= $d[0]
             .  ( $p[1] || '' )
-            .  (0 x (abs($places || 0) - length ($p[1]||'')));           # pad the fraction
+            .  (0 x max(abs($places || 0) - length ($p[1]||''), 0));     # pad the fraction
   }
 
   $amount = do {
@@ -947,11 +939,22 @@ sub parse_amount {
 
   # Make sure no code wich is not a math expression ends up in eval().
   return 0 unless $amount =~ /^ [\s \d \( \) \- \+ \* \/ \. ]* $/x;
+
+  # Prevent numbers from being parsed as octals;
+  $amount =~ s{ (?<! [\d.] ) 0+ (?= [1-9] ) }{}gx;
+
   return scalar(eval($amount)) * 1 ;
 }
 
 sub round_amount {
-  my ($self, $amount, $places) = @_;
+  my ($self, $amount, $places, $adjust) = @_;
+
+  return 0 if !defined $amount;
+
+  if ($adjust) {
+    my $precision = $::instance_conf->get_precision || 0.01;
+    return $self->round_amount( $self->round_amount($amount / $precision, 0) * $precision, $places);
+  }
 
   # We use Perl's knowledge of string representation for
   # rounding. First, convert the floating point number to a string
@@ -960,7 +963,9 @@ sub round_amount {
   # part. If an overflow occurs then apply that overflow to the part
   # before the decimal sign as well using integer arithmetic again.
 
-  my $amount_str = sprintf '%.*f', $places + 10, abs($amount);
+  my $int_amount = int(abs $amount);
+  my $str_places = max(min(10, 16 - length("$int_amount") - $places), $places);
+  my $amount_str = sprintf '%.*f', $places + $str_places, abs($amount);
 
   return $amount unless $amount_str =~ m{^(\d+)\.(\d+)$};
 
@@ -1119,7 +1124,7 @@ sub parse_template {
 
   if ($self->{media} eq 'email') {
 
-    my $mail = new Mailer;
+    my $mail = Mailer->new;
 
     map { $mail->{$_} = $self->{$_} }
       qw(cc bcc subject message version format);
@@ -1292,6 +1297,9 @@ sub generate_attachment_filename {
   } elsif ($attachment_filename && $self->{"${prefix}number"}) {
     $attachment_filename .=  "_" . $self->{"${prefix}number"} . $self->get_extension_for_format();
 
+  } elsif ($attachment_filename) {
+    $attachment_filename .=  $self->get_extension_for_format();
+
   } else {
     $attachment_filename = "";
   }
@@ -1696,36 +1704,19 @@ sub get_default_currency {
 }
 
 sub set_payment_options {
-  $main::lxdebug->enter_sub();
-
   my ($self, $myconfig, $transdate) = @_;
 
-  return $main::lxdebug->leave_sub() unless ($self->{payment_id});
-
-  my $dbh = $self->get_standard_dbh($myconfig);
-
-  my $query =
-    qq|SELECT p.terms_netto, p.terms_skonto, p.percent_skonto, p.description_long , p.description | .
-    qq|FROM payment_terms p | .
-    qq|WHERE p.id = ?|;
-
-  ($self->{terms_netto}, $self->{terms_skonto}, $self->{percent_skonto},
-   $self->{payment_terms}, $self->{payment_description}) =
-     selectrow_query($self, $dbh, $query, $self->{payment_id});
+  my $terms = $self->{payment_id} ? SL::DB::PaymentTerm->new(id => $self->{payment_id})->load : undef;
+  return if !$terms;
 
-  if ($transdate eq "") {
-    if ($self->{invdate}) {
-      $transdate = $self->{invdate};
-    } else {
-      $transdate = $self->{transdate};
-    }
-  }
+  $transdate                  ||= $self->{invdate} || $self->{transdate};
+  my $due_date                  = $self->{duedate} || $self->{reqdate};
 
-  $query =
-    qq|SELECT ?::date + ?::integer AS netto_date, ?::date + ?::integer AS skonto_date | .
-    qq|FROM payment_terms|;
-  ($self->{netto_date}, $self->{skonto_date}) =
-    selectrow_query($self, $dbh, $query, $transdate, $self->{terms_netto}, $transdate, $self->{terms_skonto});
+  $self->{$_}                   = $terms->$_ for qw(terms_netto terms_skonto percent_skonto);
+  $self->{payment_terms}        = $terms->description_long;
+  $self->{payment_description}  = $terms->description;
+  $self->{netto_date}           = $terms->calc_date(reference_date => $transdate, due_date => $due_date, terms => 'net')->to_kivitendo;
+  $self->{skonto_date}          = $terms->calc_date(reference_date => $transdate, due_date => $due_date, terms => 'discount')->to_kivitendo;
 
   my ($invtotal, $total);
   my (%amounts, %formatted_amounts);
@@ -1755,7 +1746,8 @@ sub set_payment_options {
   }
 
   if ($self->{"language_id"}) {
-    $query =
+    my $dbh   = $self->get_standard_dbh($myconfig);
+    my $query =
       qq|SELECT t.translation, l.output_numberformat, l.output_dateformat, l.output_longdates | .
       qq|FROM generic_translations t | .
       qq|LEFT JOIN language l ON t.language_id = l.id | .
@@ -1794,13 +1786,15 @@ sub set_payment_options {
   $self->{payment_terms} =~ s/<%account_number%>/$self->{account_number}/g;
   $self->{payment_terms} =~ s/<%bank%>/$self->{bank}/g;
   $self->{payment_terms} =~ s/<%bank_code%>/$self->{bank_code}/g;
+  $self->{payment_terms} =~ s/<\%bic\%>/$self->{bic}/g;
+  $self->{payment_terms} =~ s/<\%iban\%>/$self->{iban}/g;
+  $self->{payment_terms} =~ s/<\%mandate_date_of_signature\%>/$self->{mandate_date_of_signature}/g;
+  $self->{payment_terms} =~ s/<\%mandator_id\%>/$self->{mandator_id}/g;
 
   map { $self->{payment_terms} =~ s/<%${_}%>/$formatted_amounts{$_}/g; } keys %formatted_amounts;
 
   $self->{skonto_in_percent} = $formatted_amounts{skonto_in_percent};
 
-  $main::lxdebug->leave_sub();
-
 }
 
 sub get_template_language {
@@ -1864,7 +1858,7 @@ sub add_shipto {
   my $shipto;
   my @values;
 
-  foreach my $item (qw(name department_1 department_2 street zipcode city country
+  foreach my $item (qw(name department_1 department_2 street zipcode city country gln
                        contact cp_gender phone fax email)) {
     if ($self->{"shipto$item"}) {
       $shipto = 1 if ($self->{$item} ne $self->{"shipto$item"});
@@ -1882,6 +1876,7 @@ sub add_shipto {
                        shiptozipcode = ?,
                        shiptocity = ?,
                        shiptocountry = ?,
+                       shiptogln = ?,
                        shiptocontact = ?,
                        shiptocp_gender = ?,
                        shiptophone = ?,
@@ -1898,6 +1893,7 @@ sub add_shipto {
                        shiptozipcode = ? AND
                        shiptocity = ? AND
                        shiptocountry = ? AND
+                       shiptogln = ? AND
                        shiptocontact = ? AND
                        shiptocp_gender = ? AND
                        shiptophone = ? AND
@@ -1909,9 +1905,9 @@ sub add_shipto {
       if(!$insert_check){
         $query =
           qq|INSERT INTO shipto (trans_id, shiptoname, shiptodepartment_1, shiptodepartment_2,
-                                 shiptostreet, shiptozipcode, shiptocity, shiptocountry,
+                                 shiptostreet, shiptozipcode, shiptocity, shiptocountry, shiptogln,
                                  shiptocontact, shiptocp_gender, shiptophone, shiptofax, shiptoemail, module)
-             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)|;
+             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)|;
         do_query($self, $dbh, $query, $id, @values, $module);
       }
     }
@@ -1973,23 +1969,6 @@ sub get_employee_data {
   $main::lxdebug->leave_sub();
 }
 
-sub get_duedate {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $reference_date) = @_;
-
-  my $terms   = $self->{payment_id}  ? SL::DB::PaymentTerm->new(id => $self->{payment_id}) ->load
-              : $self->{customer_id} ? SL::DB::Customer   ->new(id => $self->{customer_id})->load->payment
-              : $self->{vendor_id}   ? SL::DB::Vendor     ->new(id => $self->{vendor_id})  ->load->payment
-              : $self->{invdate}     ? undef # no payment terms, therefore invdate == duedate
-              :                        croak("Missing field in \$::form: payment_id, customer_id, vendor_id or invdate");
-  my $duedate = $terms ? $terms->calc_date(reference_date => $reference_date)->to_kivitendo : undef;
-
-  $main::lxdebug->leave_sub();
-
-  return $duedate;
-}
-
 sub _get_contacts {
   $main::lxdebug->enter_sub();
 
@@ -2611,6 +2590,38 @@ sub all_vc {
   $main::lxdebug->leave_sub();
 }
 
+sub new_lastmtime {
+  my ($self, $table, $option) = @_;
+
+  return                                       unless $self->{id};
+  croak ("wrong call, no valid table defined") unless $table =~ /^(oe|ar|ap|delivery_orders|parts)$/;
+
+  my $query       = "SELECT mtime, itime FROM " . $table . " WHERE id = ?";
+  my $ref         = selectfirst_hashref_query($self, $self->get_standard_dbh, $query, $self->{id});
+  $ref->{mtime} ||= $ref->{itime};
+  $self->{lastmtime} = $ref->{mtime};
+  $main::lxdebug->message(LXDebug->DEBUG2(),"new lastmtime=".$self->{lastmtime});
+}
+
+sub mtime_ischanged {
+  my ($self, $table, $option) = @_;
+
+  return                                       unless $self->{id};
+  croak ("wrong call, no valid table defined") unless $table =~ /^(oe|ar|ap|delivery_orders|parts)$/;
+
+  my $query       = "SELECT mtime, itime FROM " . $table . " WHERE id = ?";
+  my $ref         = selectfirst_hashref_query($self, $self->get_standard_dbh, $query, $self->{id});
+  $ref->{mtime} ||= $ref->{itime};
+
+  if ($self->{lastmtime} && $self->{lastmtime} ne $ref->{mtime} ) {
+      $self->error(($option eq 'mail') ?
+        t8("The document has been changed by another user. No mail was sent. Please reopen it in another window and copy the changes to the new window") :
+        t8("The document has been changed by another user. Please reopen it in another window and copy the changes to the new window")
+      );
+    ::end_of_request();
+  }
+}
+
 sub language_payment {
   $main::lxdebug->enter_sub();
 
@@ -2762,6 +2773,7 @@ sub create_links {
       qq|SELECT
            a.cp_id, a.invnumber, a.transdate, a.${table}_id, a.datepaid,
            a.duedate, a.ordnumber, a.taxincluded, (SELECT cu.name FROM currencies cu WHERE cu.id=a.currency_id) AS currency, a.notes,
+           a.mtime, a.itime,
            a.intnotes, a.department_id, a.amount AS oldinvtotal,
            a.paid AS oldtotalpaid, a.employee_id, a.gldate, a.type,
            a.globalproject_id, ${extra_columns}
@@ -2778,7 +2790,8 @@ sub create_links {
     foreach my $key (keys %$ref) {
       $self->{$key} = $ref->{$key};
     }
-
+    $self->{mtime}   ||= $self->{itime};
+    $self->{lastmtime} = $self->{mtime};
     my $transdate = "current_date";
     if ($self->{transdate}) {
       $transdate = $dbh->quote($self->{transdate});
@@ -2861,7 +2874,9 @@ sub create_links {
            d.closedto, d.revtrans,
            (SELECT cu.name FROM currencies cu WHERE cu.id=d.currency_id) AS defaultcurrency,
            (SELECT c.accno FROM chart c WHERE d.fxgain_accno_id = c.id) AS fxgain_accno,
-           (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id = c.id) AS fxloss_accno
+           (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id = c.id) AS fxloss_accno,
+           (SELECT c.accno FROM chart c WHERE d.rndgain_accno_id = c.id) AS rndgain_accno,
+           (SELECT c.accno FROM chart c WHERE d.rndloss_accno_id = c.id) AS rndloss_accno
          FROM defaults d|;
     $ref = selectfirst_hashref_query($self, $dbh, $query);
     map { $self->{$_} = $ref->{$_} } keys %$ref;
@@ -2874,7 +2889,9 @@ sub create_links {
             current_date AS transdate, d.closedto, d.revtrans,
             (SELECT cu.name FROM currencies cu WHERE cu.id=d.currency_id) AS defaultcurrency,
             (SELECT c.accno FROM chart c WHERE d.fxgain_accno_id = c.id) AS fxgain_accno,
-            (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id = c.id) AS fxloss_accno
+            (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id = c.id) AS fxloss_accno,
+            (SELECT c.accno FROM chart c WHERE d.rndgain_accno_id = c.id) AS rndgain_accno,
+            (SELECT c.accno FROM chart c WHERE d.rndloss_accno_id = c.id) AS rndloss_accno
           FROM defaults d|;
     $ref = selectfirst_hashref_query($self, $dbh, $query);
     map { $self->{$_} = $ref->{$_} } keys %$ref;
@@ -2912,7 +2929,6 @@ sub lastname_used {
                     "d.description"           => "department",
                     "ct.name"                 => $table,
                     "cu.name"                 => "currency",
-                    "current_date + ct.terms" => "duedate",
     );
 
   if ($self->{type} =~ /delivery_order/) {
@@ -3501,9 +3517,9 @@ sub calculate_arap {
     if ( $selected_tax ) {
 
       if ( $buysell eq 'sell' ) {
-        $self->{AR_amounts}{"tax_$i"} = $selected_tax->chart->accno unless $selected_tax->taxkey == 0;
+        $self->{AR_amounts}{"tax_$i"} = $selected_tax->chart->accno if defined $selected_tax->chart;
       } else {
-        $self->{AP_amounts}{"tax_$i"} = $selected_tax->chart->accno unless $selected_tax->taxkey == 0;
+        $self->{AP_amounts}{"tax_$i"} = $selected_tax->chart->accno if defined $selected_tax->chart;
       };
 
       $self->{"taxkey_$i"} = $selected_tax->taxkey;
@@ -3745,6 +3761,17 @@ Used to override the default favicon.
 
 A html page title will be generated from this
 
+=item mtime_ischanged
+
+Tries to avoid concurrent write operations to records by checking the database mtime with a fetched one.
+
+Can be used / called with any table, that has itime and mtime attributes.
+Valid C<table> names are: oe, ar, ap, delivery_orders, parts.
+Can be called wit C<option> mail to generate a different error message.
+
+Returns undef if no save operation has been done yet ($self->{id} not present).
+Returns undef if no concurrent write process is detected otherwise a error message.
+
 =back
 
 =cut