X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FForm.pm;h=7cea6a2128851fd67afd1878e24bcbe1f2c851c0;hb=9fbc6ff48867b6125b6ae34520c399376573f8dc;hp=21817242701799503cb2e31a37aca46910a2977c;hpb=c29d964cf0a67d868e6877115bbbe0ae2eeb722e;p=kivitendo-erp.git diff --git a/SL/Form.pm b/SL/Form.pm index 218172427..7cea6a212 100644 --- a/SL/Form.pm +++ b/SL/Form.pm @@ -27,7 +27,8 @@ # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software -# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1335, USA. #====================================================================== # Utilities for parsing forms # and supporting routines for linking account numbers @@ -40,11 +41,14 @@ package Form; 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; @@ -63,6 +67,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); @@ -71,26 +76,16 @@ use SL::PrefixedNumber; use SL::Request; use SL::Template; use SL::User; +use SL::Util; use SL::X; 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; -my $standard_dbh; - -END { - disconnect_standard_dbh(); -} - -sub disconnect_standard_dbh { - return unless $standard_dbh; - $standard_dbh->disconnect(); - undef $standard_dbh; -} - sub read_version { my ($self) = @_; @@ -307,8 +302,7 @@ sub error { $self->show_generic_error($msg); } else { - print STDERR "Error: $msg\n"; - ::end_of_request(); + confess "Error: $msg\n"; } $main::lxdebug->leave_sub(); @@ -464,16 +458,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 client_js - common part_selection switchmenuframe autocomplete_part + jquery/jquery.form jquery/fixes client_js + jquery/jquery.tooltipster.min + common part_selection ), "jquery/ui/i18n/jquery.ui.datepicker-$::myconfig{countrycode}"); $self->{favicon} ||= "favicon.ico"; @@ -533,7 +529,7 @@ sub footer { print $::request->{layout}->post_content; if (my @inline_scripts = $::request->{layout}->javascripts_inline) { - print "\n"; + print "\n"; } print <header; print qq|
$info
|; - ::end_of_request(); - } - - if ($self->{"DEBUG"}) { - $additional_params->{"DEBUG"} = $self->{"DEBUG"}; - } - - if ($additional_params->{"DEBUG"}) { - $additional_params->{"DEBUG"} = - "
DEBUG INFORMATION:
" . $additional_params->{"DEBUG"} . "
"; - } - - 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; + $::dispatcher->end_request; } + $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(); @@ -673,6 +648,7 @@ sub init_template { 'COMPILE_EXT' => '.tcc', 'COMPILE_DIR' => $::lx_office_conf{paths}->{userspath} . '/templates-cache', 'ERROR' => 'templates/webpages/generic/exception.html', + 'ENCODING' => 'utf8', })) || die; } @@ -694,11 +670,10 @@ sub show_generic_error { } if ($::request->is_ajax) { - $::lxdebug->message(0, "trying to render AJAX response..."); SL::ClientJS->new ->error($error) ->render(SL::Controller::Base->new); - ::end_of_request(); + $::dispatcher->end_request; } my $add_params = { @@ -729,7 +704,7 @@ sub show_generic_error { $main::lxdebug->leave_sub(); - ::end_of_request(); + $::dispatcher->end_request; } sub show_generic_information { @@ -749,7 +724,7 @@ sub show_generic_information { $main::lxdebug->leave_sub(); - ::end_of_request(); + $::dispatcher->end_request; } sub _store_redirect_info_in_session { @@ -775,7 +750,7 @@ sub redirect { print $::form->redirect_header($self->{callback}); } - ::end_of_request(); + $::dispatcher->end_request; $main::lxdebug->leave_sub(); } @@ -801,6 +776,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 @@ -817,7 +793,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 { @@ -941,28 +917,56 @@ 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{ (?enter_sub(2); + my ($self, $amount, $places, $adjust) = @_; - my ($self, $amount, $places) = @_; - my $round_amount; + return 0 if !defined $amount; - # Rounding like "Kaufmannsrunden" (see http://de.wikipedia.org/wiki/Rundung ) + $places //= 0; - # Round amounts to eight places before rounding to the requested - # number of places. This gets rid of errors due to internal floating - # point representation. - $amount = $self->round_amount($amount, 8) if $places < 8; - $amount = $amount * (10**($places)); - $round_amount = int($amount + .5 * ($amount <=> 0)) / (10**($places)); + if ($adjust) { + my $precision = $::instance_conf->get_precision || 0.01; + return $self->round_amount( $self->round_amount($amount / $precision, 0) * $precision, $places); + } - $main::lxdebug->leave_sub(2); + # We use Perl's knowledge of string representation for + # rounding. First, convert the floating point number to a string + # with a high number of places. Then split the string on the decimal + # sign and use integer calculation for rounding the decimal places + # part. If an overflow occurs then apply that overflow to the part + # before the decimal sign as well using integer arithmetic again. + + 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+)$}; + + my ($pre, $post) = ($1, $2); + my $decimals = '1' . substr($post, 0, $places); + + my $propagation_limit = $Config{i32size} == 4 ? 7 : 18; + my $add_for_rounding = substr($post, $places, 1) >= 5 ? 1 : 0; - return $round_amount; + 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; } sub parse_template { @@ -1019,10 +1023,11 @@ sub parse_template { file_name => $self->{IN}, form => $self, myconfig => $myconfig, - userspath => $userspath); + userspath => $userspath, + %{ $self->{TEMPLATE_DRIVER_OPTIONS} || {} }); # Copy the notes from the invoice/sales order etc. back to the variable "notes" because that is where most templates expect it to be. - $self->{"notes"} = $self->{ $self->{"formname"} . "notes" }; + $self->{"notes"} = $self->{ $self->{"formname"} . "notes" } if exists $self->{ $self->{"formname"} . "notes" }; if (!$self->{employee_id}) { $self->{"employee_${_}"} = $myconfig->{$_} for qw(email tel fax name signature); @@ -1099,7 +1104,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); @@ -1117,7 +1122,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); @@ -1215,6 +1220,8 @@ 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'), + ic_supply => $main::locale->text('Intra-Community supply'), ); $main::lxdebug->leave_sub(); @@ -1229,8 +1236,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; } @@ -1266,6 +1278,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 = ""; } @@ -1306,7 +1321,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); } @@ -1361,69 +1376,29 @@ sub datetonum { } # Database routines used throughout +# DB Handling got moved to SL::DB, these are only shims for compatibility sub dbconnect { - $main::lxdebug->enter_sub(2); - - my ($self, $myconfig) = @_; - - # connect to database - my $dbh = SL::DBConnect->connect or $self->dberror; - - # set db options - if ($myconfig->{dboptions}) { - $dbh->do($myconfig->{dboptions}) || $self->dberror($myconfig->{dboptions}); - } - - $main::lxdebug->leave_sub(2); - - return $dbh; -} - -sub dbconnect_noauto { - $main::lxdebug->enter_sub(); - - my ($self, $myconfig) = @_; - - # connect to database - my $dbh = SL::DBConnect->connect(SL::DBConnect->get_connect_args(AutoCommit => 0)) or $self->dberror; - - # set db options - if ($myconfig->{dboptions}) { - $dbh->do($myconfig->{dboptions}) || $self->dberror($myconfig->{dboptions}); - } - - $main::lxdebug->leave_sub(); - - return $dbh; + SL::DB->client->dbh; } sub get_standard_dbh { - $main::lxdebug->enter_sub(2); - - my $self = shift; - my $myconfig = shift || \%::myconfig; + my $dbh = SL::DB->client->dbh; - if ($standard_dbh && !$standard_dbh->{Active}) { - $main::lxdebug->message(LXDebug->INFO(), "get_standard_dbh: \$standard_dbh is defined but not Active anymore"); - undef $standard_dbh; + if ($dbh && !$dbh->{Active}) { + $main::lxdebug->message(LXDebug->INFO(), "get_standard_dbh: \$dbh is defined but not Active anymore"); + SL::DB->client->dbh(undef); } - $standard_dbh ||= $self->dbconnect_noauto($myconfig); - - $main::lxdebug->leave_sub(2); - - return $standard_dbh; + SL::DB->client->dbh; } -sub set_standard_dbh { - my ($self, $dbh) = @_; - my $old_dbh = $standard_dbh; - $standard_dbh = $dbh; - - return $old_dbh; +sub disconnect_standard_dbh { + SL::DB->client->dbh->rollback; } +# /database + sub date_closed { $main::lxdebug->enter_sub(); @@ -1561,18 +1536,18 @@ sub save_exchangerate { my ($self, $myconfig, $currency, $transdate, $rate, $fld) = @_; - my $dbh = $self->dbconnect($myconfig); - - my ($buy, $sell); + SL::DB->client->with_transaction(sub { + my $dbh = SL::DB->client->dbh; - $buy = $rate if $fld eq 'buy'; - $sell = $rate if $fld eq 'sell'; + my ($buy, $sell); + $buy = $rate if $fld eq 'buy'; + $sell = $rate if $fld eq 'sell'; - $self->update_exchangerate($dbh, $currency, $transdate, $buy, $sell); - - $dbh->disconnect; + $self->update_exchangerate($dbh, $currency, $transdate, $buy, $sell); + 1; + }) or do { die SL::DB->client->error }; $main::lxdebug->leave_sub(); } @@ -1670,36 +1645,20 @@ sub get_default_currency { } sub set_payment_options { - $main::lxdebug->enter_sub(); + my ($self, $myconfig, $transdate, $type) = @_; - my ($self, $myconfig, $transdate) = @_; + my $terms = $self->{payment_id} ? SL::DB::PaymentTerm->new(id => $self->{payment_id})->load : undef; + return if !$terms; - return $main::lxdebug->leave_sub() unless ($self->{payment_id}); + my $is_invoice = $type =~ m{invoice}i; - 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 = ?|; + $transdate ||= $self->{invdate} || $self->{transdate}; + my $due_date = $self->{duedate} || $self->{reqdate}; - ($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}; - } - } - - $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_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); @@ -1729,38 +1688,26 @@ sub set_payment_options { } if ($self->{"language_id"}) { - $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 | . - qq|WHERE (t.language_id = ?) - AND (t.translation_id = ?) - AND (t.translation_type = 'SL::DB::PaymentTerm/description_long')|; - my ($description_long, $output_numberformat, $output_dateformat, - $output_longdates) = - selectrow_query($self, $dbh, $query, - $self->{"language_id"}, $self->{"payment_id"}); - - $self->{payment_terms} = $description_long if ($description_long); - - if ($output_dateformat) { + my $language = SL::DB::Language->new(id => $self->{language_id})->load; + + $self->{payment_terms} = $type =~ m{invoice}i ? $terms->translated_attribute('description_long_invoice', $language->id) : undef; + $self->{payment_terms} ||= $terms->translated_attribute('description_long', $language->id); + + if ($language->output_dateformat) { foreach my $key (qw(netto_date skonto_date)) { - $self->{$key} = - $main::locale->reformat_date($myconfig, $self->{$key}, - $output_dateformat, - $output_longdates); + $self->{$key} = $::locale->reformat_date($myconfig, $self->{$key}, $language->output_dateformat, $language->output_longdates); } } - if ($output_numberformat && - ($output_numberformat ne $myconfig->{"numberformat"})) { - my $saved_numberformat = $myconfig->{"numberformat"}; - $myconfig->{"numberformat"} = $output_numberformat; - map { $formatted_amounts{$_} = $self->format_amount($myconfig, $amounts{$_}) } keys %amounts; - $myconfig->{"numberformat"} = $saved_numberformat; + if ($language->output_numberformat && ($language->output_numberformat ne $myconfig->{numberformat})) { + local $myconfig->{numberformat}; + $myconfig->{"numberformat"} = $language->output_numberformat; + $formatted_amounts{$_} = $self->format_amount($myconfig, $amounts{$_}) for keys %amounts; } } + $self->{payment_terms} = $self->{payment_terms} || ($is_invoice ? $terms->description_long_invoice : undef) || $terms->description_long; + $self->{payment_terms} =~ s/<%netto_date%>/$self->{netto_date}/g; $self->{payment_terms} =~ s/<%skonto_date%>/$self->{skonto_date}/g; $self->{payment_terms} =~ s/<%currency%>/$self->{currency}/g; @@ -1768,13 +1715,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 { @@ -1825,20 +1774,25 @@ sub get_shipto { my $query = qq|SELECT * FROM shipto WHERE shipto_id = ?|; my $ref = selectfirst_hashref_query($self, $dbh, $query, $self->{shipto_id}); map({ $self->{$_} = $ref->{$_} } keys(%$ref)); + + my $cvars = CVar->get_custom_variables( + dbh => $dbh, + module => 'ShipTo', + trans_id => $self->{shipto_id}, + ); + $self->{"shiptocvar_$_->{name}"} = $_->{value} for @{ $cvars }; } $main::lxdebug->leave_sub(); } sub add_shipto { - $main::lxdebug->enter_sub(); - my ($self, $dbh, $id, $module) = @_; 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"}); @@ -1846,52 +1800,68 @@ sub add_shipto { push(@values, $self->{"shipto${item}"}); } - if ($shipto) { - if ($self->{shipto_id}) { - my $query = qq|UPDATE shipto set - shiptoname = ?, - shiptodepartment_1 = ?, - shiptodepartment_2 = ?, - shiptostreet = ?, - shiptozipcode = ?, - shiptocity = ?, - shiptocountry = ?, - shiptocontact = ?, - shiptocp_gender = ?, - shiptophone = ?, - shiptofax = ?, - shiptoemail = ? - WHERE shipto_id = ?|; - do_query($self, $dbh, $query, @values, $self->{shipto_id}); - } else { - my $query = qq|SELECT * FROM shipto - WHERE shiptoname = ? AND - shiptodepartment_1 = ? AND - shiptodepartment_2 = ? AND - shiptostreet = ? AND - shiptozipcode = ? AND - shiptocity = ? AND - shiptocountry = ? AND - shiptocontact = ? AND - shiptocp_gender = ? AND - shiptophone = ? AND - shiptofax = ? AND - shiptoemail = ? AND - module = ? AND - trans_id = ?|; - my $insert_check = selectfirst_hashref_query($self, $dbh, $query, @values, $module, $id); - if(!$insert_check){ - $query = - qq|INSERT INTO shipto (trans_id, shiptoname, shiptodepartment_1, shiptodepartment_2, - shiptostreet, shiptozipcode, shiptocity, shiptocountry, - shiptocontact, shiptocp_gender, shiptophone, shiptofax, shiptoemail, module) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)|; - do_query($self, $dbh, $query, $id, @values, $module); - } + return if !$shipto; + + my $shipto_id = $self->{shipto_id}; + + if ($self->{shipto_id}) { + my $query = qq|UPDATE shipto set + shiptoname = ?, + shiptodepartment_1 = ?, + shiptodepartment_2 = ?, + shiptostreet = ?, + shiptozipcode = ?, + shiptocity = ?, + shiptocountry = ?, + shiptogln = ?, + shiptocontact = ?, + shiptocp_gender = ?, + shiptophone = ?, + shiptofax = ?, + shiptoemail = ? + WHERE shipto_id = ?|; + do_query($self, $dbh, $query, @values, $self->{shipto_id}); + } else { + my $query = qq|SELECT * FROM shipto + WHERE shiptoname = ? AND + shiptodepartment_1 = ? AND + shiptodepartment_2 = ? AND + shiptostreet = ? AND + shiptozipcode = ? AND + shiptocity = ? AND + shiptocountry = ? AND + shiptogln = ? AND + shiptocontact = ? AND + shiptocp_gender = ? AND + shiptophone = ? AND + shiptofax = ? AND + shiptoemail = ? AND + module = ? AND + trans_id = ?|; + my $insert_check = selectfirst_hashref_query($self, $dbh, $query, @values, $module, $id); + if(!$insert_check){ + my $insert_query = + qq|INSERT INTO shipto (trans_id, shiptoname, shiptodepartment_1, shiptodepartment_2, + shiptostreet, shiptozipcode, shiptocity, shiptocountry, shiptogln, + shiptocontact, shiptocp_gender, shiptophone, shiptofax, shiptoemail, module) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)|; + do_query($self, $dbh, $insert_query, $id, @values, $module); + + $insert_check = selectfirst_hashref_query($self, $dbh, $query, @values, $module, $id); } + + $shipto_id = $insert_check->{shipto_id}; } - $main::lxdebug->leave_sub(); + return unless $shipto_id; + + CVar->save_custom_variables( + dbh => $dbh, + module => 'ShipTo', + trans_id => $shipto_id, + variables => $self, + name_prefix => 'shipto', + ); } sub get_employee { @@ -1947,23 +1917,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(); @@ -2133,8 +2086,10 @@ sub _get_taxzones { my ($self, $dbh, $key) = @_; $key = "all_taxzones" unless ($key); + my $tzfilter = ""; + $tzfilter = "WHERE obsolete is FALSE" if $key eq 'ALL_ACTIVE_TAXZONES'; - my $query = qq|SELECT * FROM tax_zones ORDER BY id|; + my $query = qq|SELECT * FROM tax_zones $tzfilter ORDER BY sortkey|; $self->{$key} = selectall_hashref_query($self, $dbh, $query); @@ -2361,8 +2316,13 @@ sub get_lists { my $dbh = $self->get_standard_dbh(\%main::myconfig); my ($sth, $query, $ref); - my $vc = $self->{"vc"} eq "customer" ? "customer" : "vendor"; - my $vc_id = $self->{"${vc}_id"}; + my ($vc, $vc_id); + 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'" unless $vc; + $vc_id = $self->{"${vc}_id"}; + } if ($params{"contacts"}) { $self->_get_contacts($dbh, $vc_id, $params{"contacts"}); @@ -2403,7 +2363,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"}) { @@ -2475,10 +2435,10 @@ sub get_name { my $where; if ($self->{customernumber} ne "") { $where = qq|(vc.customernumber ILIKE ?)|; - push(@values, '%' . $self->{customernumber} . '%'); + push(@values, like($self->{customernumber})); } else { $where = qq|(vc.name ILIKE ?)|; - push(@values, '%' . $self->{$table} . '%'); + push(@values, like($self->{$table})); } $query = @@ -2495,7 +2455,7 @@ sub get_name { JOIN $table vc ON (a.${table}_id = vc.id) WHERE NOT (a.amount = a.paid) AND (vc.name ILIKE ?) ORDER BY vc.name~; - push(@values, '%' . $self->{$table} . '%'); + push(@values, like($self->{$table})); } $self->{name_list} = selectall_hashref_query($self, $dbh, $query, @values); @@ -2571,13 +2531,53 @@ sub all_vc { # get payment terms $query = qq|SELECT id, description FROM payment_terms - ORDER BY sortkey|; + WHERE ( obsolete IS FALSE OR id = ? ) + ORDER BY sortkey |; + $self->{payment_terms} = selectall_hashref_query($self, $dbh, $query, $self->{payment_id} || undef); - $self->{payment_terms} = selectall_hashref_query($self, $dbh, $query); + $main::lxdebug->leave_sub(); +} + +sub new_lastmtime { + $main::lxdebug->enter_sub(); + + my ($self, $table, $provided_dbh) = @_; + + my $dbh = $provided_dbh ? $provided_dbh : $self->get_standard_dbh; + 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, $dbh, $query, $self->{id}); + $ref->{mtime} ||= $ref->{itime}; + $self->{lastmtime} = $ref->{mtime}; + $main::lxdebug->message(LXDebug->DEBUG2(),"new lastmtime=".$self->{lastmtime}); $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") + ); + $::dispatcher->end_request; + } +} + +# language_payment duplicates some of the functionality of all_vc (language, +# printer, payment_terms), and at least in the case of sales invoices both +# all_vc and language_payment are called when adding new invoices sub language_payment { $main::lxdebug->enter_sub(); @@ -2601,9 +2601,9 @@ sub language_payment { # get payment terms $query = qq|SELECT id, description FROM payment_terms - ORDER BY sortkey|; - - $self->{payment_terms} = selectall_hashref_query($self, $dbh, $query); + WHERE ( obsolete IS FALSE OR id = ? ) + ORDER BY sortkey |; + $self->{payment_terms} = selectall_hashref_query($self, $dbh, $query, $self->{payment_id} || undef); # get buchungsgruppen $query = qq|SELECT id, description @@ -2663,13 +2663,6 @@ sub create_links { } # now get the account numbers -# $query = qq|SELECT c.accno, c.description, c.link, c.taxkey_id, tk.tax_id -# FROM chart c, taxkeys tk -# WHERE (c.link LIKE ?) AND (c.id = tk.chart_id) AND tk.id = -# (SELECT id FROM taxkeys WHERE (taxkeys.chart_id = c.id) AND (startdate <= $transdate) ORDER BY startdate DESC LIMIT 1) -# ORDER BY c.accno|; - -# same query as above, but without expensive subquery for each row. about 80% faster $query = qq| SELECT c.accno, c.description, c.link, c.taxkey_id, tk2.tax_id FROM chart c @@ -2688,7 +2681,7 @@ sub create_links { $sth = $dbh->prepare($query); - do_statement($self, $sth, $query, '%' . $module . '%'); + do_statement($self, $sth, $query, like($module)); $self->{accounts} = ""; while ($ref = $sth->fetchrow_hashref("NAME_lc")) { @@ -2729,6 +2722,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} @@ -2745,7 +2739,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}); @@ -2761,7 +2756,7 @@ sub create_links { ORDER BY c.accno|; $sth = $dbh->prepare($query); - do_statement($self, $sth, $query, "%$module%"); + do_statement($self, $sth, $query, like($module)); $self->{accounts} = ""; while ($ref = $sth->fetchrow_hashref("NAME_lc")) { @@ -2828,7 +2823,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; @@ -2841,7 +2838,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; @@ -2879,7 +2878,6 @@ sub lastname_used { "d.description" => "department", "ct.name" => $table, "cu.name" => "currency", - "current_date + ct.terms" => "duedate", ); if ($self->{type} =~ /delivery_order/) { @@ -2949,22 +2947,6 @@ sub current_date { return $thisdate; } -sub like { - $main::lxdebug->enter_sub(); - - my ($self, $string) = @_; - - if ($string !~ /%/) { - $string = "%$string%"; - } - - $string =~ s/\'/\'\'/g; - - $main::lxdebug->leave_sub(); - - return $string; -} - sub redo_rows { $main::lxdebug->enter_sub(); @@ -2998,52 +2980,52 @@ sub update_status { my ($i, $id); - my $dbh = $self->dbconnect_noauto($myconfig); + SL::DB->client->with_transaction(sub { + my $dbh = SL::DB->client->dbh; - my $query = qq|DELETE FROM status - WHERE (formname = ?) AND (trans_id = ?)|; - my $sth = prepare_query($self, $dbh, $query); + my $query = qq|DELETE FROM status + WHERE (formname = ?) AND (trans_id = ?)|; + my $sth = prepare_query($self, $dbh, $query); - if ($self->{formname} =~ /(check|receipt)/) { - for $i (1 .. $self->{rowcount}) { - do_statement($self, $sth, $query, $self->{formname}, $self->{"id_$i"} * 1); + if ($self->{formname} =~ /(check|receipt)/) { + for $i (1 .. $self->{rowcount}) { + do_statement($self, $sth, $query, $self->{formname}, $self->{"id_$i"} * 1); + } + } else { + do_statement($self, $sth, $query, $self->{formname}, $self->{id}); } - } else { - do_statement($self, $sth, $query, $self->{formname}, $self->{id}); - } - $sth->finish(); + $sth->finish(); - my $printed = ($self->{printed} =~ /\Q$self->{formname}\E/) ? "1" : "0"; - my $emailed = ($self->{emailed} =~ /\Q$self->{formname}\E/) ? "1" : "0"; + my $printed = ($self->{printed} =~ /\Q$self->{formname}\E/) ? "1" : "0"; + my $emailed = ($self->{emailed} =~ /\Q$self->{formname}\E/) ? "1" : "0"; - my %queued = split / /, $self->{queued}; - my @values; + my %queued = split / /, $self->{queued}; + my @values; - if ($self->{formname} =~ /(check|receipt)/) { + if ($self->{formname} =~ /(check|receipt)/) { - # this is a check or receipt, add one entry for each lineitem - my ($accno) = split /--/, $self->{account}; - $query = qq|INSERT INTO status (trans_id, printed, spoolfile, formname, chart_id) - VALUES (?, ?, ?, ?, (SELECT c.id FROM chart c WHERE c.accno = ?))|; - @values = ($printed, $queued{$self->{formname}}, $self->{prinform}, $accno); - $sth = prepare_query($self, $dbh, $query); + # this is a check or receipt, add one entry for each lineitem + my ($accno) = split /--/, $self->{account}; + $query = qq|INSERT INTO status (trans_id, printed, spoolfile, formname, chart_id) + VALUES (?, ?, ?, ?, (SELECT c.id FROM chart c WHERE c.accno = ?))|; + @values = ($printed, $queued{$self->{formname}}, $self->{prinform}, $accno); + $sth = prepare_query($self, $dbh, $query); - for $i (1 .. $self->{rowcount}) { - if ($self->{"checked_$i"}) { - do_statement($self, $sth, $query, $self->{"id_$i"}, @values); + for $i (1 .. $self->{rowcount}) { + if ($self->{"checked_$i"}) { + do_statement($self, $sth, $query, $self->{"id_$i"}, @values); + } } - } - $sth->finish(); + $sth->finish(); - } else { - $query = qq|INSERT INTO status (trans_id, printed, emailed, spoolfile, formname) - VALUES (?, ?, ?, ?, ?)|; - do_query($self, $dbh, $query, $self->{id}, $printed, $emailed, - $queued{$self->{formname}}, $self->{formname}); - } - - $dbh->commit; - $dbh->disconnect; + } else { + $query = qq|INSERT INTO status (trans_id, printed, emailed, spoolfile, formname) + VALUES (?, ?, ?, ?, ?)|; + do_query($self, $dbh, $query, $self->{id}, $printed, $emailed, + $queued{$self->{formname}}, $self->{formname}); + } + 1; + }) or do { die SL::DB->client->error }; $main::lxdebug->leave_sub(); } @@ -3129,20 +3111,21 @@ sub save_history { $main::lxdebug->enter_sub(); my $self = shift; - my $dbh = shift || $self->get_standard_dbh; - - if(!exists $self->{employee_id}) { - &get_employee($self, $dbh); - } + my $dbh = shift || SL::DB->client->dbh; + SL::DB->client->with_transaction(sub { - my $query = - qq|INSERT INTO history_erp (trans_id, employee_id, addition, what_done, snumbers) | . - qq|VALUES (?, (SELECT id FROM employee WHERE login = ?), ?, ?, ?)|; - my @values = (conv_i($self->{id}), $self->{login}, - $self->{addition}, $self->{what_done}, "$self->{snumbers}"); - do_query($self, $dbh, $query, @values); + if(!exists $self->{employee_id}) { + &get_employee($self, $dbh); + } - $dbh->commit; + my $query = + qq|INSERT INTO history_erp (trans_id, employee_id, addition, what_done, snumbers) | . + qq|VALUES (?, (SELECT id FROM employee WHERE login = ?), ?, ?, ?)|; + my @values = (conv_i($self->{id}), $self->{login}, + $self->{addition}, $self->{what_done}, "$self->{snumbers}"); + do_query($self, $dbh, $query, @values); + 1; + }) or do { die SL::DB->client->error }; $main::lxdebug->leave_sub(); } @@ -3194,16 +3177,13 @@ sub get_partsgroup { my @values; if ($p->{searchitems} eq 'part') { - $query .= qq|WHERE p.inventory_accno_id > 0|; + $query .= qq|WHERE p.part_type = 'part'|; } if ($p->{searchitems} eq 'service') { - $query .= qq|WHERE p.inventory_accno_id IS NULL|; + $query .= qq|WHERE p.part_type = 'service'|; } if ($p->{searchitems} eq 'assembly') { - $query .= qq|WHERE p.assembly = '1'|; - } - if ($p->{searchitems} eq 'labor') { - $query .= qq|WHERE (p.inventory_accno_id > 0) AND (p.income_accno_id IS NULL)|; + $query .= qq|WHERE p.part_type = 'assembly'|; } $query .= qq|ORDER BY partsgroup|; @@ -3326,11 +3306,24 @@ sub prepare_for_printing { # compatibility. $self->{$_} = $defaults->$_ for qw(company address taxnumber co_ustid duns sepa_creditor_id); - # set shipto from billto unless set - my $has_shipto = any { $self->{"shipto$_"} } qw(name street zipcode city country contact); - if (!$has_shipto && ($self->{type} =~ m/^(?:purchase_order|request_quotation)$/)) { - $self->{shiptoname} = $defaults->company; - $self->{shiptostreet} = $defaults->address; + $self->{"myconfig_${_}"} = $::myconfig{$_} for grep { $_ ne 'dbpasswd' } keys %::myconfig; + + if (!$self->{employee_id}) { + $self->{"employee_${_}"} = $::myconfig{$_} for qw(email tel fax name signature); + $self->{"employee_${_}"} = $defaults->$_ for qw(address businessnumber co_ustid company duns sepa_creditor_id taxnumber); + } + + # Load shipping address from database. If shipto_id is set then it's + # one from the customer's/vendor's master data. Otherwise look an a + # customized address linking back to the current record. + my $shipto_module = $self->{type} =~ /_delivery_order$/ ? 'DO' + : $self->{type} =~ /sales_order|sales_quotation|request_quotation|purchase_order/ ? 'OE' + : 'AR'; + my $shipto = $self->{shipto_id} ? SL::DB::Shipto->new(shipto_id => $self->{shipto_id})->load + : SL::DB::Manager::Shipto->get_first(where => [ module => $shipto_module, trans_id => $self->{id} ]); + if ($shipto) { + $self->{$_} = $shipto->$_ for grep { m{^shipto} } map { $_->name } @{ $shipto->meta->columns }; + $self->{"shiptocvar_" . $_->config->name} = $_->value_as_text for @{ $shipto->cvars_by_config }; } my $language = $self->{language} ? '_' . $self->{language} : ''; @@ -3338,12 +3331,16 @@ sub prepare_for_printing { my ($language_tc, $output_numberformat, $output_dateformat, $output_longdates); if ($self->{language_id}) { ($language_tc, $output_numberformat, $output_dateformat, $output_longdates) = AM->get_language_details(\%::myconfig, $self, $self->{language_id}); - } else { - $output_dateformat = $::myconfig{dateformat}; - $output_numberformat = $::myconfig{numberformat}; - $output_longdates = 1; } + $output_dateformat ||= $::myconfig{dateformat}; + $output_numberformat ||= $::myconfig{numberformat}; + $output_longdates //= 1; + + $self->{myconfig_output_dateformat} = $output_dateformat // $::myconfig{dateformat}; + $self->{myconfig_output_longdates} = $output_longdates // 1; + $self->{myconfig_output_numberformat} = $output_numberformat // $::myconfig{numberformat}; + # Retrieve accounts for tax calculation. IC->retrieve_accounts(\%::myconfig, $self, map { $_ => $self->{"id_$_"} } 1 .. $self->{rowcount}); @@ -3410,6 +3407,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) = @_; @@ -3522,6 +3595,40 @@ 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; + $taxincluded //= 0; + + my $tax; + + if ($taxincluded) { + # 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__ @@ -3593,6 +3700,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