Merge branch 'master' of github.com:kivitendo/kivitendo-erp
authorJan Büren <jan@kivitendo-premium.de>
Fri, 27 Jun 2014 11:48:32 +0000 (13:48 +0200)
committerJan Büren <jan@kivitendo-premium.de>
Fri, 27 Jun 2014 11:48:32 +0000 (13:48 +0200)
118 files changed:
SL/BackgroundJob/CreatePeriodicInvoices.pm
SL/CT.pm
SL/CTI.pm [new file with mode: 0644]
SL/Controller/BackgroundJobHistory.pm
SL/Controller/Base.pm
SL/Controller/CTI.pm [new file with mode: 0644]
SL/Controller/CustomerVendor.pm
SL/Controller/LiquidityProjection.pm [new file with mode: 0644]
SL/Controller/LoginScreen.pm
SL/Controller/Part.pm
SL/DB/Chart.pm
SL/DB/CustomVariable.pm
SL/DB/CustomVariableConfig.pm
SL/DB/Helper/CustomVariables.pm
SL/DB/Helper/LinkedRecords.pm
SL/DB/Helper/PriceTaxCalculator.pm
SL/DB/Invoice.pm
SL/DB/Manager/Chart.pm
SL/DB/Manager/Project.pm
SL/DB/MetaSetup/Contact.pm
SL/DB/MetaSetup/Customer.pm
SL/DB/MetaSetup/Default.pm
SL/DB/MetaSetup/FollowUp.pm
SL/DB/MetaSetup/FollowUpAccess.pm
SL/DB/MetaSetup/Order.pm
SL/DB/MetaSetup/Shipto.pm
SL/DB/MetaSetup/Vendor.pm
SL/DB/Object.pm
SL/DB/Part.pm
SL/DB/Unit.pm
SL/DBUpgrade2/Base.pm
SL/DO.pm
SL/Dispatcher.pm
SL/Dispatcher/AuthHandler/User.pm
SL/Form.pm
SL/Helper/CreatePDF.pm
SL/Helper/DateTime.pm
SL/IC.pm
SL/IR.pm
SL/IS.pm
SL/LXDebug.pm
SL/LiquidityProjection.pm [new file with mode: 0644]
SL/Locale.pm
SL/Menu.pm
SL/MoreCommon.pm
SL/OE.pm
SL/RP.pm
SL/ReportGenerator.pm
SL/Request.pm
SL/USTVA.pm
SL/User.pm
bin/mozilla/ct.pl
bin/mozilla/do.pl
bin/mozilla/io.pl
bin/mozilla/ir.pl
bin/mozilla/is.pl
bin/mozilla/oe.pl
bin/mozilla/rp.pl
config/kivitendo.conf.default
css/kivitendo/main.css
css/lx-office-erp/main.css
image/icons/16x16/Program--Internal Phone List.png [new symlink]
image/icons/16x16/phone.png [new file with mode: 0644]
js/autocomplete_part.js
js/edit_periodic_invoices_config.js
js/kivi.CustomerVendor.js
js/kivi.SalesPurchase.js
js/locale/de.js
locale/de/all
locale/de/special_chars
menus/erp.ini
modules/override/Term/ReadLine/Perl/Bind.pm [deleted file]
scripts/console
scripts/locales.pl
scripts/rose_auto_create_model.pl
scripts/task_server.pl
sql/Pg-upgrade2/column_type_text_instead_of_varchar.sql [new file with mode: 0644]
sql/Pg-upgrade2/column_type_text_instead_of_varchar2.sql [new file with mode: 0644]
sql/Pg-upgrade2/column_type_text_instead_of_varchar3.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_only_customer_projects_in_sales.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_require_transaction_description.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_sales_process_limitations.sql [new file with mode: 0644]
sql/Pg-upgrade2/delete_cvars_on_trans_deletion.sql [new file with mode: 0644]
sql/Pg-upgrade2/sales_quotation_order_probability_expected_billing_date.sql [new file with mode: 0644]
t/cti/call_link.t [new file with mode: 0644]
t/cti/sanitize_number.t [new file with mode: 0644]
t/db_helper/price_tax_calculator.t
t/helper/csv.t
t/helper/datetime.t [new file with mode: 0644]
templates/webpages/admin/edit_user.html
templates/webpages/ar/search.html
templates/webpages/background_job_history/_filter.html
templates/webpages/client_config/_features.html
templates/webpages/ct/search.html
templates/webpages/cti/calling.html [new file with mode: 0644]
templates/webpages/cti/list_internal_extensions.html [new file with mode: 0644]
templates/webpages/customer_vendor/form.html
templates/webpages/customer_vendor/tabs/bank.html
templates/webpages/customer_vendor/tabs/billing.html
templates/webpages/customer_vendor/tabs/contacts.html
templates/webpages/customer_vendor/tabs/shipto.html
templates/webpages/do/form_footer.html
templates/webpages/do/form_header.html
templates/webpages/do/search.html
templates/webpages/generic/edit_email.html
templates/webpages/ic/form_header.html
templates/webpages/io/ship_to.html
templates/webpages/ir/form_header.html
templates/webpages/is/form_footer.html
templates/webpages/is/form_header.html
templates/webpages/liquidity_projection/_filter.html [new file with mode: 0644]
templates/webpages/liquidity_projection/_result.html [new file with mode: 0644]
templates/webpages/liquidity_projection/show.html [new file with mode: 0644]
templates/webpages/oe/form_footer.html
templates/webpages/oe/form_header.html
templates/webpages/oe/search.html
templates/webpages/report_generator/html_report.html
templates/webpages/rp/report.html

index dc740cc..18abf7c 100644 (file)
@@ -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 );
 
index fa1f914..b430272 100644 (file)
--- 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 (file)
index 0000000..8605ff1
--- /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;
index 2f7fefb..6572e23 100644 (file)
@@ -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'};
index 4b27cd9..a233a0e 100644 (file)
@@ -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<name> -- the name presented to the browser; defaults to
 C<$file_name>; mandatory if C<$file_name_or_content> is a reference
 
+=item * C<unlink> -- 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<url_for $url>
diff --git a/SL/Controller/CTI.pm b/SL/Controller/CTI.pm
new file mode 100644 (file)
index 0000000..24e3a84
--- /dev/null
@@ -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;
index f59fd07..af109e9 100644 (file)
@@ -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 (file)
index 0000000..24413d7
--- /dev/null
@@ -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;
index adf8b47..d38736b 100644 (file)
@@ -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]} || {} };
index e64a774..d5a7f2a 100644 (file)
@@ -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
 
index 718d422..40ac530 100644 (file)
@@ -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;
index 3f894cd..7e6b500 100644 (file)
@@ -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;
index 203f83a..2cc4e6a 100644 (file)
@@ -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;
index 737c163..7136855 100644 (file)
@@ -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;
 }
 
index cca9fe3..182f3bd 100644 (file)
@@ -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;
 
index d95d143..ed485c1 100644 (file)
@@ -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)));
     }
   }
 
index 0e96166..502654b 100644 (file)
@@ -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__
index a0167fd..f7728ae 100644 (file)
@@ -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__
index 8fcc3bd..56a7e0b 100644 (file)
@@ -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);
   },
 );
index 8a749bb..7f82175 100644 (file)
@@ -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' },
index ea5e328..b802119 100644 (file)
@@ -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' ]);
index 5242cce..2015b90 100644 (file)
@@ -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' ]);
index bda14ec..a6702b6 100644 (file)
@@ -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 => {
index 43d5386..c2fccb1 100644 (file)
@@ -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' },
   },
index 7f93573..92bb92f 100644 (file)
@@ -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' },
index bb3a9bc..b0a9f99 100644 (file)
@@ -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' },
 );
 
index ef3f37f..8367137 100644 (file)
@@ -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' ]);
index c11fb3f..81ef978 100755 (executable)
@@ -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<load_cached @ids>
+
+Loads objects from the database which haven't been cached before and
+caches them for the duration of the current request (see
+L<SL::Request/cache>).
+
+This method can be called both as an instance method and a class
+method. It loads objects for the corresponding class (e.g. both
+C<SL::DB::Part-E<gt>load_cached(…)> and
+C<$some_part-E<gt>load_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<invalidate_cached @ids>
+
+Deletes all cached instances of this class (see L</load_cached>) 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
index ae2612b..f5a0cd2 100644 (file)
@@ -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};
index b133d8f..e8d260c 100644 (file)
@@ -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 {
index 100652e..34afeae 100644 (file)
@@ -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;
 
index 1da5a8e..0960ad7 100644 (file)
--- 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");
   }
index 854b863..7ce18a4 100644 (file)
@@ -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};
index 7b2a5eb..7a8fd1d 100644 (file)
@@ -29,6 +29,7 @@ sub handle {
 
   $::auth->create_or_refresh_session;
   $::auth->delete_session_value('FLASH');
+  $::instance_conf->reload->data;
 
   return 1;
 }
