From: Jan Büren Date: Fri, 27 Jun 2014 11:48:32 +0000 (+0200) Subject: Merge branch 'master' of github.com:kivitendo/kivitendo-erp X-Git-Tag: release-3.2.0beta~411 X-Git-Url: http://wagnertech.de/gitweb/gitweb.cgi/mfinanz.git/commitdiff_plain/fa7fc7eeb3ca718914affee06c0629a08d571288?hp=9c1e389807fe1e1fbadbcffcdd3332b841fc9035 Merge branch 'master' of github.com:kivitendo/kivitendo-erp --- diff --git a/SL/BackgroundJob/CreatePeriodicInvoices.pm b/SL/BackgroundJob/CreatePeriodicInvoices.pm index dc740cc30..18abf7c1a 100644 --- a/SL/BackgroundJob/CreatePeriodicInvoices.pm +++ b/SL/BackgroundJob/CreatePeriodicInvoices.pm @@ -189,7 +189,7 @@ sub _create_periodic_invoice { sub _calculate_dates { my ($config) = @_; - return $config->calculate_invoice_dates(end_date => DateTime->today_local->add(days => 1)); + return $config->calculate_invoice_dates(end_date => DateTime->today_local); } sub _send_email { @@ -213,7 +213,7 @@ sub _send_email { return unless $template; my $email_template = $config{periodic_invoices}->{email_template}; - my $filename = $email_template || ( (SL::DB::Default->get->templates || "templates/webpages") . "/periodic_invoices_email.txt" ); + my $filename = $email_template || ( (SL::DB::Default->get->templates || "templates/webpages") . "/oe/periodic_invoices_email.txt" ); my %params = ( POSTED_INVOICES => $posted_invoices, PRINTED_INVOICES => $printed_invoices ); diff --git a/SL/CT.pm b/SL/CT.pm index fa1f91477..b430272e0 100644 --- a/SL/CT.pm +++ b/SL/CT.pm @@ -68,7 +68,7 @@ sub search { "email" => "ct.email", "street" => "ct.street", "taxnumber" => "ct.taxnumber", - "business" => "ct.business", + "business" => "b.description", "invnumber" => "ct.invnumber", "ordnumber" => "ct.ordnumber", "quonumber" => "ct.quonumber", @@ -76,7 +76,8 @@ sub search { "city" => "ct.city", "country" => "ct.country", "discount" => "ct.discount", - "salesman" => "e.name" + "salesman" => "e.name", + "payment" => "pt.description" ); $form->{sort} ||= "name"; @@ -197,11 +198,13 @@ sub search { } my $query = - qq|SELECT ct.*, b.description AS business, e.name as salesman | . + qq|SELECT ct.*, b.description AS business, e.name as salesman, |. + qq| pt.description as payment | . (qq|, NULL AS invnumber, NULL AS ordnumber, NULL AS quonumber, NULL AS invid, NULL AS module, NULL AS formtype, NULL AS closed | x!! $join_records) . qq|FROM $cv ct | . qq|LEFT JOIN business b ON (ct.business_id = b.id) | . qq|LEFT JOIN employee e ON (ct.salesman_id = e.id) | . + qq|LEFT JOIN payment_terms pt ON (ct.payment_id = pt.id) | . qq|WHERE $where|; my @saved_values = @values; @@ -215,7 +218,8 @@ sub search { push(@values, @saved_values); $query .= qq| UNION | . - qq|SELECT ct.*, b.description AS business, e.name as salesman, | . + qq|SELECT ct.*, b.description AS business, e.name as salesman, |. + qq| pt.description as payment, | . qq| a.invnumber, a.ordnumber, a.quonumber, a.id AS invid, | . qq| '$module' AS module, 'invoice' AS formtype, | . qq| (a.amount = a.paid) AS closed | . @@ -223,6 +227,7 @@ sub search { qq|JOIN $ar a ON (a.${cv}_id = ct.id) | . qq|LEFT JOIN business b ON (ct.business_id = b.id) | . qq|LEFT JOIN employee e ON (ct.salesman_id = e.id) | . + qq|LEFT JOIN payment_terms pt ON (ct.payment_id = pt.id) | . qq|WHERE $where AND (a.invoice = '1')|; } @@ -230,13 +235,15 @@ sub search { push(@values, @saved_values); $query .= qq| UNION | . - qq|SELECT ct.*, b.description AS business, e.name as salesman, | . + qq|SELECT ct.*, b.description AS business, e.name as salesman, |. + qq| pt.description as payment, | . qq| ' ' AS invnumber, o.ordnumber, o.quonumber, o.id AS invid, | . qq| 'oe' AS module, 'order' AS formtype, o.closed | . qq|FROM $cv ct | . qq|JOIN oe o ON (o.${cv}_id = ct.id) | . qq|LEFT JOIN business b ON (ct.business_id = b.id) | . qq|LEFT JOIN employee e ON (ct.salesman_id = e.id) | . + qq|LEFT JOIN payment_terms pt ON (ct.payment_id = pt.id) | . qq|WHERE $where AND (o.quotation = '0')|; } @@ -245,12 +252,14 @@ sub search { $query .= qq| UNION | . qq|SELECT ct.*, b.description AS business, e.name as salesman, | . + qq| pt.description as payment, | . qq| ' ' AS invnumber, o.ordnumber, o.quonumber, o.id AS invid, | . qq| 'oe' AS module, 'quotation' AS formtype, o.closed | . qq|FROM $cv ct | . qq|JOIN oe o ON (o.${cv}_id = ct.id) | . qq|LEFT JOIN business b ON (ct.business_id = b.id) | . qq|LEFT JOIN employee e ON (ct.salesman_id = e.id) | . + qq|LEFT JOIN payment_terms pt ON (ct.payment_id = pt.id) | . qq|WHERE $where AND (o.quotation = '1')|; } } diff --git a/SL/CTI.pm b/SL/CTI.pm new file mode 100644 index 000000000..8605ff163 --- /dev/null +++ b/SL/CTI.pm @@ -0,0 +1,52 @@ +package SL::CTI; + +use strict; + +use String::ShellQuote; + +use SL::MoreCommon qw(uri_encode); + +sub call { + my ($class, %params) = @_; + + my $config = $::lx_office_conf{cti} || {}; + my $command = $config->{dial_command} || die $::locale->text('Dial command missing in kivitendo configuration\'s [cti] section'); + my $external_prefix = $params{internal} ? '' : ($config->{external_prefix} // ''); + + my %command_args = ( + phone_extension => $::myconfig{phone_extension} || die($::locale->text('Phone extension missing in user configuration')), + phone_password => $::myconfig{phone_password} || die($::locale->text('Phone password missing in user configuration')), + number => $external_prefix . $class->sanitize_number(%params), + ); + + foreach my $key (keys %command_args) { + my $value = shell_quote($command_args{$key}); + $command =~ s{<\% ${key} \%>}{$value}gx; + } + + return `$command`; +} + +sub call_link { + my ($class, %params) = @_; + + return "controller.pl?action=CTI/call&number=" . uri_encode($class->sanitize_number(number => $params{number})) . ($params{internal} ? '&internal=1' : ''); +} + +sub sanitize_number { + my ($class, %params) = @_; + + my $config = $::lx_office_conf{cti} || {}; + my $idp = $config->{international_dialing_prefix} // '00'; + + my $number = $params{number} // ''; + $number =~ s/[^0-9+\.-]//g; # delete unsupported characters + my $countrycode = $number =~ s/^(?: $idp | \+ ) ( \d{2} )//x ? $1 : ''; # TODO: countrycodes can have more or less than 2 digits + $number =~ s/^0//x if $countrycode; # kill non standard optional zero after global identifier + + return '' unless $number; + + return ($countrycode ? $idp . $countrycode : '') . $number; +} + +1; diff --git a/SL/Controller/BackgroundJobHistory.pm b/SL/Controller/BackgroundJobHistory.pm index 2f7fefb2e..6572e231d 100644 --- a/SL/Controller/BackgroundJobHistory.pm +++ b/SL/Controller/BackgroundJobHistory.pm @@ -88,7 +88,7 @@ sub make_filter_summary { @filters; my %status = ( - failed => $::locale->text('failed'), + failure => $::locale->text('failed'), success => $::locale->text('succeeded'), ); push @filter_strings, $status{ $filter->{'status:eq_ignore_empty'} } if $filter->{'status:eq_ignore_empty'}; diff --git a/SL/Controller/Base.pm b/SL/Controller/Base.pm index 4b27cd982..a233a0eb5 100644 --- a/SL/Controller/Base.pm +++ b/SL/Controller/Base.pm @@ -157,6 +157,7 @@ sub send_file { if (!ref $file_name_or_content) { $::locale->with_raw_io(\*STDOUT, sub { print while <$file> }); $file->close; + unlink $file_name_or_content if $params{unlink}; } else { $::locale->with_raw_io(\*STDOUT, sub { print $$file_name_or_content }); } @@ -490,6 +491,10 @@ C<%params> can include the following: =item * C -- the name presented to the browser; defaults to C<$file_name>; mandatory if C<$file_name_or_content> is a reference +=item * C -- if trueish and C<$file_name_or_content> refers to +a file name then unlink the file after it has been sent to the browser +(e.g. for temporary files) + =back =item C diff --git a/SL/Controller/CTI.pm b/SL/Controller/CTI.pm new file mode 100644 index 000000000..24e3a8449 --- /dev/null +++ b/SL/Controller/CTI.pm @@ -0,0 +1,66 @@ +package SL::Controller::CTI; + +use strict; + +use SL::CTI; +use SL::DB::AuthUserConfig; +use SL::Helper::Flash; +use SL::Locale::String; + +use parent qw(SL::Controller::Base); + +use Rose::Object::MakeMethods::Generic ( + 'scalar --get_set_init' => [ qw(internal_extensions) ], +); + +sub action_call { + my ($self) = @_; + + eval { + my $res = SL::CTI->call(number => $::form->{number}, internal => $::form->{internal}); + flash('info', t8('Calling #1 now', $::form->{number})); + 1; + } or do { + flash('error', $@); + }; + + $self->render('cti/calling'); +} + +sub action_list_internal_extensions { + my ($self) = @_; + + $self->render('cti/list_internal_extensions', title => t8('Internal Phone List')); +} + +# +# filters +# + +sub init_internal_extensions { + my ($self) = @_; + + my $user_configs = SL::DB::Manager::AuthUserConfig->get_all( + where => [ + cfg_key => 'phone_extension', + '!cfg_value' => undef, + '!cfg_value' => '', + ], + with_objects => [ qw(user) ], + ); + + my %users; + foreach my $config (@{ $user_configs }) { + $users{$config->user_id} ||= { + name => $config->user->get_config_value('name') || $config->user->login, + phone_extension => $config->cfg_value, + call_link => SL::CTI->call_link(number => $config->cfg_value, internal => 1), + }; + } + + return [ + sort { lc($a->{name}) cmp lc($b->{name}) } values %users + ]; +} + +1; diff --git a/SL/Controller/CustomerVendor.pm b/SL/Controller/CustomerVendor.pm index f59fd07b5..af109e967 100644 --- a/SL/Controller/CustomerVendor.pm +++ b/SL/Controller/CustomerVendor.pm @@ -551,6 +551,8 @@ sub action_ajaj_customer_autocomplete { $::form->{column} ? ($::form->{column} => $query) : (or => [ customernumber => $query, name => $query ]) ); + push @filter, (or => [ obsolete => undef, obsolete => 0 ]) if !$::form->{obsolete}; + my $customers = SL::DB::Manager::Customer->get_all(query => [ @filter ], limit => $limit); my $value_col = $::form->{column} || 'name'; @@ -878,4 +880,15 @@ sub normalize_name { $self->{cv}->name($name); } +sub home_address_for_google_maps { + my ($self) = @_; + + my $address = $::instance_conf->get_address // ''; + $address =~ s{^\s+|\s+$|\r+}{}g; + $address =~ s{\n+}{,}g; + $address =~ s{\s+}{ }g; + + return $address; +} + 1; diff --git a/SL/Controller/LiquidityProjection.pm b/SL/Controller/LiquidityProjection.pm new file mode 100644 index 000000000..24413d781 --- /dev/null +++ b/SL/Controller/LiquidityProjection.pm @@ -0,0 +1,81 @@ +package SL::Controller::LiquidityProjection; + +use strict; + +use parent qw(SL::Controller::Base); + +use SL::Locale::String; +use SL::LiquidityProjection; +use SL::Util qw(_hashify); + +__PACKAGE__->run_before('check_auth'); + +use Rose::Object::MakeMethods::Generic ( + scalar => [ qw(liquidity) ], + 'scalar --get_set_init' => [ qw(oe_report_columns_str) ], +); + + +# +# actions +# + +sub action_show { + my ($self) = @_; + + $self->liquidity(SL::LiquidityProjection->new(%{ $::form->{params} })->create) if $::form->{params}; + + $::form->{params} ||= { + months => 6, + type => 1, + salesman => 1, + buchungsgruppe => 1, + }; + + $self->render('liquidity_projection/show', title => t8('Liquidity projection')); +} + +# +# filters +# + +sub check_auth { $::auth->assert('report') } +sub init_oe_report_columns_str { join '&', map { "$_=Y" } qw(open delivered notdelivered l_ordnumber l_transdate l_reqdate l_name l_employee l_salesman l_netamount l_amount l_transaction_description) } + +# +# helpers +# + +sub link_to_old_orders { + my $self = shift; + my %params = _hashify(0, @_); + + my $reqdate = $params{reqdate}; + my $months = $params{months} * 1; + + my $fields = ''; + + if ($reqdate eq 'old') { + $fields .= '&reqdate_unset_or_old=Y'; + + } elsif ($reqdate eq 'future') { + my @now = localtime; + $fields .= '&reqdatefrom=' . $self->iso_to_display(SL::LiquidityProjection::_the_date($now[5] + 1900, $now[4] + 1 + $months) . '-01'); + + } else { + $reqdate =~ m/(\d+)-(\d+)/; + $fields .= '&reqdatefrom=' . $self->iso_to_display($reqdate . '-01'); + $fields .= '&reqdateto=' . $self->iso_to_display($reqdate . sprintf('-%02d', DateTime->last_day_of_month(year => $1, month => $2)->day)); + + } + + return "oe.pl?action=orders&type=sales_order&vc=customer&" . $self->oe_report_columns_str . $fields; +} + +sub iso_to_display { + my ($self, $date) = @_; + + $::locale->reformat_date({ dateformat => 'yyyy-mm-dd' }, $date, $::myconfig{dateformat}); +} + +1; diff --git a/SL/Controller/LoginScreen.pm b/SL/Controller/LoginScreen.pm index adf8b471c..d38736b82 100644 --- a/SL/Controller/LoginScreen.pm +++ b/SL/Controller/LoginScreen.pm @@ -155,6 +155,7 @@ sub error_state { my %states = ( session => { warning => t8('The session has expired. Please log in again.') }, password => { error => t8('Incorrect username or password or no access to selected client!') }, + action => { warning => t8('The action is missing or invalid.') }, ); return %{ $states{$_[0]} || {} }; diff --git a/SL/Controller/Part.pm b/SL/Controller/Part.pm index e64a774a7..d5a7f2a5f 100644 --- a/SL/Controller/Part.pm +++ b/SL/Controller/Part.pm @@ -52,6 +52,7 @@ sub action_ajax_autocomplete { description => $_->description, type => $_->type, unit => $_->unit, + cvars => { map { ($_->config->name => { value => $_->value_as_text, is_valid => $_->is_valid }) } @{ $_->cvars_by_config } }, } } @{ $self->parts }; # neato: if exact match triggers we don't even need the init_parts diff --git a/SL/DB/Chart.pm b/SL/DB/Chart.pm index 718d42251..40ac530d4 100644 --- a/SL/DB/Chart.pm +++ b/SL/DB/Chart.pm @@ -16,10 +16,15 @@ __PACKAGE__->meta->initialize; sub get_active_taxkey { my ($self, $date) = @_; $date ||= DateTime->today_local; + + my $cache = $::request->cache("get_active_taxkey")->{$date} //= {}; + return $cache->{$self->id} if $cache->{$self->id}; + require SL::DB::TaxKey; - return SL::DB::Manager::TaxKey->get_all(query => [ and => [ chart_id => $self->id, - startdate => { le => $date } ] ], - sort_by => "startdate DESC")->[0]; + return $cache->{$self->id} = SL::DB::Manager::TaxKey->get_all( + query => [ and => [ chart_id => $self->id, + startdate => { le => $date } ] ], + sort_by => "startdate DESC")->[0]; } 1; diff --git a/SL/DB/CustomVariable.pm b/SL/DB/CustomVariable.pm index 3f894cd01..7e6b50080 100644 --- a/SL/DB/CustomVariable.pm +++ b/SL/DB/CustomVariable.pm @@ -20,9 +20,11 @@ sub unparsed_value { sub _ensure_config { my ($self) = @_; - return $self->config if $self->config; + return $self->config if defined $self->{config}; return undef if !defined $self->config_id; - $self->config( SL::DB::CustomVariableConfig->new(id => $self->config_id)->load ); + + no warnings 'once'; + return $::request->cache('config_by_id')->{$self->config_id} //= SL::DB::CustomVariableConfig->new(id => $self->config_id)->load; } sub parse_value { @@ -80,7 +82,8 @@ sub value { sub value_as_text { my $self = $_[0]; - my $type = $self->config->type; + my $cfg = $self->_ensure_config; + my $type = $cfg->type; die 'not an accessor' if @_ > 1; @@ -89,7 +92,7 @@ sub value_as_text { } elsif ($type eq 'timestamp') { return $::locale->reformat_date( { dateformat => 'yy-mm-dd' }, $self->timestamp_value->ymd, $::myconfig{dateformat}); } elsif ($type eq 'number') { - return $::form->format_amount(\%::myconfig, $self->number_value, $self->config->processed_options->{PRECISION}); + return $::form->format_amount(\%::myconfig, $self->number_value, $cfg->processed_options->{PRECISION}); } elsif ( $type eq 'customer' ) { require SL::DB::Customer; @@ -118,7 +121,7 @@ sub is_valid { require SL::DB::CustomVariableValidity; my $query = [config_id => $self->config_id, trans_id => $self->trans_id]; - return SL::DB::Manager::CustomVariableValidity->get_all_count(query => $query) == 0; + return (SL::DB::Manager::CustomVariableValidity->get_all_count(query => $query) == 0) ? 1 : 0; } 1; diff --git a/SL/DB/CustomVariableConfig.pm b/SL/DB/CustomVariableConfig.pm index 203f83a90..2cc4e6a90 100644 --- a/SL/DB/CustomVariableConfig.pm +++ b/SL/DB/CustomVariableConfig.pm @@ -5,6 +5,8 @@ package SL::DB::CustomVariableConfig; use strict; +use List::MoreUtils qw(any); + use SL::DB::MetaSetup::CustomVariableConfig; use SL::DB::Manager::CustomVariableConfig; use SL::DB::Helper::ActsAsList; @@ -87,4 +89,11 @@ sub has_flag { return $self->processed_flags()->{$flag}; } +sub type_dependent_default_value { + my ($self) = @_; + + return $self->default_value if $self->type ne 'select'; + return (any { $_ eq $self->default_value } @{ $self->processed_options }) ? $self->default_value : $self->processed_options->[0]; +} + 1; diff --git a/SL/DB/Helper/CustomVariables.pm b/SL/DB/Helper/CustomVariables.pm index 737c163a9..713685519 100644 --- a/SL/DB/Helper/CustomVariables.pm +++ b/SL/DB/Helper/CustomVariables.pm @@ -196,7 +196,7 @@ sub _new_cvar { # value needs config $inherited_value ? $cvar->value($inherited_value) - : $cvar->value($params{config}->default_value); + : $cvar->value($params{config}->type_dependent_default_value); return $cvar; } diff --git a/SL/DB/Helper/LinkedRecords.pm b/SL/DB/Helper/LinkedRecords.pm index cca9fe3c1..182f3bdcc 100644 --- a/SL/DB/Helper/LinkedRecords.pm +++ b/SL/DB/Helper/LinkedRecords.pm @@ -148,6 +148,9 @@ sub _linked_records_implementation { ORDER BY ${wanted}_table, ${wanted}_id, depth ASC; my $links = selectall_hashref_query($::form, $::form->get_standard_dbh, $query, $self->id, $self->meta->table); + + return [] unless @$links; + my $link_objs = SL::DB::Manager::RecordLink->get_all(query => [ id => [ map { $_->{id} } @$links ] ]); my @objects = map { $get_objects->($_) } @$link_objs; diff --git a/SL/DB/Helper/PriceTaxCalculator.pm b/SL/DB/Helper/PriceTaxCalculator.pm index d95d14309..ed485c190 100644 --- a/SL/DB/Helper/PriceTaxCalculator.pm +++ b/SL/DB/Helper/PriceTaxCalculator.pm @@ -6,14 +6,21 @@ use parent qw(Exporter); our @EXPORT = qw(calculate_prices_and_taxes); use Carp; -use List::Util qw(sum min); +use List::Util qw(sum min max); sub calculate_prices_and_taxes { my ($self, %params) = @_; + require SL::DB::Chart; + require SL::DB::Currency; + require SL::DB::Default; + require SL::DB::InvoiceItem; + require SL::DB::Part; require SL::DB::PriceFactor; require SL::DB::Unit; + SL::DB::Part->load_cached(map { $_->parts_id } @{ $self->items }); + my %units_by_name = map { ( $_->name => $_ ) } @{ SL::DB::Manager::Unit->get_all }; my %price_factors_by_id = map { ( $_->id => $_ ) } @{ SL::DB::Manager::PriceFactor->get_all }; @@ -37,6 +44,8 @@ sub calculate_prices_and_taxes { $self->netamount( 0); $self->marge_total(0); + SL::DB::Manager::Chart->cache_taxkeys(date => $self->transdate); + my $idx = 0; foreach my $item ($self->items) { $idx++; @@ -52,9 +61,8 @@ sub calculate_prices_and_taxes { sub _get_exchangerate { my ($self, $data, %params) = @_; - require SL::DB::Default; - my $currency = $self->currency_id ? $self->currency->name || '' : ''; + my $currency = $self->currency_id ? SL::DB::Currency->load_cached($self->currency_id)->name || '' : ''; if ($currency ne SL::DB::Default->get_default_currency) { $data->{exchangerate} = $::form->check_exchangerate(\%::myconfig, $currency, $self->transdate, $data->{is_sales} ? 'buy' : 'sell'); $data->{exchangerate} ||= $params{exchangerate}; @@ -65,28 +73,22 @@ sub _get_exchangerate { sub _calculate_item { my ($self, $item, $idx, $data, %params) = @_; - my $part_unit = $data->{units_by_name}->{ $item->part->unit }; - my $item_unit = $data->{units_by_name}->{ $item->unit }; + my $part = SL::DB::Part->load_cached($item->parts_id); + my $part_unit = $data->{units_by_name}->{ $part->unit }; + my $item_unit = $data->{units_by_name}->{ $item->unit }; - croak("Undefined unit " . $item->part->unit) if !$part_unit; + croak("Undefined unit " . $part->unit) if !$part_unit; croak("Undefined unit " . $item->unit) if !$item_unit; $item->base_qty($item_unit->convert_to($item->qty, $part_unit)); $item->fxsellprice($item->sellprice) if $data->{is_invoice}; - my $num_dec = _num_decimal_places($item->sellprice) || 2; - # ^ we need at least 2 decimal places ^ - # my test case 43.00 € with 0 decimal places and 0.5 discount -> - # : sellprice before:43.00000 - # : num dec before:0 - # : discount / sellprice ratio: 22 / 21 - # : discount = 43 * 0.5 _round(21.5, 0) = 22 - # TODO write a test case + my $num_dec = max 2, _num_decimal_places($item->sellprice); my $discount = _round($item->sellprice * ($item->discount || 0), $num_dec); my $sellprice = _round($item->sellprice - $discount, $num_dec); $item->price_factor( ! $item->price_factor_obj ? 1 : ($item->price_factor_obj->factor || 1)); - $item->marge_price_factor(! $item->part->price_factor ? 1 : ($item->part->price_factor->factor || 1)); + $item->marge_price_factor(! $part->price_factor ? 1 : ($part->price_factor->factor || 1)); my $linetotal = _round($sellprice * $item->qty / $item->price_factor, 2) * $data->{exchangerate}; $linetotal = _round($linetotal, 2); @@ -97,7 +99,7 @@ sub _calculate_item { $item->marge_percent(0); } else { - my $lastcost = ! ($item->lastcost * 1) ? ($item->part->lastcost || 0) : $item->lastcost; + my $lastcost = ! ($item->lastcost * 1) ? ($part->lastcost || 0) : $item->lastcost; my $linetotal_cost = _round($lastcost * $item->qty / $item->marge_price_factor, 2); $item->marge_total( $linetotal - $linetotal_cost); @@ -107,7 +109,7 @@ sub _calculate_item { $data->{lastcost_total} += $linetotal_cost; } - my $taxkey = $item->part->get_taxkey(date => $self->transdate, is_sales => $data->{is_sales}, taxzone => $self->taxzone_id); + my $taxkey = $part->get_taxkey(date => $self->transdate, is_sales => $data->{is_sales}, taxzone => $self->taxzone_id); my $tax_rate = $taxkey->tax->rate; my $tax_amount = undef; @@ -128,17 +130,17 @@ sub _calculate_item { $self->netamount($self->netamount + $sellprice * $item->qty / $item->price_factor); - my $chart = $item->part->get_chart(type => $data->{is_sales} ? 'income' : 'expense', taxzone => $self->taxzone_id); + my $chart = $part->get_chart(type => $data->{is_sales} ? 'income' : 'expense', taxzone => $self->taxzone_id); $data->{amounts}->{ $chart->id } ||= { taxkey => $taxkey->taxkey_id, tax_id => $taxkey->tax_id, amount => 0 }; $data->{amounts}->{ $chart->id }->{amount} += $linetotal; $data->{amounts}->{ $chart->id }->{amount} -= $tax_amount if $self->taxincluded; push @{ $data->{assembly_items} }, []; - if ($item->part->is_assembly) { - _calculate_assembly_item($self, $data, $item->part, $item->base_qty, $item->unit_obj->convert_to(1, $item->part->unit_obj)); - } elsif ($item->part->is_part) { + if ($part->is_assembly) { + _calculate_assembly_item($self, $data, $part, $item->base_qty, $item_unit->convert_to(1, $part_unit)); + } elsif ($part->is_part) { if ($data->{is_invoice}) { - $item->allocated(_calculate_part_item($self, $data, $item->part, $item->base_qty, $item->unit_obj->convert_to(1, $item->part->unit_obj))); + $item->allocated(_calculate_part_item($self, $data, $part, $item->base_qty, $item_unit->convert_to(1, $part_unit))); } } diff --git a/SL/DB/Invoice.pm b/SL/DB/Invoice.pm index 0e9616651..502654ba2 100644 --- a/SL/DB/Invoice.pm +++ b/SL/DB/Invoice.pm @@ -320,6 +320,15 @@ sub date { goto &transdate; } +sub transactions { + my ($self) = @_; + + return unless $self->id; + + require SL::DB::AccTransaction; + SL::DB::Manager::AccTransaction->get_all(query => [ trans_id => $self->id ]); +} + 1; __END__ diff --git a/SL/DB/Manager/Chart.pm b/SL/DB/Manager/Chart.pm index a0167fd23..f7728aef1 100644 --- a/SL/DB/Manager/Chart.pm +++ b/SL/DB/Manager/Chart.pm @@ -6,6 +6,8 @@ use SL::DB::Helper::Manager; use base qw(SL::DB::Helper::Manager); use SL::DB::Helper::Sorted; +use DateTime; +use SL::DBUtils; sub object_class { 'SL::DB::Chart' } @@ -20,6 +22,27 @@ sub link_filter { link => { like => "\%:${link}:\%" } ]); } +sub cache_taxkeys { + my ($self, %params) = @_; + + my $date = $params{date} || DateTime->today; + my $cache = $::request->cache('::SL::DB::Chart::get_active_taxkey')->{$date} //= {}; + + require SL::DB::TaxKey; + my $tks = SL::DB::Manager::TaxKey->get_all; + my %tks_by_id = map { $_->id => $_ } @$tks; + + my $rows = selectall_hashref_query($::form, $::form->get_standard_dbh, <<"", $date); + SELECT DISTINCT ON (chart_id) chart_id, startdate, id + FROM taxkeys + WHERE startdate < ? + ORDER BY chart_id, startdate DESC; + + for (@$rows) { + $cache->{$_->{chart_id}} = $tks_by_id{$_->{id}}; + } +} + 1; __END__ diff --git a/SL/DB/Manager/Project.pm b/SL/DB/Manager/Project.pm index 8fcc3bd06..56a7e0b86 100644 --- a/SL/DB/Manager/Project.pm +++ b/SL/DB/Manager/Project.pm @@ -27,7 +27,7 @@ __PACKAGE__->add_filter_specs( }, status => sub { my ($key, $value, $prefix) = @_; - return () if $value eq 'all'; + return () if $value ne 'orphaned'; return __PACKAGE__->is_not_used_filter($prefix); }, ); diff --git a/SL/DB/MetaSetup/Contact.pm b/SL/DB/MetaSetup/Contact.pm index 8a749bb3c..7f8217519 100644 --- a/SL/DB/MetaSetup/Contact.pm +++ b/SL/DB/MetaSetup/Contact.pm @@ -16,21 +16,21 @@ __PACKAGE__->meta->columns( cp_email => { type => 'text' }, cp_fax => { type => 'text' }, cp_gender => { type => 'character', length => 1 }, - cp_givenname => { type => 'varchar', length => 75 }, + cp_givenname => { type => 'text' }, cp_id => { type => 'integer', not_null => 1, sequence => 'id' }, cp_mobile1 => { type => 'text' }, cp_mobile2 => { type => 'text' }, - cp_name => { type => 'varchar', length => 75 }, - cp_phone1 => { type => 'varchar', length => 75 }, - cp_phone2 => { type => 'varchar', length => 75 }, - cp_position => { type => 'varchar', length => 75 }, + cp_name => { type => 'text' }, + cp_phone1 => { type => 'text' }, + cp_phone2 => { type => 'text' }, + cp_position => { type => 'text' }, cp_privatemail => { type => 'text' }, cp_privatphone => { type => 'text' }, cp_project => { type => 'text' }, cp_satfax => { type => 'text' }, cp_satphone => { type => 'text' }, cp_street => { type => 'text' }, - cp_title => { type => 'varchar', length => 75 }, + cp_title => { type => 'text' }, cp_zipcode => { type => 'text' }, itime => { type => 'timestamp', default => 'now()' }, mtime => { type => 'timestamp' }, diff --git a/SL/DB/MetaSetup/Customer.pm b/SL/DB/MetaSetup/Customer.pm index ea5e3281b..b80211917 100644 --- a/SL/DB/MetaSetup/Customer.pm +++ b/SL/DB/MetaSetup/Customer.pm @@ -13,32 +13,32 @@ __PACKAGE__->meta->columns( bank => { type => 'text' }, bank_code => { type => 'text' }, bcc => { type => 'text' }, - bic => { type => 'varchar', length => 100 }, + bic => { type => 'text' }, business_id => { type => 'integer' }, c_vendor_id => { type => 'text' }, cc => { type => 'text' }, - city => { type => 'varchar', length => 75 }, + city => { type => 'text' }, contact => { type => 'text' }, - country => { type => 'varchar', length => 75 }, + country => { type => 'text' }, creditlimit => { type => 'numeric', default => '0', precision => 15, scale => 5 }, currency_id => { type => 'integer', not_null => 1 }, customernumber => { type => 'text' }, delivery_term_id => { type => 'integer' }, - department_1 => { type => 'varchar', length => 75 }, - department_2 => { type => 'varchar', length => 75 }, + department_1 => { type => 'text' }, + department_2 => { type => 'text' }, depositor => { type => 'text' }, direct_debit => { type => 'boolean', default => 'false' }, discount => { type => 'float', scale => 4 }, email => { type => 'text' }, - fax => { type => 'varchar', length => 30 }, + fax => { type => 'text' }, greeting => { type => 'text' }, homepage => { type => 'text' }, hourly_rate => { type => 'numeric', precision => 8, scale => 2 }, - iban => { type => 'varchar', length => 100 }, + iban => { type => 'text' }, id => { type => 'integer', not_null => 1, sequence => 'id' }, itime => { type => 'timestamp', default => 'now()' }, klass => { type => 'integer', default => '0' }, - language => { type => 'varchar', length => 5 }, + language => { type => 'text' }, language_id => { type => 'integer' }, mandate_date_of_signature => { type => 'date' }, mandator_id => { type => 'text' }, @@ -49,16 +49,16 @@ __PACKAGE__->meta->columns( payment_id => { type => 'integer' }, phone => { type => 'text' }, salesman_id => { type => 'integer' }, - street => { type => 'varchar', length => 75 }, + street => { type => 'text' }, taxincluded => { type => 'boolean' }, taxincluded_checked => { type => 'boolean' }, taxnumber => { type => 'text' }, taxzone_id => { type => 'integer', default => '0', not_null => 1 }, terms => { type => 'integer', default => '0' }, user_password => { type => 'text' }, - username => { type => 'varchar', length => 50 }, + username => { type => 'text' }, ustid => { type => 'text' }, - zipcode => { type => 'varchar', length => 10 }, + zipcode => { type => 'text' }, ); __PACKAGE__->meta->primary_key_columns([ 'id' ]); diff --git a/SL/DB/MetaSetup/Default.pm b/SL/DB/MetaSetup/Default.pm index 5242ccea7..2015b9052 100644 --- a/SL/DB/MetaSetup/Default.pm +++ b/SL/DB/MetaSetup/Default.pm @@ -9,91 +9,97 @@ use base qw(SL::DB::Object); __PACKAGE__->meta->table('defaults'); __PACKAGE__->meta->columns( - accounting_method => { type => 'text' }, - address => { type => 'text' }, - ap_changeable => { type => 'integer', default => 2, not_null => 1 }, - ap_show_mark_as_paid => { type => 'boolean', default => 'true' }, - ar_changeable => { type => 'integer', default => 2, not_null => 1 }, - ar_paid_accno_id => { type => 'integer' }, - ar_show_mark_as_paid => { type => 'boolean', default => 'true' }, - articlenumber => { type => 'text' }, - assemblynumber => { type => 'text' }, - balance_startdate_method => { type => 'text' }, - bin_id => { type => 'integer' }, - bin_id_ignore_onhand => { type => 'integer' }, - businessnumber => { type => 'text' }, - closedto => { type => 'date' }, - cnnumber => { type => 'text' }, - co_ustid => { type => 'text' }, - coa => { type => 'text' }, - company => { type => 'text' }, - currency_id => { type => 'integer', not_null => 1 }, - customer_hourly_rate => { type => 'numeric', precision => 8, scale => 2 }, - customernumber => { type => 'text' }, - datev_check_on_ap_transaction => { type => 'boolean', default => 'true' }, - datev_check_on_ar_transaction => { type => 'boolean', default => 'true' }, - datev_check_on_gl_transaction => { type => 'boolean', default => 'true' }, - datev_check_on_purchase_invoice => { type => 'boolean', default => 'true' }, - datev_check_on_sales_invoice => { type => 'boolean', default => 'true' }, - dunning_ar => { type => 'integer' }, - dunning_ar_amount_fee => { type => 'integer' }, - dunning_ar_amount_interest => { type => 'integer' }, - duns => { type => 'text' }, - expense_accno_id => { type => 'integer' }, - fxgain_accno_id => { type => 'integer' }, - fxloss_accno_id => { type => 'integer' }, - gl_changeable => { type => 'integer', default => 2, not_null => 1 }, - id => { type => 'serial', not_null => 1 }, - income_accno_id => { type => 'integer' }, - inventory_accno_id => { type => 'integer' }, - inventory_system => { type => 'text' }, - invnumber => { type => 'text' }, - ir_changeable => { type => 'integer', default => 2, not_null => 1 }, - ir_show_mark_as_paid => { type => 'boolean', default => 'true' }, - is_changeable => { type => 'integer', default => 2, not_null => 1 }, - is_show_mark_as_paid => { type => 'boolean', default => 'true' }, - itime => { type => 'timestamp', default => 'now()' }, - language_id => { type => 'integer' }, - max_future_booking_interval => { type => 'integer', default => 360 }, - mtime => { type => 'timestamp' }, - normalize_part_descriptions => { type => 'boolean', default => 'true' }, - normalize_vc_names => { type => 'boolean', default => 'true' }, - parts_image_css => { type => 'text', default => 'border:0;float:left;max-width:250px;margin-top:20px:margin-right:10px;margin-left:10px;' }, - parts_listing_image => { type => 'boolean', default => 'true' }, - parts_show_image => { type => 'boolean', default => 'true' }, - payments_changeable => { type => 'integer', default => '0', not_null => 1 }, - pdonumber => { type => 'text' }, - ponumber => { type => 'text' }, - profit_determination => { type => 'text' }, - purchase_delivery_order_show_delete => { type => 'boolean', default => 'true' }, - purchase_order_show_delete => { type => 'boolean', default => 'true' }, - requirement_spec_section_order_part_id => { type => 'integer' }, - revtrans => { type => 'boolean', default => 'false' }, - rfqnumber => { type => 'text' }, - rmanumber => { type => 'text' }, - sales_delivery_order_show_delete => { type => 'boolean', default => 'true' }, - sales_order_show_delete => { type => 'boolean', default => 'true' }, - sdonumber => { type => 'text' }, - sepa_creditor_id => { type => 'text' }, - servicenumber => { type => 'text' }, - show_bestbefore => { type => 'boolean', default => 'false' }, - show_weight => { type => 'boolean', default => 'false', not_null => 1 }, - signature => { type => 'text' }, - sonumber => { type => 'text' }, - sqnumber => { type => 'text' }, - taxnumber => { type => 'text' }, - templates => { type => 'text' }, - transfer_default => { type => 'boolean', default => 'true' }, - transfer_default_ignore_onhand => { type => 'boolean', default => 'false' }, - transfer_default_use_master_default_bin => { type => 'boolean', default => 'false' }, - vendornumber => { type => 'text' }, - version => { type => 'varchar', length => 8 }, - vertreter => { type => 'boolean', default => 'false' }, - warehouse_id => { type => 'integer' }, - warehouse_id_ignore_onhand => { type => 'integer' }, - webdav => { type => 'boolean', default => 'false' }, - webdav_documents => { type => 'boolean', default => 'false' }, - weightunit => { type => 'varchar', length => 5 }, + accounting_method => { type => 'text' }, + address => { type => 'text' }, + allow_new_purchase_delivery_order => { type => 'boolean', default => 'true', not_null => 1 }, + allow_new_purchase_invoice => { type => 'boolean', default => 'true', not_null => 1 }, + allow_sales_invoice_from_sales_order => { type => 'boolean', default => 'true', not_null => 1 }, + allow_sales_invoice_from_sales_quotation => { type => 'boolean', default => 'true', not_null => 1 }, + ap_changeable => { type => 'integer', default => 2, not_null => 1 }, + ap_show_mark_as_paid => { type => 'boolean', default => 'true' }, + ar_changeable => { type => 'integer', default => 2, not_null => 1 }, + ar_paid_accno_id => { type => 'integer' }, + ar_show_mark_as_paid => { type => 'boolean', default => 'true' }, + articlenumber => { type => 'text' }, + assemblynumber => { type => 'text' }, + balance_startdate_method => { type => 'text' }, + bin_id => { type => 'integer' }, + bin_id_ignore_onhand => { type => 'integer' }, + businessnumber => { type => 'text' }, + closedto => { type => 'date' }, + cnnumber => { type => 'text' }, + co_ustid => { type => 'text' }, + coa => { type => 'text' }, + company => { type => 'text' }, + currency_id => { type => 'integer', not_null => 1 }, + customer_hourly_rate => { type => 'numeric', precision => 8, scale => 2 }, + customer_projects_only_in_sales => { type => 'boolean', default => 'false', not_null => 1 }, + customernumber => { type => 'text' }, + datev_check_on_ap_transaction => { type => 'boolean', default => 'true' }, + datev_check_on_ar_transaction => { type => 'boolean', default => 'true' }, + datev_check_on_gl_transaction => { type => 'boolean', default => 'true' }, + datev_check_on_purchase_invoice => { type => 'boolean', default => 'true' }, + datev_check_on_sales_invoice => { type => 'boolean', default => 'true' }, + dunning_ar => { type => 'integer' }, + dunning_ar_amount_fee => { type => 'integer' }, + dunning_ar_amount_interest => { type => 'integer' }, + duns => { type => 'text' }, + expense_accno_id => { type => 'integer' }, + fxgain_accno_id => { type => 'integer' }, + fxloss_accno_id => { type => 'integer' }, + gl_changeable => { type => 'integer', default => 2, not_null => 1 }, + id => { type => 'serial', not_null => 1 }, + income_accno_id => { type => 'integer' }, + inventory_accno_id => { type => 'integer' }, + inventory_system => { type => 'text' }, + invnumber => { type => 'text' }, + ir_changeable => { type => 'integer', default => 2, not_null => 1 }, + ir_show_mark_as_paid => { type => 'boolean', default => 'true' }, + is_changeable => { type => 'integer', default => 2, not_null => 1 }, + is_show_mark_as_paid => { type => 'boolean', default => 'true' }, + itime => { type => 'timestamp', default => 'now()' }, + language_id => { type => 'integer' }, + max_future_booking_interval => { type => 'integer', default => 360 }, + mtime => { type => 'timestamp' }, + normalize_part_descriptions => { type => 'boolean', default => 'true' }, + normalize_vc_names => { type => 'boolean', default => 'true' }, + parts_image_css => { type => 'text', default => 'border:0;float:left;max-width:250px;margin-top:20px:margin-right:10px;margin-left:10px;' }, + parts_listing_image => { type => 'boolean', default => 'true' }, + parts_show_image => { type => 'boolean', default => 'true' }, + payments_changeable => { type => 'integer', default => '0', not_null => 1 }, + pdonumber => { type => 'text' }, + ponumber => { type => 'text' }, + profit_determination => { type => 'text' }, + purchase_delivery_order_show_delete => { type => 'boolean', default => 'true' }, + purchase_order_show_delete => { type => 'boolean', default => 'true' }, + require_transaction_description_ps => { type => 'boolean', default => 'false', not_null => 1 }, + requirement_spec_section_order_part_id => { type => 'integer' }, + revtrans => { type => 'boolean', default => 'false' }, + rfqnumber => { type => 'text' }, + rmanumber => { type => 'text' }, + sales_delivery_order_show_delete => { type => 'boolean', default => 'true' }, + sales_order_show_delete => { type => 'boolean', default => 'true' }, + sdonumber => { type => 'text' }, + sepa_creditor_id => { type => 'text' }, + servicenumber => { type => 'text' }, + show_bestbefore => { type => 'boolean', default => 'false' }, + show_weight => { type => 'boolean', default => 'false', not_null => 1 }, + signature => { type => 'text' }, + sonumber => { type => 'text' }, + sqnumber => { type => 'text' }, + taxnumber => { type => 'text' }, + templates => { type => 'text' }, + transfer_default => { type => 'boolean', default => 'true' }, + transfer_default_ignore_onhand => { type => 'boolean', default => 'false' }, + transfer_default_use_master_default_bin => { type => 'boolean', default => 'false' }, + vendornumber => { type => 'text' }, + version => { type => 'varchar', length => 8 }, + vertreter => { type => 'boolean', default => 'false' }, + warehouse_id => { type => 'integer' }, + warehouse_id_ignore_onhand => { type => 'integer' }, + webdav => { type => 'boolean', default => 'false' }, + webdav_documents => { type => 'boolean', default => 'false' }, + weightunit => { type => 'varchar', length => 5 }, ); __PACKAGE__->meta->primary_key_columns([ 'id' ]); diff --git a/SL/DB/MetaSetup/FollowUp.pm b/SL/DB/MetaSetup/FollowUp.pm index bda14ec5a..a6702b68a 100644 --- a/SL/DB/MetaSetup/FollowUp.pm +++ b/SL/DB/MetaSetup/FollowUp.pm @@ -24,14 +24,14 @@ __PACKAGE__->meta->primary_key_columns([ 'id' ]); __PACKAGE__->meta->allow_inline_column_values(1); __PACKAGE__->meta->foreign_keys( - created_for => { + created_by => { class => 'SL::DB::Employee', - key_columns => { created_for_user => 'id' }, + key_columns => { created_by => 'id' }, }, - employee => { + created_for => { class => 'SL::DB::Employee', - key_columns => { created_by => 'id' }, + key_columns => { created_for_user => 'id' }, }, note => { diff --git a/SL/DB/MetaSetup/FollowUpAccess.pm b/SL/DB/MetaSetup/FollowUpAccess.pm index 43d538610..c2fccb1e9 100644 --- a/SL/DB/MetaSetup/FollowUpAccess.pm +++ b/SL/DB/MetaSetup/FollowUpAccess.pm @@ -17,12 +17,12 @@ __PACKAGE__->meta->columns( __PACKAGE__->meta->primary_key_columns([ 'id' ]); __PACKAGE__->meta->foreign_keys( - employee => { + to_follow_ups_by => { class => 'SL::DB::Employee', key_columns => { what => 'id' }, }, - employee_obj => { + with_access => { class => 'SL::DB::Employee', key_columns => { who => 'id' }, }, diff --git a/SL/DB/MetaSetup/Order.pm b/SL/DB/MetaSetup/Order.pm index 7f9357313..92bb92ffd 100644 --- a/SL/DB/MetaSetup/Order.pm +++ b/SL/DB/MetaSetup/Order.pm @@ -21,6 +21,7 @@ __PACKAGE__->meta->columns( delivery_vendor_id => { type => 'integer' }, department_id => { type => 'integer' }, employee_id => { type => 'integer' }, + expected_billing_date => { type => 'date' }, globalproject_id => { type => 'integer' }, id => { type => 'integer', not_null => 1, sequence => 'id' }, intnotes => { type => 'text' }, @@ -31,6 +32,7 @@ __PACKAGE__->meta->columns( mtime => { type => 'timestamp' }, netamount => { type => 'numeric', precision => 15, scale => 5 }, notes => { type => 'text' }, + order_probability => { type => 'integer', default => '0', not_null => 1 }, ordnumber => { type => 'text', not_null => 1 }, payment_id => { type => 'integer' }, proforma => { type => 'boolean', default => 'false' }, diff --git a/SL/DB/MetaSetup/Shipto.pm b/SL/DB/MetaSetup/Shipto.pm index bb3a9bcd7..b0a9f991b 100644 --- a/SL/DB/MetaSetup/Shipto.pm +++ b/SL/DB/MetaSetup/Shipto.pm @@ -13,18 +13,18 @@ __PACKAGE__->meta->columns( module => { type => 'text' }, mtime => { type => 'timestamp' }, shipto_id => { type => 'integer', not_null => 1, sequence => 'id' }, - shiptocity => { type => 'varchar', length => 75 }, - shiptocontact => { type => 'varchar', length => 75 }, - shiptocountry => { type => 'varchar', length => 75 }, + shiptocity => { type => 'text' }, + shiptocontact => { type => 'text' }, + shiptocountry => { type => 'text' }, shiptocp_gender => { type => 'text' }, - shiptodepartment_1 => { type => 'varchar', length => 75 }, - shiptodepartment_2 => { type => 'varchar', length => 75 }, + shiptodepartment_1 => { type => 'text' }, + shiptodepartment_2 => { type => 'text' }, shiptoemail => { type => 'text' }, - shiptofax => { type => 'varchar', length => 30 }, - shiptoname => { type => 'varchar', length => 75 }, - shiptophone => { type => 'varchar', length => 30 }, - shiptostreet => { type => 'varchar', length => 75 }, - shiptozipcode => { type => 'varchar', length => 75 }, + shiptofax => { type => 'text' }, + shiptoname => { type => 'text' }, + shiptophone => { type => 'text' }, + shiptostreet => { type => 'text' }, + shiptozipcode => { type => 'text' }, trans_id => { type => 'integer' }, ); diff --git a/SL/DB/MetaSetup/Vendor.pm b/SL/DB/MetaSetup/Vendor.pm index ef3f37f48..8367137a3 100644 --- a/SL/DB/MetaSetup/Vendor.pm +++ b/SL/DB/MetaSetup/Vendor.pm @@ -13,28 +13,28 @@ __PACKAGE__->meta->columns( bank => { type => 'text' }, bank_code => { type => 'text' }, bcc => { type => 'text' }, - bic => { type => 'varchar', length => 100 }, + bic => { type => 'text' }, business_id => { type => 'integer' }, cc => { type => 'text' }, - city => { type => 'varchar', length => 75 }, + city => { type => 'text' }, contact => { type => 'text' }, - country => { type => 'varchar', length => 75 }, + country => { type => 'text' }, creditlimit => { type => 'numeric', precision => 15, scale => 5 }, currency_id => { type => 'integer', not_null => 1 }, delivery_term_id => { type => 'integer' }, - department_1 => { type => 'varchar', length => 75 }, - department_2 => { type => 'varchar', length => 75 }, + department_1 => { type => 'text' }, + department_2 => { type => 'text' }, depositor => { type => 'text' }, direct_debit => { type => 'boolean', default => 'false' }, discount => { type => 'float', scale => 4 }, email => { type => 'text' }, - fax => { type => 'varchar', length => 30 }, + fax => { type => 'text' }, greeting => { type => 'text' }, homepage => { type => 'text' }, - iban => { type => 'varchar', length => 100 }, + iban => { type => 'text' }, id => { type => 'integer', not_null => 1, sequence => 'id' }, itime => { type => 'timestamp', default => 'now()' }, - language => { type => 'varchar', length => 5 }, + language => { type => 'text' }, language_id => { type => 'integer' }, mtime => { type => 'timestamp' }, name => { type => 'text', not_null => 1 }, @@ -43,17 +43,17 @@ __PACKAGE__->meta->columns( payment_id => { type => 'integer' }, phone => { type => 'text' }, salesman_id => { type => 'integer' }, - street => { type => 'varchar', length => 75 }, + street => { type => 'text' }, taxincluded => { type => 'boolean' }, taxnumber => { type => 'text' }, taxzone_id => { type => 'integer', default => '0', not_null => 1 }, terms => { type => 'integer', default => '0' }, - user_password => { type => 'varchar', length => 12 }, - username => { type => 'varchar', length => 50 }, + user_password => { type => 'text' }, + username => { type => 'text' }, ustid => { type => 'text' }, v_customer_id => { type => 'text' }, vendornumber => { type => 'text' }, - zipcode => { type => 'varchar', length => 10 }, + zipcode => { type => 'text' }, ); __PACKAGE__->meta->primary_key_columns([ 'id' ]); diff --git a/SL/DB/Object.pm b/SL/DB/Object.pm index c11fb3f5a..81ef97890 100755 --- a/SL/DB/Object.pm +++ b/SL/DB/Object.pm @@ -2,6 +2,7 @@ package SL::DB::Object; use strict; +use Carp; use English qw(-no_match_vars); use Rose::DB::Object; use List::MoreUtils qw(any); @@ -179,12 +180,52 @@ sub delete { return $result; } +sub load_cached { + my $class_or_self = shift; + my @ids = @_; + my $class = ref($class_or_self) || $class_or_self; + my $cache = $::request->cache("::SL::DB::Object::object_cache::${class}"); + + croak "Missing ID" unless @ids; + + my @missing_ids = grep { !exists $cache->{$_} } @ids; + + return $cache->{$ids[0]} if !@missing_ids; + + croak "Caching can only be used with classes with exactly one primary key column" if 1 != scalar(@{ $class->meta->primary_key_columns }); + + my $primary_key = $class->meta->primary_key_columns->[0]->name; + my $objects = $class->_get_manager_class->get_all(where => [ $primary_key => \@missing_ids ]); + + $cache->{$_->$primary_key} = $_ for @{ $objects}; + + return $cache->{$ids[0]}; +} + +sub invalidate_cached { + my ($class_or_self, @ids) = @_; + my $class = ref($class_or_self) || $class_or_self; + + if (ref($class_or_self) && !@ids) { + croak "Caching can only be used with classes with exactly one primary key column" if 1 != scalar(@{ $class->meta->primary_key_columns }); + + my $primary_key = $class->meta->primary_key_columns->[0]->name; + @ids = ($class_or_self->$primary_key); + } + + delete @{ $::request->cache("::SL::DB::Object::object_cache::${class}") }{ @ids }; + + return $class_or_self; +} + 1; __END__ =pod +=encoding utf8 + =head1 NAME SL::DB::Object: Base class for all of our model classes @@ -257,6 +298,31 @@ C<@attributes> equal those in C<$self> but which is not C<$self>. Can be used to check whether or not an object's columns are unique before saving or during validation. +=item C + +Loads objects from the database which haven't been cached before and +caches them for the duration of the current request (see +L). + +This method can be called both as an instance method and a class +method. It loads objects for the corresponding class (e.g. both +Cload_cached(…)> and +C<$some_part-Eload_cached(…)> will load parts). + +Currently only classes with a single primary key column are supported. + +Returns the cached object for the first ID. + +=item C + +Deletes all cached instances of this class (see L) for +the given IDs. + +If called as an instance method without further arguments then the +object's ID is used. + +Returns the object/class it was called on. + =back =head1 AUTHOR diff --git a/SL/DB/Part.pm b/SL/DB/Part.pm index ae2612b9d..f5a0cd2cc 100644 --- a/SL/DB/Part.pm +++ b/SL/DB/Part.pm @@ -144,21 +144,19 @@ sub get_taxkey { my $date = $params{date} || DateTime->today_local; my $is_sales = !!$params{is_sales}; my $taxzone = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1; + my $tk_info = $::request->cache('get_taxkey'); - $self->{__partpriv_taxkey_information} ||= { }; - my $tk_info = $self->{__partpriv_taxkey_information}; + $tk_info->{$self->id} //= {}; + $tk_info->{$self->id}->{$taxzone} //= { }; + my $cache = $tk_info->{$self->id}->{$taxzone}->{$is_sales} //= { }; - $tk_info->{$taxzone} ||= { }; - $tk_info->{$taxzone}->{$is_sales} ||= { }; - - if (!exists $tk_info->{$taxzone}->{$is_sales}->{$date}) { - $tk_info->{$taxzone}->{$is_sales}->{$date} = + if (!exists $cache->{$date}) { + $cache->{$date} = $self->get_chart(type => $is_sales ? 'income' : 'expense', taxzone => $taxzone) - ->load ->get_active_taxkey($date); } - return $tk_info->{$taxzone}->{$is_sales}->{$date}; + return $cache->{$date}; } sub get_chart { @@ -167,17 +165,22 @@ sub get_chart { my $type = (any { $_ eq $params{type} } qw(income expense inventory)) ? $params{type} : croak("Invalid 'type' parameter '$params{type}'"); my $taxzone = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1; - $self->{__partpriv_get_chart_id} ||= { }; - my $charts = $self->{__partpriv_get_chart_id}; + my $charts = $::request->cache('get_chart_id/by_part_id_and_taxzone')->{$self->id} //= {}; + my $all_charts = $::request->cache('get_chart_id/by_id'); $charts->{$taxzone} ||= { }; if (!exists $charts->{$taxzone}->{$type}) { - my $bugru = $self->buchungsgruppe; + require SL::DB::Buchungsgruppe; + my $bugru = SL::DB::Buchungsgruppe->load_cached($self->buchungsgruppen_id); my $chart_id = ($type eq 'inventory') ? ($self->inventory_accno_id ? $bugru->inventory_accno_id : undef) : $bugru->call_sub("${type}_accno_id_${taxzone}"); - $charts->{$taxzone}->{$type} = $chart_id ? SL::DB::Chart->new(id => $chart_id)->load : undef; + if ($chart_id) { + my $chart = $all_charts->{$chart_id} // SL::DB::Chart->load_cached($chart_id)->load; + $all_charts->{$chart_id} = $chart; + $charts->{$taxzone}->{$type} = $chart; + } } return $charts->{$taxzone}->{$type}; diff --git a/SL/DB/Unit.pm b/SL/DB/Unit.pm index b133d8f13..e8d260c67 100644 --- a/SL/DB/Unit.pm +++ b/SL/DB/Unit.pm @@ -39,11 +39,13 @@ sub convertible_units { sub base_factor { my ($self) = @_; - if (!defined $self->{__base_factor}) { - $self->{__base_factor} = !$self->base_unit || !$self->factor || ($self->name eq $self->base_unit) ? 1 : $self->factor * $self->base->base_factor; + my $cache = $::request->cache('base_factor'); + + if (!defined $cache->{$self->id}) { + $cache->{$self->id} = !$self->base_unit || !$self->factor || ($self->name eq $self->base_unit) ? 1 : $self->factor * $self->base->base_factor; } - return $self->{__base_factor}; + return $cache->{$self->id}; } sub convert_to { diff --git a/SL/DBUpgrade2/Base.pm b/SL/DBUpgrade2/Base.pm index 100652e15..34afeae54 100644 --- a/SL/DBUpgrade2/Base.pm +++ b/SL/DBUpgrade2/Base.pm @@ -90,11 +90,9 @@ sub add_print_templates { croak "File '${src_dir}/$_' does not exist" unless -f "${src_dir}/$_"; } - my $template_dir = $::instance_conf->reload->get_templates; + return 1 unless my $template_dir = $::instance_conf->reload->get_templates; $::lxdebug->message(LXDebug::DEBUG1(), "add_print_templates: template_dir $template_dir"); - return 1 if !$template_dir; - foreach my $src_file (@files) { my $dest_file = $template_dir . '/' . $src_file; diff --git a/SL/DO.pm b/SL/DO.pm index 1da5a8e2d..0960ad71f 100644 --- a/SL/DO.pm +++ b/SL/DO.pm @@ -391,10 +391,10 @@ sub save { conv_i($form->{id})); do_query($form, $dbh, $query, @values); - # add shipto $form->{name} = $form->{ $form->{vc} }; $form->{name} =~ s/--$form->{"$form->{vc}_id"}//; + # add shipto if (!$form->{shipto_id}) { $form->add_shipto($dbh, $form->{id}, "DO"); } diff --git a/SL/Dispatcher.pm b/SL/Dispatcher.pm index 854b863a3..7ce18a476 100644 --- a/SL/Dispatcher.pm +++ b/SL/Dispatcher.pm @@ -105,8 +105,6 @@ sub pre_request_initialization { die "cannot find locale for user " . $params{login} unless $::locale = Locale->new($::myconfig{countrycode}); $::form->{login} = $params{login}; # normaly implicit at login - - $::instance_conf->init; } } @@ -131,6 +129,7 @@ sub show_error { $::locale = Locale->new($::myconfig{countrycode}); $::form->{error} = $::locale->text('The session is invalid or has expired.') if ($error_type eq 'session'); $::form->{error} = $::locale->text('Incorrect password!') if ($error_type eq 'password'); + $::form->{error} = $::locale->text('The action is missing or invalid.') if ($error_type eq 'action'); return render_error_ajax($::form->{error}) if $::request->is_ajax; @@ -236,7 +235,7 @@ sub handle_request { $::form->read_cgi_input; my %routing; - eval { %routing = _route_request($ENV{SCRIPT_NAME}); 1; } or return; + eval { %routing = $self->_route_request($ENV{SCRIPT_NAME}); 1; } or return; ($routing_type, $script_name, $action) = @routing{qw(type controller action)}; $::lxdebug->log_request($routing_type, $script_name, $action); @@ -275,7 +274,7 @@ sub handle_request { if ( (($script eq 'login') && !$action) || ($script eq 'admin') || (SL::Auth::SESSION_EXPIRED() == $session_result)) { - $self->redirect_to_login($script); + $self->redirect_to_login(script => $script, error => 'session'); } @@ -338,8 +337,10 @@ sub handle_request { } sub redirect_to_login { - my ($self, $script) = @_; - my $action = $script =~ m/^admin/i ? 'Admin/login' : 'LoginScreen/user_login&error=session'; + my ($self, %params) = @_; + my $action = ($params{script} // '') =~ m/^admin/i ? 'Admin/login' : 'LoginScreen/user_login'; + $action .= '&error=' . $params{error} if $params{error}; + print $::request->cgi->redirect("controller.pl?action=${action}"); ::end_of_request(); } @@ -362,14 +363,15 @@ sub _interface_is_fcgi { } sub _route_request { - my $script_name = shift; + my ($self, $script_name) = @_; - return $script_name =~ m/dispatcher\.pl$/ ? (type => 'old', _route_dispatcher_request()) - : $script_name =~ m/controller\.pl/ ? (type => 'controller', _route_controller_request()) + return $script_name =~ m/dispatcher\.pl$/ ? (type => 'old', $self->_route_dispatcher_request) + : $script_name =~ m/controller\.pl/ ? (type => 'controller', $self->_route_controller_request) : (type => 'old', controller => $script_name, action => $::form->{action}); } sub _route_dispatcher_request { + my ($self) = @_; my $name_re = qr{[a-z]\w*}; my ($script_name, $action); @@ -400,9 +402,16 @@ sub _route_dispatcher_request { } sub _route_controller_request { + my ($self) = @_; my ($controller, $action, $request_type); eval { + # Redirect simple requests to controller.pl without any GET/POST + # param to the login page. + $self->redirect_to_login(error => 'action') if !$::form->{action}; + + # Show an error if the »action« parameter doesn't match the + # pattern »Controller/action«. $::form->{action} =~ m|^ ( [A-Z] [A-Za-z0-9_]* ) / ( [a-z] [a-z0-9_]* ) ( \. [a-zA-Z]+ )? $|x || die "Unroutable request -- invalid controller/action.\n"; ($controller, $action) = ($1, $2); delete $::form->{action}; diff --git a/SL/Dispatcher/AuthHandler/User.pm b/SL/Dispatcher/AuthHandler/User.pm index 7b2a5eb66..7a8fd1d75 100644 --- a/SL/Dispatcher/AuthHandler/User.pm +++ b/SL/Dispatcher/AuthHandler/User.pm @@ -29,6 +29,7 @@ sub handle { $::auth->create_or_refresh_session; $::auth->delete_session_value('FLASH'); + $::instance_conf->reload->data; return 1; } diff --git a/SL/Form.pm b/SL/Form.pm index ed31ddf68..7030454d6 100644 --- a/SL/Form.pm +++ b/SL/Form.pm @@ -40,6 +40,7 @@ package Form; use Carp; use Data::Dumper; +use Carp; use CGI; use Cwd; use Encode; @@ -307,8 +308,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(); @@ -594,8 +594,11 @@ sub _prepare_html_template { if (-f "templates/webpages/${file}.html") { $file = "templates/webpages/${file}.html"; + } elsif (ref $file eq 'SCALAR') { + # file is a scalarref, use inline mode } else { my $info = "Web page template '${file}' not found.\n"; + $::form->header; print qq|
$info
|; ::end_of_request(); } @@ -2362,8 +2365,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'"; + $vc_id = $self->{"${vc}_id"}; + } if ($params{"contacts"}) { $self->_get_contacts($dbh, $vc_id, $params{"contacts"}); @@ -3334,11 +3342,10 @@ 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. + if ($self->{shipto_id}) { + my $shipto = SL::DB::Shipto->new(id => $self->{shipto_id})->load; + $self->{$_} = $shipto->$_ for grep { m{^shipto} } map { $_->name } @{ $shipto->meta->columns }; } my $language = $self->{language} ? '_' . $self->{language} : ''; diff --git a/SL/Helper/CreatePDF.pm b/SL/Helper/CreatePDF.pm index 8d36eb749..555e75d73 100644 --- a/SL/Helper/CreatePDF.pm +++ b/SL/Helper/CreatePDF.pm @@ -29,7 +29,9 @@ sub create_pdf { my ($class, %params) = @_; my $userspath = $::lx_office_conf{paths}->{userspath}; + my $vars = $params{variables} || {}; my $form = Form->new(''); + $form->{$_} = $vars->{$_} for keys %{ $vars }; $form->{format} = 'pdf'; $form->{cwd} = getcwd(); $form->{templates} = $::instance_conf->get_templates; @@ -37,9 +39,6 @@ sub create_pdf { $form->{tmpdir} = $form->{cwd} . '/' . $userspath; my ($suffix) = $params{template} =~ m{\.(.+)}; - my $vars = $params{variables} || {}; - $form->{$_} = $vars->{$_} for keys %{ $vars }; - my $temp_fh; ($temp_fh, $form->{tmpfile}) = File::Temp::tempfile( 'kivitendo-printXXXXXX', diff --git a/SL/Helper/DateTime.pm b/SL/Helper/DateTime.pm index 94c311f14..c79dad1b8 100644 --- a/SL/Helper/DateTime.pm +++ b/SL/Helper/DateTime.pm @@ -36,6 +36,42 @@ sub from_lxoffice { goto &from_kivitendo; } +sub add_business_duration { + my ($self, %params) = @_; + + my $abs_days = abs $params{days}; + my $neg = $params{days} < 0; + my $bweek = $params{businessweek} || 5; + my $weeks = int ($abs_days / $bweek); + my $days = $abs_days % $bweek; + + if ($neg) { + $self->subtract(weeks => $weeks); + $self->add(days => 8 - $self->day_of_week) if $self->day_of_week > $bweek; + $self->subtract(days => $self->day_of_week > $days ? $days : $days + (7 - $bweek)); + } else { + $self->add(weeks => $weeks); + $self->subtract(days => $self->day_of_week - $bweek) if $self->day_of_week > $bweek; + $self->add(days => $self->day_of_week + $days <= $bweek ? $days : $days + (7 - $bweek)); + } + + $self; +} + +sub add_businessdays { + my ($self, %params) = @_; + + $self->add_business_duration(%params); +} + +sub subtract_businessdays { + my ($self, %params) = @_; + + $params{days} *= -1; + + $self->add_business_duration(%params); +} + 1; __END__ diff --git a/SL/IC.pm b/SL/IC.pm index 191a2d300..163f59069 100644 --- a/SL/IC.pm +++ b/SL/IC.pm @@ -1698,6 +1698,16 @@ sub prepare_parts_for_printing { } } + my $parts = SL::DB::Manager::Part->get_all(query => [ id => \@part_ids ]); + my %parts_by_id = map { $_->id => $_ } @$parts; + + for my $i (1..$rowcount) { + my $id = $form->{"${prefix}${i}"}; + next unless $id; + + push @{ $form->{TEMPLATE_ARRAYS}{part_type} }, $parts_by_id{$id}->type; + } + $main::lxdebug->leave_sub(); } diff --git a/SL/IR.pm b/SL/IR.pm index 301fe3b64..931df6b44 100644 --- a/SL/IR.pm +++ b/SL/IR.pm @@ -720,9 +720,10 @@ sub post_invoice { } - # add shipto $form->{name} = $form->{vendor}; $form->{name} =~ s/--\Q$form->{vendor_id}\E//; + + # add shipto $form->add_shipto($dbh, $form->{id}, "AP"); # delete zero entries @@ -1126,16 +1127,6 @@ sub get_vendor { } $sth->finish(); - # get shipto if we do not convert an order or invoice - if (!$params->{shipto}) { - delete @{$params}{qw(shiptoname shiptostreet shiptozipcode shiptocity shiptocountry shiptocontact shiptophone shiptofax shiptoemail)}; - - $query = qq|SELECT * FROM shipto WHERE (trans_id = ?) AND (module= 'CT')|; - $ref = selectfirst_hashref_query($form, $dbh, $query, $vid); - @{$params}{keys %$ref} = @{$ref}{keys %$ref}; - map { $params->{$_} = $ref->{$_} } keys %$ref; - } - if (!$params->{id} && $params->{type} !~ /_(order|quotation)/) { # setup last accounts used $query = diff --git a/SL/IS.pm b/SL/IS.pm index 64afc530a..63420a544 100644 --- a/SL/IS.pm +++ b/SL/IS.pm @@ -1145,10 +1145,10 @@ sub post_invoice { do_query($form, $dbh, qq|UPDATE ar SET paid = amount WHERE id = ?|, conv_i($form->{"id"})); } - # add shipto $form->{name} = $form->{customer}; $form->{name} =~ s/--\Q$form->{customer_id}\E//; + # add shipto if (!$form->{shipto_id}) { $form->add_shipto($dbh, $form->{id}, "AR"); } @@ -1615,12 +1615,6 @@ sub retrieve_invoice { $form->{exchangerate} = $form->get_exchangerate($dbh, $form->{currency}, $form->{invdate}, "buy"); - # get shipto - $query = qq|SELECT * FROM shipto WHERE (trans_id = ?) AND (module = 'AR')|; - $ref = selectfirst_hashref_query($form, $dbh, $query, $id); - delete $ref->{id}; - map { $form->{$_} = $ref->{$_} } keys %{ $ref }; - foreach my $vc (qw(customer vendor)) { next if !$form->{"delivery_${vc}_id"}; ($form->{"delivery_${vc}_string"}) = selectrow_query($form, $dbh, qq|SELECT name FROM customer WHERE id = ?|, $id); @@ -1837,19 +1831,6 @@ sub get_customer { } $sth->finish; - # get shipto if we did not converted an order or invoice - if (!$form->{shipto}) { - map { delete $form->{$_} } - qw(shiptoname shiptodepartment_1 shiptodepartment_2 - shiptostreet shiptozipcode shiptocity shiptocountry - shiptocontact shiptophone shiptofax shiptoemail); - - $query = qq|SELECT * FROM shipto WHERE trans_id = ? AND module = 'CT'|; - $ref = selectfirst_hashref_query($form, $dbh, $query, $cid); - delete $ref->{id}; - map { $form->{$_} = $ref->{$_} } keys %$ref; - } - # setup last accounts used for this customer if (!$form->{id} && $form->{type} !~ /_(order|quotation)/) { $query = diff --git a/SL/LXDebug.pm b/SL/LXDebug.pm index 80b41f95a..016015f52 100644 --- a/SL/LXDebug.pm +++ b/SL/LXDebug.pm @@ -260,7 +260,8 @@ sub is_tracing_enabled { sub _write { no warnings; my ($self, $prefix, $message) = @_; - my $date = strftime("%Y-%m-%d %H:%M:%S $$ [" . getppid() . "] ${prefix}: ", localtime(time())); + my @now = gettimeofday(); + my $date = strftime("%Y-%m-%d %H:%M:%S." . sprintf('%03d', int($now[1] / 1000)) . " $$ [" . getppid() . "] ${prefix}: ", localtime($now[0])); local *FILE; chomp($message); diff --git a/SL/LiquidityProjection.pm b/SL/LiquidityProjection.pm new file mode 100644 index 000000000..03a1894c2 --- /dev/null +++ b/SL/LiquidityProjection.pm @@ -0,0 +1,296 @@ +package SL::LiquidityProjection; + +use strict; + +use List::MoreUtils qw(uniq); + +use SL::DBUtils; + +sub new { + my $package = shift; + my $self = bless {}, $package; + + my %params = @_; + + $self->{params} = \%params; + + my @now = localtime; + my $now_year = $now[5] + 1900; + my $now_month = $now[4] + 1; + + $self->{min_date} = _the_date($now_year, $now_month); + $self->{max_date} = _the_date($now_year, $now_month + $params{months} - 1); + + $self; +} + +# Algorithmus: +# +# Für den aktuellen Monat und alle x Folgemonate soll der geplante +# Liquiditätszufluss aufgeschlüsselt werden. Der Zufluss berechnet +# sich dabei aus: +# +# 1. Summe aller offenen Auträge +# +# 2. abzüglich aller zu diesen Aufträgen erstellten Rechnungen +# (Teillieferungen/Teilrechnungen) +# +# 3. zuzüglich alle aktiven Wartungsverträge, die in dem jeweiligen +# Monat ihre Saldierungsperiode haben, außer Wartungsverträgen, die +# für den jeweiligen Monat bereits abgerechnet wurden. +# +# Diese Werte sollen zusätzlich optional nach Verkäufer(in) und nach +# Buchungsgruppe aufgeschlüsselt werden. +# +# Diese Lösung geht deshalb immer über die Positionen der Belege +# (wegen der Buchungsgruppe) und berechnet die Summen daraus manuell. +# +# Alle Aufträge, deren Lieferdatum leer ist, oder deren Lieferdatum +# vor dem aktuellen Monat liegt, werden in einer Kategorie 'alt' +# zusammengefasst. +# +# Alle Aufträge, deren Lieferdatum nach dem zu betrachtenden Zeitraum +# (aktueller Monat + x Monate) liegen, werden in einer Kategorie +# 'Zukunft' zusammengefasst. +# +# Insgesamt läuft es wie folgt ab: +# +# 1. Es wird das Datum aller periodisch erzeugten Rechnungen innerhalb +# des Betrachtungszeitraumes herausgesucht. +# +# 2. Alle aktiven Wartungsvertragskonfigurationen werden +# ausgelesen. Die Saldierungsmonate werden solange aufaddiert, wie der +# dabei herauskommende Monat nicht nach dem zu betrachtenden Zeitraum +# liegt. +# +# 3. Für jedes Saldierungsintervall, das innerhalb des +# Betrachtungszeitraumes liegt, und für das es für den Monat noch +# keine Rechnung gibt (siehe 1.), wird diese Konfiguration für den +# Monat vorgemerkt. +# +# 4. Es werden für alle offenen Kundenaufträge die Positionen +# ausgelesen und mit Verkäufer(in), Buchungsgruppe verknüpft. Aus +# Menge, Einzelpreis und Zeilenrabatt wird die Zeilensumme berechnet. +# +# 5. Mit den Informationen aus 3. und 4. werden Datenstrukturen +# initialisiert, die für die Gesamtsummen, für alle Verkäufer(innen), +# für alle Buchungsgruppen, für alle Monate Werte enthalten. +# +# 6. Es wird über alle Einträge aus 4. iteriert. Die Zeilensummen +# werden in den entsprechenden Datenstrukturen aus 5. addiert. +# +# 7. Es wird über alle Einträge aus 3. iteriert. Die Zeilensummen +# werden in den entsprechenden Datenstrukturen aus 5. addiert. +# +# 8. Es werden alle Rechnungspositionen ausgelesen, bei denen die +# Auftragsnummer einer der aus 5. ermittelten Aufträge entspricht. +# +# 9. Es wird über alle Einträge aus 8. iteriert. Die Zeilensummen +# werden von den entsprechenden Datenstrukturen aus 5. abgezogen. Als +# Datum wird dabei das Datum des zu der Rechnung gehörenden Auftrages +# genommen. Als Buchungsgruppe wird die Buchungsgruppe der Zeile +# genommen. Falls es passieren sollte, dass diese Buchungsgruppe in +# den Aufträgen nie vorgekommen ist (sprich Rechnung enthält +# Positionen, die im Auftrag nicht enthalten sind, und die komplett +# andere Buchungsgruppen verwenden), so wird schlicht die allererste +# in 4. gefundene Buchungsgruppe damit belastet. + +sub create { + my ($self) = @_; + my %params = %{ $self->{params} }; + + my $dbh = $params{dbh} || $::form->get_standard_dbh; + my ($sth, $ref, $query); + + $params{months} ||= 6; + + # 1. Auslesen aller erzeugten periodischen Rechnungen im + # Betrachtungszeitraum + my $q_min_date = $dbh->quote($self->{min_date} . '-01'); + $query = <= to_date($q_min_date, 'YYYY-MM-DD')) +SQL + + my %periodic_invoices; + $sth = prepare_execute_query($::form, $dbh, $query); + while ($ref = $sth->fetchrow_hashref) { + $periodic_invoices{ $ref->{config_id} } ||= { }; + $periodic_invoices{ $ref->{config_id} }->{ $ref->{period_start_date} } = 1; + } + $sth->finish; + + # 2. Auslesen aktiver Wartungsvertragskonfigurationen + $query = < 1, 'q' => 3, 'y' => 12 ); + my @scentries; + $sth = prepare_execute_query($::form, $dbh, $query); + while ($ref = $sth->fetchrow_hashref) { + my ($year, $month) = ($ref->{start_year}, $ref->{start_month}); + my $date; + + while (($date = _the_date($year, $month)) le $self->{max_date}) { + if (($date ge $self->{min_date}) && (!$periodic_invoices{ $ref->{config_id} } || !$periodic_invoices{ $ref->{config_id} }->{$date})) { + push @scentries, { buchungsgruppe => $ref->{buchungsgruppe}, + salesman => $ref->{salesman}, + linetotal => $ref->{linetotal}, + date => $date, + }; + } + + ($year, $month) = _fix_date($year, $month + ($periodicities{ $ref->{periodicity} } || 1)); + } + } + $sth->finish; + + # 4. Auslesen offener Aufträge + $query = <{salesman} } (@entries, @scentries); + my @buchungsgruppen = uniq map { $_->{buchungsgruppe} } (@entries, @scentries); + my @now = localtime; + my @dates = map { $self->_date_for($now[5] + 1900, $now[4] + $_) } (0..$self->{params}->{months} + 1); + my %dates_by_ordnumber = map { $_->{ordnumber} => $self->_date_for($_) } @entries; + my %salesman_by_ordnumber = map { $_->{ordnumber} => $_->{salesman} } @entries; + my %date_sorter = ( old => '0000-00', future => '9999-99' ); + + my $projection = { total => { map { $_ => 0 } @dates }, + order => { map { $_ => 0 } @dates }, + partial => { map { $_ => 0 } @dates }, + support => { map { $_ => 0 } @dates }, + salesman => { map { $_ => { map { $_ => 0 } @dates } } @salesmen }, + buchungsgruppe => { map { $_ => { map { $_ => 0 } @dates } } @buchungsgruppen }, + sorted => { month => [ sort { ($date_sorter{$a} || $a) cmp ($date_sorter{$b} || $b) } @dates ], + salesman => [ sort { $a cmp $b } @salesmen ], + buchungsgruppe => [ sort { $a cmp $b } @buchungsgruppen ], + type => [ qw(order partial support) ], + }, + }; + + # 6. Aufsummieren der Auftragspositionen + foreach $ref (@entries) { + my $date = $self->_date_for($ref); + + $projection->{total}->{$date} += $ref->{linetotal}; + $projection->{order}->{$date} += $ref->{linetotal}; + $projection->{salesman}->{ $ref->{salesman} }->{$date} += $ref->{linetotal}; + $projection->{buchungsgruppe}->{ $ref->{buchungsgruppe} }->{$date} += $ref->{linetotal}; + } + + # 7. Aufsummieren der Wartungsvertragspositionen + foreach $ref (@scentries) { + my $date = $ref->{date}; + + $projection->{total}->{$date} += $ref->{linetotal}; + $projection->{support}->{$date} += $ref->{linetotal}; + $projection->{salesman}->{ $ref->{salesman} }->{$date} += $ref->{linetotal}; + $projection->{buchungsgruppe}->{ $ref->{buchungsgruppe} }->{$date} += $ref->{linetotal}; + } + + if (%dates_by_ordnumber) { + # 8. Auslesen von Positionen von Teilrechnungen zu Aufträgen + my $ordnumbers = join ', ', map { $dbh->quote($_) } keys %dates_by_ordnumber; + $query = <{ordnumber} } || die; + my $salesman = $salesman_by_ordnumber{ $ref->{ordnumber} } || die; + my $buchungsgruppe = $projection->{buchungsgruppe}->{ $ref->{buchungsgruppe} } ? $ref->{buchungsgruppe} : $buchungsgruppen[0]; + + $projection->{partial}->{$date} -= $ref->{linetotal}; + $projection->{total}->{$date} -= $ref->{linetotal}; + $projection->{salesman}->{$salesman}->{$date} -= $ref->{linetotal}; + $projection->{buchungsgruppe}->{$buchungsgruppe}->{$date} -= $ref->{linetotal}; + } + } + + return $projection; +} + +# Skaliert '$year' und '$month' so, dass 1 <= Monat <= 12 gilt. Zum +# Einfachen Addieren gedacht, z.B. +# +# my ($new_year, $new_month) = _fix_date($old_year, $old_month + 6); + +sub _fix_date { + my $year = shift; + my $month = shift; + + $year += int(($month - 1) / 12); + $month = (($month - 1) % 12 ) + 1; + + ($year, $month); +} + +# Formartiert Jahr & Monat wie benötigt. + +sub _the_date { + sprintf '%04d-%02d', _fix_date(@_); +} + +# Mappt Datum auf Kategorie. Ist das Datum leer, oder liegt es vor dem +# Betrachtungszeitraum, so ist die Kategorie 'old'. Liegt das Datum +# nach dem Betrachtungszeitraum, so ist die Kategorie +# 'future'. Andernfalls ist sie das formartierte Datum selber. + +sub _date_for { + my $self = shift; + my $ref = ref $_[0] eq 'HASH' ? shift : { year => $_[0], month => $_[1] }; + + return 'old' if !$ref->{year} || !$ref->{month}; + + my $date = _the_date($ref->{year}, $ref->{month}); + + $date lt $self->{min_date} ? 'old' + : $date gt $self->{max_date} ? 'future' + : $date; +} + +1; diff --git a/SL/Locale.pm b/SL/Locale.pm index 23a399e10..4cbef1684 100644 --- a/SL/Locale.pm +++ b/SL/Locale.pm @@ -364,8 +364,8 @@ sub parse_date { ($yy, $mm, $dd) = ($date =~ /(..)(..)(..)/); } - $dd *= 1; - $mm *= 1; + $_ ||= 0 for ($dd, $mm, $yy); + $_ *= 1 for ($dd, $mm, $yy); $yy = ($yy < 70) ? $yy + 2000 : $yy; $yy = ($yy >= 70 && $yy <= 99) ? $yy + 1900 : $yy; @@ -383,9 +383,12 @@ sub parse_date_to_object { my ($date_str, $time_str) = split m{\s+}, $string, 2; my ($yy, $mm, $dd) = $self->parse_date(\%params, $date_str); - my $millisecond = 0; - my ($hour, $minute, $second) = split m/:/, $time_str; - ($second, $millisecond) = split quotemeta($num_separator), $second, 2; + my ($hour, $minute, $second) = split m/:/, ($time_str || ''); + $second ||= '0'; + + ($second, my $millisecond) = split quotemeta($num_separator), $second, 2; + $_ ||= 0 for ($hour, $minute, $millisecond); + $millisecond = substr $millisecond, 0, 3; $millisecond .= '0' x (3 - length $millisecond); diff --git a/SL/Menu.pm b/SL/Menu.pm index 06ea6ee43..19b2d2d16 100644 --- a/SL/Menu.pm +++ b/SL/Menu.pm @@ -162,6 +162,11 @@ sub parse_access_string { return SL::Auth::evaluate_rights_ary($stack[0]); } +sub parse_instance_conf_string { + my ($self, $setting) = @_; + return $::instance_conf->data->{$setting}; +} + sub set_access { my $self = shift; @@ -171,6 +176,7 @@ sub set_access { my $entry = $self->{$key}; $entry->{GRANTED} = $entry->{ACCESS} ? $self->parse_access_string($key, $entry->{ACCESS}) : 1; + $entry->{GRANTED} &&= $self->parse_instance_conf_string($entry->{INSTANCE_CONF}) if $entry->{INSTANCE_CONF}; $entry->{IS_MENU} = $entry->{submenu} || ($key !~ m/--/); $entry->{NUM_VISIBLE_CHILDREN} = 0; diff --git a/SL/MoreCommon.pm b/SL/MoreCommon.pm index 9f4fae601..b1ea817e4 100644 --- a/SL/MoreCommon.pm +++ b/SL/MoreCommon.pm @@ -6,6 +6,7 @@ our @ISA = qw(Exporter); our @EXPORT = qw(save_form restore_form compare_numbers cross); our @EXPORT_OK = qw(ary_union ary_intersect ary_diff listify ary_to_hash uri_encode uri_decode); +use Encode (); use List::MoreUtils qw(zip); use YAML; diff --git a/SL/OE.pm b/SL/OE.pm index 326e89c70..8b073b9c5 100644 --- a/SL/OE.pm +++ b/SL/OE.pm @@ -115,6 +115,7 @@ sub transactions { qq| ct.${vc}number AS vcnumber, ct.country, ct.ustid, ct.business_id, | . qq| tz.description AS taxzone | . $periodic_invoices_columns . + qq| , o.order_probability, o.expected_billing_date, (o.netamount * o.order_probability / 100) AS expected_netamount | . qq|FROM oe o | . qq|JOIN $vc ct ON (o.${vc}_id = ct.id) | . qq|LEFT JOIN employee e ON (o.employee_id = e.id) | . @@ -243,6 +244,26 @@ SQL $query .= qq| AND ${not} COALESCE(pcfg.active, 'f')|; } + if ($form->{reqdate_unset_or_old}) { + $query .= qq| AND ((o.reqdate IS NULL) OR (o.reqdate < date_trunc('month', current_date)))|; + } + + if (($form->{order_probability_value} || '') ne '') { + my $op = $form->{order_probability_value} eq 'le' ? '<=' : '>='; + $query .= qq| AND (o.order_probability ${op} ?)|; + push @values, $form->{order_probability_value}; + } + + if ($form->{expected_billing_date_from}) { + $query .= qq| AND (o.expected_billing_date >= ?)|; + push @values, conv_date($form->{expected_billing_date_from}); + } + + if ($form->{expected_billing_date_to}) { + $query .= qq| AND (o.expected_billing_date <= ?)|; + push @values, conv_date($form->{expected_billing_date_to}); + } + my $sortdir = !defined $form->{sortdir} ? 'ASC' : $form->{sortdir} ? 'ASC' : 'DESC'; my $sortorder = join(', ', map { "${_} ${sortdir} " } ("o.id", $form->sort_columns("transdate", $ordnumber, "name"))); my %allowed_sort_columns = ( @@ -561,6 +582,7 @@ sub save { delivered = ?, proforma = ?, quotation = ?, department_id = ?, language_id = ?, taxzone_id = ?, shipto_id = ?, payment_id = ?, delivery_vendor_id = ?, delivery_customer_id = ?,delivery_term_id = ?, globalproject_id = ?, employee_id = ?, salesman_id = ?, cp_id = ?, transaction_description = ?, marge_total = ?, marge_percent = ? + , order_probability = ?, expected_billing_date = ? WHERE id = ?|; @values = ($form->{ordnumber} || '', $form->{quonumber}, @@ -581,15 +603,16 @@ sub save { conv_i($form->{salesman_id}), conv_i($form->{cp_id}), $form->{transaction_description}, $form->{marge_total} * 1, $form->{marge_percent} * 1, + $form->{order_probability} * 1, conv_date($form->{expected_billing_date}), conv_i($form->{id})); do_query($form, $dbh, $query, @values); $form->{ordtotal} = $amount; - # add shipto $form->{name} = $form->{ $form->{vc} }; $form->{name} =~ s/--\Q$form->{"$form->{vc}_id"}\E//; + # add shipto if (!$form->{shipto_id}) { $form->add_shipto($dbh, $form->{id}, "OE"); } @@ -811,6 +834,7 @@ sub retrieve { d.description AS department, o.payment_id, o.language_id, o.taxzone_id, o.delivery_customer_id, o.delivery_vendor_id, o.proforma, o.shipto_id, o.globalproject_id, o.delivered, o.transaction_description, o.delivery_term_id + , o.order_probability, o.expected_billing_date FROM oe o JOIN ${vc} cv ON (o.${vc}_id = cv.id) LEFT JOIN employee e ON (o.employee_id = e.id) @@ -1387,6 +1411,8 @@ sub order_details { $form->{delivery_term} = SL::DB::Manager::DeliveryTerm->find_by(id => $form->{delivery_term_id} || undef); $form->{delivery_term}->description_long($form->{delivery_term}->translated_attribute('description_long', $form->{language_id})) if $form->{delivery_term} && $form->{language_id}; + $::form->{order} = SL::DB::Manager::Order->find_by(id => $::form->{id}); + $main::lxdebug->leave_sub(); } diff --git a/SL/RP.pm b/SL/RP.pm index 969e8adfb..3d7302658 100644 --- a/SL/RP.pm +++ b/SL/RP.pm @@ -748,6 +748,7 @@ sub trial_balance { my @headingaccounts = (); my $dpt_where; my $dpt_where_without_arapgl; + my ($customer_where, $customer_join, $customer_no_union); my $project; my $where = "1 = 1"; @@ -759,6 +760,11 @@ sub trial_balance { (SELECT department_id FROM gl WHERE gl.id=ac.trans_id), (SELECT department_id FROM ap WHERE ap.id=ac.trans_id)) = | . conv_i($department_id); } + if ($form->{customer_id}) { + $customer_join = qq| JOIN ar a ON (ac.trans_id = a.id) |; + $customer_where = qq| AND (a.customer_id = | . conv_i($form->{customer_id}, 'NULL') . qq|) |; + $customer_no_union = qq| AND 1=0 |; + } # project_id only applies to getting transactions # it has nothing to do with a trial balance @@ -805,8 +811,11 @@ sub trial_balance { my $min_max = $prefix eq 'from' ? 'min' : 'max'; $query = qq|SELECT ${min_max}(transdate) FROM acc_trans ac + $customer_join WHERE (1 = 1) $dpt_where_without_arapgl + $dpt_where + $customer_where $project|; ($form->{"${prefix}date"}) = selectfirst_array_query($form, $dbh, $query); } @@ -816,8 +825,11 @@ sub trial_balance { qq|SELECT c.accno, c.category, SUM(ac.amount) AS amount, c.description FROM acc_trans ac LEFT JOIN chart c ON (ac.chart_id = c.id) + $customer_join WHERE ((select date_trunc('year', ac.transdate::date)) = (select date_trunc('year', ?::date))) AND ac.ob_transaction $dpt_where_without_arapgl + $dpt_where + $customer_where $project GROUP BY c.accno, c.category, c.description |; @@ -915,6 +927,7 @@ sub trial_balance { SELECT c.accno, c.description, c.category, SUM(ac.amount) AS amount FROM acc_trans ac JOIN chart c ON (c.id = ac.chart_id) + $customer_join WHERE $where $dpt_where_without_arapgl $project @@ -933,6 +946,7 @@ sub trial_balance { JOIN chart c ON (p.income_accno_id = c.id) WHERE $invwhere $dpt_where + $customer_where $project GROUP BY c.accno, c.description, c.category @@ -945,6 +959,7 @@ sub trial_balance { JOIN chart c ON (p.expense_accno_id = c.id) WHERE $invwhere $dpt_where + $customer_no_union $project GROUP BY c.accno, c.description, c.category |; @@ -971,8 +986,11 @@ sub trial_balance { (SELECT SUM(ac.amount) * -1 FROM acc_trans ac JOIN chart c ON (c.id = ac.chart_id) + $customer_join WHERE $where $dpt_where_without_arapgl + $dpt_where + $customer_where $project AND (ac.amount < 0) AND (c.accno = ?)) AS debit, @@ -980,41 +998,56 @@ sub trial_balance { (SELECT SUM(ac.amount) FROM acc_trans ac JOIN chart c ON (c.id = ac.chart_id) + $customer_join WHERE $where $dpt_where_without_arapgl + $dpt_where + $customer_where $project AND ac.amount > 0 AND c.accno = ?) AS credit, (SELECT SUM(ac.amount) FROM acc_trans ac JOIN chart c ON (ac.chart_id = c.id) + $customer_join WHERE $saldowhere $dpt_where_without_arapgl + $dpt_where + $customer_where $project AND c.accno = ? AND (NOT ac.ob_transaction OR ac.ob_transaction IS NULL)) AS saldo, (SELECT SUM(ac.amount) FROM acc_trans ac JOIN chart c ON (ac.chart_id = c.id) + $customer_join WHERE $sumwhere $dpt_where_without_arapgl + $dpt_where + $customer_where $project - AND amount > 0 + AND ac.amount > 0 AND c.accno = ?) AS sum_credit, (SELECT SUM(ac.amount) FROM acc_trans ac JOIN chart c ON (ac.chart_id = c.id) + $customer_join WHERE $sumwhere $dpt_where_without_arapgl + $dpt_where + $customer_where $project - AND amount < 0 + AND ac.amount < 0 AND c.accno = ?) AS sum_debit, (SELECT max(ac.transdate) FROM acc_trans ac JOIN chart c ON (ac.chart_id = c.id) + $customer_join WHERE $where $dpt_where_without_arapgl + $dpt_where + $customer_where $project AND c.accno = ?) AS last_transaction @@ -1034,6 +1067,7 @@ sub trial_balance { JOIN chart c ON (p.expense_accno_id = c.id) WHERE $invwhere $dpt_where + $customer_no_union $project AND c.accno = ?) AS debit, @@ -1044,40 +1078,53 @@ sub trial_balance { JOIN chart c ON (p.income_accno_id = c.id) WHERE $invwhere $dpt_where + $customer_where $project AND c.accno = ?) AS credit, (SELECT SUM(ac.amount) FROM acc_trans ac JOIN chart c ON (ac.chart_id = c.id) + $customer_join WHERE $saldowhere $dpt_where_without_arapgl + $dpt_where + $customer_where $project AND c.accno = ? AND (NOT ac.ob_transaction OR ac.ob_transaction IS NULL)) AS saldo, (SELECT SUM(ac.amount) FROM acc_trans ac JOIN chart c ON (ac.chart_id = c.id) + $customer_join WHERE $sumwhere $dpt_where_without_arapgl + $dpt_where + $customer_where $project - AND amount > 0 + AND ac.amount > 0 AND c.accno = ?) AS sum_credit, (SELECT SUM(ac.amount) FROM acc_trans ac JOIN chart c ON (ac.chart_id = c.id) + $customer_join WHERE $sumwhere + $dpt_where $dpt_where_without_arapgl + $customer_where $project - AND amount < 0 + AND ac.amount < 0 AND c.accno = ?) AS sum_debit, (SELECT max(ac.transdate) FROM acc_trans ac JOIN chart c ON (ac.chart_id = c.id) + $customer_join WHERE $where $dpt_where_without_arapgl + $dpt_where + $customer_where $project AND c.accno = ?) AS last_transaction |; diff --git a/SL/ReportGenerator.pm b/SL/ReportGenerator.pm index 09ab7bd7b..6fc50e98f 100644 --- a/SL/ReportGenerator.pm +++ b/SL/ReportGenerator.pm @@ -121,7 +121,7 @@ sub add_data { $row->{$column}->{align} = $self->{columns}->{$column}->{align} unless (defined $row->{$column}->{align}); } - foreach my $field (qw(data link)) { + foreach my $field (qw(data link link_class)) { map { $row->{$_}->{$field} = [ $row->{$_}->{$field} ] if (ref $row->{$_}->{$field} ne 'ARRAY') } keys %{ $row }; } } @@ -351,6 +351,7 @@ sub prepare_html_content { push @{ $col->{CELL_ROWS} }, { 'data' => '' . $self->html_format($col->{data}->[$i]), 'link' => $col->{link}->[$i], + link_class => $col->{link_class}->[$i], }; } diff --git a/SL/Request.pm b/SL/Request.pm index 26be5f79b..47f163ca9 100644 --- a/SL/Request.pm +++ b/SL/Request.pm @@ -41,6 +41,17 @@ sub init_type { return 'html'; } +sub cache { + my ($self, $topic, $default) = @_; + + $topic = '::' . (caller(0))[0] . "::$topic" unless $topic =~ m{^::}; + + $self->{_cache} //= {}; + $self->{_cache}->{$topic} //= ($default // {}); + + return $self->{_cache}->{$topic}; +} + sub _store_value { my ($target, $key, $value) = @_; my @tokens = split /((?:\[\+?\])?(?:\.)|(?:\[\+?\]))/, $key; @@ -533,6 +544,21 @@ of L. Defaults to an isntance of L. For more information about layouts, see L. +=item C + +Caches an item for the duration of the request. C<$topic> must be an +index name referring to the thing to cache. It is used for retrieving +it later on. If C<$topic> doesn't start with C<::> then the caller's +package name is prepended to the topic. For example, if the a from +package C calls with topic = C then the +actual key will be C<::SL::StuffedStuff::get_stuff>. + +If no item exists in the cache for C<$topic> then it is created and +its initial value is set to C<$default>. If C<$default> is not given +(undefined) then a new, empty hash reference is created. + +Returns the cached item. + =back =head1 SPECIAL FUNCTIONS diff --git a/SL/USTVA.pm b/SL/USTVA.pm index bf9ebbdb9..6daa01301 100644 --- a/SL/USTVA.pm +++ b/SL/USTVA.pm @@ -759,7 +759,7 @@ sub ustva { $form->{"Z45"} = $form->{"Z43"}; - $form->{"Z53"} = $form->{"Z45"} + $form->{"53"} + $form->{"74"} + $form->{"Z53"} = $form->{"Z45"} + $form->{"47"} + $form->{"53"} + $form->{"74"} + $form->{"85"} + $form->{"65"}; $form->{"Z62"} = $form->{"Z43"} - $form->{"66"} - $form->{"61"} @@ -806,7 +806,7 @@ sub get_accounts_ustva { my $arwhere = ""; my $item; - my $gltaxkey_where = "((tk.pos_ustva>=59 AND tk.pos_ustva<=66) or (tk.pos_ustva>=89 AND tk.pos_ustva<=93))"; + my $gltaxkey_where = "((tk.pos_ustva = 46) OR (tk.pos_ustva>=59 AND tk.pos_ustva<=66) or (tk.pos_ustva>=89 AND tk.pos_ustva<=93))"; if ($fromdate) { if ($form->{method} eq 'cash') { diff --git a/SL/User.pm b/SL/User.pm index 8bb9b997b..42622f8fd 100644 --- a/SL/User.pm +++ b/SL/User.pm @@ -384,6 +384,9 @@ sub dbupdate2 { &dbconnect_vars($form, $db); + # Flush potentially held database locks. + $form->get_standard_dbh->commit; + my $dbh = SL::DBConnect->connect($form->{dbconnect}, $form->{dbuser}, $form->{dbpasswd}, SL::DBConnect->get_options) or $form->dberror; $dbh->do($form->{dboptions}) if ($form->{dboptions}); diff --git a/bin/mozilla/ct.pl b/bin/mozilla/ct.pl index 845fc3ab4..b5b83c19f 100644 --- a/bin/mozilla/ct.pl +++ b/bin/mozilla/ct.pl @@ -48,6 +48,7 @@ use POSIX qw(strftime); use SL::CT; +use SL::CTI; use SL::CVar; use SL::Request qw(flatten); use SL::DB::Business; @@ -153,9 +154,10 @@ sub list_names { } my @columns = ( - 'id', 'name', "$form->{db}number", 'contact', 'phone', 'discount', - 'fax', 'email', 'taxnumber', 'street', 'zipcode' , 'city', - 'business', 'invnumber', 'ordnumber', 'quonumber', 'salesman', 'country' + 'id', 'name', "$form->{db}number", 'contact', 'phone', 'discount', + 'fax', 'email', 'taxnumber', 'street', 'zipcode' , 'city', + 'business', 'payment', 'invnumber', 'ordnumber', 'quonumber', 'salesman', + 'country' ); my @includeable_custom_variables = grep { $_->{includeable} } @{ $cvar_configs }; @@ -184,6 +186,7 @@ sub list_names { 'country' => { 'text' => $locale->text('Country'), }, 'salesman' => { 'text' => $locale->text('Salesman'), }, 'discount' => { 'text' => $locale->text('Discount'), }, + 'payment' => { 'text' => $locale->text('Payment Terms'), }, %column_defs_cvars, ); @@ -266,6 +269,11 @@ sub list_names { my $column = $ref->{formtype} eq 'invoice' ? 'invnumber' : $ref->{formtype} eq 'order' ? 'ordnumber' : 'quonumber'; $row->{$column}->{data} = $ref->{$column}; + if (my $number = SL::CTI->sanitize_number(number => $ref->{phone})) { + $row->{phone}->{link} = SL::CTI->call_link(number => $number); + $row->{phone}->{link_class} = 'cti_call_action'; + } + $report->add_data($row); } @@ -390,6 +398,13 @@ sub list_contacts { $row->{$_}->{link} = 'mailto:' . E($ref->{$_}) if $ref->{$_}; } + for (qw(cp_phone1 cp_phone2 cp_mobile1)) { + next unless my $number = SL::CTI->sanitize_number(number => $ref->{$_}); + + $row->{$_}->{link} = SL::CTI->call_link(number => $number); + $row->{$_}->{link_class} = 'cti_call_action'; + } + $report->add_data($row); } diff --git a/bin/mozilla/do.pl b/bin/mozilla/do.pl index 67f2740c5..c16f772d1 100644 --- a/bin/mozilla/do.pl +++ b/bin/mozilla/do.pl @@ -30,6 +30,7 @@ # Delivery orders #====================================================================== +use List::MoreUtils qw(uniq); use List::Util qw(max sum); use POSIX qw(strftime); use YAML; @@ -85,6 +86,10 @@ sub add { check_do_access(); + if (($::form->{type} =~ /purchase/) && !$::instance_conf->get_allow_new_purchase_invoice) { + $::form->show_generic_error($::locale->text("You do not have the permissions to access this function.")); + } + my $form = $main::form; set_headings("add"); @@ -174,7 +179,6 @@ sub order_links { 'ids' => $form->{id}); $form->backup_vars(qw(payment_id language_id taxzone_id salesman_id taxincluded cp_id intnotes delivery_term_id currency)); - $form->{shipto} = 1 if $form->{id} || $form->{convert_from_oe_ids}; # get customer / vendor if ($form->{vc} eq 'vendor') { @@ -255,22 +259,31 @@ sub form_header { $form->{employee_id} = $form->{old_employee_id} if $form->{old_employee_id}; $form->{salesman_id} = $form->{old_salesman_id} if $form->{old_salesman_id}; - my @old_project_ids = ($form->{"globalproject_id"}); - map({ push(@old_project_ids, $form->{"project_id_$_"}) - if ($form->{"project_id_$_"}); } (1..$form->{"rowcount"})); - my $vc = $form->{vc} eq "customer" ? "customers" : "vendors"; - $form->get_lists("projects" => { - "key" => "ALL_PROJECTS", - "all" => 0, - "old_id" => \@old_project_ids - }, - $vc => "ALL_VC", + $form->get_lists($vc => "ALL_VC", "price_factors" => "ALL_PRICE_FACTORS", "departments" => "ALL_DEPARTMENTS", "business_types" => "ALL_BUSINESS_TYPES", ); + # Projects + my @old_project_ids = uniq grep { $_ } map { $_ * 1 } ($form->{"globalproject_id"}, map { $form->{"project_id_$_"} } 1..$form->{"rowcount"}); + my @old_ids_cond = @old_project_ids ? (id => \@old_project_ids) : (); + my @customer_cond; + if (($vc eq 'customers') && $::instance_conf->get_customer_projects_only_in_sales) { + @customer_cond = ( + or => [ + customer_id => $::form->{customer_id}, + billable_customer_id => $::form->{customer_id}, + ]); + } + my @conditions = ( + or => [ + and => [ active => 1, @customer_cond ], + @old_ids_cond, + ]); + + $::form->{ALL_PROJECTS} = SL::DB::Manager::Project->get_all(query => \@conditions); $::form->{ALL_EMPLOYEES} = SL::DB::Manager::Employee->get_all_sorted(query => [ or => [ id => $::form->{employee_id}, deleted => 0 ] ]); $::form->{ALL_SALESMEN} = SL::DB::Manager::Employee->get_all_sorted(query => [ or => [ id => $::form->{salesman_id}, deleted => 0 ] ]); $::form->{ALL_SHIPTO} = SL::DB::Manager::Shipto->get_all_sorted(query => [ @@ -787,7 +800,6 @@ sub invoice { require "bin/mozilla/$form->{script}"; my $currency = $form->{currency}; - $form->{shipto} = 1 if $form->{convert_from_do_ids}; invoice_links(); if ($form->{ordnumber}) { diff --git a/bin/mozilla/io.pl b/bin/mozilla/io.pl index 1cfc0ca57..4cd882269 100644 --- a/bin/mozilla/io.pl +++ b/bin/mozilla/io.pl @@ -47,9 +47,11 @@ use SL::CT; use SL::IC; use SL::IO; +use SL::DB::Customer; use SL::DB::Default; use SL::DB::Language; use SL::DB::Printer; +use SL::DB::Vendor; use SL::Helper::CreatePDF; use SL::Helper::Flash; @@ -869,8 +871,6 @@ sub order { } $form->{script} = 'oe.pl'; - $form->{shipto} = 1; - $form->{rowcount}--; $form->{cp_id} *= 1; @@ -935,8 +935,6 @@ sub quotation { $form->{script} = 'oe.pl'; - $form->{shipto} = 1; - $form->{rowcount}--; require "bin/mozilla/$form->{script}"; @@ -1232,7 +1230,7 @@ sub print_form { $form->error($::locale->text('No print templates have been created for this client yet. Please do so in the client configuration.')) if !$defaults->templates; $form->{templates} = $defaults->templates; - my ($old_form) = @_; + my ($old_form, %params) = @_; my $inv = "inv"; my $due = "due"; @@ -1417,28 +1415,6 @@ sub print_form { $form->get_shipto(\%myconfig); } - my @a = qw(name department_1 department_2 street zipcode city country contact phone fax email); - - my $shipto = 1; - - # if there is no shipto fill it in from billto - foreach my $item (@a) { - if ($form->{"shipto$item"}) { - $shipto = 0; - last; - } - } - - if ($shipto) { - if ( $form->{formname} eq 'purchase_order' - || $form->{formname} eq 'request_quotation') { - $form->{shiptoname} = $defaults->company; - $form->{shiptostreet} = $defaults->address; - } else { - map { $form->{"shipto$_"} = $form->{$_} } @a; - } - } - $form->{notes} =~ s/^\s+//g; delete $form->{printer_command}; @@ -1662,7 +1638,10 @@ sub print_form { ($form->{media} eq 'printer') ? $locale->text('sent to printer') : $locale->text('emailed to') . " $form->{email}"; - $form->redirect(qq|$form->{label} $form->{"${inv}number"} $msg|); + + if (!$params{no_redirect}) { + $form->redirect(qq|$form->{label} $form->{"${inv}number"} $msg|); + } } if ($form->{printing}) { call_sub($display_form); @@ -1736,8 +1715,11 @@ sub ship_to { $::form->{title} = $::locale->text('Ship to'); $::form->header; + my $vc_obj = ($::form->{vc} eq 'customer' ? "SL::DB::Customer" : "SL::DB::Vendor")->new(id => $::form->{$::form->{vc} . "_id"})->load; + print $::form->parse_html_template('io/ship_to', { previousform => $previous_form, nextsub => $::form->{display_form} || 'display_form', + vc_obj => $vc_obj, }); $main::lxdebug->leave_sub(); diff --git a/bin/mozilla/ir.pl b/bin/mozilla/ir.pl index e90b12cf6..951bce515 100644 --- a/bin/mozilla/ir.pl +++ b/bin/mozilla/ir.pl @@ -58,6 +58,10 @@ sub add { $main::auth->assert('vendor_invoice_edit'); + if (!$::instance_conf->get_allow_new_purchase_invoice) { + $::form->show_generic_error($::locale->text("You do not have the permissions to access this function.")); + } + return $main::lxdebug->leave_sub() if (load_draft_maybe()); $form->{title} = $locale->text('Record Vendor Invoice'); diff --git a/bin/mozilla/is.pl b/bin/mozilla/is.pl index 379544809..b6c6298a0 100644 --- a/bin/mozilla/is.pl +++ b/bin/mozilla/is.pl @@ -37,6 +37,7 @@ use SL::PE; use SL::OE; use Data::Dumper; use DateTime; +use List::MoreUtils qw(uniq); use List::Util qw(max sum); use SL::DB::Default; @@ -154,7 +155,6 @@ sub invoice_links { taxincluded currency cp_id intnotes id shipto_id delivery_term_id)); - $form->{shipto} = 1 if $editing || $form->{convert_from_oe_ids} || $form->{convert_from_do_ids}; IS->get_customer(\%myconfig, \%$form); #quote all_customer Bug 133 @@ -305,18 +305,30 @@ sub form_header { $form->{defaultcurrency} = $form->get_default_currency(\%myconfig); - my @old_project_ids = ($form->{"globalproject_id"}); - map { push @old_project_ids, $form->{"project_id_$_"} if $form->{"project_id_$_"}; } 1..$form->{"rowcount"}; - - $form->get_lists("projects" => { "key" => "ALL_PROJECTS", - "all" => 0, - "old_id" => \@old_project_ids }, - "taxzones" => "ALL_TAXZONES", + $form->get_lists("taxzones" => "ALL_TAXZONES", "currencies" => "ALL_CURRENCIES", "customers" => "ALL_CUSTOMERS", "departments" => "all_departments", "price_factors" => "ALL_PRICE_FACTORS"); + # Projects + my @old_project_ids = uniq grep { $_ } map { $_ * 1 } ($form->{"globalproject_id"}, map { $form->{"project_id_$_"} } 1..$form->{"rowcount"}); + my @old_ids_cond = @old_project_ids ? (id => \@old_project_ids) : (); + my @customer_cond; + if ($::instance_conf->get_customer_projects_only_in_sales) { + @customer_cond = ( + or => [ + customer_id => $::form->{customer_id}, + billable_customer_id => $::form->{customer_id}, + ]); + } + my @conditions = ( + or => [ + and => [ active => 1, @customer_cond ], + @old_ids_cond, + ]); + + $TMPL_VAR{ALL_PROJECTS} = SL::DB::Manager::Project->get_all(query => \@conditions); $TMPL_VAR{ALL_EMPLOYEES} = SL::DB::Manager::Employee->get_all_sorted(query => [ or => [ id => $::form->{employee_id}, deleted => 0 ] ]); $TMPL_VAR{ALL_SALESMEN} = SL::DB::Manager::Employee->get_all_sorted(query => [ or => [ id => $::form->{salesman_id}, deleted => 0 ] ]); $TMPL_VAR{ALL_SHIPTO} = SL::DB::Manager::Shipto->get_all_sorted(query => [ @@ -949,7 +961,6 @@ sub credit_note { $form->{id} = ''; $form->{rowcount}--; - $form->{shipto} = 1; $form->{title} = $locale->text('Add Credit Note'); diff --git a/bin/mozilla/oe.pl b/bin/mozilla/oe.pl index d6d8ff4d5..9a8aea41e 100644 --- a/bin/mozilla/oe.pl +++ b/bin/mozilla/oe.pl @@ -44,7 +44,7 @@ use SL::IS; use SL::MoreCommon qw(ary_diff); use SL::PE; use SL::ReportGenerator; -use List::MoreUtils qw(any none); +use List::MoreUtils qw(uniq any none); use List::Util qw(min max reduce sum); use Data::Dumper; @@ -90,6 +90,16 @@ sub check_oe_access { $main::auth->assert($right); } +sub check_oe_conversion_to_sales_invoice_allowed { + return 1 if $::form->{type} !~ m/^sales/; + return 1 if ($::form->{type} =~ m/quotation/) && $::instance_conf->get_allow_sales_invoice_from_sales_quotation; + return 1 if ($::form->{type} =~ m/order/) && $::instance_conf->get_allow_sales_invoice_from_sales_order; + + $::form->show_generic_error($::locale->text("You do not have the permissions to access this function.")); + + return 0; +} + sub set_headings { $main::lxdebug->enter_sub(); @@ -247,7 +257,6 @@ sub order_links { $form->{"$form->{vc}_id"} ||= $form->{"all_$form->{vc}"}->[0]->{id} if $form->{"all_$form->{vc}"}; $form->backup_vars(qw(payment_id language_id taxzone_id salesman_id taxincluded cp_id intnotes shipto_id delivery_term_id currency)); - $form->{shipto} = 1 if $form->{id} || $form->{convert_from_oe_ids}; # get customer / vendor IR->get_vendor(\%myconfig, \%$form) if $form->{type} =~ /(purchase_order|request_quotation)/; @@ -339,14 +348,10 @@ sub form_header { $form->{"closed"} ? "checked" : "", $locale->text('Closed') if $form->{id}; $TMPL_VAR{openclosed} = sprintf qq|%s\n|, 2 * scalar @tmp, join "\n", @tmp if @tmp; - # project ids - my @old_project_ids = ($form->{"globalproject_id"}, grep { $_ } map { $form->{"project_id_$_"} } 1..$form->{"rowcount"}); - my $vc = $form->{vc} eq "customer" ? "customers" : "vendors"; - $form->get_lists("projects" => { "key" => "ALL_PROJECTS", - "all" => 0, - "old_id" => \@old_project_ids }, - "taxzones" => "ALL_TAXZONES", + + # project ids + $form->get_lists("taxzones" => "ALL_TAXZONES", "payments" => "ALL_PAYMENTS", "currencies" => "ALL_CURRENCIES", "departments" => "ALL_DEPARTMENTS", @@ -354,6 +359,25 @@ sub form_header { limit => $myconfig{vclimit} + 1 }, "price_factors" => "ALL_PRICE_FACTORS"); + # Projects + my @old_project_ids = uniq grep { $_ } map { $_ * 1 } ($form->{"globalproject_id"}, map { $form->{"project_id_$_"} } 1..$form->{"rowcount"}); + my @old_ids_cond = @old_project_ids ? (id => \@old_project_ids) : (); + my @customer_cond; + if (($vc eq 'customers') && $::instance_conf->get_customer_projects_only_in_sales) { + @customer_cond = ( + or => [ + customer_id => $::form->{customer_id}, + billable_customer_id => $::form->{customer_id}, + ]); + } + my @conditions = ( + or => [ + and => [ active => 1, @customer_cond ], + @old_ids_cond, + ]); + + $TMPL_VAR{ALL_PROJECTS} = SL::DB::Manager::Project->get_all(query => \@conditions); + # label subs my $employee_list_query_gen = sub { $::form->{$_[0]} ? [ or => [ id => $::form->{$_[0]}, deleted => 0 ] ] : [ deleted => 0 ] }; $TMPL_VAR{ALL_EMPLOYEES} = SL::DB::Manager::Employee->get_all_sorted(query => $employee_list_query_gen->('employee_id')); @@ -469,6 +493,8 @@ sub form_header { is_pur_ord => scalar ($form->{type} =~ /purchase_order$/), ); + $TMPL_VAR{ORDER_PROBABILITIES} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ]; + print $form->parse_html_template("oe/form_header", { %TMPL_VAR }); $main::lxdebug->leave_sub(); @@ -746,6 +772,8 @@ sub search { # constants and subs for template $form->{vc_keys} = sub { "$_[0]->{name}--$_[0]->{id}" }; + $form->{ORDER_PROBABILITIES} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ]; + $form->header(); print $form->parse_html_template('oe/search', { @@ -813,6 +841,7 @@ sub orders { "vcnumber", "ustid", "country", "shippingpoint", "taxzone", + "order_probability", "expected_billing_date", "expected_netamount", ); # only show checkboxes if gotten here via sales_order form. @@ -825,6 +854,8 @@ sub orders { $form->{l_delivered} = "Y" if ($form->{delivered} && $form->{notdelivered}); $form->{l_periodic_invoices} = "Y" if ($form->{periodic_invoices_active} && $form->{periodic_invoices_inactive}); + map { $form->{"l_${_}"} = 'Y' } qw(order_probability expected_billing_date expected_netamount) if $form->{l_order_probability_expected_billing_date}; + my $attachment_basename; if ($form->{vc} eq 'vendor') { if ($form->{type} eq 'purchase_order') { @@ -851,7 +882,8 @@ sub orders { push @hidden_variables, "l_subtotal", $form->{vc}, qw(l_closed l_notdelivered open closed delivered notdelivered ordnumber quonumber cusordnumber transaction_description transdatefrom transdateto type vc employee_id salesman_id reqdatefrom reqdateto projectnumber project_id periodic_invoices_active periodic_invoices_inactive - business_id shippingpoint taxzone_id); + business_id shippingpoint taxzone_id reqdate_unset_or_old + order_probability_op order_probability_value expected_billing_date_from expected_billing_date_to); my @keys_for_url = grep { $form->{$_} } @hidden_variables; push @keys_for_url, 'taxzone_id' if $form->{taxzone_id} ne ''; # taxzone_id could be 0 @@ -889,6 +921,9 @@ sub orders { 'periodic_invoices' => { 'text' => $locale->text('Per. Inv.'), }, 'shippingpoint' => { 'text' => $locale->text('Shipping Point'), }, 'taxzone' => { 'text' => $locale->text('Steuersatz'), }, + 'order_probability' => { 'text' => $locale->text('Order probability'), }, + 'expected_billing_date' => { 'text' => $locale->text('Exp. bill. date'), }, + 'expected_netamount' => { 'text' => $locale->text('Exp. netamount'), }, ); foreach my $name (qw(id transdate reqdate quonumber ordnumber cusordnumber name employee salesman shipvia transaction_description shippingpoint taxzone)) { @@ -896,7 +931,7 @@ sub orders { $column_defs{$name}->{link} = $href . "&sort=$name&sortdir=$sortdir"; } - my %column_alignment = map { $_ => 'right' } qw(netamount tax amount curr remaining_amount remaining_netamount); + my %column_alignment = map { $_ => 'right' } qw(netamount tax amount curr remaining_amount remaining_netamount order_probability expected_billing_date expected_netamount); $form->{"l_type"} = "Y"; map { $column_defs{$_}->{visible} = $form->{"l_${_}"} ? 1 : 0 } @columns; @@ -933,6 +968,7 @@ sub orders { push @options, $locale->text('Delivery Order created') if $form->{delivered}; push @options, $locale->text('Not delivered') if $form->{notdelivered}; push @options, $locale->text('Periodic invoices active') if $form->{periodic_invoices_active}; + push @options, $locale->text('Reqdate not set or before current month') if $form->{reqdate_unset_or_old}; if ($form->{business_id}) { my $vc_type_label = $form->{vc} eq 'customer' ? $locale->text('Customer type') : $locale->text('Vendor type'); @@ -942,6 +978,16 @@ sub orders { push @options, $locale->text('Steuersatz') . " : " . SL::DB::TaxZone->new(id => $form->{taxzone_id})->load->description; } + if (($form->{order_probability_value} || '') ne '') { + push @options, $::locale->text('Order probability') . ' ' . ($form->{order_probability_op} eq 'le' ? '<=' : '>=') . ' ' . $form->{order_probability_value} . '%'; + } + + if ($form->{expected_billing_date_from} or $form->{expected_billing_date_to}) { + push @options, $locale->text('Expected billing date'); + push @options, $locale->text('From') . " " . $locale->date(\%myconfig, $form->{expected_billing_date_from}, 1) if $form->{expected_billing_date_from}; + push @options, $locale->text('Bis') . " " . $locale->date(\%myconfig, $form->{expected_billing_date_to}, 1) if $form->{expected_billing_date_to}; + } + $report->set_options('top_info_text' => join("\n", @options), 'raw_top_info_text' => $form->parse_html_template('oe/orders_top'), 'raw_bottom_info_text' => $form->parse_html_template('oe/orders_bottom', { 'SHOW_CONTINUE_BUTTON' => $allow_multiple_orders }), @@ -959,6 +1005,7 @@ sub orders { my $callback = $form->escape($href); my @subtotal_columns = qw(netamount amount marge_total marge_percent remaining_amount remaining_netamount); + push @subtotal_columns, 'expected_netamount' if $form->{l_order_probability_expected_billing_date}; my %totals = map { $_ => 0 } @subtotal_columns; my %subtotals = map { $_ => 0 } @subtotal_columns; @@ -981,7 +1028,9 @@ sub orders { $subtotals{marge_percent} = $subtotals{netamount} ? ($subtotals{marge_total} * 100 / $subtotals{netamount}) : 0; $totals{marge_percent} = $totals{netamount} ? ($totals{marge_total} * 100 / $totals{netamount} ) : 0; - map { $oe->{$_} = $form->format_amount(\%myconfig, $oe->{$_}, 2) } qw(netamount tax amount marge_total marge_percent remaining_amount remaining_netamount); + map { $oe->{$_} = $form->format_amount(\%myconfig, $oe->{$_}, 2) } qw(netamount tax amount marge_total marge_percent remaining_amount remaining_netamount expected_netamount); + + $oe->{order_probability} = ($oe->{order_probability} || 0) . '%'; my $row = { }; @@ -1313,6 +1362,7 @@ sub invoice { my $locale = $main::locale; check_oe_access(); + check_oe_conversion_to_sales_invoice_allowed(); $main::auth->assert($form->{type} eq 'purchase_order' || $form->{type} eq 'request_quotation' ? 'vendor_invoice_edit' : 'invoice_edit'); $form->{old_salesman_id} = $form->{salesman_id}; @@ -1378,7 +1428,6 @@ sub invoice { $form->{convert_from_oe_ids} = $form->{id}; $form->{transdate} = $form->{invdate} = $form->current_date(\%myconfig); $form->{duedate} = $form->current_date(\%myconfig, $form->{invdate}, $form->{terms} * 1); - $form->{shipto} = 1; $form->{defaultcurrency} = $form->get_default_currency(\%myconfig); delete @{$form}{qw(id closed)}; @@ -1611,7 +1660,6 @@ sub check_for_direct_delivery_yes { $form->{direct_delivery_checked} = 1; delete @{$form}{grep /^shipto/, keys %{ $form }}; map { s/^CFDD_//; $form->{$_} = $form->{"CFDD_${_}"} } grep /^CFDD_/, keys %{ $form }; - $form->{shipto} = 1; $form->{CFDD_shipto} = 1; purchase_order(); $main::lxdebug->leave_sub(); @@ -1710,6 +1758,7 @@ sub sales_order { if ($form->{type} eq "purchase_order") { delete($form->{ordnumber}); + $form->{"lastcost_$_"} = $form->{"sellprice_$_"} for (1..$form->{rowcount}); } $form->{cp_id} *= 1; diff --git a/bin/mozilla/rp.pl b/bin/mozilla/rp.pl index 860d38f5e..c2ca6939d 100644 --- a/bin/mozilla/rp.pl +++ b/bin/mozilla/rp.pl @@ -39,6 +39,7 @@ use POSIX qw(strftime); use SL::DB::Default; use SL::DB::Project; +use SL::DB::Customer; use SL::PE; use SL::RP; use SL::Iconv; @@ -140,6 +141,7 @@ sub report { ); $::form->{title} = $title{$::form->{report}}; + $::request->{layout}->add_javascripts('autocomplete_customer.js'); # get departments $::form->all_departments(\%::myconfig); @@ -630,6 +632,11 @@ sub generate_trial_balance { $form->{company} = $locale->text('Company') . " " . $defaults->company; push (@options, $form->{company}); + if ($::form->{customer_id}) { + my $customer = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id}); + push @options, $::locale->text('Customer') . ' ' . $customer->displayable_name; + } + $form->{template_to} = $locale->date(\%myconfig, $form->{todate}, 0); diff --git a/config/kivitendo.conf.default b/config/kivitendo.conf.default index e6849a554..25a8d4c3b 100644 --- a/config/kivitendo.conf.default +++ b/config/kivitendo.conf.default @@ -314,3 +314,24 @@ auto_reload_resources = 0 # If set to 1 each exception will include a full stack backtrace. backtrace_on_die = 0 + +[cti] +# If you want phone numbers to be clickable then this must be set to a +# command that does the actually dialing. Within this command three +# variables are replaced before it is executed: +# +# 1. <%phone_extension%> and <%phone_password%> are taken from the user +# configuration (changeable in the admin interface). +# 2. <%number%> is the number to dial. It has already been sanitized +# and formatted correctly regarding e.g. the international dialing +# prefix. +# +# The following is an example that works with the OpenUC telephony +# server: +# dial_command = curl --insecure -X PUT https://<%phone_extension%>:<%phone_password%>@IP.AD.DR.ESS:8443/sipxconfig/rest/my/call/<%number%> +dial_command = +# If you need to dial something before the actual number then set +# external_prefix to it. +external_prefix = 0 +# The prefix for international calls (numbers starting with +). +international_dialing_prefix = 00 diff --git a/css/kivitendo/main.css b/css/kivitendo/main.css index 3b8599383..6fa432dc0 100644 --- a/css/kivitendo/main.css +++ b/css/kivitendo/main.css @@ -443,3 +443,15 @@ div.ppp_line span.ppp_block_sellprice { span.toggle_selected { font-weight: bold; } + +/* CTI */ +a.cti_call_action { + display: inline-block; + padding-left: 18px; + height: 16px; + position: relative; + top: 2px; + vertical-align: center; + background-image: url(../../image/icons/16x16/phone.png); + background-repeat: no-repeat; +} diff --git a/css/lx-office-erp/main.css b/css/lx-office-erp/main.css index f42c8724f..ab71226dc 100644 --- a/css/lx-office-erp/main.css +++ b/css/lx-office-erp/main.css @@ -494,3 +494,15 @@ div.ppp_line span.ppp_block_sellprice { span.toggle_selected { font-weight: bold; } + +/* CTI */ +a.cti_call_action { + display: inline-block; + padding-left: 18px; + height: 16px; + position: relative; + top: 2px; + vertical-align: center; + background-image: url(../../image/icons/16x16/phone.png); + background-repeat: no-repeat; +} diff --git a/image/icons/16x16/Program--Internal Phone List.png b/image/icons/16x16/Program--Internal Phone List.png new file mode 120000 index 000000000..282275d6d --- /dev/null +++ b/image/icons/16x16/Program--Internal Phone List.png @@ -0,0 +1 @@ +phone.png \ No newline at end of file diff --git a/image/icons/16x16/phone.png b/image/icons/16x16/phone.png new file mode 100644 index 000000000..f189191d7 Binary files /dev/null and b/image/icons/16x16/phone.png differ diff --git a/js/autocomplete_part.js b/js/autocomplete_part.js index d0984eb01..d9bf8dbbb 100644 --- a/js/autocomplete_part.js +++ b/js/autocomplete_part.js @@ -83,7 +83,7 @@ namespace('kivi', function(k){ last_dummy = $dummy.val(); $real.trigger('change'); - if (o.fat_set_item) { + if (o.fat_set_item && item.id) { $.ajax({ url: 'controller.pl?action=Part/show.json', data: { id: item.id }, diff --git a/js/edit_periodic_invoices_config.js b/js/edit_periodic_invoices_config.js index 44ebd1916..b969e1dde 100644 --- a/js/edit_periodic_invoices_config.js +++ b/js/edit_periodic_invoices_config.js @@ -14,7 +14,3 @@ function edit_periodic_invoices_config() { // alert(url); window.open(url, "_new_generic", parm); } - -function warn_save_active_periodic_invoice() { - return confirm(kivi.t8('This sales order has an active configuration for periodic invoices. If you save then all subsequently created invoices will contain those changes as well, but not those that have already been created. Do you want to continue?')); -} diff --git a/js/kivi.CustomerVendor.js b/js/kivi.CustomerVendor.js index 59c07354e..1cb08db49 100644 --- a/js/kivi.CustomerVendor.js +++ b/js/kivi.CustomerVendor.js @@ -77,7 +77,7 @@ namespace('kivi.CustomerVendor', function(ns) { '#country' ]; - this.MapWidget = function(prefix) + this.MapWidget = function(prefix, source_address) { var $mapSearchElements = []; var $widgetWrapper; @@ -117,7 +117,9 @@ namespace('kivi.CustomerVendor', function(ns) { searchString += stmt; } - var url = 'https://maps.google.com/maps?q='+ encodeURIComponent(searchString); + source_address = source_address || ''; + var query = source_address != '' ? 'saddr=' + encodeURIComponent(source_address) + '&daddr=' : 'q='; + var url = 'https://maps.google.com/maps?' + query + encodeURIComponent(searchString); window.open(url, '_blank'); window.focus(); @@ -160,4 +162,44 @@ namespace('kivi.CustomerVendor', function(ns) { var url = "common.pl?INPUT_ENCODING=UTF-8&action=show_history&longdescription=&input_name="+ encodeURIComponent(id); window.open(url, "_new_generic", parm); }; + + this.update_dial_action = function($input) { + var $action = $('#' + $input.prop('id') + '-dial-action'); + + if (!$action) + return true; + + var number = $input.val().replace(/\s+/g, ''); + if (number == '') + $action.hide(); + else + $action.prop('href', 'controller.pl?action=CTI/call&number=' + encodeURIComponent(number)).show(); + + return true; + }; + + this.init_dial_action = function(input) { + if ($('#_cti_enabled').val() != 1) + return false; + + var $input = $(input); + var action_id = $input.prop('id') + '-dial-action'; + + if (!$('#' + action_id).size()) { + var $action = $(''); + $input.wrap('').after($action); + + $input.change(function() { kivi.CustomerVendor.update_dial_action($input); }); + } + + kivi.CustomerVendor.update_dial_action($input); + + return true; + }; }); + +function local_reinit_widgets() { + $('#cv_phone,#shipto_shiptophone,#contact_cp_phone1,#contact_cp_phone2,#contact_cp_mobile1,#contact_cp_mobile2').each(function(idx, elt) { + kivi.CustomerVendor.init_dial_action($(elt)); + }); +} diff --git a/js/kivi.SalesPurchase.js b/js/kivi.SalesPurchase.js index a390c8721..f6c163362 100644 --- a/js/kivi.SalesPurchase.js +++ b/js/kivi.SalesPurchase.js @@ -35,4 +35,47 @@ namespace('kivi.SalesPurchase', function(ns) { $element.val($edit.val()); $('#edit_longdescription_dialog').dialog('close'); }; + + this.delivery_order_check_transfer_qty = function() { + var all_match = true; + var rowcount = $('input[name=rowcount]').val(); + for (var i = 1; i < rowcount; i++) + if ($('#stock_in_out_qty_matches_' + i).val() != 1) + all_match = false; + + if (all_match) + return true; + + return confirm(kivi.t8('There are still transfers not matching the qty of the delivery order. Stock operations can not be changed later. Do you really want to proceed?')); + }; + + this.oe_warn_save_active_periodic_invoice = function() { + return confirm(kivi.t8('This sales order has an active configuration for periodic invoices. If you save then all subsequently created invoices will contain those changes as well, but not those that have already been created. Do you want to continue?')); + }; + + this.check_transaction_description = function() { + if ($('#transaction_description').val() != '') + return true; + + alert(kivi.t8('A transaction description is required.')); + return false; + }; + + this.on_submit_checks = function() { + var $button = $(this); + if (($button.data('check-transfer-qty') == 1) && !kivi.SalesPurchase.delivery_order_check_transfer_qty()) + return false; + + if (($button.data('warn-save-active-periodic-invoice') == 1) && !kivi.SalesPurchase.oe_warn_save_active_periodic_invoice()) + return false; + + if (($button.data('require-transaction-description') == 1) && !kivi.SalesPurchase.check_transaction_description()) + return false; + + return true; + }; + + this.init_on_submit_checks = function() { + $('input[type=submit]').click(kivi.SalesPurchase.on_submit_checks); + }; }); diff --git a/js/locale/de.js b/js/locale/de.js index 3d58e1684..3b7e86524 100644 --- a/js/locale/de.js +++ b/js/locale/de.js @@ -1,4 +1,5 @@ namespace("kivi").setupLocale({ +"A transaction description is required.":"Die Vorgangsbezeichnung muss eingegeben werden.", "Add function block":"Funktionsblock hinzufügen", "Add linked record":"Verknüpften Beleg hinzufügen", "Add picture":"Bild hinzufügen", @@ -56,7 +57,9 @@ namespace("kivi").setupLocale({ "The name is missing.":"Der Name fehlt.", "The name must only consist of letters, numbers and underscores and start with a letter.":"Der Name darf nur aus Buchstaben (keine Umlaute), Ziffern und Unterstrichen bestehen und muss mit einem Buchstaben beginnen.", "The option field is empty.":"Das Optionsfeld ist leer.", +"The recipient, subject or body is missing.":"Der Empfäger, der Betreff oder der Text ist leer.", "The selected database is still configured for client \"#1\". If you delete the database that client will stop working until you re-configure it. Do you still want to delete the database?":"Die auswählte Datenbank ist noch für Mandant \"#1\" konfiguriert. Wenn Sie die Datenbank löschen, wird der Mandanten nicht mehr funktionieren, bis er anders konfiguriert wurde. Wollen Sie die Datenbank trotzdem löschen?", +"There are still transfers not matching the qty of the delivery order. Stock operations can not be changed later. Do you really want to proceed?":"Einige der Lagerbewegungen sind nicht vollständig und Lagerbewegungen können nachträglich nicht mehr verändert werden. Wollen Sie wirklich fortfahren?", "There is one or more sections for which no part has been assigned yet; therefore creating the new record is not possible yet.":"Es gibt einen oder mehrere Abschnitte ohne Artikelzuweisung; daher kann der neue Beleg noch nicht erstellt werden.", "This sales order has an active configuration for periodic invoices. If you save then all subsequently created invoices will contain those changes as well, but not those that have already been created. Do you want to continue?":"Dieser Auftrag besitzt eine aktive Konfiguration für wiederkehrende Rechnungen. Wenn Sie jetzt speichern, so werden alle zukünftig hieraus erzeugten Rechnungen die Änderungen enthalten, nicht aber die bereits erzeugten Rechnungen. Wollen Sie speichern?", "Time/cost estimate actions":"Aktionen für Kosten-/Zeitabschätzung", diff --git a/locale/de/all b/locale/de/all index 56d5255d8..956d1b26a 100755 --- a/locale/de/all +++ b/locale/de/all @@ -49,6 +49,7 @@ $self->{texts} = { 'A lot of the usability of kivitendo has been enhanced with javascript. Although it is currently possible to use every aspect of kivitendo without javascript, we strongly recommend it. In a future version this may change and javascript may be necessary to access advanced features.' => 'Die Bedienung von kivitendo wurde an vielen Stellen mit Javascript verbessert. Obwohl es derzeit möglich ist, jeden Aspekt von kivitendo auch ohne Javascript zu benutzen, empfehlen wir es. In einer zukünftigen Version wird Javascript eventuell notwendig sein um weitergehende Features zu benutzen.', 'A lower-case character is required.' => 'Ein Kleinbuchstabe ist vorgeschrieben.', 'A special character is required (valid characters: #1).' => 'Ein Sonderzeichen ist vorgeschrieben (gültige Zeichen: #1).', + 'A transaction description is required.' => 'Die Vorgangsbezeichnung muss eingegeben werden.', 'A unit with this name does already exist.' => 'Eine Einheit mit diesem Namen existiert bereits.', 'A valid taxkey is missing!' => 'Einen gültiger Steuerschlüssel fehlt!', 'A variable marked as \'editable\' can be changed in each quotation, order, invoice etc.' => 'Eine als \'editierbar\' markierte Variable kann in jedem Angebot, Auftrag, jeder Rechnung etc für jede Position geändert werden.', @@ -207,6 +208,10 @@ $self->{texts} = { 'All units have either no or exactly one base unit of which they are multiples.' => 'Einheiten haben entweder keine oder genau eine Basiseinheit, von der sie ein Vielfaches sind.', 'All users' => 'Alle BenutzerInnen', 'Allow access' => 'Zugriff erlauben', + 'Allow conversion from sales orders to sales invoices' => 'Umwandlung von Verkaufsaufträgen in Verkaufsrechnungen zulassen', + 'Allow conversion from sales quotations to sales invoices' => 'Umwandlung von Verkaufsangeboten in Verkaufsrechnungen zulassen', + 'Allow direct creation of new purchase delivery orders' => 'Direktes Anlegen neuer Einkaufslieferscheine zulassen', + 'Allow direct creation of new purchase invoices' => 'Direktes Anlegen neuer Einkaufsrechnungen zulassen', 'Allow the following users access to my follow-ups:' => 'Erlaube den folgenden Benutzern Zugriff auf meine Wiedervorlagen:', 'Alternatively you can create a new part which will then be selected.' => 'Sie können auch einen neuen Artikel anlegen, der dann automatisch ausgewählt wird.', 'Amended Advance Turnover Tax Return' => 'Berichtigte Anmeldung', @@ -320,6 +325,7 @@ $self->{texts} = { 'Basic Settings for the Requirement Spec Template' => 'Grundeinstellungen der Pflichtenheftvorlage', 'Basic settings' => 'Grundeinstellungen', 'Basic settings actions' => 'Aktionen zu Grundeinstellungen', + 'Basis of calculation' => 'Berechnungsgrundlage', 'Batch Printing' => 'Druck', 'Bcc' => 'Bcc', 'Bcc E-mail' => 'BCC (E-Mail)', @@ -360,6 +366,7 @@ $self->{texts} = { 'Both' => 'Beide', 'Bottom' => 'Unten', 'Bought' => 'Gekauft', + 'Break down by' => 'Aufschlüsseln nach', 'Break up the update and contact a service provider.' => 'Diese Option bricht das Update ab. Bitte kontaktieren Sie Ihren Administrator oder beauftragen einen Dienstleister.', 'Buchungsdatum' => 'Buchungsdatum', 'Buchungsgruppe' => 'Buchungsgruppe', @@ -401,7 +408,9 @@ $self->{texts} = { 'CSV import: parts and services' => 'CSV-Import: Waren und Dienstleistungen', 'CSV import: projects' => 'CSV-Import: Projekte', 'CSV import: shipping addresses' => 'CSV-Import: Lieferadressen', + 'CTI settings' => 'CTI-Einstellungen', 'Calculate' => 'Berechnen', + 'Calling #1 now' => 'Wähle jetzt #1', 'Can not create that quantity with current stock' => 'Diese Anzahl kann mit dem gegenwärtigen Lagerbestand nicht hergestellt werden.', 'Cancel' => 'Abbrechen', 'Cancel Accounts Payables Transaction' => 'Kreditorenbuchung stornieren', @@ -479,6 +488,7 @@ $self->{texts} = { 'Choose Vendor' => 'Händler wählen', 'Choose a Tax Number' => 'Bitte eine Steuernummer angeben', 'City' => 'Stadt', + 'Clear fields' => 'Felder leeren', 'Cleared Balance' => 'abgeschlossen', 'Clearing Tax Received (No 71)' => 'Verrechnung des Erstattungsbetrages erwünscht (Zeile 71)', 'Client' => 'Mandant', @@ -531,6 +541,7 @@ $self->{texts} = { 'Conversion to PDF failed: #1' => 'Konvertierung zu PDF schlug fehl: #1', 'Copies' => 'Kopien', 'Copy' => 'Kopieren', + 'Copy address from master data' => 'Adresse aus Stammdaten kopieren', 'Copy file from #1 to #2 failed: #3' => 'Kopieren der Datei von #1 nach #2 schlug fehl: #3', 'Copy requirement spec' => 'Pflichtenheft kopieren', 'Copy template' => 'Vorlage kopieren', @@ -796,6 +807,7 @@ $self->{texts} = { 'Destination warehouse and bin' => 'Ziellager und -lagerplatz', 'Detail view' => 'Detailanzeige', 'Details (one letter abbreviation)' => 'D', + 'Dial command missing in kivitendo configuration\'s [cti] section' => 'Wählbefehl fehlt im Abschnitt [cti] der kivitendo-Konfiguration', 'Difference' => 'Differenz', 'Dimensions' => 'Abmessungen', 'Directory' => 'Verzeichnis', @@ -1017,7 +1029,7 @@ $self->{texts} = { 'Ertrag' => 'Ertrag', 'Ertrag prozentual' => 'Ertrag prozentual', 'Escape character' => 'Escape-Zeichen', - 'Etikett' => '', + 'Etikett' => 'Etikett', 'EuR' => 'EuR', 'Everyone can log in.' => 'Alle können sich anmelden.', 'Exact' => 'Genau', @@ -1042,7 +1054,10 @@ $self->{texts} = { 'Existing file on server' => 'Auf dem Server existierende Datei', 'Existing pending follow-ups for this item' => 'Noch nicht erledigte Wiedervorlagen für dieses Dokument', 'Existing profiles' => 'Existierende Profile', + 'Exp. bill. date' => 'Vorauss. Abr.datum', + 'Exp. netamount' => 'Vorauss. Summe', 'Expected Tax' => 'Erwartete Steuern', + 'Expected billing date' => 'Voraussichtliches Abrechnungsdatum', 'Expense' => 'Aufwandskonto', 'Expense Account' => 'Aufwandskonto', 'Expense/Asset' => 'Aufwand/Anlagen', @@ -1191,6 +1206,12 @@ $self->{texts} = { 'If amounts differ more than "Maximal amount difference" (see settings), this item is marked as invalid.' => 'Weichen die Beträge mehr als die "maximale Betragsabweichung" (siehe Einstellungen) ab, so wird diese Position als ungültig markiert.', 'If checked the taxkey will not be exported in the DATEV Export, but only IF chart taxkeys differ from general ledger taxkeys' => 'Falls angehakt wird der DATEV-Steuerschlüssel bei Buchungen auf dieses Konto nicht beim DATEV-Export mitexportiert, allerdings nur wenn zusätzlich der Konto-Steuerschlüssel vom Buchungs (Hauptbuch) Steuerschlüssel abweicht', 'If configured this bin will be preselected for all new parts. Also this bin will be used as the master default bin, if default transfer out with master bin is activated.' => 'Falls konfiguriert, wird dieses Lager mit Lagerplatz für neu angelegte Waren vorausgewählt.', + 'If disabled purchase delivery orders can only be created by conversion from existing requests for quotations and purchase orders.' => 'Falls deaktiviert, so können Einkaufslieferscheine nur durch Umwandlung aus bestehenden Preisanfragen und Lieferantenaufträgen angelegt werden.', + 'If disabled purchase invoices can only be created by conversion from existing requests for quotations, purchase orders and purchase delivery orders.' => 'Falls deaktiviert, so können Einkaufsrechnungen nur durch Umwandlung aus bestehenden Preisanfragen, Lieferantenaufträgen und Einkaufslieferscheinen angelegt werden.', + 'If disabled sales orders cannot be converted into sales invoices directly.' => 'Falls deaktiviert, so können Verkaufsangebote nicht direkt in Verkaufsrechnungen umgewandelt werden.', + 'If disabled sales quotations cannot be converted into sales invoices directly.' => 'Falls deaktiviert, so können Verkaufsauträge nicht direkt in Verkaufsrechnungen umgewandelt werden.', + 'If enabled only those projects that are assigned to the currently selected customer are offered for selection in sales records.' => 'Wenn eingeschaltet, so werden in Verkaufsbelegen nur diejenigen Projekte zur Auswahl angeboten, die dem aktuell ausgewählten Kunden zugewiesen wurden.', + 'If enabled purchase and sales records cannot be saved if no transaction description has been entered.' => 'Wenn angeschaltet, so können Einkaufs- und Verkaufsbelege nicht gespeichert werden, solange keine Vorgangsbezeichnung eingegeben wurde.', 'If missing then the start date will be used.' => 'Falls es fehlt, so wird die erste Rechnung für das Startdatum erzeugt.', 'If the article type is set to \'mixed\' then a column called \'type\' must be present.' => 'Falls der Artikeltyp auf \'gemischt\' gestellt wird, muss eine Spalte namens \'type\' vorhanden sein.', 'If the automatic creation of invoices for fees and interest is switched on for a dunning level then the following accounts will be used for the invoice.' => 'Wenn das automatische Erstellen einer Rechnung über Mahngebühren und Zinsen für ein Mahnlevel aktiviert ist, so werden die folgenden Konten für die Rechnung benutzt.', @@ -1249,6 +1270,7 @@ $self->{texts} = { 'Interest' => 'Zinsen', 'Interest Rate' => 'Zinssatz', 'Internal Notes' => 'Interne Bemerkungen', + 'Internal Phone List' => 'Interne Telefonliste', 'Internal comment' => 'Interne Bemerkungen', 'Internet' => 'Internet', 'Introduction of clients' => 'Einführung von Mandanten', @@ -1364,6 +1386,7 @@ $self->{texts} = { 'Link to' => 'Verknüpfen mit', 'Link to the following project:' => 'Mit dem folgenden Projekt verknüpfen:', 'Linked Records' => 'Verknüpfte Belege', + 'Liquidity projection' => 'Liquiditätsübersicht', 'List Accounts' => 'Konten anzeigen', 'List Languages' => 'Sprachen anzeigen', 'List Price' => 'Listenpreis', @@ -1518,6 +1541,7 @@ $self->{texts} = { 'No file has been uploaded yet.' => 'Es wurde noch keine Datei hochgeladen.', 'No function blocks have been created yet.' => 'Es wurden noch keine Funktionsblöcke angelegt.', 'No groups have been created yet.' => 'Es wurden noch keine Gruppen angelegt.', + 'No internal phone extensions have been configured yet.' => 'Es wurden noch keine internen Durchwahlen konfiguriert.', 'No or an unknown authenticantion module specified in "config/kivitendo.conf".' => 'Es wurde kein oder ein unbekanntes Authentifizierungsmodul in "config/kivitendo.conf" angegeben.', 'No part was found matching the search parameters.' => 'Es wurde kein Artikel gefunden, auf den die Suchparameter zutreffen.', 'No payment term has been created yet.' => 'Es wurden noch keine Zahlungsbedingungen angelegt.', @@ -1552,6 +1576,7 @@ $self->{texts} = { 'No vendor has been selected yet.' => 'Es wurde noch kein Lieferant ausgewählt.', 'No warehouse has been created yet or the quantity of the bins is not configured yet.' => 'Es wurde noch kein Lager angelegt, bzw. die dazugehörigen Lagerplätze sind noch nicht konfiguriert.', 'No.' => 'Position', + 'No/individual shipping address' => 'Keine/individuelle Lieferadresse', 'None' => 'Kein', 'Normal users cannot log in.' => 'Normale Benutzer können sich nicht anmelden.', 'Normalize Customer / Vendor names' => 'Normalisierung Kunden- / Lieferantennamen', @@ -1580,6 +1605,7 @@ $self->{texts} = { 'Number of bins' => 'Anzahl Lagerplätze', 'Number of copies' => 'Anzahl Kopien', 'Number of entries changed: #1' => 'Anzahl geänderter Einträge: #1', + 'Number of months' => 'Anzahl Monate', 'Number of new bins' => 'Anzahl neuer Lagerplätze', 'Number pages' => 'Seiten nummerieren', 'Number variables: \'PRECISION=n\' forces numbers to be shown with exactly n decimal places.' => 'Zahlenvariablen: Mit \'PRECISION=n\' erzwingt man, dass Zahlen mit n Nachkommastellen formatiert werden.', @@ -1599,6 +1625,7 @@ $self->{texts} = { 'Only Warnings and Errors' => 'Nur Warnungen und Fehler', 'Only due follow-ups' => 'Nur fällige Wiedervorlagen', 'Only groups that have been configured for the client the user logs in to will be considered.' => 'Allerdings werden nur diejenigen Gruppen herangezogen, die für den Mandanten konfiguriert sind.', + 'Only list customer\'s projects in sales records' => 'Nur Projekte des Kunden in Verkaufsbelegen anzeigen', 'Only shown in item mode' => 'werden nur im Artikelmodus angezeigt', 'Oops. No valid action found to dispatch. Please report this case to the kivitendo team.' => 'Ups. Es wurde keine gültige Funktion zum Aufrufen gefunden. Bitte berichten Sie diesen Fall den kivitendo-Entwicklern.', 'Open' => 'Offen', @@ -1620,6 +1647,8 @@ $self->{texts} = { 'Order Number missing!' => 'Auftragsnummer fehlt!', 'Order amount' => 'Auftragswert', 'Order deleted!' => 'Auftrag gelöscht!', + 'Order probability' => 'Auftragswahrscheinlichkeit', + 'Order probability & expected billing date' => 'Auftragswahrscheinlichkeit & vorrauss. Abrechnungsdatum', 'Order/Item row name' => 'Name der Auftrag-/Positions-Zeilen', 'OrderItem' => 'Position', 'Ordered' => 'Von Kunden bestellt', @@ -1650,7 +1679,7 @@ $self->{texts} = { 'PRINTED' => 'Gedruckt', 'Package name' => 'Paketname', 'Packing Lists' => 'Lieferschein', - 'Packliste' => '', + 'Packliste' => 'Packliste', 'Page' => 'Seite', 'Page #1/#2' => 'Seite #1/#2', 'Paid' => 'bezahlt', @@ -1663,6 +1692,7 @@ $self->{texts} = { 'Part Number' => 'Artikelnummer', 'Part Number missing!' => 'Artikelnummer fehlt!', 'Part picker' => 'Artikelauswahl', + 'Partial invoices' => 'Teilrechnungen', 'Partnumber' => 'Artikelnummer', 'Partnumber must not be set to empty!' => 'Die Artikelnummer darf nicht auf leer geändert werden.', 'Partnumber not unique!' => 'Artikelnummer bereits vorhanden!', @@ -1672,7 +1702,7 @@ $self->{texts} = { 'Parts must have an entry type.' => 'Waren müssen eine Buchungsgruppe haben.', 'Parts with existing part numbers' => 'Artikel mit existierender Artikelnummer', 'Parts, services and assemblies' => 'Waren, Dienstleistungen und Erzeugnisse', - 'Partsedit' => '', + 'Partsedit' => 'Wareneditor', 'Partsgroup (database ID)' => 'Warengruppe (Datenbank-ID)', 'Partsgroup (name)' => 'Warengruppe (Name)', 'Password' => 'Passwort', @@ -1711,6 +1741,10 @@ $self->{texts} = { 'Person' => 'Person', 'Personal settings' => 'Persönliche Einstellungen', 'Phone' => 'Telefon', + 'Phone extension' => 'Durchwahl', + 'Phone extension missing in user configuration' => 'Durchwahl fehlt in der Benutzerkonfiguration', + 'Phone password' => 'Telefonpasswort', + 'Phone password missing in user configuration' => 'Telefonpasswort fehlt in der Benutzerkonfiguration', 'Phone1' => 'Telefon 1 ', 'Phone2' => 'Telefon 2', 'Pick List' => 'Sammelliste', @@ -1844,6 +1878,7 @@ $self->{texts} = { 'Purchase net amount' => 'EK-Betrag', 'Purchase price' => 'EK-Preis', 'Purchase price total' => 'EK-Betrag', + 'Purchasing & Sales' => 'Einkauf & Verkauf', 'Purpose' => 'Verwendungszweck', 'Qty' => 'Menge', 'Qty according to delivery order' => 'Menge laut Lieferschein', @@ -1920,6 +1955,7 @@ $self->{texts} = { 'Representative' => 'Vertreter', 'Representative for Customer' => 'Vertreter für Kunden', 'Reqdate' => 'Liefertermin', + 'Reqdate not set or before current month' => 'Lieferdatum nicht gesetzt oder vor aktuellem Monat', 'Request Quotations' => 'Preisanfragen', 'Request for Quotation' => 'Anfrage', 'Request for Quotation Number' => 'Anfragenummer', @@ -1929,6 +1965,7 @@ $self->{texts} = { 'Requested execution date from' => 'Gewünschtes Ausführungsdatum von', 'Requested execution date to' => 'Gewünschtes Ausführungsdatum bis', 'Requests for Quotation' => 'Preisanfragen', + 'Require a transaction description in purchase and sales records' => 'Vorgangsbezeichnung in Einkaufs- und Verkaufsbelegen erzwingen', 'Required by' => 'Lieferdatum', 'Requirement Spec Status' => 'Pflichtenheftstatus', 'Requirement Spec Statuses' => 'Pflichtenheftstatus', @@ -2305,6 +2342,7 @@ $self->{texts} = { 'The access rights have been saved.' => 'Die Zugriffsrechte wurden gespeichert.', 'The account 3804 already exists, the update will be skipped.' => 'Das Konto 3804 existiert schon, das Update wird übersprungen.', 'The account 3804 will not be added automatically.' => 'Das Konto 3804 wird nicht automatisch hinzugefügt.', + 'The action is missing or invalid.' => 'Die action fehlt, oder sie ist ungültig.', 'The action you\'ve chosen has not been executed because the document does not contain any item yet.' => 'Die von Ihnen ausgewählte Aktion wurde nicht ausgeführt, weil der Beleg noch keine Positionen enthält.', 'The administration area is always accessible.' => 'Der Administrationsbereich ist immer zugänglich.', 'The application "#1" was not found on the system.' => 'Die Anwendung "#1" wurde auf dem System nicht gefunden.', @@ -2465,6 +2503,7 @@ $self->{texts} = { 'The project type has been deleted.' => 'Der Projekttyp wurde gelöscht.', 'The project type has been saved.' => 'Der Projekttyp wurde gespeichert.', 'The project type is in use and cannot be deleted.' => 'Der Projekttyp wird verwendet und kann nicht gelöscht werden.', + 'The recipient, subject or body is missing.' => 'Der Empfäger, der Betreff oder der Text ist leer.', 'The required information consists of the IBAN and the BIC.' => 'Die benötigten Informationen bestehen aus der IBAN und der BIC.', 'The required information consists of the IBAN, the BIC, the mandator ID and the mandate\'s date of signature.' => 'Die benötigten Informationen bestehen aus IBAN, BIC, Mandanten-ID und dem Unterschriftsdatum des Mandates.', 'The requirement spec has been deleted.' => 'Das Pflichtenheft wurde gelöscht.', @@ -2980,6 +3019,7 @@ $self->{texts} = { 'not yet executed' => 'Noch nicht ausgeführt', 'number' => 'Nummer', 'oe.pl::search called with unknown type' => 'oe.pl::search mit unbekanntem Typ aufgerufen', + 'old' => 'alt', 'on the same day' => 'am selben Tag', 'one-time execution' => 'einmalige Ausführung', 'only OB Transactions' => 'nur EB-Buchungen', @@ -3002,6 +3042,7 @@ $self->{texts} = { 'prev' => 'zurück', 'print' => 'drucken', 'proforma' => 'Proforma', + 'prospective' => 'zukünftig', 'purchase_delivery_order_list' => 'lieferscheinliste_einkauf', 'purchase_order' => 'Auftrag', 'purchase_order_list' => 'lieferantenauftragsliste', diff --git a/locale/de/special_chars b/locale/de/special_chars index 49fbf739c..c349536e8 100644 --- a/locale/de/special_chars +++ b/locale/de/special_chars @@ -22,7 +22,7 @@ order=< > \n \n=
[Template/LaTeX] -order=\\ & \n \r " $ % _ # ^ { } < > £ ± ² ³ ° § ® © \xad ➔ → ← +order=\\ & \n \r " $ % _ # ^ { } < > £ ± ² ³ ° § ® © \xad \xa0 ➔ → ← \\=\\textbackslash\s = "='' @@ -51,6 +51,7 @@ _=\\_ ➔=$\\rightarrow$ →=$\\rightarrow$ ←=$\\leftarrow$ +\xa0=~ [Template/OpenDocument] order=& < > " ' \x80 \n \r diff --git a/menus/erp.ini b/menus/erp.ini index dd43bdf8c..3297621f9 100644 --- a/menus/erp.ini +++ b/menus/erp.ini @@ -214,12 +214,14 @@ type=purchase_order [AP--Add Delivery Note] ACCESS=purchase_delivery_order_edit +INSTANCE_CONF=allow_new_purchase_delivery_order module=do.pl action=add type=purchase_delivery_order [AP--Add Vendor Invoice] ACCESS=vendor_invoice_edit +INSTANCE_CONF=allow_new_purchase_invoice module=ir.pl action=add type=invoice @@ -448,6 +450,10 @@ ACCESS=report module=controller.pl action=FinancialOverview/list +[Reports--Liquidity projection] +ACCESS=report +module=controller.pl +action=LiquidityProjection/show [Batch Printing] ACCESS=batch_printing @@ -781,6 +787,10 @@ action=Employee/list module=am.pl action=config +[Program--Internal Phone List] +module=controller.pl +action=CTI/list_internal_extensions + [Program--Version] module=login.pl action=company_logo diff --git a/modules/override/Term/ReadLine/Perl/Bind.pm b/modules/override/Term/ReadLine/Perl/Bind.pm deleted file mode 100644 index 2587f6dcf..000000000 --- a/modules/override/Term/ReadLine/Perl/Bind.pm +++ /dev/null @@ -1,153 +0,0 @@ -package Term::ReadLine::Perl::Bind; -### From http://www.perlmonks.org/?node_id=751611 -### Posted by repellant (http://www.perlmonks.org/?node_id=665462) - -### Set readline bindkeys for common terminals - -use warnings; -use strict; - -BEGIN { - require Exporter; - *import = \&Exporter::import; # just inherit import() only - - our $VERSION = 1.001; - our @EXPORT_OK = qw(rl_bind_action $action2key $key2codes); -} - -use Term::ReadLine; - -# http://cpansearch.perl.org/src/ILYAZ/Term-ReadLine-Perl-1.0302/ReadLine -my $got_rl_perl; - -BEGIN { - $got_rl_perl = eval { - require Term::ReadLine::Perl; - require Term::ReadLine::readline; - }; -} - -# bindkey actions for terminals -our $action2key = { - Complete => "Tab", - PossibleCompletions => "C-d", - QuotedInsert => "C-v", - - ToggleInsertMode => "Insert", - DeleteChar => "Del", - UpcaseWord => "PageUp", - DownCaseWord => "PageDown", - BeginningOfLine => "Home", - EndOfLine => "End", - - ReverseSearchHistory => "C-Up", - ForwardSearchHistory => "C-Down", - ForwardWord => "C-Right", - BackwardWord => "C-Left", - - HistorySearchBackward => "S-Up", - HistorySearchForward => "S-Down", - KillWord => "S-Right", - BackwardKillWord => "S-Left", - - Yank => "A-Down", # paste - KillLine => "A-Right", - BackwardKillLine => "A-Left", -}; - -our $key2codes = { - "Tab" => [ "TAB", ], - "C-d" => [ "C-d", ], - "C-v" => [ "C-v", ], - - "Insert" => [ qq("\e[2~"), qq("\e[2z"), qq("\e[L"), ], - "Del" => [ qq("\e[3~"), ], - "PageUp" => [ qq("\e[5~"), qq("\e[5z"), qq("\e[I"), ], - "PageDown" => [ qq("\e[6~"), qq("\e[6z"), qq("\e[G"), ], - "Home" => [ qq("\e[7~"), qq("\e[1~"), qq("\e[H"), ], - "End" => [ qq("\e[8~"), qq("\e[4~"), qq("\e[F"), ], - - "C-Up" => [ qq("\eOa"), qq("\eOA"), qq("\e[1;5A"), ], - "C-Down" => [ qq("\eOb"), qq("\eOB"), qq("\e[1;5B"), ], - "C-Right" => [ qq("\eOc"), qq("\eOC"), qq("\e[1;5C"), ], - "C-Left" => [ qq("\eOd"), qq("\eOD"), qq("\e[1;5D"), ], - - "S-Up" => [ qq("\e[a"), qq("\e[1;2A"), ], - "S-Down" => [ qq("\e[b"), qq("\e[1;2B"), ], - "S-Right" => [ qq("\e[c"), qq("\e[1;2C"), ], - "S-Left" => [ qq("\e[d"), qq("\e[1;2D"), ], - - "A-Down" => [ qq("\e\e[B"), qq("\e[1;3B"), ], - "A-Right" => [ qq("\e\e[C"), qq("\e[1;3C"), ], - "A-Left" => [ qq("\e\e[D"), qq("\e[1;3D"), ], -}; - -# warn if any keycode is clobbered -our $debug = 0; - -# check ref type -sub _is_array { ref($_[0]) && eval { @{ $_[0] } or 1 } } -sub _is_hash { ref($_[0]) && eval { %{ $_[0] } or 1 } } - -# set bindkey actions for each terminal -my %code2action; - -sub rl_bind_action { - if ($got_rl_perl) - { - my $a2k = shift(); - return () unless _is_hash($a2k); - - while (my ($action, $bindkey) = each %{ $a2k }) - { - # use default keycodes if none provided - my @keycodes = @_ ? @_ : $key2codes; - - for my $k2c (@keycodes) - { - next unless _is_hash($k2c); - - my $codes = $k2c->{$bindkey}; - next unless defined($codes); - $codes = [ $codes ] unless _is_array($codes); - - for my $code (@{ $codes }) - { - if ($debug && $code2action{$code}) - { - my $hexcode = $code; - $hexcode =~ s/^"(.*)"$/$1/; - $hexcode = join(" ", map { uc } unpack("(H2)*", $hexcode)); - - warn <<"EOT"; -rl_bind_action(): re-binding keycode [ $hexcode ] from '$code2action{$code}' to '$action' -EOT - } - - readline::rl_bind($code, $action); - $code2action{$code} = $action; - } - } - } - } - else - { - warn <<"EOT"; -rl_bind_action(): Term::ReadLine::Perl is not available. No bindkeys were set. -EOT - } - - return $got_rl_perl; -} - -# default bind -rl_bind_action($action2key); - -# bind Delete key for 'xterm' -if ($got_rl_perl && defined($ENV{TERM}) && $ENV{TERM} =~ /xterm/) -{ - rl_bind_action($action2key, +{ "Del" => qq("\x7F") }); -} - -'Term::ReadLine::Perl::Bind'; - diff --git a/scripts/console b/scripts/console index 8ce978346..4ceb9a0dd 100755 --- a/scripts/console +++ b/scripts/console @@ -14,7 +14,6 @@ use Devel::REPL 1.002001; use File::Slurp; use Getopt::Long; use Pod::Usage; -use Term::ReadLine::Perl::Bind; # use sane key binding for rxvt users use SL::LxOfficeConf; SL::LxOfficeConf->read; @@ -160,6 +159,10 @@ Spezielle Kommandos: pp DATA - zeigt die Datenstruktur mit Data::Dumper an. quit - beendet die Konsole + part - shortcuts auf die jeweilige SL::DB::{...}::find_by + customer, vendor + order, invoice + EOL # load 'module' - läd das angegebene Modul, d.h. bin/mozilla/module.pl und SL/Module.pm. } @@ -208,6 +211,32 @@ sub sql { } } +sub part { + require SL::DB::Part; + SL::DB::Manager::Part->find_by(@_) +} + +sub order { + require SL::DB::Order; + SL::DB::Manager::Order->find_by(@_) +} + +sub invoice { + require SL::DB::Invoice; + SL::DB::Manager::Invoice->find_by(@_) +} + +sub customer { + require SL::DB::Customer; + SL::DB::Manager::Customer->find_by(@_) +} + +sub vendor { + require SL::DB::Vendor; + SL::DB::Manager::Vendor->find_by(@_) +} + + 1; __END__ diff --git a/scripts/locales.pl b/scripts/locales.pl index cd5fe41b9..788c37db6 100755 --- a/scripts/locales.pl +++ b/scripts/locales.pl @@ -48,7 +48,7 @@ our $missing = {}; our @lost = (); my %ignore_unused_templates = ( - map { $_ => 1 } qw(ct/testpage.html generic/autocomplete.html oe/periodic_invoices_email.txt part/testpage.html t/render.html t/render.js) + map { $_ => 1 } qw(ct/testpage.html generic/autocomplete.html oe/periodic_invoices_email.txt part/testpage.html t/render.html t/render.js task_server/failure_notification_email.txt) ); my (%referenced_html_files, %locale, %htmllocale, %alllocales, %cached, %submit, %jslocale); diff --git a/scripts/rose_auto_create_model.pl b/scripts/rose_auto_create_model.pl index fb589c610..8106576a8 100755 --- a/scripts/rose_auto_create_model.pl +++ b/scripts/rose_auto_create_model.pl @@ -47,18 +47,32 @@ our $manager_path = "SL/DB/Manager"; my %config; +# Maps column names in tables to foreign key relationship names. For +# example: +# +# »follow_up_access« contains a column named »who«. Rose normally +# names the resulting relationship after the class the target table +# uses. In this case the target table is »employee« and the +# corresponding class SL::DB::Employee. The resulting relationship +# would be named »employee«. +# +# In order to rename this relationship we have to map »who« to +# e.g. »granted_by«: +# follow_up_access => { who => 'granted_by' }, + our %foreign_key_name_map = ( KIVITENDO => { - oe => { payment => 'payment_terms', }, - ar => { payment => 'payment_terms', }, - ap => { payment => 'payment_terms', }, + oe => { payment_id => 'payment_terms', }, + ar => { payment_id => 'payment_terms', }, + ap => { payment_id => 'payment_terms', }, - orderitems => { parts => 'part', trans => 'order', }, - delivery_order_items => { parts => 'part' }, - invoice => { parts => 'part' }, - follow_ups => { 'employee_obj' => 'created_for' }, + orderitems => { parts_id => 'part', trans_id => 'order', }, + delivery_order_items => { parts_id => 'part' }, + invoice => { parts_id => 'part' }, + follow_ups => { created_for_user => 'created_for', created_by => 'created_by', }, + follow_up_access => { who => 'with_access', what => 'to_follow_ups_by', }, - periodic_invoices_configs => { oe => 'order' }, + periodic_invoices_configs => { oe_id => 'order' }, }, ); @@ -89,6 +103,23 @@ sub setup { } } +sub fix_relationship_names { + my ($domain, $table, $fkey_text) = @_; + + if ($fkey_text !~ m/key_columns \s+ => \s+ \{ \s+ ['"]? ( [^'"\s]+ ) /x) { + die "fix_relationship_names: could not extract the key column for domain/table $domain/$table; foreign key definition text:\n${fkey_text}\n"; + } + + my $column_name = $1; + my %changes = map { %{$_} } grep { $_ } ($foreign_key_name_map{$domain}->{ALL}, $foreign_key_name_map{$domain}->{$table}); + + if (my $desired_name = $changes{$column_name}) { + $fkey_text =~ s/^ \s\s [^\s]+ \b/ ${desired_name}/msx; + } + + return $fkey_text; +} + sub process_table { my ($domain, $table, $package) = @_; my $schema = ''; @@ -139,23 +170,29 @@ CODE $foreign_key_definition =~ s/::AUTO::/::/g; if ($foreign_key_definition && ($definition =~ /\Q$foreign_key_definition\E/)) { + # These positions refer to the whole setup call, not just the + # parameters/actual relationship definitions. my ($start, $end) = ($-[0], $+[0]); - my %changes = map { %{$_} } grep { $_ } ($foreign_key_name_map{$domain}->{ALL}, $foreign_key_name_map{$domain}->{$table}); - while (my ($auto_generated_name, $desired_name) = each %changes) { - $foreign_key_definition =~ s/^ \s \s ${auto_generated_name} \b/ ${desired_name}/msx; - } + # Match the function parameters = the actual relationship + # definitions + next unless $foreign_key_definition =~ m/\(\n(.+)\n\)/s; - # Sort foreign key definitions alphabetically - if ($foreign_key_definition =~ m/\(\n(.+)\n\)/s) { - my ($list_start, $list_end) = ($-[0], $+[0]); - my @foreign_keys = split m/\n\n/m, $1; - my $sorted_foreign_keys = "(\n" . join("\n\n", sort @foreign_keys) . "\n)"; + my ($list_start, $list_end) = ($-[0], $+[0]); - substr $foreign_key_definition, $list_start, $list_end - $list_start, $sorted_foreign_keys;; - } + # Split the whole chunk on double new lines. The resulting + # elements are one relationship each. Then fix the relationship + # names and sort them by their new names. + my @new_foreign_keys = sort map { fix_relationship_names($domain, $table, $_) } split m/\n\n/m, $1; + + # Replace the function parameters = the actual relationship + # definitions with the new ones. + my $sorted_foreign_keys = "(\n" . join("\n\n", @new_foreign_keys) . "\n)"; + substr $foreign_key_definition, $list_start, $list_end - $list_start, $sorted_foreign_keys; - substr($definition, $start, $end - $start) = $foreign_key_definition; + # Replace the whole setup call in the auto-generated output with + # our new version. + substr $definition, $start, $end - $start, $foreign_key_definition; } $definition =~ s/(meta->table.*)\n/$1\n$schema_str/m if $schema; diff --git a/scripts/task_server.pl b/scripts/task_server.pl index cb332ec07..f3422d62d 100755 --- a/scripts/task_server.pl +++ b/scripts/task_server.pl @@ -75,6 +75,21 @@ sub lxinit { die "cannot find locale for user $login" unless $::locale = Locale->new('de'); } +sub per_job_initialization { + $::locale = Locale->new($::lx_office_conf{system}->{language}); + $::form = Form->new; + $::instance_conf = SL::InstanceConfiguration->new; + $::request = SL::Request->new( + cgi => CGI->new({}), + layout => SL::Layout::None->new, + ); + + $::auth->restore_session; + + $::form->{login} = $lx_office_conf{task_server}->{login}; + $::instance_conf->init; +} + sub drop_privileges { my $user = $lx_office_conf{task_server}->{run_as}; return unless $user; @@ -174,8 +189,7 @@ sub gd_run { foreach my $job (@{ $jobs }) { # Provide fresh global variables in case legacy code modifies # them somehow. - $::locale = Locale->new($::lx_office_conf{system}->{language}); - $::form = Form->new; + per_job_initialization(); chdir $exe_dir; diff --git a/sql/Pg-upgrade2/column_type_text_instead_of_varchar.sql b/sql/Pg-upgrade2/column_type_text_instead_of_varchar.sql new file mode 100644 index 000000000..b3a2ceaa2 --- /dev/null +++ b/sql/Pg-upgrade2/column_type_text_instead_of_varchar.sql @@ -0,0 +1,43 @@ +-- @tag: column_type_text_instead_of_varchar +-- @description: Spaltentyp auf Text anstelle von varchar() für diverse Spalten +-- @depends: release_3_1_0 + +-- contacts +ALTER TABLE contacts + ALTER COLUMN cp_givenname TYPE TEXT + , ALTER COLUMN cp_title TYPE TEXT + , ALTER COLUMN cp_name TYPE TEXT + , ALTER COLUMN cp_phone1 TYPE TEXT + , ALTER COLUMN cp_phone2 TYPE TEXT + , ALTER COLUMN cp_position TYPE TEXT + ; + +-- customer +ALTER TABLE customer + ALTER COLUMN bic TYPE TEXT + , ALTER COLUMN city TYPE TEXT + , ALTER COLUMN country TYPE TEXT + , ALTER COLUMN department_1 TYPE TEXT + , ALTER COLUMN department_2 TYPE TEXT + , ALTER COLUMN fax TYPE TEXT + , ALTER COLUMN iban TYPE TEXT + , ALTER COLUMN language TYPE TEXT + , ALTER COLUMN street TYPE TEXT + , ALTER COLUMN username TYPE TEXT + , ALTER COLUMN zipcode TYPE TEXT + ; + +-- vendor +ALTER TABLE vendor + ALTER COLUMN bic TYPE TEXT + , ALTER COLUMN city TYPE TEXT + , ALTER COLUMN country TYPE TEXT + , ALTER COLUMN department_1 TYPE TEXT + , ALTER COLUMN department_2 TYPE TEXT + , ALTER COLUMN fax TYPE TEXT + , ALTER COLUMN iban TYPE TEXT + , ALTER COLUMN street TYPE TEXT + , ALTER COLUMN user_password TYPE TEXT + , ALTER COLUMN username TYPE TEXT + , ALTER COLUMN zipcode TYPE TEXT + ; diff --git a/sql/Pg-upgrade2/column_type_text_instead_of_varchar2.sql b/sql/Pg-upgrade2/column_type_text_instead_of_varchar2.sql new file mode 100644 index 000000000..20c3589ac --- /dev/null +++ b/sql/Pg-upgrade2/column_type_text_instead_of_varchar2.sql @@ -0,0 +1,17 @@ +-- @tag: column_type_text_instead_of_varchar2 +-- @description: Spaltentyp auf Text anstelle von varchar() für diverse Spalten Teil 2 +-- @depends: column_type_text_instead_of_varchar + +-- shipto +ALTER TABLE shipto + ALTER COLUMN shiptocity TYPE TEXT + , ALTER COLUMN shiptocontact TYPE TEXT + , ALTER COLUMN shiptocountry TYPE TEXT + , ALTER COLUMN shiptodepartment_1 TYPE TEXT + , ALTER COLUMN shiptodepartment_2 TYPE TEXT + , ALTER COLUMN shiptofax TYPE TEXT + , ALTER COLUMN shiptoname TYPE TEXT + , ALTER COLUMN shiptophone TYPE TEXT + , ALTER COLUMN shiptostreet TYPE TEXT + , ALTER COLUMN shiptozipcode TYPE TEXT + ; diff --git a/sql/Pg-upgrade2/column_type_text_instead_of_varchar3.sql b/sql/Pg-upgrade2/column_type_text_instead_of_varchar3.sql new file mode 100644 index 000000000..4504dc919 --- /dev/null +++ b/sql/Pg-upgrade2/column_type_text_instead_of_varchar3.sql @@ -0,0 +1,6 @@ +-- @tag: column_type_text_instead_of_varchar3 +-- @description: Spaltentyp Text anstelle von varchar() in diversen Tabellen Teil 3 +-- @depends: column_type_text_instead_of_varchar2 + +-- vendor +ALTER TABLE vendor ALTER COLUMN language TYPE TEXT; diff --git a/sql/Pg-upgrade2/defaults_only_customer_projects_in_sales.sql b/sql/Pg-upgrade2/defaults_only_customer_projects_in_sales.sql new file mode 100644 index 000000000..feeb72c6d --- /dev/null +++ b/sql/Pg-upgrade2/defaults_only_customer_projects_in_sales.sql @@ -0,0 +1,8 @@ +-- @tag: defaults_only_customer_projects_in_sales +-- @description: Mandantenkonfiguration: in Verkaufsbelegen nur Projekte des ausgewählten Kunden anbieten +-- @depends: release_3_1_0 +ALTER TABLE defaults ADD COLUMN customer_projects_only_in_sales BOOLEAN; +UPDATE defaults SET customer_projects_only_in_sales = FALSE; +ALTER TABLE defaults + ALTER COLUMN customer_projects_only_in_sales SET DEFAULT FALSE, + ALTER COLUMN customer_projects_only_in_sales SET NOT NULL; diff --git a/sql/Pg-upgrade2/defaults_require_transaction_description.sql b/sql/Pg-upgrade2/defaults_require_transaction_description.sql new file mode 100644 index 000000000..250175ae1 --- /dev/null +++ b/sql/Pg-upgrade2/defaults_require_transaction_description.sql @@ -0,0 +1,9 @@ +-- @tag: defaults_require_transaction_description +-- @description: Mandantenkonfiguration: optional Existenz der Vorgangsbezeichnung erzwingen +-- @depends: release_3_1_0 +ALTER TABLE defaults ADD COLUMN require_transaction_description_ps BOOLEAN; +UPDATE defaults SET require_transaction_description_ps = FALSE; + +ALTER TABLE defaults + ALTER COLUMN require_transaction_description_ps SET DEFAULT FALSE, + ALTER COLUMN require_transaction_description_ps SET NOT NULL; diff --git a/sql/Pg-upgrade2/defaults_sales_process_limitations.sql b/sql/Pg-upgrade2/defaults_sales_process_limitations.sql new file mode 100644 index 000000000..f62cb71c0 --- /dev/null +++ b/sql/Pg-upgrade2/defaults_sales_process_limitations.sql @@ -0,0 +1,27 @@ +-- @tag: defaults_sales_purchase_process_limitations +-- @description: Mandantenkonfiguration: Einschränkungen, welche Aktionen im Einkaufs-/Verkaufsprozess erlaubt sind +-- @depends: release_3_1_0 +ALTER TABLE defaults + ADD COLUMN allow_sales_invoice_from_sales_quotation BOOLEAN, + ADD COLUMN allow_sales_invoice_from_sales_order BOOLEAN, + ADD COLUMN allow_new_purchase_delivery_order BOOLEAN, + ADD COLUMN allow_new_purchase_invoice BOOLEAN; + +UPDATE defaults +SET allow_sales_invoice_from_sales_quotation = TRUE, + allow_sales_invoice_from_sales_order = TRUE, + allow_new_purchase_delivery_order = TRUE, + allow_new_purchase_invoice = TRUE; + +ALTER TABLE defaults + ALTER COLUMN allow_sales_invoice_from_sales_quotation SET DEFAULT TRUE, + ALTER COLUMN allow_sales_invoice_from_sales_quotation SET NOT NULL, + + ALTER COLUMN allow_sales_invoice_from_sales_order SET DEFAULT TRUE, + ALTER COLUMN allow_sales_invoice_from_sales_order SET NOT NULL, + + ALTER COLUMN allow_new_purchase_delivery_order SET DEFAULT TRUE, + ALTER COLUMN allow_new_purchase_delivery_order SET NOT NULL, + + ALTER COLUMN allow_new_purchase_invoice SET DEFAULT TRUE, + ALTER COLUMN allow_new_purchase_invoice SET NOT NULL; diff --git a/sql/Pg-upgrade2/delete_cvars_on_trans_deletion.sql b/sql/Pg-upgrade2/delete_cvars_on_trans_deletion.sql new file mode 100644 index 000000000..3e00660b3 --- /dev/null +++ b/sql/Pg-upgrade2/delete_cvars_on_trans_deletion.sql @@ -0,0 +1,147 @@ +-- @tag: delete_cvars_on_trans_deletion +-- @description: Einträge in benutzerdefinierten Variablen löschen, deren Bezugsbelege gelöscht wurde +-- @depends: release_3_1_0 + +-- 1. Alle benutzerdefinierten Variablen löschen, für die es keine +-- Einträge in den dazugehörigen Tabellen mehr gibt. + +-- 1.1. Alle CVars für Artikel selber (sub_module ist leer): +DELETE FROM custom_variables +WHERE (config_id IN (SELECT id FROM custom_variable_configs WHERE module = 'IC')) + AND (COALESCE(sub_module, '') = '') + AND (trans_id NOT IN (SELECT id FROM parts)); + +-- 1.2. Alle CVars für Angebote/Aufträge, Lieferscheine, Rechnungen +-- (sub_module gesetzt): +DELETE FROM custom_variables +WHERE (config_id IN (SELECT id FROM custom_variable_configs WHERE module = 'IC')) + AND (sub_module = 'orderitems') + AND (trans_id NOT IN (SELECT id FROM orderitems)); + +DELETE FROM custom_variables +WHERE (config_id IN (SELECT id FROM custom_variable_configs WHERE module = 'IC')) + AND (sub_module = 'delivery_order_items') + AND (trans_id NOT IN (SELECT id FROM delivery_order_items)); + +DELETE FROM custom_variables +WHERE (config_id IN (SELECT id FROM custom_variable_configs WHERE module = 'IC')) + AND (sub_module = 'invoice') + AND (trans_id NOT IN (SELECT id FROM invoice)); + +-- 1.3. Alle CVars für Kunden/Lieferanten: +DELETE FROM custom_variables +WHERE (config_id IN (SELECT id FROM custom_variable_configs WHERE module = 'CT')) + AND (trans_id NOT IN (SELECT id FROM customer UNION SELECT id FROM vendor)); + +-- 1.4. Alle CVars für Ansprechpersonen: +DELETE FROM custom_variables +WHERE (config_id IN (SELECT id FROM custom_variable_configs WHERE module = 'Contacts')) + AND (trans_id NOT IN (SELECT cp_id FROM contacts)); + +-- 1.5. Alle CVars für Projekte: +DELETE FROM custom_variables +WHERE (config_id IN (SELECT id FROM custom_variable_configs WHERE module = 'Projects')) + AND (trans_id NOT IN (SELECT id FROM project)); + +-- 2. Triggerfunktionen erstellen, die die benutzerdefinierten +-- Variablen löschen. + +-- 2.1. Parametrisierte Backend-Funktion zum Löschen: +CREATE OR REPLACE FUNCTION delete_custom_variables_with_sub_module(config_module TEXT, cvar_sub_module TEXT, old_id INTEGER) +RETURNS BOOLEAN AS $$ + BEGIN + DELETE FROM custom_variables + WHERE (config_id IN (SELECT id FROM custom_variable_configs WHERE module = config_module)) + AND (COALESCE(sub_module, '') = cvar_sub_module) + AND (trans_id = old_id); + + RETURN TRUE; + END; +$$ LANGUAGE plpgsql; + +-- 2.2. Nun die Funktionen, die als Trigger aufgerufen wird und die +-- entscheidet, wie genau zu löschen ist: +CREATE OR REPLACE FUNCTION delete_custom_variables_trigger() +RETURNS TRIGGER AS $$ + BEGIN + IF (TG_TABLE_NAME IN ('orderitems', 'delivery_order_items', 'invoice')) THEN + PERFORM delete_custom_variables_with_sub_module('IC', TG_TABLE_NAME, old.id); + END IF; + + IF (TG_TABLE_NAME = 'parts') THEN + PERFORM delete_custom_variables_with_sub_module('IC', '', old.id); + END IF; + + IF (TG_TABLE_NAME IN ('customer', 'vendor')) THEN + PERFORM delete_custom_variables_with_sub_module('CT', '', old.id); + END IF; + + IF (TG_TABLE_NAME = 'contacts') THEN + PERFORM delete_custom_variables_with_sub_module('Contacts', '', old.id); + END IF; + + IF (TG_TABLE_NAME = 'project') THEN + PERFORM delete_custom_variables_with_sub_module('Projects', '', old.id); + END IF; + + RETURN old; + END; +$$ LANGUAGE plpgsql; + +-- 3. Die eigentlichen Trigger erstellen: + +-- 3.1. orderitems +DROP TRIGGER IF EXISTS orderitems_delete_custom_variables_after_deletion ON orderitems; + +CREATE TRIGGER orderitems_delete_custom_variables_after_deletion +AFTER DELETE ON orderitems +FOR EACH ROW EXECUTE PROCEDURE delete_custom_variables_trigger(); + +-- 3.2. delivery_order_items +DROP TRIGGER IF EXISTS delivery_order_items_delete_custom_variables_after_deletion ON delivery_order_items; + +CREATE TRIGGER delivery_order_items_delete_custom_variables_after_deletion +AFTER DELETE ON delivery_order_items +FOR EACH ROW EXECUTE PROCEDURE delete_custom_variables_trigger(); + +-- 3.3. invoice +DROP TRIGGER IF EXISTS invoice_delete_custom_variables_after_deletion ON invoice; + +CREATE TRIGGER invoice_delete_custom_variables_after_deletion +AFTER DELETE ON invoice +FOR EACH ROW EXECUTE PROCEDURE delete_custom_variables_trigger(); + +-- 3.4. parts +DROP TRIGGER IF EXISTS parts_delete_custom_variables_after_deletion ON parts; + +CREATE TRIGGER parts_delete_custom_variables_after_deletion +AFTER DELETE ON parts +FOR EACH ROW EXECUTE PROCEDURE delete_custom_variables_trigger(); + +-- 3.5. customer +DROP TRIGGER IF EXISTS customer_delete_custom_variables_after_deletion ON customer; + +CREATE TRIGGER customer_delete_custom_variables_after_deletion +AFTER DELETE ON customer +FOR EACH ROW EXECUTE PROCEDURE delete_custom_variables_trigger(); + +-- 3.6. vendor +DROP TRIGGER IF EXISTS vendor_delete_custom_variables_after_deletion ON vendor; + +CREATE TRIGGER vendor_delete_custom_variables_after_deletion +AFTER DELETE ON vendor +FOR EACH ROW EXECUTE PROCEDURE delete_custom_variables_trigger(); + +-- 3.7. contacts +DROP TRIGGER IF EXISTS contacts_delete_custom_variables_after_deletion ON contacts; + +CREATE TRIGGER contacts_delete_custom_variables_after_deletion +AFTER DELETE ON contacts +FOR EACH ROW EXECUTE PROCEDURE delete_custom_variables_trigger(); + +-- 3.8. project +DROP TRIGGER IF EXISTS project_delete_custom_variables_after_deletion ON project; + +CREATE TRIGGER project_delete_custom_variables_after_deletion +AFTER DELETE ON project +FOR EACH ROW EXECUTE PROCEDURE delete_custom_variables_trigger(); diff --git a/sql/Pg-upgrade2/sales_quotation_order_probability_expected_billing_date.sql b/sql/Pg-upgrade2/sales_quotation_order_probability_expected_billing_date.sql new file mode 100644 index 000000000..5c3716860 --- /dev/null +++ b/sql/Pg-upgrade2/sales_quotation_order_probability_expected_billing_date.sql @@ -0,0 +1,12 @@ +-- @tag: sales_quotation_order_probability_expected_billing_date +-- @charset: utf-8 +-- @description: Weitere Felder im Angebot: Angebotswahrscheinlichkeit, voraussichtliches Abrechnungsdatum +ALTER TABLE oe + ADD COLUMN order_probability INTEGER, + ADD COLUMN expected_billing_date DATE; + +UPDATE oe SET order_probability = 0; + +ALTER TABLE oe + ALTER COLUMN order_probability SET DEFAULT 0, + ALTER COLUMN order_probability SET NOT NULL; diff --git a/t/cti/call_link.t b/t/cti/call_link.t new file mode 100644 index 000000000..ce762399b --- /dev/null +++ b/t/cti/call_link.t @@ -0,0 +1,22 @@ +use Test::More tests => 9; + +use strict; +use lib 't'; +use utf8; + +use_ok 'SL::CTI'; + +{ + no warnings 'once'; + $::lx_office_conf{cti}->{international_dialing_prefix} = '00'; +} + +is SL::CTI->call_link(number => '0371 5347 620'), 'controller.pl?action=CTI/call&number=03715347620'; +is SL::CTI->call_link(number => '0049(0)421-22232 22'), 'controller.pl?action=CTI/call&number=0049421-2223222'; +is SL::CTI->call_link(number => '+49(0)421-22232 22'), 'controller.pl?action=CTI/call&number=0049421-2223222'; +is SL::CTI->call_link(number => 'Tel: +49 40 809064 0'), 'controller.pl?action=CTI/call&number=0049408090640'; + +is SL::CTI->call_link(number => '0371 5347 620', internal => 1), 'controller.pl?action=CTI/call&number=03715347620&internal=1'; +is SL::CTI->call_link(number => '0049(0)421-22232 22', internal => 1), 'controller.pl?action=CTI/call&number=0049421-2223222&internal=1'; +is SL::CTI->call_link(number => '+49(0)421-22232 22', internal => 1), 'controller.pl?action=CTI/call&number=0049421-2223222&internal=1'; +is SL::CTI->call_link(number => 'Tel: +49 40 809064 0', internal => 1), 'controller.pl?action=CTI/call&number=0049408090640&internal=1'; diff --git a/t/cti/sanitize_number.t b/t/cti/sanitize_number.t new file mode 100644 index 000000000..639e7167b --- /dev/null +++ b/t/cti/sanitize_number.t @@ -0,0 +1,17 @@ +use Test::More tests => 5; + +use strict; +use lib 't'; +use utf8; + +use_ok 'SL::CTI'; + +{ + no warnings 'once'; + $::lx_office_conf{cti}->{international_dialing_prefix} = '00'; +} + +is SL::CTI->sanitize_number(number => '0371 5347 620'), '03715347620'; +is SL::CTI->sanitize_number(number => '0049(0)421-22232 22'), '0049421-2223222'; +is SL::CTI->sanitize_number(number => '+49(0)421-22232 22'), '0049421-2223222'; +is SL::CTI->sanitize_number(number => 'Tel: +49 40 809064 0'), '0049408090640'; diff --git a/t/db_helper/price_tax_calculator.t b/t/db_helper/price_tax_calculator.t index e8ee4acb7..a80146d78 100644 --- a/t/db_helper/price_tax_calculator.t +++ b/t/db_helper/price_tax_calculator.t @@ -217,9 +217,106 @@ sub test_default_invoice_two_items_19_7_tax_not_included() { }, "${title}: calculated data"); } +sub test_default_invoice_three_items_sellprice_rounding_discount() { + reset_state(); + + my $item1 = new_item(qty => 1, sellprice => 5.55, discount => .05); + my $item2 = new_item(qty => 1, sellprice => 5.50, discount => .05); + my $item3 = new_item(qty => 1, sellprice => 5.00, discount => .05); + my $invoice = new_invoice( + taxincluded => 0, + invoiceitems => [ $item1, $item2, $item3 ], + ); + + # this is how price_tax_calculator is implemented. It differs from + # the way sales_order / invoice - forms are calculating: + # linetotal = sellprice 5.55 * qty 1 * (1 - 0.05) = 5.2725; rounded 5.27 + # linetotal = sellprice 5.50 * qty 1 * (1 - 0.05) = 5.225 rounded 5.23 + # linetotal = sellprice 5.00 * qty 1 * (1 - 0.05) = 4.75; rounded 4.75 + # ... + + # item 1: + # discount = sellprice 5.55 * discount (0.05) = 0.2775; rounded 0.28 + # sellprice = sellprice 5.55 - discount 0.28 = 5.27; rounded 5.27 + # linetotal = sellprice 5.27 * qty 1 = 5.27; rounded 5.27 + # 19%(5.27) = 1.0013; rounded = 1.00 + # total rounded = 6.27 + + # lastcost 1.93 * qty 1 = 1.93; rounded 1.93 + # line marge_total = 3.34 + # line marge_percent = 63.3776091081594 + + # item 2: + # discount = sellprice 5.50 * discount 0.05 = 0.275; rounded 0.28 + # sellprice = sellprice 5.50 - discount 0.28 = 5.22; rounded 5.22 + # linetotal = sellprice 5.22 * qty 1 = 5.22; rounded 5.22 + # 19%(5.22) = 0.9918; rounded = 0.99 + # total rounded = 6.21 + + # lastcost 1.93 * qty 1 = 1.93; rounded 1.93 + # line marge_total = 5.22 - 1.93 = 3.29 + # line marge_percent = 3.29/5.22 = 0.630268199233716 + + # item 3: + # discount = sellprice 5.00 * discount 0.25 = 0.25; rounded 0.25 + # sellprice = sellprice 5.00 - discount 0.25 = 4.75; rounded 4.75 + # linetotal = sellprice 4.75 * qty 1 = 4.75; rounded 4.75 + # 19%(4.75) = 0.9025; rounded = 0.90 + # total rounded = 5.65 + + # lastcost 1.93 * qty 1 = 1.93; rounded 1.93 + # line marge_total = 2.82 + # line marge_percent = 59.3684210526316 + + my $title = 'default invoice, three items, sellprice, rounding, discount'; + my %data = $invoice->calculate_prices_and_taxes; + + is($item1->marge_total, 3.34, "${title}: item1 marge_total"); + is($item1->marge_percent, 63.3776091081594, "${title}: item1 marge_percent"); + is($item1->marge_price_factor, 1, "${title}: item1 marge_price_factor"); + + is($item2->marge_total, 3.29, "${title}: item2 marge_total"); + is($item2->marge_percent, 63.0268199233716, "${title}: item2 marge_percent"); + is($item2->marge_price_factor, 1, "${title}: item2 marge_price_factor"); + + is($item3->marge_total, 2.82, "${title}: item3 marge_total"); + is($item3->marge_percent, 59.3684210526316, "${title}: item3 marge_percent"); + is($item3->marge_price_factor, 1, "${title}: item3 marge_price_factor"); + + is($invoice->netamount, 5.27 + 5.22 + 4.75, "${title}: netamount"); + + # 6.27 + 6.21 + 5.65 = 18.13 + # 1.19*(5.27 + 5.22 + 4.75) = 18.1356; rounded 18.14 + #is($invoice->amount, 6.27 + 6.21 + 5.65, "${title}: amount"); + is($invoice->amount, 18.14, "${title}: amount"); + + is($invoice->marge_total, 3.34 + 3.29 + 2.82, "${title}: marge_total"); + is($invoice->marge_percent, 62.007874015748, "${title}: marge_percent"); + + is_deeply(\%data, { + allocated => {}, + amounts => { + $buchungsgruppe->income_accno_id_0 => { + amount => 15.24, + tax_id => $tax->id, + taxkey => 3, + }, + }, + amounts_cogs => {}, + assembly_items => [ + [], [], [], + ], + exchangerate => 1, + taxes => { + $tax->chart_id => 2.9, + }, + }, "${title}: calculated data"); +} + Support::TestSetup::login(); test_default_invoice_one_item_19_tax_not_included(); test_default_invoice_two_items_19_7_tax_not_included(); +test_default_invoice_three_items_sellprice_rounding_discount(); done_testing(); diff --git a/t/helper/csv.t b/t/helper/csv.t index 3a8556cfd..bab3825df 100644 --- a/t/helper/csv.t +++ b/t/helper/csv.t @@ -552,7 +552,7 @@ ok !$csv->_check_multiplexed, 'multiplex check detects empty header'; ##### $csv = SL::Helper::Csv->new( - file => \< \ Encode::encode('utf-8', <get_data, [ { datatype => 'P', description => 'Kaffee', longdesc ##### $csv = SL::Helper::Csv->new( - file => \< \ Encode::encode('utf-8', < 50; + +use lib 't'; + +use Data::Dumper; + +use DateTime; +use_ok 'SL::Helper::DateTime'; + +sub mon { DateTime->new(year => 2014, month => 6, day => 23) } +sub tue { DateTime->new(year => 2014, month => 6, day => 24) } +sub wed { DateTime->new(year => 2014, month => 6, day => 25) } +sub thu { DateTime->new(year => 2014, month => 6, day => 26) } +sub fri { DateTime->new(year => 2014, month => 6, day => 27) } +sub sat { DateTime->new(year => 2014, month => 6, day => 28) } +sub sun { DateTime->new(year => 2014, month => 6, day => 29) } + + +is mon->add_businessdays(days => 5)->day_of_week, 1, "mon + 5 => mon"; +is mon->add_businessdays(days => 12)->day_of_week, 3, "mon + 12 => wed"; +is fri->add_businessdays(days => 2)->day_of_week, 2, "fri + 2 => tue"; +is tue->add_businessdays(days => 9)->day_of_week, 1, "tue + 9 => mon"; +is tue->add_businessdays(days => 8)->day_of_week, 5, "tue + 8 => fri"; + +# same with 6day week +is mon->add_businessdays(businessweek => 6, days => 5)->day_of_week, 6, "mon + 5 => sat (6dw)"; +is mon->add_businessdays(businessweek => 6, days => 12)->day_of_week, 1, "mon + 12 => mon (6dw)"; +is fri->add_businessdays(businessweek => 6, days => 2)->day_of_week, 1, "fri + 2 => mon (6dw)"; +is tue->add_businessdays(businessweek => 6, days => 9)->day_of_week, 5, "tue + 9 => fri (6dw)"; +is tue->add_businessdays(businessweek => 6, days => 8)->day_of_week, 4, "tue + 8 => thu (6dw)"; + +# absolute dates + +is mon->add_businessdays(days => 5), mon->add(days => 7), "mon + 5 => mon (date)"; +is mon->add_businessdays(days => 12), mon->add(days => 16), "mon + 12 => wed (date)"; +is fri->add_businessdays(days => 2), fri->add(days => 4), "fri + 2 => tue (date)"; +is tue->add_businessdays(days => 9), tue->add(days => 13), "tue + 9 => mon (date)"; +is tue->add_businessdays(days => 8), tue->add(days => 10), "tue + 8 => fri (date)"; + +# same with 6day week +is mon->add_businessdays(businessweek => 6, days => 5), mon->add(days => 5), "mon + 5 => sat (date) (6dw)"; +is mon->add_businessdays(businessweek => 6, days => 12), mon->add(days => 14), "mon + 12 => mon (date) (6dw)"; +is fri->add_businessdays(businessweek => 6, days => 2), fri->add(days => 3), "fri + 2 => mon (date) (6dw)"; +is tue->add_businessdays(businessweek => 6, days => 9), tue->add(days => 10), "tue + 9 => fri (date) (6dw)"; +is tue->add_businessdays(businessweek => 6, days => 8), tue->add(days => 9), "tue + 8 => thu (date) (6dw)"; + + +# same with substract + +is mon->subtract_businessdays(days => 5)->day_of_week, 1, "mon - 5 => mon"; +is mon->subtract_businessdays(days => 12)->day_of_week, 4, "mon - 12 => thu"; +is fri->subtract_businessdays(days => 2)->day_of_week, 3, "fri - 2 => wed"; +is tue->subtract_businessdays(days => 9)->day_of_week, 3, "tue - 9 => wed"; +is tue->subtract_businessdays(days => 8)->day_of_week, 4, "tue - 8 => thu"; + +# same with 6day week +is mon->subtract_businessdays(businessweek => 6, days => 5)->day_of_week, 2, "mon - 5 => tue (6dw)"; +is mon->subtract_businessdays(businessweek => 6, days => 12)->day_of_week, 1, "mon - 12 => mon (6dw)"; +is fri->subtract_businessdays(businessweek => 6, days => 4)->day_of_week, 1, "fri - 4 => mon (6dw)"; +is tue->subtract_businessdays(businessweek => 6, days => 9)->day_of_week, 5, "tue - 9 => fri (6dw)"; +is tue->subtract_businessdays(businessweek => 6, days => 8)->day_of_week, 6, "tue - 8 => sat (6dw)"; + +# absolute dates + +is mon->subtract_businessdays(days => 5), mon->add(days => -7), "mon - 5 => mon (date)"; +is mon->subtract_businessdays(days => 12), mon->add(days => -18), "mon - 12 => thu (date)"; +is fri->subtract_businessdays(days => 2), fri->add(days => -2), "fri - 2 => wed (date)"; +is tue->subtract_businessdays(days => 9), tue->add(days => -13), "tue - 9 => wed (date)"; +is tue->subtract_businessdays(days => 8), tue->add(days => -12), "tue - 8 => thu (date)"; + +# same with 6day week +is mon->subtract_businessdays(businessweek => 6, days => 5), mon->add(days => -6), "mon - 5 => tue (date) (6dw)"; +is mon->subtract_businessdays(businessweek => 6, days => 12), mon->add(days => -14), "mon - 12 => mon (date) (6dw)"; +is fri->subtract_businessdays(businessweek => 6, days => 4), fri->add(days => -4), "fri - 4 => mon (date) (6dw)"; +is tue->subtract_businessdays(businessweek => 6, days => 9), tue->add(days => -11), "tue - 9 => fri (date) (6dw)"; +is tue->subtract_businessdays(businessweek => 6, days => 8), tue->add(days => -10), "tue - 8 => sat (date) (6dw)"; + +# add with negative days? +is mon->add_businessdays(businessweek => 6, days => -5), mon->add(days => -6), "mon - 5 => tue (date) (6dw)"; +is mon->add_businessdays(businessweek => 6, days => -12), mon->add(days => -14), "mon - 12 => mon (date) (6dw)"; +is fri->add_businessdays(businessweek => 6, days => -4), fri->add(days => -4), "fri - 4 => mon (date) (6dw)"; +is tue->add_businessdays(businessweek => 6, days => -9), tue->add(days => -11), "tue - 9 => fri (date) (6dw)"; +is tue->add_businessdays(businessweek => 6, days => -8), tue->add(days => -10), "tue - 8 => sat (date) (6dw)"; + +# what if staring date falls into eekend? +is sun->add_businessdays(days => 1), sun->add(days => 1), "1 day after sun is mon"; +is sat->add_businessdays(days => 1), sat->add(days => 2), "1 day after sut is mon"; +is sun->add_businessdays(days => -1), sun->add(days => -2), "1 day before sun is fri"; +is sat->add_businessdays(days => -1), sat->add(days => -1), "1 day before sut is fri"; diff --git a/templates/webpages/admin/edit_user.html b/templates/webpages/admin/edit_user.html index 94db96b12..3a8f12959 100644 --- a/templates/webpages/admin/edit_user.html +++ b/templates/webpages/admin/edit_user.html @@ -99,6 +99,20 @@ +

