Lieferwertbericht um Filter nach Warengruppen erweitert
[kivitendo-erp.git] / SL / Controller / DeliveryValueReport.pm
index 1270f8d..ae2032f 100644 (file)
@@ -9,17 +9,18 @@ use SL::DB::Business;
 use SL::Controller::Helper::GetModels;
 use SL::Controller::Helper::ReportGenerator;
 use SL::Locale::String;
+use SL::Helper::ShippedQty;
 use SL::AM;
-use SL::DBUtils ();
+use SL::DBUtils qw(selectall_as_map);
+use List::MoreUtils qw(uniq);
 use Carp;
 use Data::Dumper;
 
 use Rose::Object::MakeMethods::Generic (
-  scalar => [ qw(db_args flat_filter) ],
-  'scalar --get_set_init' => [ qw(models vc all_employees all_businesses) ],
+  'scalar --get_set_init' => [ qw(models vc all_employees all_businesses all_partsgroups) ],
 );
 
-
+__PACKAGE__->run_before(sub { $::auth->assert('delivery_value_report'); });
 
 my %sort_columns = (
   reqdate                 => t8('Reqdate'),
@@ -38,7 +39,7 @@ my %sort_columns = (
   delivered_qty           => t8('transferred in / out'),
   netto_delivered_qty     => t8('Net value transferred in / out'),
   do_closed_qty           => t8('Qty in closed delivery orders'),
-  netto_do_closed_qty     => t8('Qty in closed delivery orders')
+  netto_do_closed_qty     => t8('Net value in closed delivery orders'),
 );
 
 
@@ -55,6 +56,7 @@ sub action_list {
 
   my $orderitems = $self->models->get;
   $self->calc_qtys_price($orderitems);
+  $self->setup_list_action_bar;
   $self->report_generator_list_objects(report => $self->{report}, objects => $orderitems);
 }
 
@@ -62,7 +64,6 @@ sub prepare_report {
   my ($self)      = @_;
 
   my $vc          = $self->vc;
-  my $mode        = 'delivery_value_report';
   my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
   my $csv_option  = $::form->{report_generator_output_format};
   $self->{report} = $report;
@@ -74,47 +75,35 @@ sub prepare_report {
 
   my @sortable    = qw(reqdate customer vendor ordnumber partnumber description);
 
-  # if csv report export no units (better calculation in
+  # if csv report export no units
   my $rp_csv_mod  = ($csv_option eq 'CSV') ? 1 : '';
 
   my %column_defs = (
-    reqdate           => {      sub => sub { $_[0]->reqdate_as_date || $_[0]->order->reqdate_as_date     } },
-    description       => {      sub => sub { $_[0]->description                                          },
-                           obj_link => sub { $self->link_to($_[0]->part)                                 } },
-    partnumber        => {      sub => sub { $_[0]->part->partnumber                                     },
-                           obj_link => sub { $self->link_to($_[0]->part)                                 } },
-    qty               => {      sub => sub { $_[0]->qty_as_number .
-                                             ($rp_csv_mod ? '' : ' ' .  $_[0]->unit)                     } },
-    netto_qty         => {      sub => sub { $::form->format_amount(\%::myconfig,
-                                              ($_[0]->qty * $_[0]->sellprice * (1 - $_[0]->discount) /
-                                                                         ($_[0]->price_factor || 1), 2)) },},
-    unit              => {      sub => sub {  $_[0]->unit                                                                    },
-                            visible => $rp_csv_mod                                                                           },
-    shipped_qty       => {      sub => sub { $::form->format_amount(\%::myconfig, $_[0]{shipped_qty}, 2) .
-                                             ($rp_csv_mod ? '' : ' ' .  $_[0]->unit)                                         } },
-    netto_shipped_qty => {      sub => sub { $::form->format_amount(\%::myconfig, $_[0]{netto_shipped_qty}, 2)             },},
-    not_shipped_qty   => {      sub => sub { $::form->format_amount(\%::myconfig, $_[0]->qty - $_[0]{shipped_qty} - $_[0]{delivered_qty} - $_[0]{do_closed_qty}, 2) .
-                                             ($rp_csv_mod ? '' : ' ' .  $_[0]->unit)                                         } },
-    delivered_qty     => {      sub => sub { $::form->format_amount(\%::myconfig, $_[0]{delivered_qty}, 2) .
-                                             ($rp_csv_mod ? '' : ' ' .  $_[0]->unit)                                         } },
-    netto_delivered_qty => {      sub => sub { $::form->format_amount(\%::myconfig, $_[0]{netto_delivered_qty}, 2)          },},
-
-    netto_not_shipped_qty => {      sub => sub { $::form->format_amount(\%::myconfig,(
-                                                    ($_[0]->qty - $_[0]{shipped_qty} - $_[0]{delivered_qty} - $_[0]{do_closed_qty})
-                                                  * ($_[0]->sellprice * (1 - $_[0]->discount) / ($_[0]->price_factor || 1)), 2))  },},
-    do_closed_qty     => {      sub => sub { $::form->format_amount(\%::myconfig, $_[0]{do_closed_qty}, 2) .
-                                             ($rp_csv_mod ? '' : ' ' .  $_[0]->unit)                                        },},
-
-    netto_do_closed_qty => {      sub => sub { $::form->format_amount(\%::myconfig, $_[0]{netto_do_closed_qty}, 2)            },
-                            visible => $mode eq 'delivery_value_report'                                                     },
-    ordnumber         => {      sub => sub { $_[0]->order->ordnumber                                                         },
-                           obj_link => sub { $self->link_to($_[0]->order)                                                    } },
-    vendor            => {      sub => sub { $_[0]->order->vendor->name                                                      },
+    reqdate           => {      sub => sub { $_[0]->reqdate_as_date || $_[0]->order->reqdate_as_date          } },
+    description       => {      sub => sub { $_[0]->description                                               },
+                           obj_link => sub { $self->link_to($_[0]->part)                                      } },
+    partnumber        => {      sub => sub { $_[0]->part->partnumber                                          },
+                           obj_link => sub { $self->link_to($_[0]->part)                                      } },
+    qty               => {      sub => sub { _format_qty($_[0], 'qty', $rp_csv_mod)                           } },
+    netto_qty         => {      sub => sub { _format_val($_[0], 'qty')                                        },},
+    unit              => {      sub => sub {  $_[0]->unit                                                     },
+                            visible => $rp_csv_mod                                                              },
+    shipped_qty       => {      sub => sub { _format_qty($_[0], 'shipped_qty', $rp_csv_mod)                   } },
+    netto_shipped_qty => {      sub => sub { _format_val($_[0], 'shipped_qty')                                },},
+    not_shipped_qty   => {      sub => sub { _format_qty($_[0], 'not_shipped_qty', $rp_csv_mod)               } },
+    netto_not_shipped_qty => {  sub => sub { _format_val($_[0], 'not_shipped_qty')                            },},
+    delivered_qty     => {      sub => sub { _format_qty($_[0], 'delivered_qty', $rp_csv_mod)                 } },
+    netto_delivered_qty => {    sub => sub { _format_val($_[0], 'delivered_qty')                              },},
+    do_closed_qty     => {      sub => sub { _format_qty($_[0], 'do_closed_qty', $rp_csv_mod)                 },},
+    netto_do_closed_qty => {    sub => sub { _format_val($_[0], 'do_closed_qty')                              },},
+    ordnumber         => {      sub => sub { $_[0]->order->ordnumber                                           },
+                           obj_link => sub { $self->link_to($_[0]->order)                                      } },
+    vendor            => {      sub => sub { $_[0]->order->vendor->name                                        },
                             visible => $vc eq 'vendor',
-                           obj_link => sub { $self->link_to($_[0]->order->vendor)                                            } },
-    customer          => {      sub => sub { $_[0]->order->customer->name                                                    },
+                           obj_link => sub { $self->link_to($_[0]->order->vendor)                              } },
+    customer          => {      sub => sub { $_[0]->order->customer->name                                      },
                             visible => $vc eq 'customer',
-                           obj_link => sub { $self->link_to($_[0]->order->customer)                                          } },
+                           obj_link => sub { $self->link_to($_[0]->order->customer)                            } },
   );
 
   $column_defs{$_}->{text} = $sort_columns{$_} for keys %column_defs;
@@ -123,11 +112,9 @@ sub prepare_report {
     std_column_visibility => 1,
     controller_class      => 'DeliveryValueReport',
     output_format         => 'HTML',
-    top_info_text         => ($vc eq 'customer') ?  (($mode eq 'delivery_plan') ? $::locale->text('Delivery Plan for currently outstanding sales orders') :
-                                                   $::locale->text('Delivery Value Report for currently open sales orders')) :
-                                                   ($mode eq 'delivery_value_report') ? $::locale->text('Delivery Value Report for currently outstanding purchase orders') :
-                                                   $::locale->text('Delivery Plan for currently outstanding purchase orders'),
-    title                 => ($mode eq 'delivery_plan') ? $::locale->text('Delivery Plan') : $::locale->text('Delivery Value Report'),
+    top_info_text         => ($vc eq 'customer') ? t8('Delivery Value Report for currently open sales orders') :
+                                                   t8('Delivery Value Report for currently outstanding purchase orders'),
+    title                 => $::locale->text('Delivery Value Report'),
     allow_pdf_export      => 1,
     allow_csv_export      => 1,
   );
@@ -154,14 +141,14 @@ sub prepare_report {
 sub make_filter_summary {
   my ($self) = @_;
   my $vc     = $self->vc;
-  my $mode   = 'delivery_value_report';
-  my ($business, $employee);
+  my ($business, $employee, $partsgroup);
 
   my $filter = $::form->{filter} || {};
   my @filter_strings;
 
-  $business = SL::DB::Business->new(id => $filter->{order}{customer}{"business_id"})->load->description if $filter->{order}{customer}{"business_id"};
-  $employee = SL::DB::Employee->new(id => $filter->{order}{employee_id})->load->name if $filter->{order}{employee_id};
+  $business   = SL::DB::Business->new(id => $filter->{order}{customer}{"business_id"})->load->description if $filter->{order}{customer}{"business_id"};
+  $employee   = SL::DB::Employee->new(id => $filter->{order}{employee_id})->load->name                    if $filter->{order}{employee_id};
+  $partsgroup = SL::DB::PartsGroup->new(id => $filter->{part}{partsgroup_id})->load->partsgroup           if $filter->{part}{partsgroup_id};
 
   my @filters = (
     [ $filter->{order}{"ordnumber:substr::ilike"},                    $::locale->text('Number')                                             ],
@@ -177,8 +164,10 @@ sub make_filter_summary {
     [ $filter->{order}{customer}{"customernumber:substr::ilike"},     $::locale->text('Customer Number')                                    ],
     [ $business,                                                      $::locale->text('Customer type')                                      ],
     [ $employee,                                                      $::locale->text('Employee')                                           ],
+    [ $partsgroup,                                                    $::locale->text('Partsgroup')                                         ],
   );
 
+  # flags for with_object 'part'
   my %flags = (
     part     => $::locale->text('Parts'),
     service  => $::locale->text('Services'),
@@ -203,7 +192,6 @@ sub make_filter_summary {
 sub init_models {
   my ($self) = @_;
   my $vc     = $self->vc;
-  $main::lxdebug->message(0, 'vc hier:' . $vc);
   SL::Controller::Helper::GetModels->new(
     controller            => $self,
     model                 => 'OrderItem',
@@ -214,15 +202,16 @@ sub init_models {
       },
       %sort_columns,
     },
-    # show only open orders
-    query                 => [ 'order.closed' => '0',  "order.${vc}_id" => { gt => 0 } ],
-    with_objects          => [ 'order', "order.$vc" ],
+    # show only open (sales|purchase) orders
+    query                 => [ 'order.closed' => '0',  "order.${vc}_id" => { gt => 0 },
+                               'order.quotation' => 0                                  ],
+    with_objects          => [ 'order', "order.$vc", 'part' ],
     additional_url_params => { vc => $vc},
-  );
+  )
 }
 
 sub init_vc {
-  return $::form->{vc} if ($::form->{vc} eq 'customer' || $::form->{vc} eq 'vendor') || croak "self (DeliveryPlan) has no vc defined";
+  return $::form->{vc} if ($::form->{vc} eq 'customer' || $::form->{vc} eq 'vendor') || croak "self (DeliveryValueReport) has no vc defined";
 }
 sub init_all_employees {
   return SL::DB::Manager::Employee->get_all_sorted;
@@ -230,6 +219,9 @@ sub init_all_employees {
 sub init_all_businesses {
   return SL::DB::Manager::Business->get_all_sorted;
 }
+sub init_all_partsgroups {
+  return SL::DB::Manager::PartsGroup->get_all_sorted;
+}
 
 
 sub link_to {
@@ -243,11 +235,15 @@ sub link_to {
     my $vc     = $object->is_sales ? 'customer' : 'vendor';
     my $id     = $object->id;
 
-    return "oe.pl?action=$action&type=$type&vc=$vc&id=$id";
+    if ($::instance_conf->get_feature_experimental_order) {
+      return "controller.pl?action=Order/$action&type=$type&id=$id";
+    } else {
+      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";
+    return "controller.pl?action=Part/$action&part.id=$id";
   }
   if ($object->isa('SL::DB::Customer')) {
     my $id     = $object->id;
@@ -255,55 +251,72 @@ sub link_to {
   }
 }
 
+sub _format_qty {
+  my ($item, $col, $csv_mod) = @_;
+
+  $::form->format_amount(\%::myconfig, $item->{$col}, 2) .  ($csv_mod ? '' : ' ' .  $item->unit)
+}
+
+sub _format_val {
+  my ($item, $col) = @_;
+
+  $::form->format_amount(\%::myconfig, $item->{$col} * $item->sellprice * (1 - $item->discount) / ($item->price_factor || 1), 2)
+}
+
 
 sub calc_qtys_price {
   my ($self, $orderitems) = @_;
-  # using $orderitem->shipped_qty 40 times is far too slow. need to do it manually
-  #
 
   return unless scalar @$orderitems;
 
-  my %orderitems_by_id = map { $_->id => $_ } @$orderitems;
+  SL::Helper::ShippedQty
+    ->new(require_stock_out => 1)
+    ->calculate($orderitems)
+    ->write_to_objects;
 
-  my $query = <<SQL;
-    SELECT oi.id, doi.qty, doi.qty, doi.unit, doe.delivered, doe.closed, -- dois.qty as qty_stocked,
-           oi.sellprice, oi.discount, oi.price_factor
-    FROM record_links rl
-    INNER JOIN delivery_order_items doi ON (doi.id = rl.to_id)
-    INNER JOIN orderitems oi            ON (oi.id  = rl.from_id)
-    INNER JOIN delivery_orders doe      ON (doe.id = doi.delivery_order_id)
-    --INNER JOIN delivery_order_items_stock dois ON (doi.id = dois.delivery_order_item_id)
-    WHERE rl.from_table = 'orderitems'
-      AND rl.to_table   = 'delivery_order_items'
-      AND oi.id IN (@{[ join ', ', ("?")x @$orderitems ]})
-SQL
-
-  my $result = SL::DBUtils::selectall_hashref_query($::form, $::form->get_standard_dbh, $query, map { $_->id } @$orderitems);
-
-  for my $row (@$result) {
-    my $item = $orderitems_by_id{ $row->{id} };
-    $item->{shipped_qty}   ||= 0;
-    $item->{delivered_qty} ||= 0;
-    $item->{do_closed_qty} ||= 0;
-    $item->{shipped_qty}    += AM->convert_unit($row->{unit} => $item->unit) * $row->{qty} unless ($row->{delivered} || $row->{closed});
-    $item->{delivered_qty}  += AM->convert_unit($row->{unit} => $item->unit) * $row->{qty} if ($row->{delivered} && !$row->{closed});
-    $item->{do_closed_qty}  += AM->convert_unit($row->{unit} => $item->unit) * $row->{qty} if ($row->{closed});
-    #$item->{do_closed_qty}  += AM->convert_unit($row->{unit} => $item->unit) * $row->{qty_stocked} if ($row->{closed});
-    $item->{not_shipped_qty} += AM->convert_unit($row->{unit} => $item->unit) * $row->{qty} unless ($row->{delivered});
-    #  my $price_factor = $self->price_factor || 1;
-    #$self->_delivered_qty;
-    #$item->{netto_qty}              += $row->{qty} * $row->{sellprice} * (1 - $row->{discount} ) / ($row->{price_factor} || 1);
-    # no sum
-    $item->{netto_shipped_qty}      = $item->{shipped_qty} * $row->{sellprice} * (1 - $row->{discount} ) / ($row->{price_factor} || 1);
-#    $item->{netto_not_shipped_qty}  += $item->{shipped_qty} * $row->{sellprice} * (1 - $row->{discount} ) / ($row->{price_factor} || 1);
-    $item->{netto_delivered_qty}    = $item->{delivered_qty} * $row->{sellprice} * (1 - $row->{discount} ) / ($row->{price_factor} || 1);
-    $item->{netto_do_closed_qty}    = $item->{do_closed_qty} * $row->{sellprice} * (1 - $row->{discount} ) / ($row->{price_factor} || 1);
+  $_->{delivered_qty} = delete $_->{shipped_qty} for @$orderitems;
+
+  my $helper = SL::Helper::ShippedQty
+    ->new(require_stock_out => 0, keep_matches => 1)
+    ->calculate($orderitems)
+    ->write_to_objects;
+
+  for my $item (@$orderitems) {
+    $item->{not_shipped_qty} = $item->qty - $item->{shipped_qty};
+    $item->{do_closed_qty}   = 0;
 
+    my $price_factor = $item->price_factor || 1;
   }
-}
 
+  if (my @all_doi_ids = uniq map { $_->[1] } @{ $helper->matches }) {
+    my %oi_by_id = map { $_->id => $_ } @$orderitems;
+    my $query    = sprintf <<'', join ', ', ("?")x@all_doi_ids;
+      SELECT DISTINCT doi.id, closed FROM delivery_orders
+      LEFT JOIN delivery_order_items doi ON (doi.delivery_order_id = delivery_orders.id)
+      WHERE doi.id IN (%s)
+
+    my %doi_is_closed = selectall_as_map($::form, SL::DB->client->dbh, $query, (id => 'closed'), @all_doi_ids);
 
+    for my $match (@{ $helper->matches }) {
+      next unless $doi_is_closed{$match->[1]};
+      $oi_by_id{$match->[0]}->{do_closed_qty} += $match->[2];
+    }
+  }
+}
 
+sub setup_list_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#filter_form', { action => 'DeliveryValueReport/list' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
 
 1;
 
@@ -322,7 +335,7 @@ SL::Controller::DeliveryValueReport - Controller for Delivery Value Report
 
 Controller class for Delivery Value Report
 
-The goal of the record is to determine which goods and at what costs are already delivered, transfered in
+The goal of the report is to determine which goods and at what costs are already delivered, transfered in
 relation to open orders, orders in process.
 
 
@@ -339,16 +352,60 @@ for individual item reqdates.
 After some arguing we decided to implement persistent ids for all items and link them directly via record_links.
 This linking has been secrectly active since version 3.2, therefore this redesign is possible.
 Currently the report even works correctly even if the same part has been manually put in another position, renamed or some
- other metadata for the position has been altered. This is due to the fact that a hidden converted_from_previous_document is
+other metadata for the position has been altered. This is due to the fact that a hidden converted_from_previous_document is
 used in the position.
 
-  from_table |   id    | to_table             |   id
-  orderitems |    7    | delivery_order_items |   11
+The main intelligence is this query (qty_stocked as comments):
+
+    SELECT oi.id,and more metadata , -- dois.qty as qty_stocked,
+    FROM record_links rl
+    INNER JOIN delivery_order_items doi ON (doi.id = rl.to_id)
+    INNER JOIN orderitems oi            ON (oi.id  = rl.from_id)
+    INNER JOIN delivery_orders doe      ON (doe.id = doi.delivery_order_id)
+    --INNER JOIN delivery_order_items_stock dois ON (doi.id = dois.delivery_order_item_id)
+    WHERE rl.from_table = 'orderitems'
+      AND rl.to_table   = 'delivery_order_items'
+
+Get all entries which were converted from orderitems to delivery_order_items (WHERE).
+The persistent id are in rl, therefore we can fetch orderitems and delivery_order_items.
+The join on delivery_orders (doe) is only needed for the current state of the delivery order (closed, delivered).
 
 =head1 FUNCTIONS
 
 =over 2
 
-=item C<action_list_sales_delivery_orders>
+=item C<action_list>
+
+=item C<prepare_report>
+
+=item C<make_filter_summary>
+
+=item C<calc_qtys_price>
+
+=item C<link_to>
+
+=item C<init_models>
+
+=item C<init_vc>
+
+=item C<init_all_employees>
+
+=item C<init_all_businesses>
+
+=back
+
+=head1 TODOS
+
+Currently no foreign currencies and OrderItems with taxincluded are calculated / supported. The report can be easily extended
+for the real stocked qty. The report is really easy to implement and customise if your model is focussed straight.
+For long term maintaineance it would be wise to add more testcases for the conversion from orders to delivery_orders.
+Right now record_links are tested only from document to document and the convert_invoice method (via task server) has a
+test case with record_links items included. Furhtermore I personally dislike the calcs in the %columns_def, but for a quick report
+this is ok, though if we redesign this further, the taxincluded / currency cases should be implemented as well.
+
+
+=head1 AUTHOR
+
+Jan Büren E<lt>jan@kivitendo-premium.deE<gt> (based on DeliveryPlan.pm by Sven)
 
-L
+=cut