Merge branch 'master' of vc.linet-services.de:public/lx-office-erp
authorG. Richardson <information@lx-office-hosting.de>
Mon, 30 Apr 2012 08:56:09 +0000 (10:56 +0200)
committerG. Richardson <information@lx-office-hosting.de>
Mon, 30 Apr 2012 08:56:09 +0000 (10:56 +0200)
43 files changed:
SL/CT.pm
SL/CVar.pm
SL/Controller/Base.pm
SL/Controller/DeliveryPlan.pm [new file with mode: 0644]
SL/Controller/Helper/ParseFilter.pm
SL/Controller/SellPriceInformation.pm [new file with mode: 0644]
SL/DB/Manager/Order.pm
SL/DB/MetaSetup/Taxkey.pm [deleted file]
SL/DB/Order.pm
SL/DB/OrderItem.pm
SL/DBUtils.pm
SL/InstallationCheck.pm
SL/LxOfficeConf.pm
SL/Request.pm
SL/Template/Plugin/L.pm
VERSION
bin/mozilla/amcvar.pl
bin/mozilla/ct.pl
bin/mozilla/ic.pl
bin/mozilla/io.pl
css/kivitendo/main.css
locale/de/all
locale/de_DE/all
menu.ini
scripts/installation_check.pl
t/request/flatten.t [new file with mode: 0644]
templates/webpages/amcvar/render_inputs.html
templates/webpages/amcvar/render_inputs_block.html
templates/webpages/amcvar/search_filter.html
templates/webpages/ct/_contact.html
templates/webpages/ct/form_header.html
templates/webpages/ct/search_contact.html [new file with mode: 0644]
templates/webpages/delivery_plan/_filter.html [new file with mode: 0644]
templates/webpages/delivery_plan/_list.html [new file with mode: 0644]
templates/webpages/delivery_plan/list.html [new file with mode: 0644]
templates/webpages/delivery_plan/report_bottom.html [new file with mode: 0644]
templates/webpages/delivery_plan/report_top.html [new file with mode: 0644]
templates/webpages/ic/form_footer.html
templates/webpages/ic/form_header.html
templates/webpages/ic/sales_price_information.html [new file with mode: 0644]
templates/webpages/menu/menuv3.html
templates/webpages/oe/sales_price_information.html [new file with mode: 0644]
templates/webpages/price_information/report_bottom.html [new file with mode: 0644]

index eece19e..8886348 100644 (file)
--- a/SL/CT.pm
+++ b/SL/CT.pm
@@ -448,6 +448,12 @@ sub save_customer {
                               'trans_id'  => $form->{id},
                               'variables' => $form,
                               'always_valid' => 1);
+  CVar->save_custom_variables('dbh'       => $dbh,
+                              'module'    => 'Contacts',
+                              'trans_id'  => $form->{cp_id},
+                              'variables' => $form,
+                              'name_prefix'  => 'cp',
+                              'always_valid' => 1);
 
   my $rc = $dbh->commit();
 
@@ -657,6 +663,12 @@ sub save_vendor {
                               'trans_id'  => $form->{id},
                               'variables' => $form,
                               'always_valid' => 1);
+  CVar->save_custom_variables('dbh'       => $dbh,
+                              'module'    => 'Contacts',
+                              'trans_id'  => $form->{cp_id},
+                              'variables' => $form,
+                              'name_prefix'  => 'cp',
+                              'always_valid' => 1);
 
   my $rc = $dbh->commit();
 
@@ -1195,4 +1207,91 @@ sub parse_excel_file {
   $main::lxdebug->leave_sub();
 }
 