index ed31ddf..7030454 100644 (file)
@@ -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|<pre>$info</pre>|;
     ::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} : '';
index 8d36eb7..555e75d 100644 (file)
@@ -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',
index 94c311f..c79dad1 100644 (file)
@@ -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__
index 191a2d3..163f590 100644 (file)
--- 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();
 }
 
index 301fe3b..931df6b 100644 (file)
--- 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 =
index 64afc53..63420a5 100644 (file)
--- 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 =
index 80b41f9..016015f 100644 (file)
@@ -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 (file)
index 0000000..03a1894
--- /dev/null
@@ -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         = <<SQL;
+    SELECT pi.config_id, to_char(pi.period_start_date, 'YYYY-MM') AS period_start_date
+    FROM periodic_invoices pi
+    LEFT JOIN periodic_invoices_configs pcfg ON (pi.config_id = pcfg.id)
+    WHERE pcfg.active
+      AND (pi.period_start_date >= 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 = <<SQL;
+    SELECT (oi.qty * (1 - oi.discount) * oi.sellprice) AS linetotal,
+      bg.description AS buchungsgruppe,
+      CASE WHEN COALESCE(e.name, '') = '' THEN e.login ELSE e.name END AS salesman,
+      pcfg.periodicity, pcfg.id AS config_id,
+      EXTRACT(year FROM pcfg.start_date) AS start_year, EXTRACT(month FROM pcfg.start_date) AS start_month
+    FROM orderitems oi
+    LEFT JOIN oe                             ON (oi.trans_id                              = oe.id)
+    LEFT JOIN periodic_invoices_configs pcfg ON (oi.trans_id                              = pcfg.oe_id)
+    LEFT JOIN parts p                        ON (oi.parts_id                              = p.id)
+    LEFT JOIN buchungsgruppen bg             ON (p.buchungsgruppen_id                     = bg.id)
+    LEFT JOIN employee e                     ON (COALESCE(oe.salesman_id, oe.employee_id) = e.id)
+    WHERE pcfg.active
+SQL
+
+  # 3. Iterieren über Saldierungsintervalle, vormerken
+  my %periodicities = ( 'm' => 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 = <<SQL;
+    SELECT (oi.qty * (1 - oi.discount) * oi.sellprice) AS linetotal,
+      bg.description AS buchungsgruppe,
+      CASE WHEN COALESCE(e.name, '') = '' THEN e.login ELSE e.name END AS salesman,
+      oe.ordnumber, EXTRACT(month FROM oe.reqdate) AS month, EXTRACT(year  FROM oe.reqdate) AS year
+    FROM orderitems oi
+    LEFT JOIN oe                 ON (oi.trans_id                              = oe.id)
+    LEFT JOIN parts p            ON (oi.parts_id                              = p.id)
+    LEFT JOIN buchungsgruppen bg ON (p.buchungsgruppen_id                     = bg.id)
+    LEFT JOIN employee e         ON (COALESCE(oe.salesman_id, oe.employee_id) = e.id)
+    WHERE (oe.customer_id IS NOT NULL)
+      AND NOT COALESCE(oe.quotation, FALSE)
+      AND NOT COALESCE(oe.closed,    FALSE)
+      AND (oe.id NOT IN (SELECT oe_id FROM periodic_invoices_configs))
+SQL
+
+  # 5. Initialisierung der Datenstrukturen zum Speichern der
+  # Ergebnisse
+  my @entries               = selectall_hashref_query($::form, $dbh, $query);
+  my @salesmen              = uniq map { $_->{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         = <<SQL;
+      SELECT (i.qty * (1 - i.discount) * i.sellprice) AS linetotal,
+        bg.description AS buchungsgruppe,
+        ar.ordnumber
+      FROM invoice i
+      LEFT JOIN ar                 ON (i.trans_id           = ar.id)
+      LEFT JOIN parts p            ON (i.parts_id           = p.id)
+      LEFT JOIN buchungsgruppen bg ON (p.buchungsgruppen_id = bg.id)
+      WHERE (ar.ordnumber IN ($ordnumbers))
+SQL
+
+    @entries = selectall_hashref_query($::form, $dbh, $query);
+
+    # 9. Abziehen der abgerechneten Positionen
+    foreach $ref (@entries) {
+      my $date           = $dates_by_ordnumber{    $ref->{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;
index 23a399e..4cbef16 100644 (file)
@@ -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);
 
index 06ea6ee..19b2d2d 100644 (file)
@@ -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;
 
index 9f4fae6..b1ea817 100644 (file)
@@ -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;
 
index 326e89c..8b073b9 100644 (file)
--- 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();
 }
 
index 969e8ad..3d73026 100644 (file)
--- 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
  |;
index 09ab7bd..6fc50e9 100644 (file)
@@ -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],
           };
         }
 
index 26be5f7..47f163c 100644 (file)
@@ -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<SL::Layout::Base>. Defaults to an isntance of L<SL::Layout::None>.
 
 For more information about layouts, see L<SL::Layout::Dispatcher>.
 
+=item C<cache $topic[, $default ]>
+
+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<SL::StuffedStuff> calls with topic = C<get_stuff> 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
index bf9ebbd..6daa013 100644 (file)
@@ -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') {
index 8bb9b99..42622f8 100644 (file)
@@ -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});
index 845fc3a..b5b83c1 100644 (file)
@@ -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);
   }
 
index 67f2740..c16f772 100644 (file)
@@ -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}) {
index 1cfc0ca..4cd8822 100644 (file)
@@ -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();
index e90b12c..951bce5 100644 (file)
@@ -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');
index 3795448..b6c6298 100644 (file)
@@ -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');
index d6d8ff4..9a8aea4 100644 (file)
@@ -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|<tr><td colspan=%d align=center>%s</td></tr>\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;
index 860d38f..c2ca693 100644 (file)
@@ -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);
 
index e6849a5..25a8d4c 100644 (file)
@@ -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
index 3b85993..6fa432d 100644 (file)
@@ -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;
+}
index f42c872..ab71226 100644 (file)
@@ -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 (symlink)
index 0000000..282275d
--- /dev/null
@@ -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 (file)
index 0000000..f189191
Binary files /dev/null and b/image/icons/16x16/phone.png differ
index d0984eb..d9bf8db 100644 (file)
@@ -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 },
index 44ebd19..b969e1d 100644 (file)
@@ -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?'));
-}
index 59c0735..1cb08db 100644 (file)
@@ -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 = $('<a href="" id="' + action_id + '" class="cti_call_action" target="_blank" tabindex="-1"></a>');
+      $input.wrap('<span nobr></span>').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));
+  });
+}
index a390c87..f6c1633 100644 (file)
@@ -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);
+  };
 });
index 3d58e16..3b7e865 100644 (file)
@@ -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",
index 56d5255..956d1b2 100755 (executable)
@@ -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&ouml;nnen auch einen neuen Artikel anlegen, der dann automatisch ausgew&auml;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&uuml;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 &uuml;ber Mahngeb&uuml;hren und Zinsen f&uuml;r ein Mahnlevel aktiviert ist, so werden die folgenden Konten f&uuml;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&auml;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&auml;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&auml;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&auml;ndert werden.',
   'Partnumber not unique!'      => 'Artikelnummer bereits vorhanden!',
