X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FForm.pm;h=eee73db63cf767f99a7ee9d6f8139abcc995ed3d;hb=92de2a26c90b0980490586f586011a8c69034329;hp=1368df738a27461745b299b2a3a634ccd295c188;hpb=2f8ead125d69d2b91db9b1517bb6a3604ede1f6b;p=kivitendo-erp.git diff --git a/SL/Form.pm b/SL/Form.pm index 1368df738..eee73db63 100644 --- a/SL/Form.pm +++ b/SL/Form.pm @@ -41,11 +41,13 @@ use Carp; use Data::Dumper; use Carp; +use Config; use CGI; use Cwd; use Encode; use File::Copy; use IO::File; +use Math::BigInt; use SL::Auth; use SL::Auth::DB; use SL::Auth::LDAP; @@ -64,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); @@ -77,6 +80,7 @@ use Template; use URI; use List::Util qw(first max min sum); use List::MoreUtils qw(all any apply); +use SL::DB::Tax; use strict; @@ -88,7 +92,8 @@ END { sub disconnect_standard_dbh { return unless $standard_dbh; - $standard_dbh->disconnect(); + + $standard_dbh->rollback(); undef $standard_dbh; } @@ -464,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 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 autocomplete_part + jquery/jquery.tooltipster.min + common part_selection switchmenuframe ), "jquery/ui/i18n/jquery.ui.datepicker-$::myconfig{countrycode}"); $self->{favicon} ||= "favicon.ico"; @@ -533,7 +540,7 @@ sub footer { print $::request->{layout}->post_content; if (my @inline_scripts = $::request->{layout}->javascripts_inline) { - print "\n"; + print "\n"; } print <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(); @@ -804,6 +822,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 @@ -820,7 +839,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 { @@ -944,34 +963,47 @@ 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{ (?round_amount($amount, 8) if $places < 8; + return $amount unless $amount_str =~ m{^(\d+)\.(\d+)$}; - # Remember the amount's sign but calculate in positive values only. - my $sign = $amount <=> 0; - $amount = abs $amount; + my ($pre, $post) = ($1, $2); + my $decimals = '1' . substr($post, 0, $places); - # Shift the amount left by $places+1 decimal places and truncate it - # to integer. Then to the integer equivalent of rounding to the next - # multiple of 10: first add half of it (5). Then truncate it back to - # the lower multiple of 10 by subtracting $amount modulo 10. - my $shift = 10 ** ($places + 1); - $amount = int($amount * $shift) + 5; - $amount -= $amount % 10; + my $propagation_limit = $Config{i32size} == 4 ? 7 : 18; + my $add_for_rounding = substr($post, $places, 1) >= 5 ? 1 : 0; - # Lastly shift the amount back right by $places+1 decimal places and - # restore its sign. Then we're done. - $amount = ($amount / $shift) * $sign; + if ($places > $propagation_limit) { + $decimals = Math::BigInt->new($decimals)->badd($add_for_rounding); + $pre = Math::BigInt->new($decimals)->badd(1) if substr($decimals, 0, 1) eq '2'; + + } else { + $decimals += $add_for_rounding; + $pre += 1 if substr($decimals, 0, 1) eq '2'; + } + + $amount = ("${pre}." . substr($decimals, 1)) * ($amount <=> 0); return $amount; } @@ -1111,7 +1143,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); @@ -1129,7 +1161,7 @@ sub parse_template { $full_signature =~ s/\n/
\n/g; $mail->{message} .= $full_signature; - open(IN, "<", $self->{tmpfile}) + open(IN, "<:encoding(UTF-8)", $self->{tmpfile}) or $self->error($self->cleanup . "$self->{tmpfile} : $!"); $mail->{message} .= $_ while ; close(IN); @@ -1227,6 +1259,7 @@ sub get_formname_translation { sales_delivery_order => $main::locale->text('Delivery Order'), purchase_delivery_order => $main::locale->text('Delivery Order'), dunning => $main::locale->text('Dunning'), + letter => $main::locale->text('Letter') ); $main::lxdebug->leave_sub(); @@ -1241,8 +1274,13 @@ sub get_number_prefix_for_type { (first { $self->{type} eq $_ } qw(invoice credit_note)) ? 'inv' : ($self->{type} =~ /_quotation$/) ? 'quo' : ($self->{type} =~ /_delivery_order$/) ? 'do' + : ($self->{type} =~ /letter/) ? 'letter' : 'ord'; + # better default like this? + # : ($self->{type} =~ /(sales|purcharse)_order/ : 'ord'; + # : 'prefix_undefined'; + $main::lxdebug->leave_sub(); return $prefix; } @@ -1278,6 +1316,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 = ""; } @@ -1318,7 +1359,7 @@ sub cleanup { push @err, $::locale->text('The application "#1" was not found on the system.', $application || 'pdflatex') . ' ' . $::locale->text('Please contact your administrator.'); } elsif (-f "$self->{tmpfile}.err") { - open(FH, "$self->{tmpfile}.err"); + open(FH, "<:encoding(UTF-8)", "$self->{tmpfile}.err"); @err = ; close(FH); } @@ -1682,36 +1723,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 $terms = $self->{payment_id} ? SL::DB::PaymentTerm->new(id => $self->{payment_id})->load : undef; + return if !$terms; - 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}); - - 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); @@ -1741,7 +1765,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 | . @@ -1780,13 +1805,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 { @@ -1959,23 +1986,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(); @@ -2146,7 +2156,7 @@ sub _get_taxzones { $key = "all_taxzones" unless ($key); my $tzfilter = ""; - $tzfilter = "WHERE obsolete is FALSE" if $key eq 'ALL_ACTIVE_TAXZONES'; + $tzfilter = "WHERE obsolete is FALSE" if $key eq 'ALL_ACTIVE_TAXZONES'; my $query = qq|SELECT * FROM tax_zones $tzfilter ORDER BY sortkey|; @@ -2379,7 +2389,7 @@ sub get_lists { if ($params{contacts} || $params{shipto}) { $vc = 'customer' if $self->{"vc"} eq "customer"; $vc = 'vendor' if $self->{"vc"} eq "vendor"; - die "invalid use of get_lists, need 'vc'"; + die "invalid use of get_lists, need 'vc'" unless $vc; $vc_id = $self->{"${vc}_id"}; } @@ -2422,7 +2432,7 @@ sub get_lists { } if ($params{"salesmen"}) { - $self->_get_employees($dbh, "all_salesmen", $params{"salesmen"}); + $self->_get_employees($dbh, $params{"salesmen"}); } if ($params{"business_types"}) { @@ -2597,6 +2607,25 @@ sub all_vc { $main::lxdebug->leave_sub(); } +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(); @@ -2748,6 +2777,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} @@ -2764,7 +2794,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}); @@ -2898,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/) { @@ -3439,6 +3469,82 @@ sub prepare_for_printing { return $self; } +sub calculate_arap { + my ($self,$buysell,$taxincluded,$exchangerate,$roundplaces) = @_; + + # this function is used to calculate netamount, total_tax and amount for AP and + # AR transactions (Kreditoren-/Debitorenbuchungen) by going over all lines + # (1..$rowcount) + # Thus it needs a fully prepared $form to work on. + # calculate_arap assumes $form->{amount_$i} entries still need to be parsed + + # The calculated total values are all rounded (default is to 2 places) and + # returned as parameters rather than directly modifying form. The aim is to + # make the calculation of AP and AR behave identically. There is a test-case + # for this function in t/form/arap.t + + # While calculating the totals $form->{amount_$i} and $form->{tax_$i} are + # modified and formatted and receive the correct sign for writing straight to + # acc_trans, depending on whether they are ar or ap. + + # check parameters + die "taxincluded needed in Form->calculate_arap" unless defined $taxincluded; + die "exchangerate needed in Form->calculate_arap" unless defined $exchangerate; + die 'illegal buysell parameter, has to be \"buy\" or \"sell\" in Form->calculate_arap\n' unless $buysell =~ /^(buy|sell)$/; + $roundplaces = 2 unless $roundplaces; + + my $sign = 1; # adjust final results for writing amount to acc_trans + $sign = -1 if $buysell eq 'buy'; + + my ($netamount,$total_tax,$amount); + + my $tax; + + # parse and round amounts, setting correct sign for writing to acc_trans + for my $i (1 .. $self->{rowcount}) { + $self->{"amount_$i"} = $self->round_amount($self->parse_amount(\%::myconfig, $self->{"amount_$i"}) * $exchangerate * $sign, $roundplaces); + + $amount += $self->{"amount_$i"} * $sign; + } + + for my $i (1 .. $self->{rowcount}) { + next unless $self->{"amount_$i"}; + ($self->{"tax_id_$i"}) = split /--/, $self->{"taxchart_$i"}; + my $tax_id = $self->{"tax_id_$i"}; + + my $selected_tax = SL::DB::Manager::Tax->find_by(id => "$tax_id"); + + if ( $selected_tax ) { + + if ( $buysell eq 'sell' ) { + $self->{AR_amounts}{"tax_$i"} = $selected_tax->chart->accno if defined $selected_tax->chart; + } else { + $self->{AP_amounts}{"tax_$i"} = $selected_tax->chart->accno if defined $selected_tax->chart; + }; + + $self->{"taxkey_$i"} = $selected_tax->taxkey; + $self->{"taxrate_$i"} = $selected_tax->rate; + }; + + ($self->{"amount_$i"}, $self->{"tax_$i"}) = $self->calculate_tax($self->{"amount_$i"},$self->{"taxrate_$i"},$taxincluded,$roundplaces); + + $netamount += $self->{"amount_$i"}; + $total_tax += $self->{"tax_$i"}; + + } + $amount = $netamount + $total_tax; + + # due to $sign amount_$i und tax_$i already have the right sign for acc_trans + # but reverse sign of totals for writing amounts to ar + if ( $buysell eq 'buy' ) { + $netamount *= -1; + $amount *= -1; + $total_tax *= -1; + }; + + return($netamount,$total_tax,$amount); +} + sub format_dates { my ($self, $dateformat, $longformat, @indices) = @_; @@ -3551,6 +3657,39 @@ sub layout { return $layout; } +sub calculate_tax { + # this function calculates the net amount and tax for the lines in ar, ap and + # gl and is used for update as well as post. When used with update the return + # value of amount isn't needed + + # calculate_tax should always work with positive values, or rather as the user inputs them + # calculate_tax uses db/perl numberformat, i.e. parsed numbers + # convert to negative numbers (when necessary) only when writing to acc_trans + # the amount from $form for ap/ar/gl is currently always rounded to 2 decimals before it reaches here + # for post_transaction amount already contains exchangerate and correct sign and is rounded + # calculate_tax doesn't (need to) know anything about exchangerate + + my ($self,$amount,$taxrate,$taxincluded,$roundplaces) = @_; + + $roundplaces = 2 unless defined $roundplaces; + + my $tax; + + if ($taxincluded *= 1) { + # calculate tax (unrounded), subtract from amount, round amount and round tax + $tax = $amount - ($amount / ($taxrate + 1)); # equivalent to: taxrate * amount / (taxrate + 1) + $amount = $self->round_amount($amount - $tax, $roundplaces); + $tax = $self->round_amount($tax, $roundplaces); + } else { + $tax = $amount * $taxrate; + $tax = $self->round_amount($tax, $roundplaces); + } + + $tax = 0 unless $tax; + + return ($amount,$tax); +}; + 1; __END__ @@ -3622,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 names are: oe, ar, ap, delivery_orders, parts. +Can be called wit C