Lieferwertbericht auf eigenen Controller umgestellt und erweitert
[kivitendo-erp.git] / SL / Controller / DeliveryValueReport.pm
diff --git a/SL/Controller/DeliveryValueReport.pm b/SL/Controller/DeliveryValueReport.pm
new file mode 100644 (file)
index 0000000..1270f8d
--- /dev/null
@@ -0,0 +1,354 @@
+package SL::Controller::DeliveryValueReport;
+
+use strict;
+use parent qw(SL::Controller::Base);
+
+use Clone qw(clone);
+use SL::DB::OrderItem;
+use SL::DB::Business;
+use SL::Controller::Helper::GetModels;
+use SL::Controller::Helper::ReportGenerator;
+use SL::Locale::String;
+use SL::AM;
+use SL::DBUtils ();
+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) ],
+);
+
+
+
+my %sort_columns = (
+  reqdate                 => t8('Reqdate'),
+  customer                => t8('Customer'),
+  vendor                  => t8('Vendor'),
+  ordnumber               => t8('Order'),
+  partnumber              => t8('Part Number'),
+  description             => t8('Description'),
+  qty                     => t8('Qty in Order'),
+  unit                    => t8('Unit'),
+  netto_qty               => t8('Net value in Order'),
+  not_shipped_qty         => t8('not shipped'),
+  netto_not_shipped_qty   => t8('Net value without delivery orders'),
+  shipped_qty             => t8('Qty in delivery orders'),
+  netto_shipped_qty       => t8('Net Value in delivery orders'),
+  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')
+);
+
+
+
+
+#
+# action
+#
+
+sub action_list {
+  my ($self) = @_;
+  $self->make_filter_summary;
+  $self->prepare_report;
+
+  my $orderitems = $self->models->get;
+  $self->calc_qtys_price($orderitems);
+  $self->report_generator_list_objects(report => $self->{report}, objects => $orderitems);
+}
+
+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;
+
+  my @columns     = qw(reqdate customer vendor ordnumber partnumber description unit qty netto_qty
+                       not_shipped_qty netto_not_shipped_qty shipped_qty netto_shipped_qty delivered_qty
+                       netto_delivered_qty do_closed_qty netto_do_closed_qty);
+
+
+  my @sortable    = qw(reqdate customer vendor ordnumber partnumber description);
+
+  # if csv report export no units (better calculation in
+  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                                                      },
+                            visible => $vc eq 'vendor',
+                           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)                                          } },
+  );
+
+  $column_defs{$_}->{text} = $sort_columns{$_} for keys %column_defs;
+
+  $report->set_options(
+    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'),
+    allow_pdf_export      => 1,
+    allow_csv_export      => 1,
+  );
+  $report->set_columns(%column_defs);
+  $report->set_column_order(@columns);
+  $report->set_export_options(qw(list filter vc));
+  $report->set_options_from_form;
+  $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
+  $self->models->finalize; # for filter laundering
+  $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
+  $report->set_options(
+    raw_top_info_text     => $self->render('delivery_value_report/report_top',    { output => 0 }),
+    raw_bottom_info_text  => $self->render('delivery_value_report/report_bottom', { output => 0 }, models => $self->models),
+  );
+}
+
+
+
+
+#
+# filter
+#
+
+sub make_filter_summary {
+  my ($self) = @_;
+  my $vc     = $self->vc;
+  my $mode   = 'delivery_value_report';
+  my ($business, $employee);
+
+  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};
+
+  my @filters = (
+    [ $filter->{order}{"ordnumber:substr::ilike"},                    $::locale->text('Number')                                             ],
+    [ $filter->{order}{globalproject}{"projectnumber:substr::ilike"}, $::locale->text('Document Project 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}{vendor}{"name:substr::ilike"},                 $::locale->text('Vendor')                                             ],
+    [ $filter->{order}{vendor}{"vendornumber:substr::ilike"},         $::locale->text('Vendor Number')                                      ],
+    [ $filter->{order}{customer}{"name:substr::ilike"},               $::locale->text('Customer')                                           ],
+    [ $filter->{order}{customer}{"customernumber:substr::ilike"},     $::locale->text('Customer Number')                                    ],
+    [ $business,                                                      $::locale->text('Customer type')                                      ],
+    [ $employee,                                                      $::locale->text('Employee')                                           ],
+  );
+
+  my %flags = (
+    part     => $::locale->text('Parts'),
+    service  => $::locale->text('Services'),
+    assembly => $::locale->text('Assemblies'),
+  );
+  my @flags = map { $flags{$_} } @{ $filter->{part}{type} || [] };
+
+  for (@flags) {
+    push @filter_strings, $_ if $_;
+  }
+  for (@filters) {
+    push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
+  }
+  $self->{filter_summary} = join ', ', @filter_strings;
+}
+
+
+
+#
+# helpers
+#
+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',
+    sorted                => {
+      _default              => {
+        by                    => 'reqdate',
+        dir                   => 1,
+      },
+      %sort_columns,
+    },
+    # show only open orders
+    query                 => [ 'order.closed' => '0',  "order.${vc}_id" => { gt => 0 } ],
+    with_objects          => [ 'order', "order.$vc" ],
+    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";
+}
+sub init_all_employees {
+  return SL::DB::Manager::Employee->get_all_sorted;
+}
+sub init_all_businesses {
+  return SL::DB::Manager::Business->get_all_sorted;
+}
+
+
+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 "controller.pl?action=CustomerVendor/$action&id=$id&db=customer";
+  }
+}
+
+
+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;
+
+  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);
+
+  }
+}
+
+
+
+
+1;
+
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Controller::DeliveryValueReport - Controller for Delivery Value Report
+
+=head2 OVERVIEW
+
+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
+relation to open orders, orders in process.
+
+
+Inherited from the base controller class, this controller implements the Delivery Value Report.
+Historically the idea derived from a customer extension by thinking: Ah, we just need the Delivery Plan
+put some more columns in it and then we have a pseudo "Production, Planing, Report" with an additional
+emphasis on expected future cashflow.
+Some problems exists with the current report: The definition of not fully delivered sales / purchase order
+is very (customer) special, in general a simple check on order is open should be a sensible workflow value.
+Secondly a major database flaw (no persistent ids in order_items) made it impossible to determine the origin
+of items in terms of linked records. One assumption build in the original DeliveryPlan was that the part_ids
+are equal. This breaks if the document has the same item on different positions. The next idea was to check
+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
+used in the position.
+
+  from_table |   id    | to_table             |   id
+  orderitems |    7    | delivery_order_items |   11
+
+=head1 FUNCTIONS
+
+=over 2
+
+=item C<action_list_sales_delivery_orders>
+
+L