+sub search_contacts {
+  $::lxdebug->enter_sub;
+
+  my $self      = shift;
+  my %params    = @_;
+
+  my $dbh       = $params{dbh} || $::form->get_standard_dbh;
+  my $vc        = $params{db} eq 'customer' ? 'customer' : 'vendor';
+
+  my %sortspecs = (
+    'cp_name'   => 'cp_name, cp_givenname',
+    'vcname'    => 'vcname, cp_name, cp_givenname',
+    'vcnumber'  => 'vcnumber, cp_name, cp_givenname',
+    );
+
+  my %sortcols  = map { $_ => 1 } qw(cp_name cp_givenname cp_phone1 cp_phone2 cp_mobile1 cp_email vcname vcnumber);
+
+  my $order_by  = $sortcols{$::form->{sort}} ? $::form->{sort} : 'cp_name';
+  $::form->{sort} = $order_by;
+  $order_by     = $sortspecs{$order_by} if ($sortspecs{$order_by});
+
+  my $sortdir   = $::form->{sortdir} ? 'ASC' : 'DESC';
+  $order_by     =~ s/,/ ${sortdir},/g;
+  $order_by    .= " $sortdir";
+
+  my @where_tokens = ();
+  my @values;
+
+  if ($params{search_term}) {
+    my @tokens;
+    push @tokens,
+      'cp.cp_name      ILIKE ?',
+      'cp.cp_givenname ILIKE ?',
+      'cp.cp_email     ILIKE ?';
+    push @values, ('%' . $params{search_term} . '%') x 3;
+
+    if (($params{search_term} =~ m/\d/) && ($params{search_term} !~ m/[^\d \(\)+\-]/)) {
+      my $number =  $params{search_term};
+      $number    =~ s/[^\d]//g;
+      $number    =  join '[ /\(\)+\-]*', split(m//, $number);
+
+      push @tokens, map { "($_ ~ '$number')" } qw(cp_phone1 cp_phone2 cp_mobile1 cp_mobile2);
+    }
+
+    push @where_tokens, map { "($_)" } join ' OR ', @tokens;
+  }
+
+  my ($cvar_where, @cvar_values) = CVar->build_filter_query('module'         => 'Contacts',
+                                                            'trans_id_field' => 'cp.cp_id',
+                                                            'filter'         => $params{filter});
+
+  if ($cvar_where) {
+    push @where_tokens, $cvar_where;
+    push @values, @cvar_values;
+  }
+
+  if (my $filter = $params{filter}) {
+    for (qw(name title givenname email project abteilung)) {
+      next unless $filter->{"cp_$_"};
+      add_token(\@where_tokens, \@values, col =>  "cp.cp_$_", val => $filter->{"cp_$_"}, method => 'ILIKE', esc => 'substr');
+    }
+
+    push @where_tokens, 'cp.cp_cv_id IS NOT NULL' if $filter->{status} eq 'active';
+    push @where_tokens, 'cp.cp_cv_id IS NULL'     if $filter->{status} eq 'orphaned';
+  }
+
+  my $where = @where_tokens ? 'WHERE ' . join ' AND ', @where_tokens : '';
+
+  my $query     = qq|SELECT cp.*,
+                       COALESCE(c.id,             v.id)           AS vcid,
+                       COALESCE(c.name,           v.name)         AS vcname,
+                       COALESCE(c.customernumber, v.vendornumber) AS vcnumber,
+                       CASE WHEN c.name IS NULL THEN 'vendor' ELSE 'customer' END AS db
+                     FROM contacts cp
+                     LEFT JOIN customer c ON (cp.cp_cv_id = c.id)
+                     LEFT JOIN vendor v   ON (cp.cp_cv_id = v.id)
+                     $where
+                     ORDER BY $order_by|;
+
+  my $contacts  = selectall_hashref_query($::form, $dbh, $query, @values);
+
+  $::lxdebug->leave_sub;
+
+  return @{ $contacts };
+}
+
+
 1;
index 6d04f35..c0b3f43 100644 (file)
@@ -7,7 +7,7 @@ use Scalar::Util qw(blessed);
 use Data::Dumper;
 
 use SL::DBUtils;
-use SL::MoreCommon qw(listify);
+use SL::MoreCommon qw(any listify);
 
 sub get_configs {
   $main::lxdebug->enter_sub();
@@ -243,6 +243,8 @@ sub get_custom_variables {
                      : $cvar->{type} eq 'timestamp' ? $act_var->{timestamp_value}
                      : $cvar->{type} eq 'number'    ? $act_var->{number_value}
                      : $cvar->{type} eq 'customer'  ? $act_var->{number_value}
+                     : $cvar->{type} eq 'vendor'    ? $act_var->{number_value}
+                     : $cvar->{type} eq 'part'      ? $act_var->{number_value}
                      : $cvar->{type} eq 'bool'      ? $act_var->{bool_value}
                      :                                $act_var->{text_value};
       $cvar->{valid} = $valid;
@@ -279,6 +281,12 @@ sub get_custom_variables {
     } elsif ($cvar->{type} eq 'customer') {
       require SL::DB::Customer;
       $cvar->{value} = SL::DB::Manager::Customer->find_by(id => $cvar->{value} * 1);
+    } elsif ($cvar->{type} eq 'vendor') {
+      require SL::DB::Vendor;
+      $cvar->{value} = SL::DB::Manager::Vendor->find_by(id => $cvar->{value} * 1);
+    } elsif ($cvar->{type} eq 'part') {
+      require SL::DB::Part;
+      $cvar->{value} = SL::DB::Manager::Part->find_by(id => $cvar->{value} * 1);
     }
   }
 
@@ -340,7 +348,7 @@ sub save_custom_variables {
 
     } elsif ($config->{type} eq 'bool') {
       push @values, $value ? 't' : 'f', undef, undef, undef;
-    } elsif ($config->{type} eq 'customer') {
+    } elsif (any { $config->{type} eq $_ } qw(customer vendor part)) {
       push @values, undef, undef, undef, $value * 1;
     }
 
@@ -402,6 +410,7 @@ sub render_search_options {
 
   $params{include_prefix}   = 'l_' unless defined($params{include_prefix});
   $params{include_value}  ||= '1';
+  $params{filter_prefix}  ||= '';
 
   my $filter  = $form->parse_html_template('amcvar/search_filter',  \%params);
   my $include = $form->parse_html_template('amcvar/search_include', \%params);
@@ -497,10 +506,16 @@ sub build_filter_query {
 
       $not = 'NOT' if ($params{filter}->{$name} eq 'no');
       push @sub_where,  qq|COALESCE(cvar.bool_value, false) = TRUE|;
-    } elsif ($config->{type} eq 'customer') {
+    } elsif (any { $config->{type} eq $_ } qw(customer vendor part)) {
       next unless $params{filter}->{$name};
 
-      push @sub_where, qq|cvar.number_value * 1 IN (SELECT id FROM customer WHERE name ILIKE ?)|;
+      my $table = $config->{type};
+      push @sub_where, qq|cvar.number_value * 1 IN (SELECT id FROM $table WHERE name ILIKE ?)|;
+      push @sub_values, "%$params{filter}->{$name}%";
+    } elsif ($config->{type} eq 'part') {
+      next unless $params{filter}->{$name};
+
+      push @sub_where, qq|cvar.number_value * 1 IN (SELECT id FROM parts WHERE partnumber ILIKE ?)|;
       push @sub_values, "%$params{filter}->{$name}%";
     }
 
@@ -576,6 +591,8 @@ sub add_custom_variables_to_report {
         : $cfg->{type} eq 'timestamp' ? $ref->{timestamp_value}
         : $cfg->{type} eq 'number'    ? $form->format_amount($myconfig, $ref->{number_value} * 1, $cfg->{precision})
         : $cfg->{type} eq 'customer'  ? (SL::DB::Manager::Customer->find_by(id => 1*$ref->{number_value}) || SL::DB::Customer->new)->name
+        : $cfg->{type} eq 'vendor'    ? (SL::DB::Manager::Vendor->find_by(id => 1*$ref->{number_value})   || SL::DB::Vendor->new)->name
+        : $cfg->{type} eq 'part'      ? (SL::DB::Manager::Part->find_by(id => 1*$ref->{number_value})     || SL::DB::Part->new)->partnumber
         : $cfg->{type} eq 'bool'      ? ($ref->{bool_value} ? $locale->text('Yes') : $locale->text('No'))
         :                               $ref->{text_value};
     }
index 285bfce..ac09057 100644 (file)
@@ -7,6 +7,8 @@ use parent qw(Rose::Object);
 use Carp;
 use IO::File;
 use List::Util qw(first);
+use SL::Request qw(flatten);
+use SL::MoreCommon qw(uri_encode);
 
 #
 # public/helper functions
@@ -21,7 +23,7 @@ sub url_for {
   my $controller  = delete($params{controller}) || $self->_controller_name;
   my $action      = delete($params{action})     || 'dispatch';
   $params{action} = "${controller}/${action}";
-  my $query       = join('&', map { $::form->escape($_) . '=' . $::form->escape($params{$_}) } keys %params);
+  my $query       = join '&', map { uri_encode($_->[0]) . '=' . uri_encode($_->[1]) } @{ flatten(\%params) };
 
   return "controller.pl?${query}";
 }
diff --git a/SL/Controller/DeliveryPlan.pm b/SL/Controller/DeliveryPlan.pm
new file mode 100644 (file)
index 0000000..e280a1b
--- /dev/null
@@ -0,0 +1,313 @@
+package SL::Controller::DeliveryPlan;
+
+use strict;
+use parent qw(SL::Controller::Base);
+
+use Clone qw(clone);
+use SL::DB::OrderItem;
+use SL::Controller::Helper::ParseFilter;
+use SL::Controller::Helper::ReportGenerator;
+
+__PACKAGE__->run_before(sub { $::auth->assert('sales_order_edit'); });
+
+sub action_list {
+  my ($self) = @_;
+  my %list_params = (
+    sort_by  => $::form->{sort_by} || 'reqdate',
+    sort_dir => $::form->{sort_dir},
+    filter   => $::form->{filter},
+    page     => $::form->{page},
+  );
+
+  my $db_args = $self->setup_for_list(%list_params);
+  $self->{pages} = SL::DB::Manager::OrderItem->paginate(%list_params, args => $db_args);
+  $self->{flat_filter} = { map { $_->{key} => $_->{value} } $::form->flatten_variables('filter') };
+  $self->make_filter_summary;
+
+  my $top    = $::form->parse_html_template('delivery_plan/report_top', { FORM => $::form, SELF => $self });
+  my $bottom = $::form->parse_html_template('delivery_plan/report_bottom', { SELF => $self });
+
+  $self->prepare_report(
+    report_generator_options => {
+      raw_top_info_text    => $top,
+      raw_bottom_info_text => $bottom,
+      controller_class     => 'DeliveryPlan',
+    },
+    report_generator_export_options => [
+      'list', qw(filter sort_by sort_dir),
+    ],
+    db_args => $db_args,
+  );
+
+  $self->{orderitems} = SL::DB::Manager::OrderItem->get_all(%$db_args);
+
+  $self->list_objects;
+}
+
+# private functions
+
+sub setup_for_list {
+  my ($self, %params) = @_;
+  $self->{filter} = {};
+  my %args = (
+    parse_filter(
+      $self->_pre_parse_filter($::form->{filter}, $self->{filter}),
+      with_objects => [ 'order', 'order.customer', 'part' ],
+      launder_to => $self->{filter},
+    ),
+    sort_by => $self->set_sort_params(%params),
+    page    => $params{page},
+  );
+
+  $args{query} = [ @{ $args{query} || [] },
+    (
+      'order.customer_id' => { gt => 0 },
+      'order.closed' => 0,
+      or => [ 'order.quotation' => 0, 'order.quotation' => undef ],
+
+      # filter by shipped_qty < qty, read from innermost to outermost
+      'id' => [ \"
+        -- 3. resolve the desired information about those
+        SELECT oi.id FROM (
+          -- 2. slice only part, orderitem and both quantities from it
+          SELECT parts_id, trans_id, qty, SUM(doi_qty) AS doi_qty FROM (
+            -- 1. join orderitems and deliverorder items via record_links.
+            --    also add customer data to filter for sales_orders
+            SELECT oi.parts_id, oi.trans_id, oi.id, oi.qty, doi.qty AS doi_qty
+            FROM orderitems oi, oe, record_links rl, delivery_order_items doi
+            WHERE
+              oe.id = oi.trans_id AND
+              oe.customer_id IS NOT NULL AND
+              (oe.quotation = 'f' OR oe.quotation IS NULL) AND
+              NOT oe.closed AND
+              rl.from_id = oe.id AND
+              rl.from_id = oi.trans_id AND
+              oe.id = oi.trans_id AND
+              rl.from_table = 'oe' AND
+              rl.to_table = 'delivery_orders' AND
+              rl.to_id = doi.delivery_order_id AND
+              oi.parts_id = doi.parts_id
+          ) tuples GROUP BY parts_id, trans_id, qty
+        ) partials
+        LEFT JOIN orderitems oi ON partials.parts_id = oi.parts_id AND partials.trans_id = oi.trans_id
+        WHERE oi.qty > doi_qty
+
+        UNION ALL
+
+        -- 4. since the join over record_links fails for sales_orders wihtout any delivery order
+        --    retrieve those without record_links at all
+        SELECT oi.id FROM orderitems oi, oe
+        WHERE
+          oe.id = oi.trans_id AND
+          oe.customer_id IS NOT NULL AND
+          (oe.quotation = 'f' OR oe.quotation IS NULL) AND
+          NOT oe.closed AND
+          oi.trans_id NOT IN (
+            SELECT from_id
+            FROM record_links rl
+            WHERE
+              rl.from_table ='oe' AND
+              rl.to_table = 'delivery_orders'
+          )
+      " ],
+    )
+  ];
+
+  return \%args;
+}
+
+sub set_sort_params {
+  my ($self, %params) = @_;
+  my $sort_str;
+  ($self->{sort_by}, $self->{sort_dir}, $sort_str) =
+    SL::DB::Manager::OrderItem->make_sort_string(%params);
+  return $sort_str;
+}
+
+sub prepare_report {
+  my ($self, %params) = @_;
+
+  my $objects  = $params{objects} || [];
+  my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
+  $self->{report} = $report;
+
+  my @columns  = qw(reqdate customer ordnumber partnumber description qty shipped_qty);
+  my @visible  = qw(reqdate partnumber description qty shipped_qty ordnumber customer);
+  my @sortable = qw(reqdate partnumber description                 ordnumber customer);
+
+  my %column_defs = (
+    reqdate                 => { text => $::locale->text('Reqdate'),
+                                  sub => sub { $_[0]->reqdate_as_date || $_[0]->order->reqdate_as_date }},
+    description             => { text => $::locale->text('Description'),
+                                  sub => sub { $_[0]->description },
+                             obj_link => sub { $self->link_to($_[0]->part) }},
+    partnumber              => { text => $::locale->text('Part Number'),
+                                  sub => sub { $_[0]->part->partnumber },
+                             obj_link => sub { $self->link_to($_[0]->part) }},
+    qty                     => { text => $::locale->text('Qty'),
+                                  sub => sub { $_[0]->qty_as_number . ' ' . $_[0]->unit }},
+    missing                 => { text => $::locale->text('Missing qty'),
+                                  sub => sub { $::form->format_amount(\%::myconfig, $_[0]->qty - $_[0]->shipped_qty, 2) . ' ' . $_[0]->unit }},
+    shipped_qty             => { text => $::locale->text('shipped'),
+                                  sub => sub { $::form->format_amount(\%::myconfig, $_[0]->shipped_qty, 2) . ' ' . $_[0]->unit }},
+    ordnumber               => { text => $::locale->text('Order'),
+                                  sub => sub { $_[0]->order->ordnumber },
+                             obj_link => sub { $self->link_to($_[0]->order) }},
+    customer                => { text => $::locale->text('Customer'),
+                                  sub => sub { $_[0]->order->customer->name },
+                             obj_link => sub { $self->link_to($_[0]->order->customer) }},
+  );
+
+
+  for my $col (@sortable) {
+    $column_defs{$col}{link} = $self->url_for(
+      action   => 'list',
+      sort_by  => $col,
+      sort_dir => ($self->{sort_by} eq $col ? 1 - $self->{sort_dir} : $self->{sort_dir}),
+      page     => $self->{pages}{cur},
+      %{ $self->{flat_filter} },
+    );
+  }
+
+  map { $column_defs{$_}->{visible} = 1 } @visible;
+
+  $report->set_columns(%column_defs);
+  $report->set_column_order(@columns);
+  $report->set_options(allow_pdf_export => 1, allow_csv_export => 1);
+  $report->set_sort_indicator(%params);
+  $report->set_export_options(@{ $params{report_generator_export_options} || [] });
+  $report->set_options(
+    %{ $params{report_generator_options} || {} },
+    output_format        => 'HTML',
+    top_info_text        => $::locale->text('Delivery Plan for currently outstanding sales orders'),
+    title                => $::locale->text('Delivery Plan'),
+  );
+  $report->set_options_from_form;
+
+  SL::DB::Manager::OrderItem->disable_paginating(args => $params{db_args}) if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
+
+  $self->{report_data} = {
+    column_defs => \%column_defs,
+    columns     => \@columns,
+    visible     => \@visible,
+    sortable    => \@sortable,
+  };
+}
+
+sub list_objects {
+  my ($self) = @_;
+  my $column_defs = $self->{report_data}{column_defs};
+  for my $obj (@{ $self->{orderitems} || [] }) {
+    $self->{report}->add_data({
+      map {
+        $_ => {
+          data => $column_defs->{$_}{sub} ? $column_defs->{$_}{sub}->($obj)
+                : $obj->can($_)           ? $obj->$_
+                :                           $obj->{$_},
+          link => $column_defs->{$_}{obj_link} ? $column_defs->{$_}{obj_link}->($obj) : '',
+        },
+      } @{ $self->{report_data}{columns} || {} }
+    });
+  }
+
+  return $self->{report}->generate_with_headers;
+}
+
+sub make_filter_summary {
+  my ($self) = @_;
+
+  my $filter = $::form->{filter} || {};
+  my @filter_strings;
+
+  my @filters = (
+    [ $filter->{order}{"ordnumber:substr::ilike"}, $::locale->text('Number') ],
+    [ $filter->{part}{"partnumber:substr::ilike"}, $::locale->text('Part Number') ],
+    [ $filter->{"description:substr::ilike"}, $::locale->text('Part Description') ],
+    [ $filter->{"reqdate:date::ge"}, $::locale->text('Delivery Date') . " " . $::locale->text('From Date') ],
+    [ $filter->{"reqdate:date::le"}, $::locale->text('Delivery Date') . " " . $::locale->text('To Date') ],
+    [ $filter->{"qty:number"}, $::locale->text('Quantity') ],
+    [ $filter->{order}{customer}{"name:substr::ilike"}, $::locale->text('Customer') ],
+    [ $filter->{order}{customer}{"customernumber:substr::ilike"}, $::locale->text('Customer Number') ],
+  );
+
+  my @flags = (
+    [ $filter->{part}{type}{part}, $::locale->text('Parts') ],
+    [ $filter->{part}{type}{service}, $::locale->text('Services') ],
+    [ $filter->{part}{type}{assembly}, $::locale->text('Assemblies') ],
+  );
+
+  for (@flags) {
+    push @filter_strings, "$_->[1]" if $_->[0];
+  }
+  for (@filters) {
+    push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
+  }
+
+  $self->{filter_summary} = join ', ', @filter_strings;
+}
+
+sub link_to {
+  my ($self, $object, %params) = @_;
+
+  return unless $object;
+  my $action = $params{action} || 'edit';
+
+  if ($object->isa('SL::DB::Order')) {
+    my $type   = $object->type;
+    my $vc     = $object->is_sales ? 'customer' : 'vendor';
+    my $id     = $object->id;
+
+    return "oe.pl?action=$action&type=$type&vc=$vc&id=$id";
+  }
+  if ($object->isa('SL::DB::Part')) {
+    my $id     = $object->id;
+    return "ic.pl?action=$action&id=$id";
+  }
+  if ($object->isa('SL::DB::Customer')) {
+    my $id     = $object->id;
+    return "ct.pl?action=$action&id=$id&db=customer";
+  }
+}
+
+# unfortunately ParseFilter can't handle compount filters.
+# so we clone the original filter (still need that for serializing)
+# rip out the options we know an replace them with the compound options.
+# ParseFilter will take care of the prefixing then.
+sub _pre_parse_filter {
+  my ($self, $orig_filter, $launder_to) = @_;
+
+  return undef unless $orig_filter;
+
+  my $filter = clone($orig_filter);
+  if ($filter->{part} && $filter->{part}{type}) {
+    $launder_to->{part}{type} = delete $filter->{part}{type};
+    my @part_filters = grep $_, map {
+      $launder_to->{part}{type}{$_} ? SL::DB::Manager::Part->type_filter($_) : ()
+    } qw(part service assembly);
+
+    push @{ $filter->{and} }, or => [ @part_filters ] if @part_filters;
+  }
+
+  for my $op (qw(le ge)) {
+    if ($filter->{"reqdate:date::$op"}) {
+      $launder_to->{"reqdate_date__$op"} = delete $filter->{"reqdate:date::$op"};
+      my $parsed_date = DateTime->from_lxoffice($launder_to->{"reqdate_date__$op"});
+      push @{ $filter->{and} }, or => [
+        'reqdate' => { $op => $parsed_date },
+        and => [
+          'reqdate' => undef,
+          'order.reqdate' => { $op => $parsed_date },
+        ]
+      ] if $parsed_date;
+    }
+  }
+
+  if (my $style = delete $filter->{searchstyle}) {
+    $self->{searchstyle}       = $style;
+    $launder_to->{searchstyle} = $style;
+  }
+
+  return $filter;
+}
+
+1;
index 7c393b0..3bf1b65 100644 (file)
@@ -20,11 +20,13 @@ my %filters = (
 );
 
 my %methods = (
-  lt     => sub { +{ lt    => $_[0] } },
-  gt     => sub { +{ gt    => $_[0] } },
-  ilike  => sub { +{ ilike => $_[0] } },
-  like   => sub { +{ like  => $_[0] } },
   enable => sub { ;;;; },
+  map {
+    # since $_ is an alias it can't be used in a closure. even "".$_ or "$_"
+    # does not work, we need a real copy.
+    my $_copy = "$_";
+    $_   => sub { +{ $_copy    => $_[0] } },
+  } qw(similar match imatch regex regexp like ilike rlike is is_not ne eq lt gt le ge),
 );
 
 sub parse_filter {
diff --git a/SL/Controller/SellPriceInformation.pm b/SL/Controller/SellPriceInformation.pm
new file mode 100644 (file)
index 0000000..522f101
--- /dev/null
@@ -0,0 +1,212 @@
+package SL::Controller::SellPriceInformation;
+
+use strict;
+use parent qw(SL::Controller::Base);
+
+use Clone qw(clone);
+use SL::DB::OrderItem;
+use SL::Controller::Helper::ParseFilter;
+use SL::Controller::Helper::ReportGenerator;
+
+sub action_list {
+  my ($self) = @_;
+  $self->{action} = 'list';
+
+  my %list_params = (
+    sort_by  => $::form->{sort_by} || 'reqdate',
+    sort_dir => $::form->{sort_dir},
+    filter   => $::form->{filter},
+    page     => $::form->{page},
+  );
+
+  my $db_args = $self->setup_for_list(%list_params);
+  $self->{pages} = SL::DB::Manager::OrderItem->paginate(%list_params, args => $db_args, per_page => 10);
+
+  my $bottom = $::form->parse_html_template('price_information/report_bottom', { SELF => $self });
+
+  $self->prepare_report(
+    report_generator_options => {
+      raw_bottom_info_text => $bottom,
+      controller_class     => 'SellPriceInformation',
+    },
+    db_args => $db_args,
+  );
+
+  $self->{orderitems} = SL::DB::Manager::OrderItem->get_all(%$db_args);
+
+  $self->list_objects;
+}
+
+# private functions
+
+sub setup_for_list {
+  my ($self, %params) = @_;
+  $self->{filter} = _pre_parse_filter($params{filter});
+
+  my %args = (
+    parse_filter($self->{filter},
+      with_objects => [ 'order', 'order.customer', 'part' ],
+    ),
+    sort_by => $self->set_sort_params(%params),
+    page    => $params{page},
+  );
+
+  return \%args;
+}
+
+sub set_sort_params {
+  my ($self, %params) = @_;
+  my $sort_str;
+  ($self->{sort_by}, $self->{sort_dir}, $sort_str) =
+    SL::DB::Manager::OrderItem->make_sort_string(%params);
+  return $sort_str;
+}
+
+sub column_defs {
+  my ($self) = @_;
+  return {
+    transdate   => { text => $::locale->text('Date'),
+                      sub => sub { $_[0]->order->transdate_as_date }},
+    ordnumber   => { text => $::locale->text('Number'),
+                      sub => sub { $_[0]->order->number },
+                 obj_link => sub { $self->link_to($_[0]->order) }},
+    customer    => { text => $::locale->text('Customer'),
+                      sub => sub { $_[0]->order->customer->name },
+                 obj_link => sub { $self->link_to($_[0]->order->customer) }},
+    customer    => { text => $::locale->text('Customer'),
+                      sub => sub { $_[0]->order->customer->name },
+                 obj_link => sub { $self->link_to($_[0]->order->customer) }},
+    ship        => { text => $::locale->text('Delivered'),
+                      sub => sub { $::form->format_amount(\%::myconfig, $_[0]->shipped_qty) . ' ' . $_[0]->unit }},
+    qty         => { text => $::locale->text('Qty'),
+                      sub => sub { $_[0]->qty_as_number . ' ' . $_[0]->unit }},
+    sellprice   => { text => $::locale->text('Sell Price'),
+                      sub => sub { $_[0]->sellprice_as_number }},
+    discount    => { text => $::locale->text('Discount'),
+                      sub => sub { $_[0]->discount_as_percent . "%" }},
+    amount      => { text => $::locale->text('Amount'),
+                      sub => sub { $::form->format_amount(\%::myconfig, $_[0]->qty * $_[0]->sellprice * (1 - $_[0]->discount), 2) }},
+  };
+}
+
+sub prepare_report {
+  my ($self, %params) = @_;
+
+  my $objects  = $params{objects} || [];
+  my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
+  $self->{report} = $report;
+
+  my @columns  = qw(transdate ordnumber customer ship qty sellprice discount amount);
+  my @visible  = qw(transdate ordnumber customer ship qty sellprice discount amount);
+  my @sortable = qw(transdate ordnumber customer          sellprice discount       );
+
+  my $column_defs = $self->column_defs;
+
+  for my $col (@sortable) {
+    $column_defs->{$col}{link} = $self->self_url(
+      sort_by  => $col,
+      sort_dir => ($self->{sort_by} eq $col ? 1 - $self->{sort_dir} : $self->{sort_dir}),
+      page     => $self->{pages}{cur},
+    );
+  }
+
+  map { $column_defs->{$_}{visible} = 1 } @visible;
+
+  $report->set_columns(%$column_defs);
+  $report->set_column_order(@columns);
+  $report->set_options(allow_pdf_export => 0, allow_csv_export => 0);
+  $report->set_sort_indicator(%params);
+  $report->set_export_options(@{ $params{report_generator_export_options} || [] });
+  $report->set_options(
+    %{ $params{report_generator_options} || {} },
+    output_format        => 'HTML',
+    top_info_text        => $self->displayable_filter($::form->{filter}),
+    title                => $::locale->text('Sales Price information'),
+  );
+  $report->set_options_from_form;
+
+  $self->{report_data} = {
+    column_defs => $column_defs,
+    columns     => \@columns,
+    visible     => \@visible,
+    sortable    => \@sortable,
+  };
+}
+
+sub list_objects {
+  my ($self) = @_;
+  my $column_defs = $self->{report_data}{column_defs};
+  for my $obj (@{ $self->{orderitems} || [] }) {
+    $self->{report}->add_data({
+      map {
+        $_ => {
+          data => $column_defs->{$_}{sub} ? $column_defs->{$_}{sub}->($obj)
+                : $obj->can($_)           ? $obj->$_
+                :                           $obj->{$_},
+          link => $column_defs->{$_}{obj_link} ? $column_defs->{$_}{obj_link}->($obj) : '',
+        },
+      } @{ $self->{report_data}{columns} || {} }
+    });
+  }
+
+  return $self->{report}->generate_with_headers;
+}
+
+sub link_to {
+  my ($self, $object, %params) = @_;
+
+  return unless $object;
+  my $action = $params{action} || 'edit';
+
+  if ($object->isa('SL::DB::Order')) {
+    my $type   = $object->type;
+    my $vc     = $object->is_sales ? 'customer' : 'vendor';
+    my $id     = $object->id;
+
+    return "oe.pl?action=$action&type=$type&vc=$vc&id=$id";
+  }
+  if ($object->isa('SL::DB::Customer')) {
+    my $id     = $object->id;
+    return "ct.pl?action=$action&id=$id&db=customer";
+  }
+}
+
+sub _pre_parse_filter {
+  my $filter = clone(shift);
+
+  if (  exists $filter->{order}
+     && exists $filter->{order}{type}) {
+     push @{ $filter->{and} }, SL::DB::Manager::Order->type_filter(delete $filter->{order}{type}, "order."),
+  }
+
+  return $filter;
+}
+
+sub displayable_filter {
+  my ($self, $filter) = @_;
+
+  my $column_defs = $self->column_defs;
+  my @texts;
+
+  push @texts, [ $::locale->text('Type'),    $::locale->text($filter->{order}{type}) ] if $filter->{order}{type};
+  push @texts, [ $::locale->text('Sort By'), $column_defs->{$self->{sort_by}}{text}  ] if $column_defs->{$self->{sort_by}}{text};
+  push @texts, [ $::locale->text('Page'),    $::locale->text($self->{pages}{cur})    ] if $self->{pages}{cur} != 1;
+
+  return join '; ', map { "$_->[0]: $_->[1]" } @texts;
+}
+
+sub self_url {
+  my ($self, %params) = @_;
+  %params = (
+    action   => $self->{action},
+    sort_by  => $self->{sort_by},
+    sort_dir => $self->{sort_dir},
+    page     => $self->{pages}{cur},
+    filter   => $::form->{filter},
+    %params,
+  );
+
+  return $self->url_for(%params);
+}
+
+1;
index fc2b796..ff8ece2 100644 (file)
@@ -10,13 +10,14 @@ sub object_class { 'SL::DB::Order' }
 __PACKAGE__->make_manager_methods;
 
 sub type_filter {
-  my $class = shift;
-  my $type  = lc(shift || '');
-
-  return (and => [ '!customer_id' => undef,         quotation => 1                       ]) if $type eq 'sales_quotation';
-  return (and => [ '!vendor_id'   => undef,         quotation => 1                       ]) if $type eq 'request_quotation';
-  return (and => [ '!customer_id' => undef, or => [ quotation => 0, quotation => undef ] ]) if $type eq 'sales_order';
-  return (and => [ '!vendor_id'   => undef, or => [ quotation => 0, quotation => undef ] ]) if $type eq 'purchase_order';
+  my $class  = shift;
+  my $type   = lc(shift || '');
+  my $prefix = shift || '';
+
+  return (and => [ "!${prefix}customer_id" => undef,         "${prefix}quotation" => 1                       ]) if $type eq 'sales_quotation';
+  return (and => [ "!${prefix}vendor_id"   => undef,         "${prefix}quotation" => 1                       ]) if $type eq 'request_quotation';
+  return (and => [ "!${prefix}customer_id" => undef, or => [ "${prefix}quotation" => 0, "${prefix}quotation" => undef ] ]) if $type eq 'sales_order';
+  return (and => [ "!${prefix}vendor_id"   => undef, or => [ "${prefix}quotation" => 0, "${prefix}quotation" => undef ] ]) if $type eq 'purchase_order';
 
   die "Unknown type $type";
 }
diff --git a/SL/DB/MetaSetup/Taxkey.pm b/SL/DB/MetaSetup/Taxkey.pm
deleted file mode 100644 (file)
index 558163f..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-# This file has been auto-generated. Do not modify it; it will be overwritten
-# by rose_auto_create_model.pl automatically.
-package SL::DB::Taxkey;
-
-use strict;
-
-use base qw(SL::DB::Object);
-
-__PACKAGE__->meta->setup(
-  table   => 'taxkeys',
-
-  columns => [
-    id        => { type => 'integer', not_null => 1, sequence => 'id' },
-    chart_id  => { type => 'integer' },
-    tax_id    => { type => 'integer' },
-    taxkey_id => { type => 'integer' },
-    pos_ustva => { type => 'integer' },
-    startdate => { type => 'date' },
-  ],
-
-  primary_key_columns => [ 'id' ],
-
-  foreign_keys => [
-    tax => {
-      class       => 'SL::DB::Tax',
-      key_columns => { tax_id => 'id' },
-    },
-  ],
-);
-
-1;
-;
index e28a87c..8212a64 100644 (file)
@@ -149,6 +149,19 @@ sub convert_to_invoice {
   return $invoice;
 }
 
+sub number {
+  my $self = shift;
+
+  my %number_method = (
+    sales_order       => 'ordnumber',
+    sales_quotation   => 'quonumber',
+    puchase_order     => 'ordnumber',
+    request_quotation => 'quonumber',
+  );
+
+  return $self->${ \ $number_method{$self->type} }(@_);
+}
+
 1;
 
 __END__
index 6efee00..f24abca 100644 (file)
@@ -2,6 +2,9 @@ package SL::DB::OrderItem;
 
 use strict;
 
+use List::Util qw(sum);
+use SL::AM;
+
 use SL::DB::MetaSetup::OrderItem;
 use SL::DB::Helper::CustomVariables (
   sub_module  => 'orderitems',
@@ -44,6 +47,15 @@ sub is_price_update_available {
   return $self->origprice > $self->part->sellprice;
 }
 
+sub shipped_qty {
+  my ($self) = @_;
+
+  my $d_orders = $self->order->linked_records(direction => 'to', to => 'SL::DB::DeliveryOrder');
+  my @doi      = grep { $_->parts_id == $self->parts_id } map { $_->orderitems } @$d_orders;
+
+  return sum(map { AM->convert_unit($_->unit => $self->unit) * $_->qty } @doi);
+}
+
 package SL::DB::Manager::OrderItem;
 
 use SL::DB::Helper::Paginated;
@@ -57,7 +69,11 @@ sub _sort_spec {
                         ordnumber     => [ 'order.ordnumber'      ],
                         customer      => [ 'lower(customer.name)', ],
                         position      => [ 'trans_id', 'runningnumber' ],
-                        transdate     => [ 'transdate', 'lower(order.reqdate::text)' ],
+                        reqdate       => [ 'COALESCE(orderitems.reqdate, order.reqdate)' ],
+                        orddate       => [ 'order.orddate' ],
+                        sellprice     => [ 'sellprice' ],
+                        discount      => [ 'discount' ],
+                        transdate     => [ 'transdate::date', 'order.reqdate' ],
                       },
            default => [ 'position', 1 ],
            nulls   => { }
index 4a4ac25..5fc33ce 100644 (file)
@@ -19,6 +19,14 @@ sub conv_i {
   return (defined($value) && "$value" ne "") ? $value * 1 : $default;
 }
 
+# boolean escape
+sub conv_b {
+  my ($value, $default) = @_;
+  return !defined $value && defined $default ? $default
+       :          $value                     ? 't'
+       :                                       'f';
+}
+
 sub conv_date {
   my ($value) = @_;
   return (defined($value) && "$value" ne "") ? $value : undef;
@@ -307,16 +315,29 @@ sub add_token {
   my %params = @_;
   my $col    = $params{col};
   my $val    = $params{val};
-  my $method = $params{method} || '=';
   my $escape = $params{esc} || sub { $_ };
+  my $method = $params{esc} =~ /^start|end|substr$/ ? 'ILIKE' : $params{method} || '=';
 
   $val = [ $val ] unless ref $val eq 'ARRAY';
 
   my %escapes = (
     id     => \&conv_i,
+    bool   => \&conv_b,
     date   => \&conv_date,
+    start  => sub { $_[0] . '%' },
+    end    => sub { '%' . $_[0] },
+    substr => sub { '%' . $_[0] . '%' },
   );
 
+  my $_long_token = sub {
+    my $op = shift;
+    sub {
+      my $col = shift;
+      return scalar @_ ? join ' OR ', ("$col $op ?") x scalar @_,
+           :             undef;
+    }
+  };
+
   my %methods = (
     '=' => sub {
       my $col = shift;
@@ -324,6 +345,7 @@ sub add_token {
            : scalar @_ == 1 ? sprintf '%s = ?',     $col
            :                  undef;
     },
+    map({ $_ => $_long_token->($_) } qw(LIKE ILIKE >= <= > <)),
   );
 
   $method = $methods{$method} || $method;
index 3100ac6..935d131 100644 (file)
@@ -11,6 +11,7 @@ BEGIN {
 @required_modules = (
   { name => "parent",                              url => "http://search.cpan.org/~corion/",    debian => 'libparent-perl' },
   { name => "Archive::Zip",    version => '1.16',  url => "http://search.cpan.org/~adamk/",     debian => 'libarchive-zip-perl' },
+  { name => "Clone",                               url => "http://search.cpan.org/~rdf/",       debian => 'libclone-perl' },
   { name => "Config::Std",                         url => "http://search.cpan.org/~dconway/",   debian => 'libconfig-std-perl' },
   { name => "DateTime",                            url => "http://search.cpan.org/~drolsky/",   debian => 'libdatetime-perl' },
   { name => "DBI",             version => '1.50',  url => "http://search.cpan.org/~timb/",      debian => 'libdbi-perl' },
@@ -36,6 +37,8 @@ BEGIN {
 
 @optional_modules = (
   { name => "Digest::SHA",                         url => "http://search.cpan.org/~mshelor/",   debian => 'libdigest-sha-perl' },
+  { name => "IO::Socket::SSL",                     url => "http://search.cpan.org/~sullr/",     debian => 'libio-socket-ssl-perl' },
+  { name => "Net::LDAP",                           url => "http://search.cpan.org/~gbarr/",     debian => 'libnet-ldap-perl' },
 );
 
 @developer_modules = (
@@ -97,7 +100,7 @@ my %conditional_dependencies;
 sub check_for_conditional_dependencies {
   return if $conditional_dependencies{net_ldap}++;
 
-  push @required_modules, { 'name' => 'Net::LDAP', 'url' => 'http://search.cpan.org/~gbarr/' }
+  push @required_modules, grep { $_->{name} eq 'Net::LDAP' } @optional_modules
     if $::lx_office_conf{authentication} && ($::lx_office_conf{authentication}->{module} eq 'LDAP');
 }
 
index 51ad991..3cd462c 100644 (file)
@@ -2,27 +2,51 @@ package SL::LxOfficeConf;
 
 use strict;
 
-use Config::Std;
-use Encode;
-
 my $environment_initialized;
 
+sub safe_require {
+  my ($class, $may_fail);
+  my $failed;
+  $failed = !eval {
+    require Config::Std;
+    require Encode;
+  };
+
+  if ($failed) {
+    if ($may_fail) {
+      warn $@;
+      return 0;
+    } else {
+      die $@;
+    }
+  }
+
+  Config::Std->import;
+  Encode->import;
+
+  return 1;
+}
+
 sub read {
-  my ($class, $file_name) = @_;
+  my ($class, $file_name, $may_fail) = @_;
 
-  read_config 'config/lx_office.conf.default' => %::lx_office_conf;
+  return unless $class->safe_require($may_fail);
+
+  read_config('config/lx_office.conf.default' => \%::lx_office_conf);
   _decode_recursively(\%::lx_office_conf);
 
   $file_name ||= 'config/lx_office.conf';
 
   if (-f $file_name) {
-    read_config $file_name => my %local_conf;
+    read_config($file_name => \ my %local_conf);
     _decode_recursively(\%local_conf);
     _flat_merge(\%::lx_office_conf, \%local_conf);
   }
 
   _init_environment();
   _determine_application_paths();
+
+  return 1;
 }
 
 sub _decode_recursively {
index 3b8262f..6466287 100644 (file)
@@ -6,12 +6,13 @@ use SL::Common;
 use SL::MoreCommon qw(uri_encode uri_decode);
 use List::Util qw(first max min sum);
 use List::MoreUtils qw(all any apply);
+use Exporter qw(import);
 
-sub _store_value {
-  $::lxdebug->enter_sub(2);
+our @EXPORT_OK = qw(flatten unflatten read_cgi_input);
 
+sub _store_value {
   my ($target, $key, $value) = @_;
-  my @tokens = split /((?:\[\+?\])?(?:\.|$))/, $key;
+  my @tokens = split /((?:\[\+?\])?(?:\.)|(?:\[\+?\]))/, $key;
   my $curr;
 
   if (scalar @tokens) {
@@ -22,7 +23,9 @@ sub _store_value {
     my $sep = shift @tokens;
     my $key = shift @tokens;
 
-    $curr = \ $$curr->[++$#$$curr], next if $sep eq '[]';
+    $curr = \ $$curr->[$#$$curr], next   if $sep eq '[]' && @tokens;
+    $curr = \ $$curr->[++$#$$curr], next if $sep eq '[]' && !@tokens;
+    $curr = \ $$curr->[++$#$$curr], next if $sep eq '[+]';
     $curr = \ $$curr->[max 0, $#$$curr]  if $sep eq '[].';
     $curr = \ $$curr->[++$#$$curr]       if $sep eq '[+].';
     $curr = \ $$curr->{$key}
@@ -30,8 +33,6 @@ sub _store_value {
 
   $$curr = $value;
 
-  $::lxdebug->leave_sub(2);
-
   return $curr;
 }
 
@@ -249,20 +250,216 @@ sub read_cgi_input {
   return $target;
 }
 
+sub flatten {
+  my ($source, $target, $prefix, $in_array) = @_;
+  $target ||= [];
+
+  # there are two edge cases that need attention. first: more than one hash
+  # inside an array.  only the first of each nested can have a [+].  second: if
+  # an array contains mixed values _store_value will rely on autovivification.
+  # so any type change must have a [+]
+  # this closure decides one recursion step AFTER an array has been found if a
+  # [+] needs to be generated
+  my $arr_prefix = sub {
+    return $_[0] ? '[+]' : '[]' if $in_array;
+    return '';
+  };
+
+  for (ref $source) {
+    /^HASH$/ && do {
+      my $first = 1;
+      for my $key (keys %$source) {
+        flatten($source->{$key} => $target, (defined $prefix ? $prefix . $arr_prefix->($first) . '.' : '') . $key);
+        $first = 0;
+      };
+      next;
+    };
+    /^ARRAY$/ && do {
+      for my $i (0 .. $#$source) {
+        flatten($source->[$i] => $target, $prefix . $arr_prefix->($i == 0), '1');
+      }
+      next;
+    };
+    !$_ && do {
+      die "can't flatten a pure scalar" unless defined $prefix;
+      push @$target, [ $prefix . $arr_prefix->(0) => $source ];
+      next;
+    };
+    die "unrecognized reference of a data structure $_. cannot serialize refs, globs and code yet. to serialize Form please use the method there";
+  }
+
+  return $target;
+}
+
+
+sub unflatten {
+  my ($data, $target) = @_;
+  $target ||= {};
+
+  for my $pair (@$data) {
+    _store_value($target, @$pair) if defined $pair->[0];
+  }
+
+  return $target;
+}
+
 1;
 
 __END__
 
 =head1 NAME
 
-SL::Form.pm - main data object.
+SL::Request.pm - request parsing and data serialization
 
 =head1 SYNOPSIS
 
-This module handles unpacking of cgi parameters. usually you donÄt want to call
-anything in here directly,
+This module handles unpacking of cgi parameters. usually you don't want to call
+anything in here directly.
+
+  use SL::Request qw(read_cgi_input);
+
+  # read cgi input depending on request type, unflatten and recode
+  read_cgi_input($target_hash_ref);
+
+  # $hashref and $new_hashref should be identical
+  my $new_arrayref = flatten($hashref);
+  my $new_hashref  = unflatten($new_arrayref);
+
+
+=head1 DESCRIPTION
+
+This module handles flattening and unflattening of data for request
+roundtrip purposes. Lx-Office uses the format as described below:
+
+=over 4
+
+=item Hashes
+
+Hash entries will be connected with a dot (C<.>). A simple hash like this
+
+  order => {
+    item     => 2,
+    customer => 5
+  }
+
+will be serialized to
+
+  [ order.item     => 2 ],
+  [ order.customer => 5 ],
+
+=item Arrays
+
+Arrays will by trailing empty brackets (C<[]>). An hash like this
+
+  selected_id => [ 2, 6, 8, 9 ]
+
+will be flattened to
+
+  [ selected_id[] => 2 ],
+  [ selected_id[] => 6 ],
+  [ selected_id[] => 8 ],
+  [ selected_id[] => 9 ],
+
+Since this will produce identical keys, the resulting flattened list can not be
+used as a hash. It is however very easy to use this in a template to generate
+input:
+
+  [% FOREACH id = selected_ids %]
+    <input type="hidden" name="selected_id[]" value="[% id | html %]">
+  [% END %]
+
+=item Nested structures
+
+A special version of this are nested hashs in an array, which is very common.
+The combined operator (C<[].>) will be used. As a special case, every time a new
+array slice is started, the special convention (C<[+].>) will be used. Again this
+is because it's easy to write a template with it.
+
+So this
+
+  order => {
+    orderitems => [
+      {
+        id   => 1,
+        part => 15
+      },
+      {
+        id   => 2,
+        part => 7
+      },
+    ]
+  }
+
+will be
+
+  [ order.orderitems[+].id  => 1  ],
+  [ order.orderitems[].part => 15 ],
+  [ order.orderitems[+].id  => 2  ],
+  [ order.orderitems[].part => 7  ],
+
+=item Limitations
+
+  The format currently does have certain limitations when compared to other
+  serialization formats.
+
+=over 4
+
+=item Order
+
+The order of serialized values matters to reconstruct arrays properly. This
+should rarely be a problem if you just flatten and dump into a url or a field
+of hiddens.
+
+=item Empty Keys
+
+The current implementation of flatten does produce correct serialization of
+empty keys, but unflatten is unable to resolve these. Do no use C<''> or
+C<undef> as keys. C<0> is fine.
+
+=item Key Escaping
+
+You cannot use the tokens C<[]>, C<[+]> and C<.> in keys. No way around it.
+
+=item Sparse Arrays
+
+It is not possible to serialize somehing like
+
+  sparse_array => do { my $sa = []; $sa[100] = 1; $sa },
+
+This is a feature, as perl doesn't do well with very large arrays.
+
+=item Recursion
+
+There is currently no support nor prevention for flattening a circular structure.
+
+=item Custom Delimiter
+
+No support for other delimiters, sorry.
+
+=item Other References
+
+No support for globs, scalar refs, code refs, filehandles and the like. These will die.
+
+=back
+
+=back
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<flatten HASHREF [ ARRAYREF ]>
+
+This function will flatten the provided hash ref into the provided array ref.
+The array ref may be non empty, but will be changed in this case.
+
+Return value is the flattened array ref.
+
+=item C<unflatten ARRAYREF [ HASHREF ]>
+
+This function will parse the array ref, and will store the contents into the hash ref. The hash ref may be non empty, in this case any new keys will override the old ones only on leafs with same type. Type changes on a node will die.
 
-  SL::Request::read_cgi_input($target_hash_ref);
+=back
 
 =head1 SPECIAL FUNCTIONS
 
@@ -271,7 +468,7 @@ anything in here directly,
 parses a complex var name, and stores it in the form.
 
 syntax:
-  $form->_store_value($key, $value);
+  _store_value($target, $key, $value);
 
 keys must start with a string, and can contain various tokens.
 supported key structures are:
index f7ffc11..0b4967d 100644 (file)
@@ -342,6 +342,36 @@ autocomplete_customer('#$name_e\_name');
 JS
 }
 
+# simple version with select_tag
+sub vendor_selector {
+  my ($self, $name, $value, %params) = @_;
+
+  my $actual_vendor_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"}) ? $::form->{"$name"}->id : $::form->{"$name"}) :
+                         (ref $value && $value->can('id')) ? $value->id : '';
+  my $options_str = $self->options_for_select(SL::DB::Manager::Vendor->get_all(),
+                                              default      => $actual_vendor_id,
+                                              title_sub    => sub { $_[0]->vendornumber . " : " . $_[0]->name },
+                                              'with_empty' => 1);
+  
+  return $self->select_tag($name, $options_str, %params);
+}
+
+
+# simple version with select_tag
+sub part_selector {
+  my ($self, $name, $value, %params) = @_;
+
+  my $actual_part_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"})? $::form->{"$name"}->id : $::form->{"$name"}) :
+                       (ref $value && $value->can('id')) ? $value->id : '';
+  my $options_str = $self->options_for_select(SL::DB::Manager::Part->get_all(),
+                                              default      => $actual_part_id,
+                                              title_sub    => sub { $_[0]->partnumber . " : " . $_[0]->description },
+                                              'with_empty' => 1);
+  
+  return $self->select_tag($name, $options_str, %params);
+}
+
+
 sub javascript_tag {
   my $self = shift;
   my $code = '';
diff --git a/VERSION b/VERSION
index 24ba9a3..dd8693a 100644 (file)
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.7.0
+2.7.1-unstable
index ea023b2..343350a 100644 (file)
@@ -55,11 +55,14 @@ our %translations = ('text'      => $locale->text('Free-form text'),
                      'bool'      => $locale->text('Yes/No (Checkbox)'),
                      'select'    => $locale->text('Selection'),
                      'customer'  => $locale->text('Customer'),
+                     'vendor'    => $locale->text('Vendor'),
+                     'part'      => $locale->text('Part'),
                      );
 
-our @types = qw(text textfield number date bool select customer); # timestamp
+our @types = qw(text textfield number date bool select customer vendor part); # timestamp
 
 our @modules = ({ module => 'CT',       description => $locale->text('Customers and vendors')          },
+                { module => 'Contacts', description => $locale->text('Contact persons')                },
                 { module => 'IC',       description => $locale->text('Parts, services and assemblies') },
                 { module => 'Projects', description => $locale->text('Projects')                       },
                );
index 9e814e1..56fad89 100644 (file)
@@ -49,6 +49,7 @@ use POSIX qw(strftime);
 
 use SL::CT;
 use SL::CVar;
+use SL::Request qw(flatten);
 use SL::DB::Business;
 use SL::DB::Default;
 use SL::Helper::Flash;
@@ -110,6 +111,24 @@ sub search {
   $main::lxdebug->leave_sub();
 }
 
+sub search_contact {
+  $::lxdebug->enter_sub;
+  $::auth->assert('customer_vendor_edit');
+
+  $::form->{CUSTOM_VARIABLES}                  = CVar->get_configs('module' => 'Contacts');
+  ($::form->{CUSTOM_VARIABLES_FILTER_CODE},
+   $::form->{CUSTOM_VARIABLES_INCLUSION_CODE}) = CVar->render_search_options('variables'    => $::form->{CUSTOM_VARIABLES},
+                                                                           'include_prefix' => 'l.',
+                                                                           'filter_prefix'  => 'filter.',
+                                                                           'include_value'  => 'Y');
+
+  $::form->{title} = $::locale->text('Search contacts');
+  $::form->header;
+  print $::form->parse_html_template('ct/search_contact');
+
+  $::lxdebug->leave_sub;
+}
+
 sub list_names {
   $main::lxdebug->enter_sub();
 
@@ -268,6 +287,121 @@ sub list_names {
   $main::lxdebug->leave_sub();
 }
 
+sub list_contacts {
+  $::lxdebug->enter_sub;
+  $::auth->assert('customer_vendor_edit');
+
+  $::form->{sortdir} = 1 unless defined $::form->{sortdir};
+
+  my @contacts     = CT->search_contacts(
+    search_term => $::form->{search_term},
+    filter      => $::form->{filter},
+  );
+
+  my $cvar_configs = CVar->get_configs('module' => 'Contacts');
+
+  my @columns      = qw(
+    cp_id vcname vcnumber cp_name cp_givenname cp_street cp_phone1 cp_phone2
+    cp_mobile1 cp_mobile2 cp_email cp_abteilung cp_birthday cp_gender
+  );
+
+  my @includeable_custom_variables = grep { $_->{includeable} } @{ $cvar_configs };
+  my @searchable_custom_variables  = grep { $_->{searchable} }  @{ $cvar_configs };
+  my %column_defs_cvars            = map { +"cvar_$_->{name}" => { 'text' => $_->{description} } } @includeable_custom_variables;
+
+  push @columns, map { "cvar_$_->{name}" } @includeable_custom_variables;
+
+  my @visible_columns;
+  if ($::form->{l}) {
+    @visible_columns = grep { $::form->{l}{$_} } @columns;
+    push @visible_columns, qw(cp_phone1 cp_phone2)   if $::form->{l}{cp_phone};
+    push @visible_columns, qw(cp_mobile1 cp_mobile2) if $::form->{l}{cp_mobile};
+  } else {
+   @visible_columns = qw(vcname vcnumber cp_name cp_givenname cp_phone1 cp_phone2 cp_mobile1 cp_email);
+  }
+
+  my %column_defs  = (
+    'cp_id'        => { 'text' => $::locale->text('ID'), },
+    'vcname'       => { 'text' => $::locale->text('Customer/Vendor'), },
+    'vcnumber'     => { 'text' => $::locale->text('Customer/Vendor Number'), },
+    'cp_name'      => { 'text' => $::locale->text('Name'), },
+    'cp_givenname' => { 'text' => $::locale->text('Given Name'), },
+    'cp_street'    => { 'text' => $::locale->text('Street'), },
+    'cp_phone1'    => { 'text' => $::locale->text('Phone1'), },
+    'cp_phone2'    => { 'text' => $::locale->text('Phone2'), },
+    'cp_mobile1'   => { 'text' => $::locale->text('Mobile1'), },
+    'cp_mobile2'   => { 'text' => $::locale->text('Mobile2'), },
+    'cp_email'     => { 'text' => $::locale->text('E-mail'), },
+    'cp_abteilung' => { 'text' => $::locale->text('Department'), },
+    'cp_birthday'  => { 'text' => $::locale->text('Birthday'), },
+    'cp_gender'    => { 'text' => $::locale->text('Gender'), },
+    %column_defs_cvars,
+  );
+
+  map { $column_defs{$_}->{visible} = 1 } @visible_columns;
+
+  my @hidden_variables  = (qw(search_term filter l));
+  my $hide_vars         = { map { $_ => $::form->{$_} } @hidden_variables };
+  my @hidden_nondefault = grep({ $::form->{$_} } @hidden_variables);
+  my $callback          = build_std_url('action=list_contacts', join '&', map { E($_->[0]) . '=' . E($_->[1]) } @{ flatten($hide_vars) });
+  $::form->{callback}     = "$callback&sort=" . E($::form->{sort});
+
+  map { $column_defs{$_}->{link} = "${callback}&sort=${_}&sortdir=" . ($::form->{sort} eq $_ ? 1 - $::form->{sortdir} : $::form->{sortdir}) } @columns;
+
+  $::form->{title} = $::locale->text('Contacts');
+
+  my $report     = SL::ReportGenerator->new(\%::myconfig, $::form);
+
+  my @options;
+  push @options, $::locale->text('Search term') . ': ' . $::form->{search_term} if $::form->{search_term};
+  for (qw(cp_name cp_givenname cp_title cp_email cp_abteilung cp_project)) {
+    push @options, $column_defs{$_}{text} . ': ' . $::form->{filter}{$_} if $::form->{filter}{$_};
+  }
+  if ($::form->{filter}{status}) {
+    push @options, $::locale->text('Status') . ': ' . (
+      $::form->{filter}{status} =~ /active/   ? $::locale->text('Active')   :
+      $::form->{filter}{status} =~ /orphaned/ ? $::locale->text('Orphaned') :
+      $::form->{filter}{status} =~ /all/      ? $::locale->text('All')      : ''
+    );
+  }
+
+
+  $report->set_options('top_info_text'       => join("\n", @options),
+                       'output_format'       => 'HTML',
+                       'title'               => $::form->{title},
+                       'attachment_basename' => $::locale->text('contact_list') . strftime('_%Y%m%d', localtime time),
+    );
+  $report->set_options_from_form;
+
+  $report->set_columns(%column_defs);
+  $report->set_column_order(@columns);
+
+  $report->set_export_options('list_contacts', @hidden_variables);
+
+  $report->set_sort_indicator($::form->{sort}, $::form->{sortdir});
+
+  CVar->add_custom_variables_to_report('module'         => 'Contacts',
+                                       'trans_id_field' => 'cp_id',
+                                       'configs'        => $cvar_configs,
+                                       'column_defs'    => \%column_defs,
+                                       'data'           => \@contacts);
+
+
+  foreach my $ref (@contacts) {
+    my $row = { map { $_ => { 'data' => $ref->{$_} } } @columns };
+
+    $row->{vcname}->{link}   = build_std_url('action=edit', 'id=' . E($ref->{vcid}), 'db=' . E($ref->{db}), 'callback', @hidden_nondefault);
+    $row->{vcnumber}->{link} = $row->{vcname}->{link};
+    $row->{cp_email}->{link} = 'mailto:' . E($ref->{cp_email});
+
+    $report->add_data($row);
+  }
+
+  $report->generate_with_headers;
+
+  $::lxdebug->leave_sub;
+}
+
 sub edit {
   $main::lxdebug->enter_sub();
 
@@ -353,9 +487,18 @@ sub form_header {
     $form->{currency} = $form->{curr};
   }
 
-  $form->{CUSTOM_VARIABLES} = CVar->get_custom_variables('module' => 'CT', 'trans_id' => $form->{id});
+  $::form->{CUSTOM_VARIABLES} = { };
+  my %specs = ( CT       => { field => 'id',    name_prefix => '',   },
+                Contacts => { field => 'cp_id', name_prefix => 'cp', },
+              );
+
+  for my $module (keys %specs) {
+    my $spec = $specs{$module};
 
-  CVar->render_inputs('variables' => $form->{CUSTOM_VARIABLES}) if (scalar @{ $form->{CUSTOM_VARIABLES} });
+    $::form->{CUSTOM_VARIABLES}->{$module} = CVar->get_custom_variables(module => $module, trans_id => $::form->{ $spec->{field} });
+    CVar->render_inputs(variables => $::form->{CUSTOM_VARIABLES}->{$module}, name_prefix => $spec->{name_prefix})
+      if scalar @{ $::form->{CUSTOM_VARIABLES}->{$module} };
+  }
 
   $form->header;
   print $form->parse_html_template('ct/form_header');
@@ -672,6 +815,10 @@ sub get_contact {
   CT->query_titles_and_greetings(\%::myconfig, $::form);
   CT->get_contact(\%::myconfig, $::form) if $::form->{cp_id};
 
+  $::form->{CUSTOM_VARIABLES}{Contacts} = CVar->get_custom_variables(module => 'Contacts', trans_id => $::form->{cp_id});
+  CVar->render_inputs(variables => $::form->{CUSTOM_VARIABLES}{Contacts}, name_prefix => 'cp')
+    if scalar @{ $::form->{CUSTOM_VARIABLES}->{Contacts} };
+
   $::form->{contacts_label} = \&_contacts_label;
 
   print $::form->ajax_response_header(), $::form->parse_html_template('ct/_contact');
index 0b607da..3eb6738 100644 (file)
@@ -1335,9 +1335,16 @@ sub generate_report {
 
     # set module stuff
     if ($ref->{module} eq 'oe') {
-      my $edit_oe_link = build_std_url("script=oe.pl", 'action=edit', 'type=' . E($ref->{cv} eq 'vendor' ? 'purchase_order' : 'sales_order'), 'id=' . E($ref->{trans_id}), 'callback');
-      $row->{ordnumber}{link} = $edit_oe_link;
-      $row->{quonumber}{link} = $edit_oe_link if (!$ref->{ordnumber});
+      # für oe gibt es vier fälle, jeweils nach kunde oder lieferant unterschiedlich:
+      #
+      # | ist bestellt  | Vom Kunde bestellt |  -> edit_oe_ord_link
+      # | Anfrage       | Angebot            |  -> edit_oe_quo_link
+
+      my $edit_oe_ord_link = build_std_url("script=oe.pl", 'action=edit', 'type=' . E($ref->{cv} eq 'vendor' ? 'purchase_order' : 'sales_order'), 'id=' . E($ref->{trans_id}), 'callback');
+      my $edit_oe_quo_link = build_std_url("script=oe.pl", 'action=edit', 'type=' . E($ref->{cv} eq 'vendor' ? 'request_quotation' : 'sales_quotation'), 'id=' . E($ref->{trans_id}), 'callback');
+
+      $row->{ordnumber}{link} = $edit_oe_ord_link;
+      $row->{quonumber}{link} = $edit_oe_quo_link if (!$ref->{ordnumber});
 
     } else {
       $row->{invnumber}{link} = build_std_url("script=$ref->{module}.pl", 'action=edit', 'type=invoice', 'id=' . E($ref->{trans_id}), 'callback');
index 022f039..fa46e64 100644 (file)
@@ -38,6 +38,7 @@
 
 use Carp;
 use CGI;
+use List::MoreUtils qw(uniq);
 use List::Util qw(min max first);
 
 use SL::CVar;
@@ -1504,14 +1505,8 @@ sub print_form {
   push @template_files, "$form->{formname}$form->{language}$printer_code.$extension";
   push @template_files, "$form->{formname}.$extension";
   push @template_files, "default.$extension";
-
-  $form->{IN} = undef;
-  for my $filename (@template_files) {
-    if (-f "$myconfig{templates}/$filename") {
-      $form->{IN} = $filename;
-      last;
-    }
-  }
+  @template_files = uniq @template_files;
+  $form->{IN}     = first { -f "$myconfig{templates}/$_" } @template_files;
 
   if (!defined $form->{IN}) {
     $::form->error($::locale->text('Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.', join ', ', map { "'$_'"} @template_files));
index 46e4ba1..4cd3644 100644 (file)
@@ -141,7 +141,7 @@ body.menu {
 .message_error {
        padding: 5px;
        background-color: #CC0000;
-       color: black;
+       color: white;
        font-weight: bolder;
        text-align: center;
        border-style: solid;
@@ -159,6 +159,7 @@ body.menu {
 .message_error_label {
        padding: 0.5em;
        background-color: #E00000;
+  color: white;
        font-weight: normal;
        text-align: left;
        border-style: solid;
index cf22326..7b16fc0 100644 (file)
@@ -208,6 +208,7 @@ $self->{texts} = {
   'Article type (see below)'    => 'Artikeltyp (siehe unten)',
   'As a result, the saved onhand values of the present goods can be stored into a warehouse designated by you, or will be reset for a proper warehouse tracking' => 'Als Konsequenz k&ouml;nnen die gespeicherten Mengen entweder in ein Lager &uuml;berf&uuml;hrt werden, oder f&uuml;r eine frische Lagerverwaltung resettet werden.',
   'Assemblies'                  => 'Erzeugnisse',
+  'Assembly'                    => 'Erzeugnis',
   'Assembly Description'        => 'Erzeugnis-Beschreibung',
   'Assembly Number'             => 'Erzeugnis-Nummer',
   'Assembly Number missing!'    => 'Erzeugnisnummer fehlt!',
@@ -422,6 +423,7 @@ $self->{texts} = {
   'Contact deleted.'            => 'Ansprechpartner gelöscht.',
   'Contact is in use and was flagged invalid.' => 'Ansprechpartner ist noch in Verwendung, und wurde als ungültig markiert.',
   'Contact person (surname)'    => 'Ansprechpartner (Nachname)',
+  'Contact persons'             => 'Ansprechpartner',
   'Contacts'                    => 'Ansprechpartner',
   'Continue'                    => 'Weiter',
   'Contra'                      => 'gegen',
@@ -511,6 +513,8 @@ $self->{texts} = {
   'Customer type'               => 'Kundentyp',
   'Customer/Vendor'             => 'Kunde/Lieferant',
   'Customer/Vendor (database ID)' => 'Kunde/Lieferant (Datenbank-ID)',
+  'Customer/Vendor Name'        => 'Kunde/Lieferant',
+  'Customer/Vendor Number'      => 'Kundennummer/Lieferantennummer',
   'Customername'                => 'Kundenname',
   'Customernumberinit'          => 'Kunden-/Lieferantennummernkreis',
   'Customers'                   => 'Kunden',
@@ -589,6 +593,11 @@ $self->{texts} = {
   'Delivery Order created'      => 'Lieferschein erstellt',
   'Delivery Order deleted!'     => 'Lieferschein gel&ouml;scht!',
   'Delivery Orders'             => 'Lieferscheine',
+  'Delivery Orders for this document' => 'Lieferscheine für dieses Dokument',
+  'Delivery Plan'               => 'Lieferplan',
+  'Delivery Plan for currently outstanding sales orders' => 'Lieferplan für offene Verkaufsaufträge',
+  'Delivery information deleted.' => 'Lieferinformation gelöscht.',
+  'Delivery information saved.' => 'Lieferinformation gespeichert.',
   'Department'                  => 'Abteilung',
   'Department 1'                => 'Abteilung (1)',
   'Department 2'                => 'Abteilung (2)',
@@ -900,6 +909,7 @@ $self->{texts} = {
   'Help Template Variables'     => 'Hilfe zu Dokumenten-Variablen',
   'Help on column names'        => 'Hilfe zu Spaltennamen',
   'Here\'s an example command line:' => 'Hier ist eine Kommandozeile, die als Beispiel dient:',
+  'Hide Filter'                 => 'Filter verbergen',
   'Hide by default'             => 'Standardm&auml;&szlig;ig verstecken',
   'Hide help text'              => 'Hilfetext verbergen',
   'History'                     => 'Historie',
@@ -1090,7 +1100,6 @@ $self->{texts} = {
   'Logout now'                  => 'Lx-Office jetzt verlassen',
   'Long Dates'                  => 'Lange Monatsnamen',
   'Long Description'            => 'Langtext',
-  'Lx-Office'                   => 'Lx-Office',
   'MAILED'                      => 'Gesendet',
   'MSG_BROWSER_DOES_NOT_SUPPORT_IFRAMES' => 'Ihr Browser kann leider keine eingebetteten Frames anzeigen. Bitte w&auml;hlen Sie ein anderes Men&uuml; in der Benutzerkonfiguration im Administrationsmen&uuml; aus.',
   'Main Preferences'            => 'Grundeinstellungen',
@@ -1137,8 +1146,9 @@ $self->{texts} = {
   'Missing user id!'            => 'Benutzer ID fehlt!',
   'Mitarbeiter'                 => 'Mitarbeiter',
   'Mixed (requires column "type")' => 'Gemischt (erfordert Spalte "type")',
-  'Mobile1'                     => 'Mobile 1',
-  'Mobile2'                     => 'Mobile 2',
+  'Mobile'                      => 'Mobiltelefon',
+  'Mobile1'                     => 'Mobil 1',
+  'Mobile2'                     => 'Mobil 2',
   'Model'                       => 'Lieferanten-Art-Nr.',
   'Model (with X being a number)' => 'Lieferanten-Art-Nr. (X ist eine fortlaufende Zahl)',
   'Module'                      => 'Modul',
@@ -1293,6 +1303,7 @@ $self->{texts} = {
   'POSTED AS NEW'               => 'Als neu gebucht',
   'PRINTED'                     => 'Gedruckt',
   'Packing Lists'               => 'Lieferschein',
+  'Page'                        => 'Seite',
   'Page #1/#2'                  => 'Seite #1/#2',
   'Paid'                        => 'bezahlt',
   'Part'                        => 'Ware',
@@ -1393,6 +1404,7 @@ $self->{texts} = {
   'Price factor (name)'         => 'Preisfaktor (Name)',
   'Price factor deleted!'       => 'Preisfaktor gel&ouml;scht.',
   'Price factor saved!'         => 'Preisfaktor gespeichert.',
+  'Price information'           => 'Preisinformation',
   'Pricegroup'                  => 'Preisgruppe',
   'Pricegroup deleted!'         => 'Preisgruppe gelöscht!',
   'Pricegroup missing!'         => 'Preisgruppe fehlt!',
@@ -1513,6 +1525,7 @@ $self->{texts} = {
   'Requested execution date from' => 'Gewünschtes Ausführungsdatum von',
   'Requested execution date to' => 'Gewünschtes Ausführungsdatum bis',
   'Required by'                 => 'Lieferdatum',
+  'Reset'                       => 'Zurücksetzen',
   'Restore Dataset'             => 'Datenbank wiederherstellen',
   'Revenue'                     => 'Erlöskonto',
   'Revenue Account'             => 'Erlöskonto',
@@ -1536,6 +1549,7 @@ $self->{texts} = {
   'Sales Invoices'              => 'Kundenrechnung',
   'Sales Order'                 => 'Kundenauftrag',
   'Sales Orders'                => 'Aufträge',
+  'Sales Price information'     => 'Verkaufspreisinformation',
   'Sales Report'                => 'Verkaufsbericht',
   'Sales and purchase invoices with inventory transactions with taxkeys' => 'Einkaufs- und Verkaufsrechnungen mit Warenbestandsbuchungen mit Steuerschlüsseln',
   'Sales delivery order'        => 'Lieferschein (Verkauf)',
@@ -1572,6 +1586,8 @@ $self->{texts} = {
   'Screen'                      => 'Bildschirm',
   'Search AP Aging'             => 'Offene Verbindlichkeiten',
   'Search AR Aging'             => 'Offene Forderungen',
+  'Search contacts'             => 'Ansprechpartnersuche',
+  'Search term'                 => 'Suchbegriff',
   'Searchable'                  => 'Durchsuchbar',
   'Select'                      => 'auswählen',
   'Select a Customer'           => 'Endkunde auswählen',
@@ -1627,6 +1643,7 @@ $self->{texts} = {
   'Shopartikel'                 => 'Shopartikel',
   'Short'                       => 'Knapp',
   'Show'                        => 'Zeigen',
+  'Show Filter'                 => 'Filter zeigen',
   'Show Salesman'               => 'Verkäufer anzeigen',
   'Show TODO list'              => 'Aufgabenliste anzeigen',
   'Show by default'             => 'Standardm&auml;&szlig;ig anzeigen',
@@ -1645,6 +1662,7 @@ $self->{texts} = {
   'Skonto Terms'                => 'Zahlungsziel Skonto',
   'Sold'                        => 'Verkauft',
   'Solution'                    => 'Lösung',
+  'Sort By'                     => 'Sortiert nach',
   'Source'                      => 'Beleg',
   'Source BIC'                  => 'Quell-BIC',
   'Source IBAN'                 => 'Quell-IBAN',
@@ -2180,6 +2198,7 @@ $self->{texts} = {
   'config/lx_office.conf: Key "authentication/ldap" is missing.' => 'config/lx_office.conf: Der Schlüssel "authentication/ldap" fehlt.',
   'config/lx_office.conf: Missing parameters in "authentication/database". Required parameters are "host", "db" and "user".' => 'config/lx_office.conf: Fehlende Parameter in "authentication/database". Ben&ouml;tigte Parameter sind "host", "db" und "user".',
   'config/lx_office.conf: Missing parameters in "authentication/ldap". Required parameters are "host", "attribute" and "base_dn".' => 'config/lx_office.conf: Fehlende Parameter in "authentication/ldap". Benötigt werden "host", "attribute" und "base_dn".',
+  'contact_list'                => 'ansprechpartner_liste',
   'continue'                    => 'weiter',
   'correction'                  => 'Korrektur',
   'cp_greeting to cp_gender migration' => 'Datenumwandlung von Titel nach Geschlecht (cp_greeting to cp_gender)',
@@ -2187,6 +2206,7 @@ $self->{texts} = {
   'customer_list'               => 'kundenliste',
   'debug'                       => 'Debug',
   'delete'                      => 'Löschen',
+  'delivered'                   => 'geliefert',
   'deliverydate'                => 'Lieferdatum',
   'direct debit'                => 'Lastschrift',
   'disposed'                    => 'Entsorgung',
@@ -2233,11 +2253,13 @@ $self->{texts} = {
   'month'                       => 'Monatliche Abgabe',
   'monthly'                     => 'monatlich',
   'new Window'                  => 'neues Fenster',
+  'next'                        => 'vor',
   'no'                          => 'nein',
   'no bestbefore'               => 'keine Mindesthaltbarkeit',
   'no chargenumber'             => 'keine Chargennummer',
   'none (pricegroup)'           => 'keine',
   'not configured'              => 'nicht konfiguriert',
+  'not delivered'               => 'nicht geliefert',
   'not executed'                => 'nicht ausgeführt',
   'not transferred in yet'      => 'noch nicht eingelagert',
   'not transferred out yet'     => 'noch nicht ausgelagert',
@@ -2259,6 +2281,7 @@ $self->{texts} = {
   'pos_eur'                     => 'E/ÃœR',
   'pos_ustva'                   => 'UStVA',
   'posted!'                     => 'gebucht',
+  'prev'                        => 'zurück',
   'print'                       => 'drucken',
   'proforma'                    => 'Proforma',
   'project_list'                => 'projektliste',
index 57fe6da..8d090b8 100644 (file)
@@ -1144,8 +1144,9 @@ $self->{texts} = {
   'Missing user id!'            => 'Benutzer ID fehlt!',
   'Mitarbeiter'                 => 'Mitarbeiter',
   'Mixed (requires column "type")' => 'Gemischt (erfordert Spalte "type")',
-  'Mobile1'                     => 'Mobile 1',
-  'Mobile2'                     => 'Mobile 2',
+  'Mobile'                      => 'Mobiltelefon',
+  'Mobile1'                     => 'Mobil 1',
+  'Mobile2'                     => 'Mobil 2',
   'Model'                       => 'Lieferanten-Art-Nr.',
   'Model (with X being a number)' => 'Lieferanten-Art-Nr. (X ist eine fortlaufende Zahl)',
   'Module'                      => 'Modul',
@@ -1300,6 +1301,7 @@ $self->{texts} = {
   'POSTED AS NEW'               => 'Als neu gebucht',
   'PRINTED'                     => 'Gedruckt',
   'Packing Lists'               => 'Lieferschein',
+  'Page'                        => '',
   'Page #1/#2'                  => 'Seite #1/#2',
   'Paid'                        => 'bezahlt',
   'Part'                        => 'Ware',
@@ -1400,6 +1402,7 @@ $self->{texts} = {
   'Price factor (name)'         => 'Preisfaktor (Name)',
   'Price factor deleted!'       => 'Preisfaktor gel&ouml;scht.',
   'Price factor saved!'         => 'Preisfaktor gespeichert.',
+  'Price information'           => '',
   'Pricegroup'                  => 'Preisgruppe',
   'Pricegroup deleted!'         => 'Preisgruppe gelöscht!',
   'Pricegroup missing!'         => 'Preisgruppe fehlt!',
@@ -1543,6 +1546,7 @@ $self->{texts} = {
   'Sales Invoices'              => 'Ausgangsrechnungen',
   'Sales Order'                 => 'Kundenauftrag',
   'Sales Orders'                => 'Aufträge',
+  'Sales Price information'     => '',
   'Sales Report'                => 'Verkaufsbericht',
   'Sales and purchase invoices with inventory transactions with taxkeys' => 'Einkaufs- und Verkaufsrechnungen mit Warenbestandsbuchungen mit Steuerschlüsseln',
   'Sales delivery order'        => 'Lieferschein (Verkauf)',
@@ -1652,6 +1656,7 @@ $self->{texts} = {
   'Skonto Terms'                => 'Zahlungsziel Skonto',
   'Sold'                        => 'Verkauft',
   'Solution'                    => 'Lösung',
+  'Sort By'                     => '',
   'Source'                      => 'Beleg',
   'Source BIC'                  => 'Quell-BIC',
   'Source IBAN'                 => 'Quell-IBAN',
@@ -2193,6 +2198,7 @@ $self->{texts} = {
   'customer_list'               => 'kundenliste',
   'debug'                       => 'Debug',
   'delete'                      => 'Löschen',
+  'delivered'                   => '',
   'deliverydate'                => 'Lieferdatum',
   'direct debit'                => 'Lastschrift',
   'disposed'                    => 'Entsorgung',
@@ -2232,11 +2238,13 @@ $self->{texts} = {
   'month'                       => 'Monatliche Abgabe',
   'monthly'                     => 'monatlich',
   'new Window'                  => 'neues Fenster',
+  'next'                        => 'nächster',
   'no'                          => 'nein',
   'no bestbefore'               => 'keine Mindesthaltbarkeit',
   'no chargenumber'             => 'keine Chargennummer',
   'none (pricegroup)'           => 'keine',
   'not configured'              => 'nicht konfiguriert',
+  'not delivered'               => '',
   'not executed'                => 'nicht ausgeführt',
   'not transferred in yet'      => 'noch nicht eingelagert',
   'not transferred out yet'     => 'noch nicht ausgelagert',
@@ -2258,6 +2266,7 @@ $self->{texts} = {
   'pos_eur'                     => 'E/ÃœR',
   'pos_ustva'                   => 'UStVA',
   'posted!'                     => 'gebucht',
+  'prev'                        => 'vorheriger',
   'print'                       => 'drucken',
   'proforma'                    => 'Proforma',
   'project_list'                => 'projektliste',
index 5d39fd6..ac8f9b9 100644 (file)
--- a/menu.ini
+++ b/menu.ini
@@ -59,6 +59,12 @@ module=ct.pl
 action=search
 db=vendor
 
+[Master Data--Reports--Contacts]
+ACCESS=customer_vendor_edit
+module=ct.pl
+action=search_contact
+db=customer
+
 [Master Data--Reports--Parts]
 ACCESS=part_service_assembly_edit
 module=ic.pl
@@ -160,6 +166,10 @@ ACCESS=dunning_edit
 module=dn.pl
 action=search
 
+[AR--Reports--Delivery Plan]
+ACCESS=sales_order_edit
+module=controller.pl
+action=DeliveryPlan/list
 
 [AP]
 
index 520b4f1..a50d184 100755 (executable)
@@ -14,6 +14,7 @@ BEGIN {
 }
 
 use SL::InstallationCheck;
+use SL::LxOfficeConf;
 
 my %check;
 Getopt::Long::Configure ("bundling");
@@ -47,10 +48,16 @@ if ($check{a}) {
 
 $| = 1;
 
+if (!SL::LxOfficeConf->read(undef, 'may fail')) {
+  print_header('Could not load the config file. If you have dependancies from any features enabled in the configuration these will still show up as optional because of this. Please rerun this script after installing the dependancies needed to load the cofiguration.')
+} else {
+  SL::InstallationCheck::check_for_conditional_dependencies();
+}
+
 if ($check{r}) {
   print_header('Checking Required Modules');
   check_module($_, required => 1) for @SL::InstallationCheck::required_modules;
-  print_header('Standard check for required modules done. See additional parameters for more checks (-- help)') if $default_run;
+  print_header('Standard check for required modules done. See additional parameters for more checks (--help)') if $default_run;
 }
 if ($check{o}) {
   print_header('Checking Optional Modules');
diff --git a/t/request/flatten.t b/t/request/flatten.t
new file mode 100644 (file)
index 0000000..0cfd65f
--- /dev/null
@@ -0,0 +1,159 @@
+use Test::More;
+use Test::Deep;
+use Data::Dumper;
+
+use_ok 'SL::Request', qw(flatten unflatten);
+
+use constant DEBUG => 0;
+
+sub f ($$$) {
+  my $flat = flatten($_[0]);
+  print Dumper($flat) if DEBUG;
+
+  my $unflat = unflatten($flat);
+  print Dumper($unflat) if DEBUG;
+
+  cmp_deeply($flat, $_[1], $_[2]);
+  cmp_deeply($unflat, $_[0], $_[2]);
+}
+
+f {
+  test => 1,
+  whut => 2
+},
+[
+  [ test => 1 ],
+  [ whut => 2 ],
+], 'simple case';
+
+f { a => { b => 2 } },
+[
+ [ 'a.b' => 2 ]
+], 'simple hash nesting';
+
+f { a => [ 2,  4 ] },
+[
+ [  'a[]' => 2 ],
+ [  'a[]' => 4 ],
+], 'simple array';
+
+f { a => [ { c => 1, d => 2 }, { c => 3, d => 4 }, ] },
+[
+  [ 'a[+].c', 1 ],
+  [ 'a[].d', 2 ],
+  [ 'a[+].c', 3 ],
+  [ 'a[].d', 4  ],
+], 'array of hashes';
+
+# tests from Hash::Flatten below
+f {
+  'x' => 1,
+  'y' => {
+    'a' => 2,
+    'b' => {
+      'p' => 3,
+      'q' => 4
+    },
+  }
+}, bag(
+ [ 'x'     => 1, ],
+ [ 'y.a'   => 2, ],
+ [ 'y.b.p' => 3, ],
+ [ 'y.b.q' => 4  ],
+), 'Hash::Flatten 1';
+
+
+f {
+  'x' => 1,
+  '0' => {
+    '1' => 2,
+  },
+  'a' => [1,2,3],
+},
+bag (
+ ['x'    => 1, ],
+ ['0.1'  => 2, ],
+ ['a[]'  => 1, ],
+ ['a[]'  => 2, ],
+ ['a[]'  => 3, ],
+), 'Hash::Flatten 2 - weird keys and values';
+
+
+f {
+  'x' => 1,
+  'ay' => {
+    'a' => 2,
+    'b' => {
+      'p' => 3,
+      'q' => 4
+    },
+  },
+  'y' => [
+    'a', 2,
+    {
+      'baz' => 'bum',
+    },
+  ]
+},
+bag(
+  [ 'ay.b.p'  => 3,       ],
+  [ 'ay.b.q'  => 4,       ],
+  [ 'ay.a'    => 2,       ],
+  [ 'x'       => 1,       ],
+  [ 'y[]'     => 'a',    ],
+  [ 'y[]'     => 2        ],
+  [ 'y[+].baz' => 'bum',  ],
+), 'Hash::Flatten 3 - mixed';
+
+f {
+  'x' => 1,
+  'y' => [
+    [
+      'a', 'fool', 'is',
+    ],
+    [
+      'easily', [ 'parted', 'from' ], 'his'
+    ],
+    'money',
+  ]
+},
+bag(
+ [ 'x'        => 1,        ],
+ [ 'y[][]'    => 'his',    ],
+ [ 'y[][+][]' => 'parted', ],
+ [ 'y[][][]'  => 'from',   ],
+ [ 'y[+][]'   => 'a',      ],
+ [ 'y[+][]'   => 'easily', ],
+ [ 'y[][]'    => 'fool',   ],
+ [ 'y[][]'    => 'is'      ],
+ [ 'y[]'      => 'money',  ],
+), 'Hash::Flatten 4 - array nesting';
+
+f {
+  'x' => 1,
+  'ay' => {
+    'a' => 2,
+    'b' => {
+      'p' => 3,
+      'q' => 4
+    },
+  },
+  's' => 'hey',
+  'y' => [
+    'a', 2, {
+      'baz' => 'bum',
+    },
+  ]
+},
+bag(
+  [ 'x'        => 1,     ],
+  [ 's'        => 'hey', ],
+  [ 'ay.a'     => 2,     ],
+  [ 'y[+].baz' => 'bum', ],
+  [ 'ay.b.p'   => 3,     ],
+  [ 'y[]'      => 'a',   ],
+  [ 'ay.b.q'   => 4,     ],
+  [ 'y[]'      => 2      ],
+), 'Hash::Flatten 5 - deep mix';
+
+done_testing();
index a69101b..dc9c09f 100644 (file)
 [%- ELSIF var.type == 'customer' %]
 [% L.customer_picker(var_name, var.value) %]
 
+[%- ELSIF var.type == 'vendor' %]
+[% L.vendor_selector(var_name, var.value) %]
+
+[%- ELSIF var.type == 'part' %]
+[% L.part_selector(var_name, var.value) %]
+
 [%- ELSIF var.type == 'select' %]
 
 <select name="[% var_name %]">
index 3f7461c..33623f6 100644 (file)
 [%- ELSIF cvar.var.type == 'customer' %]
 [%- L.customer_picker(render_input_blocks__cvar_name, cvar.value) %]
 
+[%- ELSIF cvar.var.type == 'vendor' %]
+[% L.vendor_selector(render_input_blocks__cvar_name, cvar.value) %]
+
+[%- ELSIF cvar.var.type == 'part' %]
+[% L.part_selector(render_input_blocks__cvar_name, cvar.value) %]
+
 [%- ELSIF cvar.var.type == 'number' %]
 [%- L.input_tag(render_input_blocks__cvar_name, LxERP.format_amount(cvar.value, -2)) %]
 
index db37d08..37f722b 100644 (file)
@@ -8,7 +8,7 @@
     <td valign="top">
 
      [%- IF var.type == 'bool' %]
-     <select name="cvar_[% HTML.escape(var.name) %]">
+     <select name="[% filter_prefix %]cvar_[% HTML.escape(var.name) %]">
       <option value="">---</option>
       <option value="yes">[% 'Yes' | $T8 %]</option>
       <option value="no">[% 'No' | $T8 %]</option>
 
      [%- ELSIF var.type == 'date' %]
      [% 'from (time)' | $T8 %]
-     <input name="cvar_[% HTML.escape(var.name) %]_from" id="cvar_[% HTML.escape(var.name) %]_from" size="12">
+     <input name="[% filter_prefix %]cvar_[% HTML.escape(var.name) %]_from" id="cvar_[% HTML.escape(var.name) %]_from" size="12">
      <input type="button" name="cvar_[% HTML.escape(var.name) %]_from_button" id="cvar_[% HTML.escape(var.name) %]_from_trigger" value="?">
      [% 'to (time)' | $T8 %]
-     <input name="cvar_[% HTML.escape(var.name) %]_to" id="cvar_[% HTML.escape(var.name) %]_to" size="12">
+     <input name="[% filter_prefix %]cvar_[% HTML.escape(var.name) %]_to" id="cvar_[% HTML.escape(var.name) %]_to" size="12">
      <input type="button" name="cvar_[% HTML.escape(var.name) %]_to_button" id="cvar_[% HTML.escape(var.name) %]_to_trigger" value="?">
 
      <script type="text/javascript">
@@ -36,7 +36,7 @@
      </script>
 
      [%- ELSIF var.type == 'number' %]
-     <select name="cvar_[% HTML.escape(var.name) %]_qtyop">
+     <select name="[% filter_prefix %]cvar_[% HTML.escape(var.name) %]_qtyop">
       <option selected>==</option>
       <option>=/=</option>
       <option>&gt;</option>
      <input name="cvar_[% HTML.escape(var.name) %]"[% IF var.maxlength %]maxlength="[% HTML.escape(var.maxlength) %]"[% END %]>
 
      [%- ELSIF var.type == 'customer' %]
-     <input name="cvar_[% var.name | html %]">
+     <input name="[% filter_prefix %]cvar_[% var.name | html %]">
 
      [% ELSIF var.type == 'select' %]
-     <select name="cvar_[% HTML.escape(var.name) %]">
+     <select name="[% filter_prefix %]cvar_[% HTML.escape(var.name) %]">
       <option value="" selected>---</option>
       [%- FOREACH option = var.OPTIONS %]
       <option>[% HTML.escape(option.value) %]</option>
@@ -58,7 +58,7 @@
      </select>
 
      [%- ELSE %]
-     <input name="cvar_[% HTML.escape(var.name) %]"[% IF var.maxlength %]maxlength="[% HTML.escape(var.maxlength) %]"[% END %]>
+     <input name="[% filter_prefix %]cvar_[% HTML.escape(var.name) %]"[% IF var.maxlength %]maxlength="[% HTML.escape(var.maxlength) %]"[% END %]>
 
      [%- END %]
     </td>
index 7552600..1c24fe1 100644 (file)
       <th align="left" nowrap>[% 'Bcc' | $T8 %]</th>
       <td><input name="bcc" size="40" value="[% HTML.escape(bcc) %]"></td>
      </tr>
+     [% IF CUSTOM_VARIABLES.Contacts.size %]
+     <tr>
+      <td colspan="2"><hr></td>
+     </tr>
+
+     [%- FOREACH var = CUSTOM_VARIABLES.Contacts %]
+     <tr>
+      <th align="left" valign="top" nowrap>[% HTML.escape(var.description) %]</th>
+      <td valign="top">[% var.HTML_CODE %]</td>
+     </tr>
+     [%- END %]
+     [%- END %]
+
     </table>
 
 [% IF cp_id %]
index 8f71e65..8263d87 100644 (file)
@@ -17,7 +17,7 @@
    <li><a href="#" rel="deliveries">[% 'Supplies' | $T8 %]</a></li>
 [%- END %]
    <li><a href="#" rel="vcnotes">[% 'Notes' | $T8 %]</a></li>
-   [%- IF CUSTOM_VARIABLES.size %]
+   [%- IF CUSTOM_VARIABLES.CT.size %]
    <li><a href="#" rel="custom_variables">[% 'Custom Variables' | $T8 %]</a></li>
    [%- END %]
   </ul>
       </td>
       [%- IF is_customer && !conf_vertreter %]
       <th align="right">[% 'Salesman' | $T8 %]</th>
-      <td>[% L.select_tag('salesman_id', L.options_for_select(ALL_SALESMEN, default=salesman_id, show_empty=1, title='safe_name')) %]</td>
+      <td>[% L.select_tag('salesman_id', L.options_for_select(ALL_SALESMEN, default=salesman_id, with_empty=1, title='safe_name')) %]</td>
       [%- END %]
      </tr>
     </table>
     <br style="clear: left" />
    </div>
 
-   [%- IF CUSTOM_VARIABLES.size %]
+   [%- IF CUSTOM_VARIABLES.CT.size %]
    <div id="custom_variables" class="tabcontent">
 
     <p>
      <table>
-      [%- FOREACH var = CUSTOM_VARIABLES %]
+      [%- FOREACH var = CUSTOM_VARIABLES.CT %]
       <tr>
        <td align="right" valign="top">[% HTML.escape(var.description) %]</td>
        <td valign="top">[% var.HTML_CODE %]</td>
diff --git a/templates/webpages/ct/search_contact.html b/templates/webpages/ct/search_contact.html
new file mode 100644 (file)
index 0000000..be67814
--- /dev/null
@@ -0,0 +1,124 @@
+[%- USE HTML %]
+[%- USE T8 %]
+<body onload="fokus()">
+
+ <form method="post" action="ct.pl" name="Form">
+
+  <input type="hidden" name="db" value="[% db | html %]">
+
+  <div class="listtop">[% 'Contacts' | $T8 %]</div>
+
+  <table>
+   <tr>
+    <th align="right" nowrap>[% 'Name' | $T8 %]</th>
+    <td><input name="filter.cp_name" size="35"></td>
+   </tr>
+   <tr>
+    <th align="right" nowrap>[% 'Greeting' | $T8 %]</th>
+    <td><input name="filter.cp_greeting" size="35"></td>
+   </tr>
+   <tr>
+    <th align="right" nowrap>[% 'Title' | $T8 %]</th>
+    <td><input name="filter.cp_title" size="35"></td>
+   </tr>
+   <tr>
+    <th align="right" nowrap>[% 'E-mail' | $T8 %]</th>
+    <td><input name="filter.cp_email" size="35"></td>
+   </tr>
+   <tr>
+    <th align="right" nowrap>[% 'Department' | $T8 %]</th>
+    <td><input name="filter.cp_abteilung" size="35"></td>
+   </tr>
+   <tr>
+    <th align="right" nowrap>[% 'Project' | $T8 %]</th>
+    <td><input name="filter.cp_project" size="35"></td>
+   </tr>
+
+   [% CUSTOM_VARIABLES_FILTER_CODE %]
+
+   <tr>
+    <td></td>
+    <td>
+     <input name="filter.status" class="radio" type="radio" value="active" checked> [% 'Active' | $T8 %]
+     <input name="filter.status" class="radio" type="radio" value="all"> [% 'All' | $T8 %]
+     <input name="filter.status" class="radio" type="radio" value="orphaned"> [% 'Orphaned' | $T8 %]
+    </td>
+   </tr>
+
+   <tr>
+    <th align="right" nowrap>[% 'Include in Report' | $T8 %]</th>
+    <td>
+     <table border="0">
+      <tr>
+       <td>
+        <input name="l.cp_id" id="l_cp_id" type="checkbox" class="checkbox" value="Y">
+        <label for="l_cp_id">[% 'ID' | $T8 %]</label>
+       </td>
+       <td>
+        <input name="l.vcnumber" id="l_vcnumber" type="checkbox" class="checkbox" value="Y" checked>
+        <label for="l_vcnumber">[% 'Customer/Vendor Number' | $T8 %]</label>
+       </td>
+       <td>
+        <input name="l.vcname" id="l_vcname" type="checkbox" class="checkbox" value="Y" checked>
+        <label for="l_vcname">[% 'Customer/Vendor Name' | $T8 %]</label>
+       </td>
+      </tr>
+
+      <tr>
+       <td>
+        <input name="l.cp_name" id="l_cp_name" type="checkbox" class="checkbox" value="Y" checked>
+        <label for="l_cp_name">[% 'Name' | $T8 %]</label>
+       </td>
+       <td>
+        <input name="l.cp_givenname" id="l_cp_givenname" type="checkbox" class="checkbox" value="Y" checked>
+        <label for="l_cp_givenname">[% 'Given Name' | $T8 %]</label>
+       </td>
+       <td>
+        <input name="l.cp_street" id="l_cp_street" type="checkbox" class="checkbox" value="Y">
+        <label for="l_cp_street">[% 'Street' | $T8 %]</label>
+       </td>
+      </tr>
+      <tr>
+       <td>
+        <input name="l.cp_phone" id="l_cp_phone" type="checkbox" class="checkbox" value="Y" checked>
+        <label for="l_cp_phone">[% 'Phone' | $T8 %]</label>
+       </td>
+       <td>
+        <input name="l.cp_mobile" id="l_cp_mobile" type="checkbox" class="checkbox" value="Y" checked>
+        <label for="l_cp_mobile">[% 'Mobile' | $T8 %]</label>
+       </td>
+       <td>
+        <input name="l.cp_email" id="l_cp_email" type="checkbox" class="checkbox" value="Y" checked>
+        <label for="l_cp_email">[% 'E-mail' | $T8 %]</label>
+       </td>
+      </tr>
+
+      <tr>
+       <td>
+        <input name="l.cp_birthday" id="l_cp_birthday" type="checkbox" class="checkbox" value="Y">
+        <label for="l_cp_birthday">[% 'Birthday' | $T8 %]</label>
+       </td>
+       <td>
+        <input name="l.cp_abteilung" id="l_cp_abteilung" type="checkbox" class="checkbox" value="Y">
+        <label for="l_cp_abteilung">[% 'Department' | $T8 %]</label>
+       </td>
+       <td>
+        <input name="l.cp_gender" id="l_cp_gender" type="checkbox" class="checkbox" value="Y">
+        <label for="l_cp_gender">[% 'Gender' | $T8 %]</label>
+       </td>
+      </tr>
+
+      [% CUSTOM_VARIABLES_INCLUSION_CODE %]
+
+     </table>
+    </td>
+   </tr>
+  </table>
+
+  <input type="hidden" name="nextsub" value="list_contacts">
+
+  <input type="submit" class="submit" name="action" value="[% 'Continue' | $T8 %]">
+ </form>
+
+</body>
+</html>
diff --git a/templates/webpages/delivery_plan/_filter.html b/templates/webpages/delivery_plan/_filter.html
new file mode 100644 (file)
index 0000000..9bba140
--- /dev/null
@@ -0,0 +1,66 @@
+[%- USE T8 %]
+[%- USE L %]
+[%- USE LxERP %]
+[%- USE HTML %]
+<form action='controller.pl' method='post'>
+<div class='filter_toggle'>
+<a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Show Filter' | $T8 %]</a>
+  [% SELF.filter_summary %]
+</div>
+<div class='filter_toggle' style='display:none'>
+<a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Hide Filter' | $T8 %]</a>
+ <table id='filter_table'>
+  <tr>
+   <th align="right">[% 'Number' | $T8 %]</th>
+   <td>[% L.input_tag('filter.order.ordnumber:substr::ilike', filter.order.ordnumber_substr__ilike, size = 20) %]</td>
+  </tr>
+  <tr>
+   <th align="right">[% 'Part Number' | $T8 %]</th>
+   <td>[% L.input_tag('filter.part.partnumber:substr::ilike', filter.part.partnumber_substr__ilike, size = 20) %]</td>
+  </tr>
+  <tr>
+   <th align="right">[% 'Part Description' | $T8 %]</th>
+   <td>[% L.input_tag('filter.description:substr::ilike', filter.description_substr__ilike, size = 20) %]</td>
+  </tr>
+  <tr>
+   <th align="right">[% 'Delivery Date' | $T8 %] [% 'From Date' | $T8 %]</th>
+   <td>[% L.date_tag('filter.reqdate:date::ge', filter.reqdate_date__ge, cal_align = 'BR') %]</td>
+  </tr>
+  <tr>
+   <th align="right">[% 'Delivery Date' | $T8 %] [% 'To Date' | $T8 %]</th>
+   <td>[% L.date_tag('filter.reqdate:date::le', filter.reqdate_date__le, cal_align = 'BR') %]</td>
+  </tr>
+  <tr>
+   <th align="right">[% 'Quantity' | $T8 %]</th>
+   <td>[% L.input_tag('filter.qty:number', filter.qty_number, size = 20) %]</td>
+  </tr>
+  <tr>
+   <th align="right">[% 'Customer' | $T8 %]</th>
+   <td>[% L.input_tag('filter.order.customer.name:substr::ilike', filter.order.customer.name_substr__ilike, size = 20) %]</td>
+  </tr>
+  <tr>
+   <th align="right">[% 'Customer Number' | $T8 %]</th>
+   <td>[% L.input_tag('filter.order.customer.customernumber:substr::ilike', filter.order.customer.customernumber_substr__ilike, size = 20) %]</td>
+  </tr>
+  <tr>
+   <th align="right">[% 'Type' | $T8 %]</th>
+   <td>
+     [% L.checkbox_tag('filter.part.type.part',     checked=filter.part.type.part,     label=LxERP.t8('Part')) %]
+     [% L.checkbox_tag('filter.part.type.service',  checked=filter.part.type.service,  label=LxERP.t8('Service')) %]
+     [% L.checkbox_tag('filter.part.type.assembly', checked=filter.part.type.assembly, label=LxERP.t8('Assembly')) %]
+   </td>
+  </tr>
+ </table>
+
+[% L.hidden_tag('action', 'DeliveryPlan/dispatch') %]
+[% L.hidden_tag('sort_by', FORM.sort_by) %]
+[% L.hidden_tag('sort_dir', FORM.sort_dir) %]
+[% L.hidden_tag('page', FORM.page) %]
+[% L.input_tag('action_list', LxERP.t8('Continue'), type = 'submit', class='submit')%]
+
+
+<a href='#' onClick='javascript:$("#filter_table input").attr("value","");$("#filter_table option").attr("selected","")'>[% 'Reset' | $T8 %]</a>
+
+</div>
+
+</form>
diff --git a/templates/webpages/delivery_plan/_list.html b/templates/webpages/delivery_plan/_list.html
new file mode 100644 (file)
index 0000000..f0bb277
--- /dev/null
@@ -0,0 +1,44 @@
+[% USE HTML %][% USE T8 %][% USE L %][% USE LxERP %]
+
+[% BLOCK header %]
+ [% SET new_sort_dir = SELF.sort_by == sort_by ? 1 - SELF.sort_dir : SELF.sort_dir %]
+ <th width="[% size %]%">
+  <a href="[% SELF.url_for(action => 'list') %]&sort_by=[% sort_by %]&sort_dir=[% new_sort_dir %]&page=[% FORM.page %]">
+   [%- title %]
+   [%- IF SELF.sort_by == sort_by %]
+    <img src="image/[% IF SELF.sort_dir %]down[% ELSE %]up[% END %].png" border="0">
+   [%- END %]
+  </a>
+ </th>
+[% END %]
+
+<div id="orders">
+[%- IF !SELF.orderitems.size %]
+ <p>[%- 'There are no outstanding deliveries at the moment.' | $T8 %]</p>
+[%- ELSE %]
+
+ <table width=100%>
+  <tr class="listheading">
+   [% PROCESS header   title=LxERP.t8('Date')        sort_by='transdate',     size=15 %]
+   [% PROCESS header   title=LxERP.t8('Description') sort_by='description',   size=15 %]
+   [% PROCESS header   title=LxERP.t8('Part Number') sort_by='partnumber',    size=15 %]
+   [% PROCESS header   title=LxERP.t8('Qty')         sort_by='qty',           size=10 %]
+   [% PROCESS header   title=LxERP.t8('Order')       sort_by='ordnumber',     size=10 %]
+   [% PROCESS header   title=LxERP.t8('Customer')    sort_by='customer',      size=10 %]
+  </tr>
+
+  [%- FOREACH row = SELF.orderitems %]
+  <tr class="listrow[% loop.count % 2 %]">
+   <td>[% row.transdate ? row.transdate : row.order.reqdate_as_date %]</td>
+   <td>[% row.part.partnumber | html %]</td>
+   <td>[% row.description | html %]</td>
+   <td class='numeric'>[% LxERP.format_amount(row.qty, 2) | html %]</td>
+   <td>[% row.order.ordnumber | html %]</td>
+   <td>[% row.order.customer.name | html %]</td>
+  </tr>
+  [%- END %]
+ </table>
+ <p align=right>[% PROCESS 'common/paginate.html' pages=SELF.pages, base_url=SELF.url_for(action='list', sort_dir=SELF.sort_dir, sort_by=SELF.sort_by) %]</p>
+
+[%- END %]
+</div>
diff --git a/templates/webpages/delivery_plan/list.html b/templates/webpages/delivery_plan/list.html
new file mode 100644 (file)
index 0000000..16ec656
--- /dev/null
@@ -0,0 +1,7 @@
+[%- USE T8 %]
+
+<h1>[% 'Delivery Plan' | $T8 %]</h1>
+
+[%- PROCESS 'delivery_plan/_filter.html' filter=FORM.filter %]
+ <hr>
+[%- PROCESS 'delivery_plan/_list.html' %]
diff --git a/templates/webpages/delivery_plan/report_bottom.html b/templates/webpages/delivery_plan/report_bottom.html
new file mode 100644 (file)
index 0000000..3193961
--- /dev/null
@@ -0,0 +1,4 @@
+[% SET report_bottom_url_args = {} %]
+[%     report_bottom_url_args.import(SELF.flat_filter) %]
+[%     report_bottom_url_args.import({action='list', sort_dir=SELF.sort_dir, sort_by=SELF.sort_by}) %]
+<p align=right>[% PROCESS 'common/paginate.html' pages=SELF.pages, base_url=SELF.url_for(report_bottom_url_args) %]</p>
diff --git a/templates/webpages/delivery_plan/report_top.html b/templates/webpages/delivery_plan/report_top.html
new file mode 100644 (file)
index 0000000..cc35146
--- /dev/null
@@ -0,0 +1,3 @@
+[%- USE L %]
+[%- PROCESS 'delivery_plan/_filter.html' filter=SELF.filter %]
+ <hr>
index b916011..9022e6d 100644 (file)
  <br style="clear: left" />
 </div>
 
+[%- IF id %]
+<div id="sales_price_information" class="tabcontent">
+  [% PROCESS ic/sales_price_information.html id=id %]
+</div>
+[%- END %]
+
 [%- IF CUSTOM_VARIABLES.size %]
 <div id="custom_variables" class="tabcontent">
 
index de1a796..31308ba 100644 (file)
@@ -25,6 +25,9 @@
 
   <ul id="maintab" class="shadetabs">
    <li class="selected"><a href="#" rel="master_data">[% 'Basic Data' | $T8 %]</a></li>
+   [%- IF id %]
+   <li><a href="#" rel="sales_price_information">[% 'Price information' | $T8 %]</a></li>
+   [%- END %]
    [%- IF CUSTOM_VARIABLES.size %]
    <li><a href="#" rel="custom_variables">[% 'Custom Variables' | $T8 %]</a></li>
    [%- END %]
diff --git a/templates/webpages/ic/sales_price_information.html b/templates/webpages/ic/sales_price_information.html
new file mode 100644 (file)
index 0000000..d7b92bd
--- /dev/null
@@ -0,0 +1,29 @@
+<div id='sales_price_information_sales_order'></div>
+<div id='sales_price_information_sales_quotation'></div>
+
+<script type='text/javascript'>
+  function get_report(target, source, data){
+    $.ajax({
+      url:        source,
+//      beforeSend: function () { $(target).html('<img src="image/spinner.gif">') },
+      success:    function (rsp) {
+        $(target).html(rsp);
+        $(target).find('.paginate').find('a').click(function(event){ redirect_event(event, target) });
+        $(target).find('a.report-generator-header-link').click(function(event){ redirect_event(event, target) });
+      },
+      data:       data,
+    });
+  };
+
+  function redirect_event(event, target){
+    event.preventDefault();
+    get_report(target, event.target + '', {});
+  }
+
+  $(document).ready(function(){
+    get_report('#sales_price_information_sales_order', 'controller.pl', { action: 'SellPriceInformation/list', 'filter.part.id': [% id %], 'filter.order.type': 'sales_order' });
+    get_report('#sales_price_information_sales_quotation', 'controller.pl', { action: 'SellPriceInformation/list', 'filter.part.id': [% id %], 'filter.order.type': 'sales_quotation' });
+  });
+
+
+</script>
index e02d273..6faa762 100644 (file)
@@ -1,8 +1,33 @@
 [%- USE T8 %]
 [% USE HTML %]<body style="padding:0px; margin:0px;">
 
+ <script type="text/javascript" src="js/jquery.js"></script>
  <script type="text/javascript">
-<!--
+ <!--
+function on_keydown_quicksearch(e) {
+  var key;
+
+  if (window.event)
+    key = window.event.keyCode;   // IE
+  else
+    key = e.which;                // Firefox
+
+  if (key != 13)
+    return true;
+
+  var search_term = $("#search_term");
+  var value       = search_term.val();
+  if (!value)
+    return true;
+
+  var url = "ct.pl?action=list_contacts&INPUT_ENCODING=utf-8&filter.status=active&search_term=" + encodeURIComponent(value);
+
+  search_term.val('');
+  $("#win1").attr('src', url);
+
+  return false;
+}
+
 function clockon() {
   var now = new Date();
   var h = now.getHours();
@@ -16,11 +41,13 @@ window.onload=clockon
 
  <table border="0" width="100%" background="image/bg_titel.gif" cellpadding="0" cellspacing="0">
   <tr>
-   <td style="color:white; font-family:verdana,arial,sans-serif; font-size: 12px;">
+   <td style="color:white; font-family:verdana,arial,sans-serif; font-size: 12px;" nowrap>
     &nbsp;
     [<a href="menuv3.pl?action=display" target="_blank">[% 'new Window' | $T8 %]</a>]
     &nbsp;
     [<a href="JavaScript:top.main_window.print()">[% 'print' | $T8 %]</a>]
+    &nbsp;
+    [[% 'Search contacts' | $T8 %] <input size="15" name="search_term" id="search_term" onkeydown="return on_keydown_quicksearch(event)">]
    </td>
    <td align="right" style="vertical-align:middle; color:white; font-family:verdana,arial,sans-serif; font-size: 12px;" nowrap>
     [[% 'User' | $T8 %]: [% HTML.escape(login) %] -
diff --git a/templates/webpages/oe/sales_price_information.html b/templates/webpages/oe/sales_price_information.html
new file mode 100644 (file)
index 0000000..115f077
--- /dev/null
@@ -0,0 +1,113 @@
+[% USE HTML %]
+[% USE LxERP %]
+[% USE T8 %]
+[%- IF !TABDIALOG %]
+<body>
+
+ <p><div class="listtop">[% 'Price information' | $T8 %]</div></p>
+
+ <p>
+  <input type="button" class="submit" onclick="window.close()" value="[% 'Close window' | $T8 %]">
+ </p>
+
+ <p>
+  <table>
+   <tr>
+    <td>[% 'Part Number' | $T8 %]:</td>
+    <td>[% HTML.escape(part_info.partnumber) %]</td>
+   </tr>
+
+   <tr>
+    <td>[% 'Description' | $T8 %]:</td>
+    <td>[% HTML.escape(part_info.description) %]</td>
+   </tr>
+
+   [%- UNLESS part_info.type == 'service' %]
+    <tr>
+     <td>[% 'Stocked' | $T8 %]:</td>
+     <td>[% HTML.escape(LxERP.format_amount_units(stocked, part_info.unit, part_info.unit)) %]</td>
+    </tr>
+   [%- END %]
+  </table>
+ </p>
+
+[%- END %]
+
+ <p>
+  <table>
+   [%- SET custom_texts = {
+          sales_order       => {
+            head   => LxERP.t8('Sales Orders'),
+            empty  => LxERP.t8('This part has not been used in a sales order yet.'),
+            date   => LxERP.t8('Order Date'),
+            number => LxERP.t8('Order Number'),
+            vc     => LxERP.t8('Customer'),
+          },
+          sales_quotation   => {
+            head   => LxERP.t8('Quotations'),
+            empty  => LxERP.t8('This part has not been used in a quotation yet.'),
+            date   => LxERP.t8('Quotation Date'),
+            number => LxERP.t8('Quotation Number'),
+            vc     => LxERP.t8('Customer'),
+          },
+          purchase_order    => {
+            head   => LxERP.t8('Purchase Orders'),
+            empty  => LxERP.t8('This part has not been used in a purchase order yet.'),
+            date   => LxERP.t8('Order Date'),
+            number => LxERP.t8('Order Number'),
+            vc     => LxERP.t8('Vendor'),
+          },
+          request_quotation => {
+            head   => LxERP.t8('RFQs'),
+            empty  => LxERP.t8('This part has not been used in a request quotation yet.'),
+            date   => LxERP.t8('Quotation Date'),
+            number => LxERP.t8('Quotation Number'),
+            vc     => LxERP.t8('Vendor'),
+          },
+        }
+   %]
+   [%- FOREACH type = ['sales_order', 'sales_quotation', 'purchase_order', 'request_quotation'] %]
+
+    <tr>
+     <th class="listtop" colspan="8">[% custom_texts.$type.head %]</th>
+    </tr>
+
+    [%- IF !PRICE_INFO.$type.size %]
+    <tr>
+     <td colspan="7">[% custom_texts.$type.empty %]</td>
+    </tr>
+
+    [%- ELSE %]
+
+     <tr>
+      <th class="listheading">[% custom_texts.$type.date %]</th>
+      <th class="listheading">[% custom_texts.$type.number %]</th>
+      <th class="listheading">[% custom_texts.$type.vc %]</th>
+      <th class="listheading">[% 'Delivered' | $T8 %]</th>
+      <th class="listheading" align="right">[% 'Quantity' | $T8 %]</th>
+      <th class="listheading" align="right">[% 'Unit price' | $T8 %]</th>
+      <th class="listheading" align="right">[% 'Discount' | $T8 %]</th>
+      <th class="listheading" align="right">[% 'Line total' | $T8 %]</th>
+     </tr>
+
+     [%- FOREACH row = PRICE_INFO.$type %]
+      <tr class="listrow[% loop.count % 2 %]">
+       <td>[% HTML.escape(row.date) %]</td>
+       <td><a href="oe.pl?action=edit&type=[% type | html %]&id=[% row.id | html %]">[% HTML.escape(row.number) %]</a></td>
+       <td>[% HTML.escape(row.vc) %]</td>
+       <td align="right">[% HTML.escape(LxERP.format_amount(row.ship)) %] [% HTML.escape(row.unit) %]</td>
+       <td align="right">[% HTML.escape(LxERP.format_amount(row.qty)) %] [% HTML.escape(row.unit) %]</td>
+       <td align="right">[% HTML.escape(LxERP.format_amount(row.sellprice, 2)) %]</td>
+       <td align="right">[% HTML.escape(LxERP.format_amount(row.discount * 100)) %]</td>
+       <td align="right">[% HTML.escape(LxERP.format_amount(row.linetotal, 2)) %]</td>
+      </tr>
+     [%- END %]
+    [%- END %]
+   [%- END %]
+  </table>
+ </p>
+
+[%- IF !TABDIALOG %]
+</body>
+</html>
+[%- END %]
diff --git a/templates/webpages/price_information/report_bottom.html b/templates/webpages/price_information/report_bottom.html
new file mode 100644 (file)
index 0000000..587fb06
--- /dev/null
@@ -0,0 +1 @@
+<p align=right>[% PROCESS 'common/paginate.html' pages=SELF.pages, base_url=SELF.self_url %]</p>