]> wagnertech.de Git - mfinanz.git/commitdiff
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 dc740cc30b892e9da07deb54ef71fb4864c205a2..18abf7c1adb6b4e98d500a7e70ef0d6ac9a5e498 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 fa1f914773542ebdfcb41ad033a766f290300d1e..b430272e018216cdca776c04c6dd1e250b8e68b1 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 2f7fefb2e3eec80fe1ada3b09b5294018b23f6e7..6572e231dab1ec19624d1480a20ab2847feaacbf 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 4b27cd98244588a63b162a7e40bfcd2bd3e2fef3..a233a0eb515cc4ce18971f6ec5bdad0e2bd4437a 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 f59fd07b54a3411970187744a5b3afce921d0a4f..af109e967e1077134dd66958e73c350b4f17592d 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 adf8b471c0057a72a8f4ddb8d96f3d1800160c9b..d38736b82289fb884124556e9cb54d52918ddb8c 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 e64a774a76f0fb9c56a69f903e0fef13223a7e64..d5a7f2a5f139464f4aaffedaf19e147d7f6b2e76 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 718d42251cab9ae6e8664a0d5b2b0f55357c8d2b..40ac530d440718d524c99039d43efcf902df8855 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 3f894cd012ed3b9275249b8dffb3aed6d7dcc597..7e6b5008079dff871a9d51ca7ec30e0106284677 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 203f83a90d0a374d7725dc680a812a86cf727e5d..2cc4e6a902319b0526dc28d0606c1de253b757bb 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 737c163a93ecc7adee149723a00bd5275e54ec0f..713685519fa50962b319e8a0a582c98ffd6ab225 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 cca9fe3c1e3e5ec4d98254eb5e66b73c0ccde885..182f3bdcc68121175b18450fa41ad3b6629b9b14 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 d95d1430937e89bd4d2d3b978a362b9b3faa8716..ed485c1908db84e7a41261b952280c8b360f277c 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 0e9616651530f3243daae5e105f3294aef0a0bb4..502654ba262168fed42635a5b0581f6052757150 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 a0167fd23f53aea92c6b87df23db53bf4007dfdf..f7728aef18d4ac2d4e2f08da421ac44704bea4b4 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 8fcc3bd0662626f4e66e4cb013f36ba64c6f6232..56a7e0b86fcb9490b5a1e540b229e23fff26df05 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 8a749bb3c6b4d075eeb70bb2d736327b66afa663..7f821751983d62bd925687e97b99da1686a331e8 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 ea5e3281bad6fdee717ff9d445f2cedfdbc934fd..b80211917c6fbe1fdb3da9eba719e8903dfb7371 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 5242ccea71bb8048f23c4e088f758c658e87a4bc..2015b9052624c6723c638a7135fb8a394699a957 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 bda14ec5a82919114086985a85a8a01a45c4ac87..a6702b68ac3e4d520f7ee0c8702f0fb66e611740 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 43d53861045bdb132de0b6025763981615a152b4..c2fccb1e96ac98739dcc48af73953995c22fe554 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 7f935731344d5d8658830a0e4c59d607054e5d34..92bb92ffd2e734a1ba91d547a2bb0e07e87a127e 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 bb3a9bcd773ec4964615c6c58548caf8d6eec40b..b0a9f991b3597bfdc0a614388c991d69bea057a6 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 ef3f37f48e25c23e9cde6a31526ad5568c070ca3..8367137a351ade37efc9a71ec591f5117371ef7c 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 c11fb3f5ae48586b5df75e75f65f557a77787102..81ef97890c7cdb047f1c3d024591c7247f8b75b5 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 ae2612b9d3e4a6feca4b495b177ee2abe00bef0e..f5a0cd2ccce7e1115bfd252799258d71c88f1ba0 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 b133d8f131263cf39e3dea9c4b7da4827cf97c7b..e8d260c6797d795a4d9cb989dc6cdcbf27011790 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 100652e15e985c070cce283bb9138bc9b3da10f4..34afeae54504abbb6dc1b319f7b626908cf27961 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 1da5a8e2d107597bd330c6f8accfc3833a9df163..0960ad71f52248a178f9aff077145541382497e0 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 854b863a3f4e8fcc9b1a319ef5afe02040d9eeaf..7ce18a4765c3e41f080eb3cad02520371ab14cc7 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 7b2a5eb660459354d7cc2fc7af852999536d8384..7a8fd1d75efbfdc309b328740e7edb530512d962 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 ed31ddf68ecc52bd21b902c5c74d35c4a619a298..7030454d6989cab2428bae6f3ff6bf438b9db4a1 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 8d36eb74920a1871460e139379e8082bb7618b4b..555e75d7374c16b5b00b27bf85948e60f14d844f 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 94c311f14066395b206d1b7f7cc9dd5095359e2c..c79dad1b84d919cee4485fe7914cdaa43dc7e35a 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 191a2d30051e9540cff3221e6d754c9a8a711937..163f590696a14956833795e5032290e60c5dfb98 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 301fe3b646e8ebc5e9cbe82f95e031c32d289658..931df6b4487849d59318ca6814843200f7f7df65 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 64afc530a1abc7c4043bfcc2c2b90a217498f648..63420a54484a67bd9505c586e7150770932ef325 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 80b41f95a091a5da28baa13caf19fe9d983d4148..016015f520914a0347797c87278b09e61b2c8c39 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 23a399e10fe1b37ea23bfbf8e678640faee12b50..4cbef16844fad3ba04d3627b1c7b8a7fd31e0276 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 06ea6ee43ed44c04b21459694f7e7ebce836b05b..19b2d2d16c66ecf1507e6b6b7c0e495455336708 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 9f4fae60115bac0d2941fe9621b121bb25f244b8..b1ea817e486d7be87ba3ff05361928da4ceda7e5 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 326e89c704edc36d58ae333d1d28c37f590536d6..8b073b9c58c9c288a421e238d68ddffca67f5246 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 969e8adfb373fa58e253d9478e4fa6e87d9ba69d..3d730265849d7c9b9255aa470f6397b065851263 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 09ab7bd7bb7c33e8c17391e6b07445d2ea0074ad..6fc50e98f9384db85df8900630d1761f67e6cccf 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 26be5f79bd9b48664dfba500d8830eb7abc722f2..47f163ca91655b36cef40d76ab0693a6d7626728 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 bf9ebbdb9ec2b742534ceb0b44f5dea4efe36616..6daa0130118c8b055a04d66e6c345c490a2f59e9 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 8bb9b997bd68475b7518ad4fe1b9fa2e86a165dc..42622f8fdc5d81c9684f840cea1052069aa80993 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 845fc3ab4a0971e3a8683c27aaebeb9a90385671..b5b83c19f01d4d20e144546d695958d4de78f749 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 67f2740c5966665c57b7e7841b49fd35d5e48e82..c16f772d168591bb7c06bcdd45e7f80f4d642a86 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 1cfc0ca57e1cfe8ca389f506872002700f4fb8c7..4cd88226972e18772ddbc39a3aef2f62f9835820 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 e90b12cf651161bc64a9bdc9dbfc7f53aeb0fc82..951bce515780c73bce59bd7d344d93ca91e72fc3 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 379544809fee59ef1336cedecb55888c01fd1d78..b6c6298a0ab5bf471de0379f02c78e9f3a38adea 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 d6d8ff4d5134545a989fd198065b6b1321d83231..9a8aea41e2e4be5403c7b34b55d09a360d86368a 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 860d38f5ef96ae9036c0d190a6585eae361351eb..c2ca6939dcf727d04191865de819414c48110119 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 e6849a554e5cc41a1f029e8773b770ae1186df6e..25a8d4c3bcd730d6ef581a7f49fd4acd3cce6426 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 3b859938386d36c4f3936c258d107ecb7c47d773..6fa432dc00426c4a7451e5ab0d0718e5a36f7b25 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 f42c8724fe27123f0d5d7335ff0b4a9854fcc2a5..ab71226dc4ea4a52390dbb740210bb9a8699d027 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 d0984eb01e3b7a2b40811efc9226fb015ee57bac..d9bf8dbbba48f494dce4ba44cc29690f1630ce1c 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 44ebd1916c1f8d9d85b25b3c7b33a9181eede924..b969e1dde59ae0f23d20fbf614703b615fd72a79 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 59c07354edf70ab6df6d9f6823df01bf57b5ecc7..1cb08db499ad8cbea8d9d3aac0c82e7f0d979bbe 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 a390c8721c81f3d026134081aa355c464da9acb5..f6c163362dff13c4dc4c63abad43213dcd4bcbe3 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 3d58e1684d5524a7b3578f7bb4520c51968a4c73..3b7e86524b584d82e98ac442742f3261c4eff327 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 56d5255d80351b30efc86b07bc457b16916f3081..956d1b26aa6886f5ddf95965cccb6fa7a1d0b19d 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 49fbf739c3cec982bf33dc2b2ef648d62a733c7b..c349536e8a608a4f57bbbad88ae879575c60a79f 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 dd43bdf8c210ce47c05e0e32df294dde99dff328..3297621f942108f38626449280c51608ab2e5e9a 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 8ce978346329df8645bc85bd65d879012b211a8f..4ceb9a0ddedc3a7e3292afc18861b025b7c0cd1a 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 cd5fe41b9f7db8e174d586c7b84f71dd2277203f..788c37db684b7c04dc465f21dd36ec9b27691242 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 fb589c610f1c45dbdb23f4a13b827d24241412c0..8106576a84b3e69342d69f24acd58a3c5d9b189e 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 cb332ec07ef0dfac39cc131410337d588e1400f3..f3422d62df31169159dfe8a98db9e4cf9e9b776c 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 e8ee4acb76115273202de28ee4f392ba2690e0c8..a80146d7845bea984ba198d02ccb6958a01d233d 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 3a8556cfd334eb411f168665e3b7196ad052b142..bab3825df595fd24586336c86d0ea3aa2d2d5e70 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 94db96b121722cca608466e0035d5fe850eb85a0..3a8f129592d6201cde13fbce7c35316b58d56343 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 467d4b290f350b65a092faad7db49f36ce7b748f..b095f08c320c89283a91032f46e32d0001e8e618 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 f7e49fdd7e94e03880678c9b6ae9075fb3000478..862acbcd8dd5f9ef5e96ab104d43ca332177abaf 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 af504a8806690bcc96dacb49cbbaeb6f309c2b0e..416eb815f1f80c372ff4d7dc183e735776578415 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 2a7748c58678562b0332426ee45d719152715474..2d237264ab9e380f390aaa6277d5c17a104a3e4a 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 ad2da05abff1b57037291ffdfbcbf3c42056728e..e56b3e58413ff7d68a812910a7e9405f42d4fcfd 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 1f08af4b5153cdea5177ef1a921a542cfe0c514c..99608bbc6f15da101b87c99ada48bfb5b7e7da94 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 d67740a9f02c052f2a0bfef5d5311d3129585a27..82ff5f2b4c58de2b8803083c40ed4084fdd1167a 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 aca56cd00f92d12dae49ecb43d76aa736dcea89a..af4b94241caa4fc014312275770443d2849390a0 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 8f4997772a08675bc00658ffdb4d53d686032613..97cfa9d1f9260bf518ffc5e91ac18000205b485e 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 eea92171aa8f12fc06b47883e50b6064b6bc67c3..e15b4409937ca2079583a7d0d70bafbb1d1f33ce 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 2961b5c6894134f00eb0a38c252e790567102e7d..99c8ebede5edd54b6b6ee6a0a33a6678cacdf92f 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 7483f8e1b1611a9a64acb43431bb0aadbc78e44b..905215c5627f276bad523561174170e094404377 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 b31fde030d6774d4104a00ae5da0e5838dd4e3c4..7654e4ff51ab366bc25cdec5586d9e5486926726 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 20ff5a995e882397a868a8c01eef971b09819d45..9ce47f1f73113f5c04774137c73c5513a13ca519 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 10f0ce2cc4edb9fe4c94a95faf9a5588e635ec46..ca5560c1035e7fbb55daba87cb1f6e8b51cfbb6e 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 c42139ea0f63672f4293faea45237fe6b2ddf5fb..c0f39c76035d02ded2de08b50e03d275fc74ab4a 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 5ab55b7398addab81977c32e36f236af5a61d093..d3c8ccf3a58c864683c70f05ef0365de8818897a 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 8c5c9f8a46f7d2916a6d293ab33ab8bf90f84317..5fa5bb10502d40af86f7ed4e58c0415d0314d1b7 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 1896f7609e288bd7f8ff2ef04e5e70d0e4fcf688..9dd893e8deffaf9958251b03019fbdcf867cc574 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 c68d6c9653a00db35325d0ed6c6103f0b5c1794e..e01734932548bc32f525e139c0a18844a7bb29db 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 1fccfc415e9a9cb2f1fa41a52e01ed5839d149c3..b651bf035eb5bc965e6231fa173422f732835fa3 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 c2fe759b3da9f7e7a6c8958e6d4fdfc44b20d91f..1430969dd940b3d882550cb33f4995693d95a62d 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 08eacfd748d119249e2e1f4a3dac534db392e850..39931075a702973bd2448300b586e9a5e00a2969 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>