@@ -1672,7 +1702,7 @@ $self->{texts} = {
   'Parts must have an entry type.' => 'Waren m&uuml;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&ouml;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',
index 49fbf73..c349536 100644 (file)
@@ -22,7 +22,7 @@ order=< > \n
 \n=<br>
 
 [Template/LaTeX]
-order=\\ <pagebreak> & \n \r " $ <bullet> % _ # ^ { } < > £ ± ² ³ ° § ® © \xad ➔ → ←
+order=\\ <pagebreak> & \n \r " $ <bullet> % _ # ^ { } < > £ ± ² ³ ° § ® © \xad \xa0 ➔ → ←
 \\=\\textbackslash\s
 <pagebreak>=
 "=''
@@ -51,6 +51,7 @@ _=\\_
 ➔=$\\rightarrow$
 →=$\\rightarrow$
 ←=$\\leftarrow$
+\xa0=~
 
 [Template/OpenDocument]
 order=& < > " ' \x80 \n \r
index dd43bdf..3297621 100644 (file)
@@ -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 (file)
index 2587f6d..0000000
+++ /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';
-
index 8ce9783..4ceb9a0 100755 (executable)
@@ -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__
index cd5fe41..788c37d 100755 (executable)
@@ -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);
index fb589c6..8106576 100755 (executable)
@@ -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;
index cb332ec..f3422d6 100755 (executable)
@@ -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 (file)
index 0000000..b3a2cea
--- /dev/null
@@ -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 (file)
index 0000000..20c3589
--- /dev/null
@@ -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 (file)
index 0000000..4504dc9
--- /dev/null
@@ -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 (file)
index 0000000..feeb72c
--- /dev/null
@@ -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 (file)
index 0000000..250175a
--- /dev/null
@@ -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 (file)
index 0000000..f62cb71
--- /dev/null
@@ -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 (file)
index 0000000..3e00660
--- /dev/null
@@ -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 (file)
index 0000000..5c37168
--- /dev/null
@@ -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 (file)
index 0000000..ce76239
--- /dev/null
@@ -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 (file)
index 0000000..639e716
--- /dev/null
@@ -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';
index e8ee4ac..a80146d 100644 (file)
@@ -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();
index 3a8556c..bab3825 100644 (file)
@@ -552,7 +552,7 @@ ok !$csv->_check_multiplexed, 'multiplex check detects empty header';
 #####
 
 $csv = SL::Helper::Csv->new(
-  file   => \<<EOL,
+  file   => \ Encode::encode('utf-8', <<EOL),
 description;longdescription;datatype
 name;customernumber;datatype
 Kaffee;"lecker Kaffee";P
@@ -580,7 +580,7 @@ is_deeply $csv->get_data, [ { datatype => 'P', description => 'Kaffee', longdesc
 #####
 
 $csv = SL::Helper::Csv->new(
-  file   => \<<EOL,
+  file   => \ Encode::encode('utf-8', <<EOL),
 datatype;description;longdescription
 name;datatype;customernumber
 P;Kaffee;"lecker Kaffee"
diff --git a/t/helper/datetime.t b/t/helper/datetime.t
new file mode 100644 (file)
index 0000000..afaa49e
--- /dev/null
@@ -0,0 +1,89 @@
+use Test::More tests => 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";
index 94db96b..3a8f129 100644 (file)
   </tr>
  </table>
 
+ <h2>[%- LxERP.t8("CTI settings") %]</h2>
+
+ <table>
+  <tr>
+   <th align="right">[% LxERP.t8("Phone extension") %]</th>
+   <td>[% L.input_tag("user.config_values.phone_extension", props.phone_extension) %]</td>
+  </tr>
+
+  <tr>
+   <th align="right">[% LxERP.t8("Phone password") %]</th>
+   <td>[% L.input_tag("user.config_values.phone_password", props.phone_password) %]</td>
+  </tr>
+ </table>
+
  <h2>[%- LxERP.t8("Access to clients") %]</h2>
 
 [% IF SELF.all_clients.size %]
index 467d4b2..b095f08 100644 (file)
            <td nowrap>[% 'Subtotal' | $T8 %]</td>
            <td align=right><input name="l_globalprojectnumber" class=checkbox type=checkbox value=Y></td>
            <td nowrap>[% 'Document Project Number' | $T8 %]</td>
-           <td align=right><input name="l_transaction_description" class=checkbox type=checkbox value=Y></td>
+           <td align=right><input name="l_transaction_description" class=checkbox type=checkbox value=Y[% IF INSTANCE_CONF.get_require_transaction_description_ps %] checked[% END %]></td>
            <td nowrap>[% 'Transaction description' | $T8 %]</td>
           </tr>
           <tr>
index f7e49fd..862acbc 100644 (file)
@@ -22,7 +22,7 @@
    </tr>
    <tr>
     <th align="right">[% LxERP.t8('Status') %]</th>
-    <td>[% L.select_tag('filter.status:eq_ignore_empty', [ [ '', '' ], [ 'failed', LxERP.t8('failed') ], [ 'success', LxERP.t8('succeeded') ] ], default=filter.status_eq_ignore_empty) %]</td>
+    <td>[% L.select_tag('filter.status:eq_ignore_empty', [ [ '', '' ], [ 'failure', LxERP.t8('failed') ], [ 'success', LxERP.t8('succeeded') ] ], default=filter.status_eq_ignore_empty) %]</td>
    </tr>
    <tr>
     <th align="right">[% LxERP.t8('Run at') %] [% LxERP.t8('From Date') %]</th>
index af504a8..416eb81 100644 (file)
   </tr>
 </tr>
 
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("Purchasing & Sales") %]</td></tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8('Require a transaction description in purchase and sales records') %]</td>
+   <td>[% L.yes_no_tag('defaults.require_transaction_description_ps', SELF.defaults.require_transaction_description_ps) %]</td>
+   <td>[% LxERP.t8('If enabled purchase and sales records cannot be saved if no transaction description has been entered.') %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("Only list customer's projects in sales records") %]</td>
+   <td>[% L.yes_no_tag("defaults.customer_projects_only_in_sales", SELF.defaults.customer_projects_only_in_sales) %]</td>
+   <td>[% LxERP.t8("If enabled only those projects that are assigned to the currently selected customer are offered for selection in sales records.") %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8('Allow conversion from sales quotations to sales invoices') %]</td>
+   <td>[% L.yes_no_tag('defaults.allow_sales_invoice_from_sales_quotation', SELF.defaults.allow_sales_invoice_from_sales_quotation) %]</td>
+   <td>[% LxERP.t8('If disabled sales quotations cannot be converted into sales invoices directly.') %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8('Allow conversion from sales orders to sales invoices') %]</td>
+   <td>[% L.yes_no_tag('defaults.allow_sales_invoice_from_sales_order', SELF.defaults.allow_sales_invoice_from_sales_order) %]</td>
+   <td>[% LxERP.t8('If disabled sales orders cannot be converted into sales invoices directly.') %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8('Allow direct creation of new purchase delivery orders') %]</td>
+   <td>[% L.yes_no_tag('defaults.allow_new_purchase_delivery_order', SELF.defaults.allow_new_purchase_delivery_order) %]</td>
+   <td>[% LxERP.t8('If disabled purchase delivery orders can only be created by conversion from existing requests for quotations and purchase orders.') %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8('Allow direct creation of new purchase invoices') %]</td>
+   <td>[% L.yes_no_tag('defaults.allow_new_purchase_invoice', SELF.defaults.allow_new_purchase_invoice) %]</td>
+   <td>[% LxERP.t8('If disabled purchase invoices can only be created by conversion from existing requests for quotations, purchase orders and purchase delivery orders.') %]</td>
+  </tr>
+
   <tr><td class="listheading" colspan="4">[% LxERP.t8("Requirement Specs") %]</td></tr>
 
   <tr>
index 2a7748c..2d23726 100644 (file)
         <input name="l_discount" id="l_discount" type="checkbox" class="checkbox" value="Y">
         <label for="l_discount">[% 'Discount' | $T8 %]</label>
        </td>
+       <td>
+        <input name="l_payment" id="l_payment" type="checkbox" class="checkbox" value="Y">
+        <label for="l_payment">[% 'Payment Terms' | $T8 %]</label>
+       </td>
       [% IF IS_CUSTOMER %]
       <td>
        <input name="l_salesman" id="l_salesman" type="checkbox" class="checkbox" value="Y">
diff --git a/templates/webpages/cti/calling.html b/templates/webpages/cti/calling.html
new file mode 100644 (file)
index 0000000..60bbcd7
--- /dev/null
@@ -0,0 +1,4 @@
+<body>
+[% PROCESS 'common/flash.html' %]
+</body>
+</html>
diff --git a/templates/webpages/cti/list_internal_extensions.html b/templates/webpages/cti/list_internal_extensions.html
new file mode 100644 (file)
index 0000000..1a5dfaf
--- /dev/null
@@ -0,0 +1,25 @@
+[%- USE HTML %][%- USE LxERP -%]
+<body>
+
+<h1>[% HTML.escape(title) %]</h1>
+
+[% IF !SELF.internal_extensions.size %]
+ <p>[% LxERP.t8("No internal phone extensions have been configured yet.") %]</p>
+
+[% ELSE %]
+ <table>
+  <tr class="listheading">
+   <th>[% LxERP.t8("Name") %]</th>
+   <th>[% LxERP.t8("Phone extension") %]</th>
+  </tr>
+
+  [%- FOREACH extension = SELF.internal_extensions %]
+   <tr class="listrow">
+    <td>[% HTML.escape(extension.name) %]</td>
+    <td><a href="[% HTML.escape(extension.call_link) %]" class="cti_call_action">[% HTML.escape(extension.phone_extension) %]</a></td>
+   </tr>
+  [%- END %]
+ </table>
+[% END %]
+</body>
+</html>
index ad2da05..e56b3e5 100644 (file)
@@ -2,6 +2,8 @@
 [%- USE LxERP %]
 [%- USE L %]
 
+[% L.hidden_tag('_cti_enabled', !!LXCONFIG.cti.dial_command) %]
+
 [% cv_cvars = SELF.cv.cvars_by_config %]
 
 <form method="post" action="controller.pl">
index 1f08af4..99608bb 100644 (file)
       <th align="right">[% 'IBAN' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('cv.iban', SELF.cv.iban, size = 34, maxlength = 100) %]
+        [% L.input_tag('cv.iban', SELF.cv.iban, size = 34) %]
       </td>
 
 
       <th align="right">[% 'BIC' | $T8 %]</th>
       <td>
-        [% L.input_tag('cv.bic', SELF.cv.bic, size = 20, maxlength = 100) %]
+        [% L.input_tag('cv.bic', SELF.cv.bic, size = 20) %]
       </td>
 
       <th align="right">[% 'Bank' | $T8 %]</th>
       <th align="right">[% 'Account Number' | $T8 %]</th>
 
       <td>
-        [% 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) %]
       </td>
 
 
       <th align="right">[% 'Bank Code Number' | $T8 %]</th>
 
       <td>
-        [% 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) %]
       </td>
     </tr>
   </table>