[%- LxERP.t8("CTI settings") %]

+ + + + + + + + + + + +
[% LxERP.t8("Phone extension") %][% L.input_tag("user.config_values.phone_extension", props.phone_extension) %]
[% LxERP.t8("Phone password") %][% L.input_tag("user.config_values.phone_password", props.phone_password) %]
+

[%- LxERP.t8("Access to clients") %]

[% IF SELF.all_clients.size %] diff --git a/templates/webpages/ar/search.html b/templates/webpages/ar/search.html index 467d4b290..b095f08c3 100644 --- a/templates/webpages/ar/search.html +++ b/templates/webpages/ar/search.html @@ -188,7 +188,7 @@ [% 'Subtotal' | $T8 %] [% 'Document Project Number' | $T8 %] - + [% 'Transaction description' | $T8 %] diff --git a/templates/webpages/background_job_history/_filter.html b/templates/webpages/background_job_history/_filter.html index f7e49fdd7..862acbcd8 100644 --- a/templates/webpages/background_job_history/_filter.html +++ b/templates/webpages/background_job_history/_filter.html @@ -22,7 +22,7 @@ [% LxERP.t8('Status') %] - [% L.select_tag('filter.status:eq_ignore_empty', [ [ '', '' ], [ 'failed', LxERP.t8('failed') ], [ 'success', LxERP.t8('succeeded') ] ], default=filter.status_eq_ignore_empty) %] + [% L.select_tag('filter.status:eq_ignore_empty', [ [ '', '' ], [ 'failure', LxERP.t8('failed') ], [ 'success', LxERP.t8('succeeded') ] ], default=filter.status_eq_ignore_empty) %] [% LxERP.t8('Run at') %] [% LxERP.t8('From Date') %] diff --git a/templates/webpages/client_config/_features.html b/templates/webpages/client_config/_features.html index af504a880..416eb815f 100644 --- a/templates/webpages/client_config/_features.html +++ b/templates/webpages/client_config/_features.html @@ -57,6 +57,44 @@ + [% LxERP.t8("Purchasing & Sales") %] + + + [% LxERP.t8('Require a transaction description in purchase and sales records') %] + [% L.yes_no_tag('defaults.require_transaction_description_ps', SELF.defaults.require_transaction_description_ps) %] + [% LxERP.t8('If enabled purchase and sales records cannot be saved if no transaction description has been entered.') %] + + + + [% LxERP.t8("Only list customer's projects in sales records") %] + [% L.yes_no_tag("defaults.customer_projects_only_in_sales", SELF.defaults.customer_projects_only_in_sales) %] + [% LxERP.t8("If enabled only those projects that are assigned to the currently selected customer are offered for selection in sales records.") %] + + + + [% LxERP.t8('Allow conversion from sales quotations to sales invoices') %] + [% L.yes_no_tag('defaults.allow_sales_invoice_from_sales_quotation', SELF.defaults.allow_sales_invoice_from_sales_quotation) %] + [% LxERP.t8('If disabled sales quotations cannot be converted into sales invoices directly.') %] + + + + [% LxERP.t8('Allow conversion from sales orders to sales invoices') %] + [% L.yes_no_tag('defaults.allow_sales_invoice_from_sales_order', SELF.defaults.allow_sales_invoice_from_sales_order) %] + [% LxERP.t8('If disabled sales orders cannot be converted into sales invoices directly.') %] + + + + [% LxERP.t8('Allow direct creation of new purchase delivery orders') %] + [% L.yes_no_tag('defaults.allow_new_purchase_delivery_order', SELF.defaults.allow_new_purchase_delivery_order) %] + [% LxERP.t8('If disabled purchase delivery orders can only be created by conversion from existing requests for quotations and purchase orders.') %] + + + + [% LxERP.t8('Allow direct creation of new purchase invoices') %] + [% L.yes_no_tag('defaults.allow_new_purchase_invoice', SELF.defaults.allow_new_purchase_invoice) %] + [% LxERP.t8('If disabled purchase invoices can only be created by conversion from existing requests for quotations, purchase orders and purchase delivery orders.') %] + + [% LxERP.t8("Requirement Specs") %] diff --git a/templates/webpages/ct/search.html b/templates/webpages/ct/search.html index 2a7748c58..2d237264a 100644 --- a/templates/webpages/ct/search.html +++ b/templates/webpages/ct/search.html @@ -159,6 +159,10 @@ + + + + [% IF IS_CUSTOMER %] diff --git a/templates/webpages/cti/calling.html b/templates/webpages/cti/calling.html new file mode 100644 index 000000000..60bbcd729 --- /dev/null +++ b/templates/webpages/cti/calling.html @@ -0,0 +1,4 @@ + +[% PROCESS 'common/flash.html' %] + + diff --git a/templates/webpages/cti/list_internal_extensions.html b/templates/webpages/cti/list_internal_extensions.html new file mode 100644 index 000000000..1a5dfafd4 --- /dev/null +++ b/templates/webpages/cti/list_internal_extensions.html @@ -0,0 +1,25 @@ +[%- USE HTML %][%- USE LxERP -%] + + +

