X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FForm.pm;h=5b868ec5edb6be3053c6ed5baa1d3c3c21e59f51;hb=1132e5ed817c03abbe55fe57b98f815ccafb8a3c;hp=ed31ddf68ecc52bd21b902c5c74d35c4a619a298;hpb=766f5705ecb9cd56adfbffd94c871959bb64c6fd;p=kivitendo-erp.git diff --git a/SL/Form.pm b/SL/Form.pm index ed31ddf68..5b868ec5e 100644 --- a/SL/Form.pm +++ b/SL/Form.pm @@ -40,11 +40,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 +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); @@ -71,11 +75,13 @@ 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; @@ -87,7 +93,8 @@ END { sub disconnect_standard_dbh { return unless $standard_dbh; - $standard_dbh->disconnect(); + + $standard_dbh->rollback(); undef $standard_dbh; } @@ -307,8 +314,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 +470,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 ), "jquery/ui/i18n/jquery.ui.datepicker-$::myconfig{countrycode}"); $self->{favicon} ||= "favicon.ico"; @@ -533,7 +541,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; - } - + $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(); @@ -801,6 +788,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 +805,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 +929,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; + + 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'; + } - return $round_amount; + $amount = ("${pre}." . substr($decimals, 1)) * ($amount <=> 0); + return $amount; } sub parse_template { @@ -1023,7 +1039,7 @@ sub parse_template { %{ $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); @@ -1100,7 +1116,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); @@ -1118,7 +1134,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); @@ -1216,6 +1232,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(); @@ -1230,8 +1248,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; } @@ -1267,6 +1290,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 = ""; } @@ -1307,7 +1333,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); } @@ -1671,36 +1697,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); + $transdate ||= $self->{invdate} || $self->{transdate}; + my $due_date = $self->{duedate} || $self->{reqdate}; - 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}; - } - } - - $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); @@ -1730,7 +1739,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 | . @@ -1769,13 +1779,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 { @@ -1826,20 +1838,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"}); @@ -1847,52 +1864,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 { @@ -1948,23 +1981,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(); @@ -2134,8 +2150,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); @@ -2362,8 +2380,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"}); @@ -2404,7 +2427,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"}) { @@ -2579,6 +2602,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(); @@ -2730,6 +2785,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} @@ -2746,7 +2802,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}); @@ -2829,7 +2886,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; @@ -2842,7 +2901,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; @@ -2880,7 +2941,6 @@ sub lastname_used { "d.description" => "department", "ct.name" => $table, "cu.name" => "currency", - "current_date + ct.terms" => "duedate", ); if ($self->{type} =~ /delivery_order/) { @@ -2951,19 +3011,9 @@ sub current_date { } sub like { - $main::lxdebug->enter_sub(); - my ($self, $string) = @_; - if ($string !~ /%/) { - $string = "%$string%"; - } - - $string =~ s/\'/\'\'/g; - - $main::lxdebug->leave_sub(); - - return $string; + return "%" . SL::Util::trim($string // '') . "%"; } sub redo_rows { @@ -3334,11 +3384,17 @@ sub prepare_for_printing { $self->{"employee_${_}"} = $defaults->$_ for qw(address businessnumber co_ustid company duns sepa_creditor_id taxnumber); } - # 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; + # 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} : ''; @@ -3346,15 +3402,15 @@ 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; } - $self->{myconfig_output_dateformat} = $output_dateformat; - $self->{myconfig_output_longdates} = $output_longdates; - $self->{myconfig_output_numberformat} = $output_numberformat; + $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}); @@ -3422,6 +3478,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) = @_; @@ -3534,6 +3666,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__ @@ -3605,6 +3771,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