index d67740a..82ff5f2 100644 (file)
@@ -2,6 +2,7 @@
 [%- USE HTML %]
 [%- USE LxERP %]
 [%- USE L %]
+[%- USE JavaScript -%]
 
 <div id="billing">
   <table width="100%">
@@ -76,8 +77,8 @@
       <th align="right" nowrap>[% 'Department' | $T8 %]</th>
 
       <td>
-        [% 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) %]
       </td>
     </tr>
 
       <th align="right" nowrap>[% 'Street' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('cv.street', SELF.cv.street, size = 35, maxlength = 75) %]
+        [% L.input_tag('cv.street', SELF.cv.street, size = 35) %]
         <span id="billing_map"></span>
         <script type="text/javascript">
-          billingMapWidget = new kivi.CustomerVendor.MapWidget('cv_');
+          billingMapWidget = new kivi.CustomerVendor.MapWidget('cv_', '[% JavaScript.escape(SELF.home_address_for_google_maps) %]');
           $(function() {
             billingMapWidget.render($('#billing_map'));
           });
       <th align="right" nowrap>[% 'Zipcode' | $T8 %]/[% 'City' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('cv.zipcode', SELF.cv.zipcode, size = 5 maxlength = 10) %]
-        [% L.input_tag('cv.city', SELF.cv.city, size = 30 maxlength = 75) %]
+        [% L.input_tag('cv.zipcode', SELF.cv.zipcode, size = 5) %]
+        [% L.input_tag('cv.city', SELF.cv.city, size = 30) %]
       </td>
     </tr>
 
       <th align="right" nowrap>[% 'Country' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('cv.country', SELF.cv.country, size = 30 maxlength = 75) %]
+        [% L.input_tag('cv.country', SELF.cv.country, size = 30) %]
       </td>
     </tr>
 
       <th align="right" nowrap>[% 'Contact' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('cv.contact', SELF.cv.contact, size = 28 maxlength = 75) %]
+        [% L.input_tag('cv.contact', SELF.cv.contact, size = 28) %]
       </td>
     </tr>
 
       <th align="right" nowrap>[% 'Fax' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('cv.fax', SELF.cv.fax, size = 30 maxlength = 30) %]
+        [% L.input_tag('cv.fax', SELF.cv.fax, size = 30) %]
       </td>
     </tr>
 
       <th align="right">[% 'sales tax identification number' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('cv.ustid', SELF.cv.ustid, maxlength = 14 size = 20 ) %]
+        [% L.input_tag('cv.ustid', SELF.cv.ustid, size = 20 ) %]
       </td>
 
 
index aca56cd..af4b942 100644 (file)
@@ -18,7 +18,7 @@
             empty_title = LxERP.t8('New contact'),
             value_key = 'cp_id',
             title_key = 'full_name',
-            onchange = "kivi.CustomerVendor.selectContact({onFormSet: function(){contactsMapWidget.testInputs();}});",
+            onchange = "kivi.CustomerVendor.selectContact({onFormSet: function(){ contactsMapWidget.testInputs(); local_reinit_widgets(); }});",
           )
         %]
       </td>
@@ -42,7 +42,7 @@
       <th align="left" nowrap>[% 'Title' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('contact.cp_title', SELF.contact.cp_title, size = 40 maxlength = 75) %]
+        [% L.input_tag('contact.cp_title', SELF.contact.cp_title, size = 40) %]
         [% L.select_tag('contact_cp_title_select', SELF.all_titles, with_empty = 1, onchange = '$("#contact_cp_title").val(this.value);') %]
       </td>
     </tr>
@@ -60,7 +60,7 @@
       <th align="left" nowrap>[% 'Function/position' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('contact.cp_position', SELF.contact.cp_position, size = 40, maxlength = 75) %]
+        [% L.input_tag('contact.cp_position', SELF.contact.cp_position, size = 40) %]
       </td>
     </tr>
 
@@ -68,7 +68,7 @@
       <th align="left" nowrap>[% 'Given Name' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('contact.cp_givenname', SELF.contact.cp_givenname, size = 40, maxlength = 75) %]
+        [% L.input_tag('contact.cp_givenname', SELF.contact.cp_givenname, size = 40) %]
       </td>
     </tr>
 
@@ -76,7 +76,7 @@
       <th align="left" nowrap>[% 'Name' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('contact.cp_name', SELF.contact.cp_name, size = 40, maxlength = 75) %]
+        [% L.input_tag('contact.cp_name', SELF.contact.cp_name, size = 40) %]
       </td>
     </tr>
 
@@ -92,7 +92,7 @@
       <th align="left" nowrap>[% 'Phone1' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('contact.cp_phone1', SELF.contact.cp_phone1, size = 40, maxlength = 75) %]
+        [% L.input_tag('contact.cp_phone1', SELF.contact.cp_phone1, size = 40) %]
       </td>
     </tr>
 
       <th align="left" nowrap>[% 'Phone2' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('contact.cp_phone2', SELF.contact.cp_phone2, size = 40, maxlength = 75) %]
+        [% L.input_tag('contact.cp_phone2', SELF.contact.cp_phone2, size = 40) %]
       </td>
     </tr>
 
       <th align="left" nowrap>[% 'Street' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('contact.cp_street', SELF.contact.cp_street, size = 40, maxlength = 75) %]
+        [% L.input_tag('contact.cp_street', SELF.contact.cp_street, size = 40) %]
         <span id="contact_map"></span>
         <script type="text/javascript">
           var contactsMapWidget = new kivi.CustomerVendor.MapWidget('contact_cp_');
       <th align="left" nowrap>[% 'Zip, City' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('contact.cp_zipcode', SELF.contact.cp_zipcode, size = 5, maxlength = 10) %]
-        [% L.input_tag('contact.cp_city', SELF.contact.cp_city, size = 25, maxlength = 75) %]
+        [% L.input_tag('contact.cp_zipcode', SELF.contact.cp_zipcode, size = 5) %]
+        [% L.input_tag('contact.cp_city', SELF.contact.cp_city, size = 25) %]
       </td>
     </tr>
 
index 8f49977..97cfa9d 100644 (file)
@@ -16,7 +16,7 @@
              title_key = 'displayable_id',
              with_empty = 1,
              empty_title = LxERP.t8('New shipto'),
-             onchange = "kivi.CustomerVendor.selectShipto({onFormSet: function(){shiptoMapWidget.testInputs();}});",
+             onchange = "kivi.CustomerVendor.selectShipto({onFormSet: function(){ shiptoMapWidget.testInputs(); local_reinit_widgets(); }});",
            )
         %]
       </td>
@@ -26,7 +26,7 @@
       <th align="right" nowrap>[% 'Name' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('shipto.shiptoname', SELF.shipto.shiptoname,  size = 35, maxlength = 75) %]
+        [% L.input_tag('shipto.shiptoname', SELF.shipto.shiptoname,  size = 35) %]
       </td>
     </tr>
 
@@ -34,8 +34,8 @@
       <th align="right" nowrap>[% 'Abteilung' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('shipto.shiptodepartment_1', SELF.shipto.shiptodepartment_1,  size = 16, maxlength = 75) %]
-        [% L.input_tag('shipto.shiptodepartment_2', SELF.shipto.shiptodepartment_2,  size = 16, maxlength = 75) %]
+        [% L.input_tag('shipto.shiptodepartment_1', SELF.shipto.shiptodepartment_1,  size = 16) %]
+        [% L.input_tag('shipto.shiptodepartment_2', SELF.shipto.shiptodepartment_2,  size = 16) %]
       </td>
     </tr>
 
@@ -43,7 +43,7 @@
       <th align="right" nowrap>[% 'Street' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('shipto.shiptostreet', SELF.shipto.shiptostreet,  size = 35, maxlength = 75) %]
+        [% L.input_tag('shipto.shiptostreet', SELF.shipto.shiptostreet,  size = 35) %]
 
         <span id="shipto_map"></span>
         <script type="text/javascript">
@@ -59,8 +59,8 @@
       <th align="right" nowrap>[% 'Zipcode' | $T8 %]/[% 'City' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('shipto.shiptozipcode', SELF.shipto.shiptozipcode,  size = 5, maxlength = 75) %]
-        [% L.input_tag('shipto.shiptocity', SELF.shipto.shiptocity,  size = 30, maxlength = 75) %]
+        [% L.input_tag('shipto.shiptozipcode', SELF.shipto.shiptozipcode,  size = 5) %]
+        [% L.input_tag('shipto.shiptocity', SELF.shipto.shiptocity,  size = 30) %]
       </td>
     </tr>
 