[% HTML.escape(title) %]

+ +[% IF !SELF.internal_extensions.size %] +

[% LxERP.t8("No internal phone extensions have been configured yet.") %]

+ +[% ELSE %] + + + + + + + [%- FOREACH extension = SELF.internal_extensions %] + + + + + [%- END %] +
[% LxERP.t8("Name") %][% LxERP.t8("Phone extension") %]
[% HTML.escape(extension.name) %][% HTML.escape(extension.phone_extension) %]
+[% END %] + + diff --git a/templates/webpages/customer_vendor/form.html b/templates/webpages/customer_vendor/form.html index ad2da05ab..e56b3e584 100644 --- a/templates/webpages/customer_vendor/form.html +++ b/templates/webpages/customer_vendor/form.html @@ -2,6 +2,8 @@ [%- USE LxERP %] [%- USE L %] +[% L.hidden_tag('_cti_enabled', !!LXCONFIG.cti.dial_command) %] + [% cv_cvars = SELF.cv.cvars_by_config %]
diff --git a/templates/webpages/customer_vendor/tabs/bank.html b/templates/webpages/customer_vendor/tabs/bank.html index 1f08af4b5..99608bbc6 100644 --- a/templates/webpages/customer_vendor/tabs/bank.html +++ b/templates/webpages/customer_vendor/tabs/bank.html @@ -29,13 +29,13 @@ [% 'IBAN' | $T8 %] - [% L.input_tag('cv.iban', SELF.cv.iban, size = 34, maxlength = 100) %] + [% L.input_tag('cv.iban', SELF.cv.iban, size = 34) %] [% 'BIC' | $T8 %] - [% L.input_tag('cv.bic', SELF.cv.bic, size = 20, maxlength = 100) %] + [% L.input_tag('cv.bic', SELF.cv.bic, size = 20) %] [% 'Bank' | $T8 %] @@ -49,14 +49,14 @@ [% 'Account Number' | $T8 %] - [% L.input_tag('cv.account_number', SELF.cv.account_number, size = 20, maxlength = 100) %] + [% L.input_tag('cv.account_number', SELF.cv.account_number, size = 20) %] [% 'Bank Code Number' | $T8 %] - [% L.input_tag('cv.bank_code', SELF.cv.bank_code, size = 20, maxlength = 100) %] + [% L.input_tag('cv.bank_code', SELF.cv.bank_code, size = 20) %] diff --git a/templates/webpages/customer_vendor/tabs/billing.html b/templates/webpages/customer_vendor/tabs/billing.html index d67740a9f..82ff5f2b4 100644 --- a/templates/webpages/customer_vendor/tabs/billing.html +++ b/templates/webpages/customer_vendor/tabs/billing.html @@ -2,6 +2,7 @@ [%- USE HTML %] [%- USE LxERP %] [%- USE L %] +[%- USE JavaScript -%]
@@ -76,8 +77,8 @@ @@ -85,10 +86,10 @@
[% 'Department' | $T8 %] - [% L.input_tag('cv.department_1', SELF.cv.department_1, size = 16, maxlength = 75) %] - [% L.input_tag('cv.department_2', SELF.cv.department_2, size = 16, maxlength = 75) %] + [% L.input_tag('cv.department_1', SELF.cv.department_1, size = 16) %] + [% L.input_tag('cv.department_2', SELF.cv.department_2, size = 16) %]
[% 'Street' | $T8 %] - [% L.input_tag('cv.street', SELF.cv.street, size = 35, maxlength = 75) %] + [% L.input_tag('cv.street', SELF.cv.street, size = 35) %] diff --git a/templates/webpages/do/form_header.html b/templates/webpages/do/form_header.html index 2961b5c68..99c8ebede 100644 --- a/templates/webpages/do/form_header.html +++ b/templates/webpages/do/form_header.html @@ -11,6 +11,7 @@ +