@@ -68,7 +68,7 @@
       <th align="right" nowrap>[% 'Country' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('shipto.shiptocountry', SELF.shipto.shiptocountry,  size = 35, maxlength = 75) %]
+        [% L.input_tag('shipto.shiptocountry', SELF.shipto.shiptocountry,  size = 35) %]
       </td>
     </tr>
 
@@ -76,7 +76,7 @@
       <th align="right" nowrap>[% 'Contact' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('shipto.shiptocontact', SELF.shipto.shiptocontact,  size = 30, maxlength = 75) %]
+        [% L.input_tag('shipto.shiptocontact', SELF.shipto.shiptocontact,  size = 30) %]
       </td>
     </tr>
 
@@ -84,7 +84,7 @@
       <th align="right" nowrap>[% 'Phone' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('shipto.shiptophone', SELF.shipto.shiptophone,  size = 30, maxlength = 30) %]
+        [% L.input_tag('shipto.shiptophone', SELF.shipto.shiptophone,  size = 30) %]
       </td>
     </tr>
 
@@ -92,7 +92,7 @@
       <th align="right" nowrap>[% 'Fax' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('shipto.shiptofax', SELF.shipto.shiptofax,  size = 30, maxlength = 30) %]
+        [% L.input_tag('shipto.shiptofax', SELF.shipto.shiptofax,  size = 30) %]
       </td>
     </tr>
 
index eea9217..e15b440 100644 (file)
    <input class="submit" type="submit" name="action_ship_to" value="[% 'Ship to' | $T8 %]">
    [%- END %]
    [%- END %]
-   <input class="submit" type="submit" name="action_print" value="[% 'Print' | $T8 %]">
-   <input class="submit" type="submit" name="action_e_mail" value="[% 'E-mail' | $T8 %]">
+   <input class="submit" type="submit" name="action_print" value="[% 'Print' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
+   <input class="submit" type="submit" name="action_e_mail" value="[% 'E-mail' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
    [%- UNLESS delivered %]
-   <input class="submit" type="submit" name="action_save" value="[% 'Save' | $T8 %]">
+   <input class="submit" type="submit" name="action_save" value="[% 'Save' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
    [%- IF vc == 'customer' %]
-   <input class="submit" type="submit" name="action_transfer_out" onclick="return check_transfer_qty()" value="[% 'Transfer out' | $T8 %]">
+   <input class="submit" type="submit" name="action_transfer_out" value="[% 'Transfer out' | $T8 %]" data-check-transfer-qty="1" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
    [% IF transfer_default %]
-   <input class="submit" type="submit" name="action_transfer_out_default" value="[% 'Transfer out via default' | $T8 %]">
+   <input class="submit" type="submit" name="action_transfer_out_default" value="[% 'Transfer out via default' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
    [%- END %]
    [%- ELSE %]
-   <input class="submit" type="submit" name="action_transfer_in"  onclick="return check_transfer_qty()" value="[% 'Transfer in' | $T8 %]">
+   <input class="submit" type="submit" name="action_transfer_in" value="[% 'Transfer in' | $T8 %]" data-check-transfer-qty="1" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
    [% IF transfer_default %]
-   <input class="submit" type="submit" name="action_transfer_in_default" value="[% 'Transfer in via default' | $T8 %]">
+   <input class="submit" type="submit" name="action_transfer_in_default" value="[% 'Transfer in via default' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
    [%- END %]
    [%- END %]
    [%- END %]
    [%- IF id %]
      <input type="button" class="submit" onclick="follow_up_window()" value="[% 'Follow-Up' | $T8 %]">
    [%- UNLESS closed %]
-   <input class="submit" type="submit" name="action_mark_closed" value="[% 'Mark closed' | $T8 %]">
+   <input class="submit" type="submit" name="action_mark_closed" value="[% 'Mark closed' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
    [%- END %]
    <input type="button" class="submit" onclick="set_history_window([% id %]);" name="history" id="history" value="[% 'history' | $T8 %]">
    [%- END %]
@@ -96,7 +96,7 @@
   [%- IF id %]
   <p>
    [% 'Workflow Delivery Order' | $T8 %]<br>
-   <input class="submit" type="submit" name="action_save_as_new" value="[% 'Save as new' | $T8 %]">
+   <input class="submit" type="submit" name="action_save_as_new" value="[% 'Save as new' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
    [% UNLESS delivered || (vc == 'customer' && !INSTANCE_CONF.get_sales_delivery_order_show_delete) || (vc == 'vendor' && !INSTANCE_CONF.get_purchase_delivery_order_show_delete) %]
     [% L.submit_tag('action_delete', LxERP.t8('Delete'), confirm=LxERP.t8('Are you sure?')) %]
    [% END %]
 
  </form>
 <script type='text/javascript'>
-  function check_transfer_qty() {
-    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;
-    } else {
-      return confirm("[% '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?' | $T8 %]");
-    }
-  }
+ $(kivi.SalesPurchase.init_on_submit_checks);
 </script>
index 2961b5c..99c8ebe 100644 (file)
@@ -11,6 +11,7 @@
  <script type="text/javascript" src="js/calculate_qty.js"></script>
  <script type="text/javascript" src="js/stock_in_out.js"></script>
  <script type="text/javascript" src="js/follow_up.js"></script>
+ <script type="text/javascript" src="js/kivi.SalesPurchase.js"></script>
 
  <style type="text/css">
   .fixed_width {
@@ -46,7 +47,7 @@
 
  <form method="post" name="do" action="do.pl">
 
- <div class="tabwidget">
+ <div id="do_tabs" class="tabwidget">
   <ul>
    <li><a href="#ui-tabs-basic-data">[% 'Basic Data' | $T8 %]</a></li>
 [%- IF INSTANCE_CONF.get_webdav %]
           [%- END %]
 
           [%- ELSE %]
-            [% L.select_tag('shipto_id', ALL_SHIPTO, default = shipto_id, value_key = 'shipto_id', title_key = 'displayable_id', with_empty = 1, class = 'fixed_width') %]
+            [% shiptos = [ [ "", LxERP.t8("No/individual shipping address") ] ] ;
+               L.select_tag('shipto_id', shiptos.import(ALL_SHIPTO), default=shipto_id, value_key='shipto_id', title_key='displayable_id', style='width: 250px') %]
           [%- END %]
          </td>
         </tr>
 
        <tr>
         <th align="right">[% 'Transaction description' | $T8 %]</th>
-        <td colspan="3"><input name="transaction_description" size="35" value="[% HTML.escape(transaction_description) %]"[% RO %]></td>
+        <td colspan="3"><input name="transaction_description" id="transaction_description" size="35" value="[% HTML.escape(transaction_description) %]"[% RO %]></td>
        </tr>
 
       </table>
index 7483f8e..905215c 100644 (file)
         </td>
 
         <td>
-         <input name="l_transaction_description" id="l_transaction_description" class="checkbox" type="checkbox" value="Y">
+         <input name="l_transaction_description" id="l_transaction_description" class="checkbox" type="checkbox" value="Y"[% IF INSTANCE_CONF.get_require_transaction_description_ps %] checked[% END %]>
          <label for="l_transaction_description">[% 'Transaction description' | $T8 %]</label>
         </td>
        </tr>
index b31fde0..7654e4f 100644 (file)
@@ -1,5 +1,5 @@
 [%- USE T8 %]
-[%- USE HTML %][%- USE L -%]
+[%- USE HTML %][%- USE LxERP -%][%- USE L -%]
 <form name="Form" method="post" action="[% script %]">
 
 <table width="100%">
@@ -44,7 +44,7 @@
           <th align="left" nowrap>[% 'Message' | $T8 %]</th>
         </tr>
         <tr>
-          <td><textarea name="message" rows="15" cols="60" wrap="soft">[% HTML.escape(message) %]</textarea></td>
+          <td><textarea name="message" id="message" rows="15" cols="60" wrap="soft">[% HTML.escape(message) %]</textarea></td>
 
         </tr>
       </table>
 <input type="hidden" name="nextsub" value="send_email">
 
 <br>
-<input name="action" class="submit" type="submit" value="[% 'Continue' | $T8 %]">
+[% L.submit_tag('action', LxERP.t8('Continue'), onclick="return check_prerequisites();") %]
 </form>
+
+<script type="text/javascript">
+<!--
+function check_prerequisites() {
+  if (!$('#email,#subject,#message').filter(function(idx, elt) { return $(elt).val() === ""; }).size())
+    return true;
+
+  alert(kivi.t8('The recipient, subject or body is missing.'));
+  return false;
+}
+-->
+</script>
index 20ff5a9..9ce47f1 100644 (file)
@@ -21,7 +21,7 @@
   <input name="original_partnumber" type="hidden" value="[% HTML.escape(original_partnumber) %]">
   <input name="currow" type="hidden" value="[% HTML.escape(currow) %]">
 
-  <div class="tabwidget">
+  <div id="ic_tabs" class="tabwidget">
    <ul>
     <li><a href="#master_data">[% 'Basic Data' | $T8 %]</a></li>
 [% IF LANGUAGES.size %]
index 10f0ce2..ca5560c 100644 (file)
@@ -1,6 +1,81 @@
-[% USE HTML %][% USE L %][% USE LxERP %]
+[% USE HTML %][% USE L %][% USE LxERP %][%- USE JavaScript -%]
+
+<script type="text/javascript">
+  var addresses = [
+    { shiptoname:         "[% JavaScript.escape(vc_obj.name) %]",
+      shiptodepartment_1: "[% JavaScript.escape(vc_obj.department_1) %]",
+      shiptodepartment_2: "[% JavaScript.escape(vc_obj.department_2) %]",
+      shiptostreet:       "[% JavaScript.escape(vc_obj.street) %]",
+      shiptozipcode:      "[% JavaScript.escape(vc_obj.zipcode) %]",
+      shiptocity:         "[% JavaScript.escape(vc_obj.city) %]",
+      shiptocountry:      "[% JavaScript.escape(vc_obj.country) %]",
+      shiptocontact:      "[% JavaScript.escape(vc_obj.contact) %]",
+      shiptocp_gender:    "[% JavaScript.escape(vc_obj.cp_gender) %]",
+      shiptophone:        "[% JavaScript.escape(vc_obj.phone) %]",
+      shiptofax:          "[% JavaScript.escape(vc_obj.fax) %]",
+      shiptoemail:        "[% JavaScript.escape(vc_obj.email) %]"
+    }
+
+  [% FOREACH shipto = vc_obj.shipto %]
+    ,
+    { shiptoname:         "[% JavaScript.escape(shipto.shiptoname) %]",
+      shiptodepartment_1: "[% JavaScript.escape(shipto.shiptodepartment_1) %]",
+      shiptodepartment_2: "[% JavaScript.escape(shipto.shiptodepartment_2) %]",
+      shiptostreet:       "[% JavaScript.escape(shipto.shiptostreet) %]",
+      shiptozipcode:      "[% JavaScript.escape(shipto.shiptozipcode) %]",
+      shiptocity:         "[% JavaScript.escape(shipto.shiptocity) %]",
+      shiptocountry:      "[% JavaScript.escape(shipto.shiptocountry) %]",
+      shiptocontact:      "[% JavaScript.escape(shipto.shiptocontact) %]",
+      shiptocp_gender:    "[% JavaScript.escape(shipto.shiptocp_gender) %]",
+      shiptophone:        "[% JavaScript.escape(shipto.shiptophone) %]",
+      shiptofax:          "[% JavaScript.escape(shipto.shiptofax) %]",
+      shiptoemail:        "[% JavaScript.escape(shipto.shiptoemail) %]"
+    }
+  [% END %]
+  ];
+
+  function copy_address() {
+    var shipto = addresses[ $('#shipto_to_copy').val() ];
+    for (key in shipto)
+      $('#' + key).val(shipto[key]);
+  }
+
+  function clear_fields() {
+    var shipto = addresses[0];
+    for (key in shipto)
+      $('#' + key).val('');
+    $('#shiptocp_gender').val('m');
+  }
+
+  function clear_shipto_id_before_submit() {
+    var shipto = addresses[0];
+    for (key in shipto)
+      if ((key != 'shiptocp_gender') && ($('#' + key).val() != '')) {
+        $('#shipto_id').val('');
+        break;
+      }
+
+    $('form').submit();
+  }
+</script>
+
+[% select_options = [ [ 0, LxERP.t8("Billing Address") ] ] ;
+   FOREACH shipto = vc_obj.shipto ;
+     city  = shipto.shiptozipcode _ ' ' _ shipto.shiptocity ;
+     title = [ shipto.shiptoname, shipto.shiptostreet, city ] ;
+     CALL select_options.import([ [ loop.count, title.grep('\S').join("; ") ] ]) ;
+   END ;
+   '' %]
 
  <form method="post" action="[% HTML.escape(script) %]">
+  [% L.hidden_tag("shipto_id", shipto_id) %]
+
+  <p>
+   [% LxERP.t8("Copy address from master data") %]:
+   [% L.select_tag("", select_options, id="shipto_to_copy", style="width: 400px") %]
+   [% L.button_tag("copy_address()", LxERP.t8("Copy")) %]
+   [% L.button_tag("clear_fields()", LxERP.t8("Clear fields")) %]
+  </p>
 
   <table>
    <tr class="listheading">
   [% L.hidden_tag("nextsub", nextsub) %]
   [% L.hidden_tag("previousform", previousform) %]
 
-  [% L.submit_tag("__dummy", LxERP.t8("Continue")) %]
+  [% L.button_tag("clear_shipto_id_before_submit()", LxERP.t8("Continue")) %]
  </form>
index c42139e..c0f39c7 100644 (file)
@@ -26,7 +26,7 @@
 [%- INCLUDE 'common/flash.html' %]
 [%- INCLUDE 'generic/set_longdescription.html' %]
 
-<div class="tabwidget">
+<div id="ir_tabs" class="tabwidget">
  <ul>
   <li><a href="#ui-tabs-basic-data">[% 'Basic Data' | $T8 %]</a></li>
 [%- IF INSTANCE_CONF.get_webdav %]
index 5ab55b7..d3c8ccf 100644 (file)
 
     <input class="submit" type="submit" accesskey="u" name="action" id="update_button" value="[% 'Update' | $T8 %]">
     <input class="submit" type="submit" name="action" value="[% 'Ship to' | $T8 %]">
-    <input class="submit" type="submit" name="action" value="[% 'Print' | $T8 %]">
-    <input class="submit" type="submit" name="action" value="[% 'E-mail' | $T8 %]">
+    <input class="submit" type="submit" name="action" value="[% 'Print' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
+    <input class="submit" type="submit" name="action" value="[% 'E-mail' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
 [% IF  show_storno %]
-    <input class="submit" type="submit" name="action" value="[% 'Storno' | $T8 %]">
+    <input class="submit" type="submit" name="action" value="[% 'Storno' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
 [% END %]
     <input class="submit" type="submit" name="action" value="[% 'Post Payment' | $T8 %]">
     <input class="submit" type="submit" name="action" value="[% 'Use As New' | $T8 %]">
 
 [% IF id && !is_type_credit_note %]
-    <input class="submit" type="submit" name="action" value="[% 'Credit Note' | $T8 %]">
+    <input class="submit" type="submit" name="action" value="[% 'Credit Note' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
 [% END %]
 [% IF show_delete && !storno %]
     <input class="submit" type="submit" name="action" value="[% 'Delete' | $T8 %]">
-    <input class="submit" type="submit" name="action" value="[% 'Post' | $T8 %]">
+    <input class="submit" type="submit" name="action" value="[% 'Post' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
 [% END %]
     <input class="submit" type="submit" name="action" value="[% 'Order' | $T8 %]">
     <input type="button" class="submit" onclick="follow_up_window()" value="[% 'Follow-Up' | $T8 %]">
    [% UNLESS locked %]
       <input class="submit" type="submit" name="action" id="update_button" value="[% 'Update' | $T8 %]">
       <input class="submit" type="submit" name="action" value="[% 'Ship to' | $T8 %]">
-      <input class="submit" type="submit" name="action" value="[% 'Preview' | $T8 %]">
-      <input class="submit" type="submit" name="action" value="[% 'Post and E-mail' | $T8 %]">
-      <input class="submit" type="submit" name="action" value="[% 'Print and Post' | $T8 %]">
-      <input class="submit" type="submit" name="action" value="[% 'Post' | $T8 %]">
+      <input class="submit" type="submit" name="action" value="[% 'Preview' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
+      <input class="submit" type="submit" name="action" value="[% 'Post and E-mail' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
+      <input class="submit" type="submit" name="action" value="[% 'Print and Post' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
+      <input class="submit" type="submit" name="action" value="[% 'Post' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
       <input class="submit" type="submit" name="action" value="[% 'Save Draft' | $T8 %]">
    [%- END %]
  [% END # id %]
 <input type="hidden" name="customer_discount" value="[% customer_discount %]">
 <input type="hidden" name="gldate" value="[% gldate %]">
 </form>
+<script type='text/javascript'>
+ $(kivi.SalesPurchase.init_on_submit_checks);
+</script>
index 8c5c9f8..5fa5bb1 100644 (file)
@@ -27,7 +27,7 @@
 [%- PROCESS 'common/flash.html' %]
 [%- INCLUDE 'generic/set_longdescription.html' %]
 
-<div class="tabwidget">
+<div id="is_tabs" class="tabwidget">
  <ul>
   <li><a href="#ui-tabs-basic-data">[% 'Basic Data' | $T8 %]</a></li>
 [%- IF INSTANCE_CONF.get_webdav %]
@@ -77,7 +77,8 @@
         <tr>
           <th align="right">[% 'Shipping Address' | $T8 %]</th>
           <td>
-            [% L.select_tag('shipto_id', ALL_SHIPTO, default = shipto_id, value_key = 'shipto_id', title_key = 'displayable_id', with_empty = 1, style='width: 250px', onChange = "document.getElementById('update_button').click();") %]
+            [% shiptos = [ [ "", LxERP.t8("No/individual shipping address") ] ] ;
+               L.select_tag('shipto_id', shiptos.import(ALL_SHIPTO), default=shipto_id, value_key='shipto_id', title_key='displayable_id', style='width: 250px') %]
           </td>
         </tr>
 [%- END %]
         </tr>
         <tr>
           <th align="right">[% 'Transaction description' | $T8 %]</th>
-          <td colspan="3"><input size='35' name="transaction_description" value="[% HTML.escape(transaction_description) %]"></td>
+          <td colspan="3"><input size='35' name="transaction_description" id="transaction_description" value="[% HTML.escape(transaction_description) %]"></td>
         </tr>
       </table>
     </td>
diff --git a/templates/webpages/liquidity_projection/_filter.html b/templates/webpages/liquidity_projection/_filter.html
new file mode 100644 (file)
index 0000000..34bf90a
--- /dev/null
@@ -0,0 +1,27 @@
+[%- USE LxERP -%][%- USE L -%]
+
+<form method="post" action="controller.pl">
+ [% L.hidden_tag('action', 'LiquidityProjection/show') %]
+
+ <table border="0">
+  <tr>
+   <th align="right">[% LxERP.t8("Number of months") %]</th>
+   <td>[% L.input_tag("params.months", FORM.params.months, class="initial_focus") %]</td>
+  </tr>
+
+  <tr>
+   <th align="right" valign="top">[% LxERP.t8("Break down by") %]</th>
+   <td valign="top">
+    [% L.checkbox_tag("params.type",           value=1, checked=FORM.params.type,           label=LxERP.t8("Basis of calculation")) %]
+    <br>
+    [% L.checkbox_tag("params.salesman",       value=1, checked=FORM.params.salesman,       label=LxERP.t8("Salesman")) %]
+    <br>
+    [% L.checkbox_tag("params.buchungsgruppe", value=1, checked=FORM.params.buchungsgruppe, label=LxERP.t8("Buchungsgruppe")) %]
+   </td>
+  </tr>
+ </table>
+
+ <p>
+  [% L.submit_tag("dummy", LxERP.t8("Show")) %]
+ </p>
+</form>
diff --git a/templates/webpages/liquidity_projection/_result.html b/templates/webpages/liquidity_projection/_result.html
new file mode 100644 (file)
index 0000000..bcc6e20
--- /dev/null
@@ -0,0 +1,74 @@
+[%- USE HTML -%][%- USE LxERP -%]
+[%- SET name_col = FORM.params.salesman || FORM.params.buchungsgruppe || FORM.params.type %]
+
+<table border="0">
+ <tr>
+  <th class="listheading">[% LxERP.t8("Type") %]</th>
+  [%- IF name_col %]
+   <th class="listheading">[% LxERP.t8("Name") %]</th>
+  [%- END %]
+  [%- FOREACH month = SELF.liquidity.sorted.month %]
+   <th class="listheading" align="right">[%- IF month == 'old' %][% LxERP.t8("old") %][% ELSIF month == 'future' %][% LxERP.t8("prospective") %][% ELSE %][%- HTML.escape(month) %][% END %]</th>
+  [%- END %]
+ </tr>
+
+ [% IF FORM.params.type %]
+  [% FOREACH type = SELF.liquidity.sorted.type %]
+   <tr class="listrow">
+    <td>[% IF loop.first %][% LxERP.t8("Basis of calculation") %][% END %]</td>
+    <td>
+     [% IF    type == 'order' %][% LxERP.t8("Sales Orders") %]
+     [% ELSIF type == 'partial' %][% LxERP.t8("Partial invoices") %]
+     [% ELSE %][% LxERP.t8("Periodic Invoices") %]
+     [% END %]
+    </td>
+
+    [%- FOREACH month = SELF.liquidity.sorted.month %]
+     <td align="right">[% LxERP.format_amount(SELF.liquidity.$type.$month, 2) %]</td>
+    [%- END %]
+   </tr>
+  [%- END %]
+ [%- END %]
+
+ [%- IF FORM.params.salesman %]
+  [%- FOREACH salesman = SELF.liquidity.sorted.salesman %]
+   <tr class="listrow">
+    <td>[% IF loop.first %][% LxERP.t8("Salesman") %][% END %]</td>
+    <td>[%- HTML.escape(salesman) %]</td>
+
+    [%- FOREACH month = SELF.liquidity.sorted.month %]
+     <td align="right">[% LxERP.format_amount(SELF.liquidity.salesman.$salesman.$month, 2) %]</td>
+    [%- END %]
+   </tr>
+  [%- END %]
+ [%- END %]
+
+ [%- IF FORM.params.buchungsgruppe %]
+  [%- FOREACH buchungsgruppe = SELF.liquidity.sorted.buchungsgruppe %]
+   <tr class="listrow">
+    <td>[% IF loop.first %][% LxERP.t8("Buchungsgruppe") %][% END %]</td>
+    <td>[%- HTML.escape(buchungsgruppe) %]</td>
+
+    [%- FOREACH month = SELF.liquidity.sorted.month %]
+     <td align="right">[% LxERP.format_amount(SELF.liquidity.buchungsgruppe.$buchungsgruppe.$month, 2) %]</td>
+    [%- END %]
+   </tr>
+  [%- END %]
+ [%- END %]
+
+ <tr class="listrow listtotal">
+  <td>[% LxERP.t8("Total") %]</td>
+  [% IF name_col %]<td></td>[% END %]
+  [%- FOREACH month = SELF.liquidity.sorted.month %]
+   <td align="right">
+    [% IF SELF.liquidity.total.$month > 0 %]
+     <a href="[% HTML.escape(SELF.link_to_old_orders(reqdate=month, months=params.months)) %]">
+    [% END %]
+    [% LxERP.format_amount(SELF.liquidity.total.$month, 2) %]
+    [% IF SELF.liquidity.total.$month > 0 %]
+     </a>
+    [% END %]
+   </td>
+  [%- END %]
+ </tr>
+</table>
diff --git a/templates/webpages/liquidity_projection/show.html b/templates/webpages/liquidity_projection/show.html
new file mode 100644 (file)
index 0000000..ce6bf82
--- /dev/null
@@ -0,0 +1,15 @@
+[% USE HTML %][% USE LxERP %][%- USE L -%]
+<body>
+
+ <h1>[% HTML.escape(title) %]</h1>
+
+ [% PROCESS 'liquidity_projection/_filter.html' %]
+
+ [%- IF SELF.liquidity %]
+  <hr>
+
+  [% PROCESS 'liquidity_projection/_result.html' %]
+ [% END %]
+
+</body>
+</html>
index 1896f76..9dd893e 100644 (file)
@@ -2,6 +2,15 @@
 [%- USE HTML %]
 [%- USE LxERP %]
 [%- USE L %]
+[%- IF is_req_quo || is_pur_ord %]
+  [%- SET allow_invoice=1 %]
+[%- ELSIF is_sales_quo && INSTANCE_CONF.get_allow_sales_invoice_from_sales_quotation %]
+  [%- SET allow_invoice=1 %]
+[%- ELSIF is_sales_ord && INSTANCE_CONF.get_allow_sales_invoice_from_sales_order %]
+  [%- SET allow_invoice=1 %]
+[%- ELSE %]
+  [%- SET allow_invoice=0 %]
+[%- END %]
   <tr>
     <td>
       <table width="100%">
 [% label_edit %]<br>
 <input class="submit" type="submit" name="action_update" id="update_button" value="[% 'Update' | $T8 %]">
 <input class="submit" type="submit" name="action_ship_to" value="[% 'Ship to' | $T8 %]">
-<input class="submit" type="submit" name="action_print" value="[% 'Print' | $T8 %]">
-<input class="submit" type="submit" name="action_e_mail" value="[% 'E-mail' | $T8 %]">
-<input class="submit" type="submit" name="action_save" value="[% 'Save' | $T8 %]"[% IF warn_save_active_periodic_invoice %] onclick="return warn_save_active_periodic_invoice();"[% END %]>
-<input class="submit" type="submit" name="action_save_and_close" value="[% 'Save and Close' | $T8 %]"[% IF warn_save_active_periodic_invoice %] onclick="return warn_save_active_periodic_invoice();"[% END %]>
+<input class="submit" type="submit" name="action_print" value="[% 'Print' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
+<input class="submit" type="submit" name="action_e_mail" value="[% 'E-mail' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
+<input class="submit" type="submit" name="action_save" value="[% 'Save' | $T8 %]"[% IF warn_save_active_periodic_invoice %] data-warn-save-active-periodic-invoice="1"[% END %] data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
+<input class="submit" type="submit" name="action_save_and_close" value="[% 'Save and Close' | $T8 %]"[% IF warn_save_active_periodic_invoice %] data-warn-save-active-periodic-invoice="1"[% END %] data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
 
 [%- IF id %]
   <input type="button" class="submit" onclick="follow_up_window()" value="[% 'Follow-Up' | $T8 %]">
   <input type="button" class="submit" onclick="set_history_window([% HTML.escape(id) %])" name="history" id="history" value="[% 'history' | $T8 %]">
 
   <br>[% label_workflow %]<br>
-  <input class="submit" type="submit" name="action_save_as_new" value="[% 'Save as new' | $T8 %]">
+  <input class="submit" type="submit" name="action_save_as_new" value="[% 'Save as new' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
 
   [%- UNLESS (is_sales_ord && !INSTANCE_CONF.get_sales_order_show_delete) || (is_pur_ord && !INSTANCE_CONF.get_purchase_order_show_delete) %]
     [% L.submit_tag('action_delete', LxERP.t8('Delete'), confirm=LxERP.t8('Are you sure?')) %]
     <input class="submit" type="submit" name="action_delivery_order" value="[% 'Delivery Order' | $T8 %]">
   [%- END %]
 
+  [%- IF allow_invoice %]
   <input class="submit" type="submit" name="action_invoice" value="[% 'Invoice' | $T8 %]">
+  [%- END %]
 
   [%- IF is_sales_ord || is_pur_ord %]
     <br>[% heading %] als neue Vorlage verwenden f&uuml;r<br>
 [% END %]
 
 </form>
+<script type='text/javascript'>
+ $(kivi.SalesPurchase.init_on_submit_checks);
+</script>
index c68d6c9..e017349 100644 (file)
@@ -30,7 +30,7 @@
 [%- INCLUDE 'common/flash.html' %]
 [%- INCLUDE 'generic/set_longdescription.html' %]
 
-    <div class="tabwidget">
+    <div id="oe_tabs" class="tabwidget">
      <ul>
       <li><a href="#ui-tabs-basic-data">[% 'Basic Data' | $T8 %]</a></li>
 [%- IF INSTANCE_CONF.get_webdav %]
@@ -79,7 +79,8 @@
                   <tr>
                     <th align="right">[% 'Shipping Address' | $T8 %]</th>
                     <td>
-                      [% L.select_tag('shipto_id', ALL_SHIPTO, default=shipto_id, value_key='shipto_id', title_key='displayable_id', with_empty=1, style='width: 250px', onChange="document.getElementById('update_button').click();") %]
+                      [% shiptos = [ [ "", LxERP.t8("No/individual shipping address") ] ] ;
+                         L.select_tag('shipto_id', shiptos.import(ALL_SHIPTO), default=shipto_id, value_key='shipto_id', title_key='displayable_id', style='width: 250px') %]
                     </td>
                   </tr>
 [%- END %]
                   </tr>
                   <tr>
                     <th align="right">[% 'Transaction description' | $T8 %]</th>
-                    <td colspan="3"><input name="transaction_description" size="35" value="[% HTML.escape(transaction_description) %]"></td>
+                    <td colspan="3"><input name="transaction_description" id="transaction_description" size="35" value="[% HTML.escape(transaction_description) %]"></td>
                   </tr>
 [%- IF show_delivery_customer %]
                   <tr>
                       [%- L.select_tag('globalproject_id', ALL_PROJECTS, title_key='projectnumber', default=globalproject_id, with_empty='1', onChange="document.getElementById('update_button').click();") %]
                     </td>
                   </tr>
+[%- IF type == 'sales_quotation' %]
+                  <tr>
+                    <th width="70%" align="right" nowrap>[% 'Order probability' | $T8 %]</th>
+                    <td nowrap>
+                      [%- L.select_tag('order_probability', ORDER_PROBABILITIES, title='title', default=order_probability) %]%
+                    </td>
+                  </tr>
+                  <tr>
+                    <th width="70%" align="right" nowrap>[% 'Expected billing date' | $T8 %]</th>
+                    <td nowrap>
+                      [%- L.date_tag('expected_billing_date', expected_billing_date 'BL') %]
+                    </td>
+                  </tr>
+[%- END %]
                 </table>
               </td>
             </tr>
index 1fccfc4..b651bf0 100644 (file)
        [% L.date_tag('reqdateto') %]
      </td>
     </tr>
+[%- IF type == 'sales_quotation' %]
+    <tr>
+     <th align="right">[% 'Expected billing date' | $T8 %] [% 'From' | $T8 %]</th>
+     <td>
+      [% L.date_tag('expected_billing_date_from', '' 'BL') %]
+     </td>
+     <th align="right">[% 'Expected billing date' | $T8 %] [% 'Bis' | $T8 %]</th>
+     <td>
+      [% L.date_tag('expected_billing_date_to', '' 'BL') %]
+     </td>
+    </tr>
+    <tr>
+     <th align="right">[% 'Order probability' | $T8 %]</th>
+     <td colspan="3">
+      [% L.select_tag('order_probability_op', [[ 'ge', '>=' ], [ 'le', '<=' ]]) %]
+      [% L.select_tag('order_probability_value', ORDER_PROBABILITIES, title='title', with_empty=1) %]
+     </td>
+    </tr>
+[%- END %]
     <tr>
      <th align="right">[% 'Include in Report' | $T8 %]</th>
      <td colspan="5">
          <label for="l_globalprojectnumber">[% 'Project Number' | $T8 %]</label>
         </td>
         <td>
-         <input name="l_transaction_description" id="l_transaction_description" class="checkbox" type="checkbox" value="Y">
+         <input name="l_transaction_description" id="l_transaction_description" class="checkbox" type="checkbox" value="Y"[% IF INSTANCE_CONF.get_require_transaction_description_ps %] checked[% END %]>
          <label for="l_transaction_description">[% 'Transaction description' | $T8 %]</label>
         </td>
        </tr>
          <label for="l_salesman">[% 'Salesman' | $T8 %]</label>
         </td>
        </tr>
+[% IF type == 'sales_quotation' %]
+       <tr>
+        <td colspan="2">
+         <input name="l_order_probability_expected_billing_date" id="l_order_probability_expected_billing_date" class="checkbox" type="checkbox" value="Y">
+         <label for="l_order_probability_expected_billing_date">[% 'Order probability & expected billing date' | $T8 %]</label>
+        </td>
+       </tr>
+[%- END %]
        <tr>
         <td>
          <input name="l_remaining_amount" id="l_remaining_amount" class="checkbox" type="checkbox" value="Y">
index c2fe759..1430969 100644 (file)
@@ -35,7 +35,7 @@
      [%- IF col.align %] align="[% HTML.escape(col.align) %]" style="text-align: [% HTML.escape(col.align) %]"[% END -%]
      [%- IF col.colspan && col.colspan > 1 %] colspan="[% HTML.escape(col.colspan) %]"[% END -%]
      >
-      [%- IF col.link -%]<a class='report-generator-header-link' href="[% HTML.escape(col.link) %]">[%- END -%]
+      [%- IF col.link -%]<a class="[% col.link_class ? col.link_class : 'report-generator-header-link' %]" href="[% HTML.escape(col.link) %]">[%- END -%]
       [%- col.text -%]
       [%- IF col.show_sort_indicator -%]<img border="0" src="image/[% IF col.sort_indicator_direction %]down[% ELSE %]up[% END %].png">[%- END -%]
       [%- IF col.link -%]</a>[%- END -%]
@@ -63,7 +63,7 @@
        [%- ELSE %]
         [%- USE iterator(col.CELL_ROWS) %][%- FOREACH cell_row = iterator %]
          [%- IF cell_row.data != '' %]
-          [%- IF cell_row.link %]<a href="[% HTML.escape(cell_row.link) %]">[%- END %]
+          [%- IF cell_row.link %]<a href="[% HTML.escape(cell_row.link) %]"[% IF cell_row.link_class %] class="[% cell_row.link_class %]"[% END %]>[%- END %]
           [%- cell_row.data %]
           [%- IF cell_row.link %]</a>[%- END %]
          [%- END %]
 
   </form>
  [% END %]
-
index 08eacfd..3993107 100644 (file)
     </td>
   </tr>
 [%- END %]
+[%- BLOCK customer %]
+  <tr>
+    <th align=right nowrap>[% 'Customer' | $T8 %]</th>
+    <td colspan=3>[% L.customer_picker('customer_id') %]</td>
+  </tr>
+[%- END %]
 [%- BLOCK projectnumber %]
   <tr>
     <th align=right nowrap>[% 'Project' | $T8 %]</th>
 [%- END %]
 
 [%- IF is_trial_balance %]
+[%- PROCESS customer %]
 [%- PROCESS projectnumber %]
   <input type=hidden name=nextsub value=generate_trial_balance>
 </table>