Verbrauchsbericht: Lager->Berichte->Lagerentnahme
authorMartin Helmling martin.helmling@octosoft.eu <martin.helmling@octosoft.eu>
Mon, 19 Sep 2016 15:54:40 +0000 (17:54 +0200)
committerMartin Helmling martin.helmling@octosoft.eu <martin.helmling@octosoft.eu>
Thu, 22 Sep 2016 06:09:43 +0000 (08:09 +0200)
Ein Bericht über jährliche/quartalsweise/monatliche Lagerentnahme

Anforderungen waren:

1. Zahlen im Bericht einheitlich Formatieren (mit Tausender-Trennpunkt)

2. Spaltenüberschrift:
- "Lagerverbrauch" (Letzte zwei Spalten) -> "Entnommen (ohne Korrekturen)"
- "Verbrauch monatlich" (vorletzte Spalte) -> "Im Zeitraum"
- "Ø Verbr." (letzte Spalte -> "Ø mtl."

3. In der Suchmaske unter "Vorgewählte Zeiträume" sollte das aktuelle Jahr vorgegeben sein.

4. Filter "Freier Zeitraum" funktioniert nicht: "Es wurden keine Daten gefunden."

Bei der Berechnung des "Ø mtl." wird Folgendes beachtet:
Falls ein Zeitraum angegeben ist mit einem Enddatum in der Zukunft (z.B. auch bei "Jährlich" oder "Quartal")
muss der Bezugszeitraum mit dem aktuellen Datum (heute) begrenzt werden, ansonsten wird der Durchschnitt falsch berechnet!

Bei Zeitraum kleiner einem Monat, wird Durchschnitt des Entnommenen über einen Monat berechnet

Bei PDF/CSV Export müssen alle Werte berechnet werden
Wegen PDF-Erzeugung ausserhalb Papier X-Rand default reduziert auf 0,8 cm

SL/Controller/Inventory.pm
bin/mozilla/wh.pl
doc/changelog
locale/de/all
menus/user/12-warehouse-usage.yaml [new file with mode: 0644]
templates/webpages/inventory/report_bottom.html [new file with mode: 0644]
templates/webpages/inventory/warehouse_usage.html [new file with mode: 0644]
templates/webpages/report_generator/pdf_export_options.html

index 6392ecc..454b012 100644 (file)
@@ -2,6 +2,7 @@ package SL::Controller::Inventory;
 
 use strict;
 use warnings;
+use POSIX qw(strftime);
 
 use parent qw(SL::Controller::Base);
 
@@ -10,10 +11,12 @@ use SL::DB::Part;
 use SL::DB::Warehouse;
 use SL::DB::Unit;
 use SL::WH;
+use SL::ReportGenerator;
 use SL::Locale::String qw(t8);
 use SL::Presenter;
 use SL::DBUtils;
 use SL::Helper::Flash;
+use SL::Controller::Helper::ReportGenerator;
 
 use English qw(-no_match_vars);
 
@@ -30,7 +33,7 @@ __PACKAGE__->run_before('load_wh_from_form',     only => [ qw(stock_in warehouse
 __PACKAGE__->run_before('load_bin_from_form',    only => [ qw(stock_in stock) ]);
 __PACKAGE__->run_before('set_target_from_part',  only => [ qw(part_changed) ]);
 __PACKAGE__->run_before('mini_stock',            only => [ qw(stock_in mini_stock) ]);
-__PACKAGE__->run_before('sanitize_target',       only => [ qw(stock_in warehouse_changed part_changed) ]);
+__PACKAGE__->run_before('sanitize_target',       only => [ qw(stock_usage stock_in warehouse_changed part_changed) ]);
 __PACKAGE__->run_before('set_layout');
 
 sub action_stock_in {
@@ -44,6 +47,349 @@ sub action_stock_in {
   $self->render('inventory/warehouse_selection_stock', title => $::form->{title}, TRANSFER_TYPES => $transfer_types );
 }
 
+sub action_stock_usage {
+  my ($self) = @_;
+
+  $::form->{title}   = t8('UsageE');
+
+  $::form->get_lists('warehouses' => { 'key'    => 'WAREHOUSES',
+                                       'bins'   => 'BINS', });
+  $::request->layout->use_javascript("${_}.js") for qw(kivi.PartsWarehouse);
+
+  $self->render('inventory/warehouse_usage',
+                title => $::form->{title},
+                year => DateTime->today->year,
+  #              PARTSCLASSIFICATIONS => SL::DB:Manager::PartsClassification->get_all_classifications_by_name() ,
+                WAREHOUSES => $::form->{WAREHOUSES},
+                WAREHOUSE_FILTER => 1,
+                warehouse_id => 0,
+                bin_id => 0
+      );
+
+}
+
+sub getnumcolumns {
+  my ($self) = @_;
+  return qw(stock incorrection found insum back outcorrection disposed 
+                     missing shipped used outsum consumed averconsumed);
+}
+
+sub action_usage {
+  my ($self) = @_;
+
+  $main::lxdebug->enter_sub();
+
+  my $form     = $main::form;
+  my %myconfig = %main::myconfig;
+  my $locale   = $main::locale;
+
+  $form->{title}   = t8('UsageE');
+  $form->{report_generator_output_format} = 'HTML' if !$form->{report_generator_output_format};
+
+  my $report = SL::ReportGenerator->new(\%myconfig, $form);
+
+  my @columns = qw(partnumber partdescription);
+
+  push @columns , qw(ptype unit) if $form->{report_generator_output_format} eq 'HTML';
+
+  my @numcolumns = qw(stock incorrection found insum back outcorrection disposed 
+                     missing shipped used outsum consumed averconsumed);
+
+  push @columns , $self->getnumcolumns();
+
+  my @hidden_variables = qw(reporttype year duetyp fromdate todate 
+                            warehouse_id bin_id partnumber description bestbefore chargenumber partstypes_id);
+  my %column_defs = (
+    'partnumber'      => { 'text' => $locale->text('Part Number'), },
+ #   'partclass'       => { 'text' => $locale->text('Part Classification'), },
+    'partdescription' => { 'text' => $locale->text('Part_br_Description'), },
+    'unit'            => { 'text' => $locale->text('Unit'), },
+    'stock'           => { 'text' => $locale->text('stock_br'), },
+    'incorrection'    => { 'text' => $locale->text('correction_br'), },
+    'found'           => { 'text' => $locale->text('found_br'), },
+    'insum'           => { 'text' => $locale->text('sum'), },
+    'back'            => { 'text' => $locale->text('back_br'), },
+    'outcorrection'   => { 'text' => $locale->text('correction_br'), },
+    'disposed'        => { 'text' => $locale->text('disposed_br'), },
+    'missing'         => { 'text' => $locale->text('missing_br'), },
+    'shipped'         => { 'text' => $locale->text('shipped_br'), },
+    'used'            => { 'text' => $locale->text('used_br'), },
+    'outsum'          => { 'text' => $locale->text('sum'), },
+    'consumed'        => { 'text' => $locale->text('consumed'), },
+    'averconsumed'    => { 'text' => $locale->text('averconsumed_br'), },
+  );
+
+
+  map { $column_defs{$_}->{visible} = 1 } @columns;
+  #map { $column_defs{$_}->{visible} = $form->{"l_${_}"} ? 1 : 0 } @columns;
+  map { $column_defs{$_}->{align} = 'right' } @numcolumns;
+
+  my @custom_headers = ();
+  # Zeile 1:
+  push @custom_headers, [
+      { 'text' => $locale->text('Part'),   
+        'colspan' => ($form->{report_generator_output_format} eq 'HTML'?4:2), 'align' => 'center'},
+      { 'text' => $locale->text('Into bin'), 'colspan' => 4, 'align' => 'center'},
+      { 'text' => $locale->text('From bin'), 'colspan' => 7, 'align' => 'center'},
+      { 'text' => $locale->text('UsageWithout'),    'colspan' => 2, 'align' => 'center'},
+  ];
+
+  # Zeile 2:
+  my @line_2 = ();
+  map { push @line_2 , $column_defs{$_} } @columns;
+  push @custom_headers, [ @line_2 ];
+
+  $report->set_custom_headers(@custom_headers);
+  $report->set_columns( %column_defs );
+  $report->set_column_order(@columns);
+
+  $report->set_export_options('usage', @hidden_variables );
+
+  $report->set_sort_indicator($form->{sort}, $form->{order});
+  $report->set_options('output_format'        => 'HTML',
+                       'controller_class'     => 'Inventory',
+                       'title'                => $form->{title},
+#                      'html_template'        => 'inventory/usage_report',
+                       'attachment_basename'  => strftime($locale->text('warehouse_usage_list') . '_%Y%m%d', localtime time));
+  $report->set_options_from_form;
+
+  my %searchparams ;
+# form vars
+#   reporttype = custom
+#   year = 2014
+#   duetyp = 7
+
+  my $start       = DateTime->now_local;
+  my $end         = DateTime->now_local;
+  my $actualepoch = $end->epoch();
+  my $days = 365;
+  my $mdays=30;
+  $searchparams{reporttype} = $form->{reporttype};
+  if ($form->{reporttype} eq "custom") {
+    my $smon = 1;
+    my $emon = 12;
+    my $sday = 1;
+    my $eday = 31;
+    #forgotten the year --> thisyear
+    if ($form->{year} !~ m/^\d\d\d\d$/) {
+      $locale->date(\%myconfig, $form->current_date(\%myconfig), 0) =~
+        /(\d\d\d\d)/;
+      $form->{year} = $1;
+    }
+    my $leapday = ($form->{year} % 4 == 0) ? 1:0;
+    #yearly report
+    if ($form->{duetyp} eq "13") {
+        $days += $leapday;
+    }
+
+    #Quater reports
+    if ($form->{duetyp} eq "A") {
+      $emon = 3;
+      $days = 90 + $leapday;
+    }
+    if ($form->{duetyp} eq "B") {
+      $smon = 4;
+      $emon = 6;
+      $eday = 30;
+      $days = 91;
+    }
+    if ($form->{duetyp} eq "C") {
+      $smon = 7;
+      $emon = 9;
+      $eday = 30;
+      $days = 92;
+    }
+    if ($form->{duetyp} eq "D") {
+      $smon = 10;
+      $days = 92;
+    }
+    #Monthly reports
+    if ($form->{duetyp} eq "1" || $form->{duetyp} eq "3" || $form->{duetyp} eq "5" ||
+        $form->{duetyp} eq "7" || $form->{duetyp} eq "8" || $form->{duetyp} eq "10" ||
+        $form->{duetyp} eq "12") {
+        $smon = $emon = $form->{duetyp}*1;
+        $mdays=$days = 31;
+    }
+    if ($form->{duetyp} eq "2" || $form->{duetyp} eq "4" || $form->{duetyp} eq "6" ||
+        $form->{duetyp} eq "9" || $form->{duetyp} eq "11" ) {
+        $smon = $emon = $form->{duetyp}*1;
+        $eday = 30;
+        if ($form->{duetyp} eq "2" ) {
+            #this works from 1901 to 2099, 1900 and 2100 fail.
+            $eday = ($form->{year} % 4 == 0) ? 29 : 28;
+        }
+        $mdays=$days = $eday;
+    }
+    $searchparams{year} = $form->{year};
+    $searchparams{duetyp} = $form->{duetyp};
+    $start->set_month($smon);
+    $start->set_day($sday);
+    $start->set_year($form->{year}*1);
+    $end->set_month($emon);
+    $end->set_day($eday);
+    $end->set_year($form->{year}*1);
+  }  else {
+    $searchparams{fromdate} = $form->{fromdate};
+    $searchparams{todate} = $form->{todate};
+#   reporttype = free
+#   fromdate = 01.01.2014
+#   todate = 31.05.2014
+    my ($yy, $mm, $dd) = $locale->parse_date(\%myconfig,$form->{fromdate});
+    $start->set_year($yy);
+    $start->set_month($mm);
+    $start->set_day($dd);
+    ($yy, $mm, $dd) = $locale->parse_date(\%myconfig,$form->{todate});
+    $end->set_year($yy);
+    $end->set_month($mm);
+    $end->set_day($dd);
+    my $dur = $start->delta_md($end);
+    $days = $dur->delta_months()*30 + $dur->delta_days() ;
+  }
+  $start->set_second(0);
+  $start->set_minute(0);
+  $start->set_hour(0);
+  $end->set_second(59);
+  $end->set_minute(59);
+  $end->set_hour(23);
+  if ( $end->epoch() > $actualepoch ) { 
+      $end = DateTime->now_local;
+      my $dur = $start->delta_md($end);
+      $days = $dur->delta_months()*30 + $dur->delta_days() ;
+  }
+  if ( $start->epoch() > $end->epoch() ) { $start = $end;$days = 1;}
+  $days = $mdays if $days < $mdays;
+  #$main::lxdebug->message(LXDebug->DEBUG2(), "start=".$start->epoch());
+  #$main::lxdebug->message(LXDebug->DEBUG2(), "  end=".$end->epoch());
+  #$main::lxdebug->message(LXDebug->DEBUG2(), " days=".$days);
+  my @andfilter = (shippingdate => { ge => $start }, shippingdate => { le => $end } );
+  if ( $form->{warehouse_id} ) {
+      push @andfilter , ( warehouse_id => $form->{warehouse_id});
+      $searchparams{warehouse_id} = $form->{warehouse_id};
+      if ( $form->{bin_id} ) {
+          push @andfilter , ( bin_id => $form->{bin_id});
+          $searchparams{bin_id} = $form->{bin_id};
+      }
+  }
+  # alias class t2 entspricht parts
+  if ( $form->{partnumber} ) {
+      push @andfilter , ( 't2.partnumber' => { ilike => '%'. $form->{partnumber} .'%' });
+      $searchparams{partnumber} = $form->{partnumber};
+  }
+  if ( $form->{description} ) {
+      push @andfilter , ( 't2.description' => { ilike => '%'. $form->{description} .'%'  });
+      $searchparams{description} = $form->{description};
+  }
+  if ( $form->{bestbefore} ) {
+    push @andfilter , ( bestbefore => { eq => $form->{bestbefore} });
+      $searchparams{bestbefore} = $form->{bestbefore};
+  }
+  if ( $form->{chargenumber} ) {
+      push @andfilter , ( chargenumber => { ilike => '%'.$form->{chargenumber}.'%' });
+      $searchparams{chargenumber} = $form->{chargenumber};
+  }
+  if ( $form->{partstypes_id} ) {
+      push @andfilter , ( 't2.partstypes_id' => $form->{partstypes_id} );
+      $searchparams{partstypes_id} = $form->{partstypes_id};
+  }
+
+  my @filter = (and => [ @andfilter ] );
+
+  my $objs = SL::DB::Manager::Inventory->get_all(with_objects => ['parts'], where => [ @filter ] , sort_by => 'parts.partnumber ASC');
+  #my $objs = SL::DB::Inventory->_get_manager_class->get_all(...);
+
+  # manual paginating, yuck
+  my $page = $::form->{page} || 1;
+  my $pages = {};
+  $pages->{per_page}        = $::form->{per_page} || 20;
+  my $first_nr = ($page - 1) * $pages->{per_page};
+  my $last_nr  = $first_nr + $pages->{per_page};
+
+  my $last_partid = 0;
+  my $last_row = { };
+  my $row_ind = 0;
+  my $allrows = 0;
+  $allrows = 1 if $form->{report_generator_output_format} ne 'HTML' ;
+  #$main::lxdebug->message(LXDebug->DEBUG2(), "first_nr=".$first_nr." last_nr=".$last_nr);
+  foreach my $entry (@{ $objs } ) {
+      if ( $entry->parts_id != $last_partid ) {
+          if ( $last_partid > 0 ) {
+              if ( $allrows || ($row_ind >= $first_nr && $row_ind < $last_nr )) {
+                  $self->make_row_result($last_row,$days,$last_partid);
+                  $report->add_data($last_row);
+              }
+              $row_ind++ ;
+          } 
+          $last_partid = $entry->parts_id;
+          $last_row = { };
+          $last_row->{partnumber}->{data} = $entry->part->partnumber;
+          $last_row->{partdescription}->{data} = $entry->part->description;
+          $last_row->{unit}->{data} = $entry->part->unit;
+          $last_row->{stock}->{data} = 0;
+          $last_row->{incorrection}->{data} = 0;
+          $last_row->{found}->{data} = 0;
+          $last_row->{back}->{data} = 0;
+          $last_row->{outcorrection}->{data} = 0;
+          $last_row->{disposed}->{data} = 0;
+          $last_row->{missing}->{data} = 0;
+          $last_row->{shipped}->{data} = 0;
+          $last_row->{used}->{data} = 0;
+          $last_row->{insum}->{data} = 0;
+          $last_row->{outsum}->{data} = 0;
+          $last_row->{consumed}->{data} = 0;
+          $last_row->{averconsumed}->{data} = 0;
+      }
+      if ( !$allrows && $row_ind >= $last_nr ) {
+          next;
+      }
+      my $prefix='';
+      if ( $entry->trans_type->description eq 'correction' ) {
+          $prefix = $entry->trans_type->direction;
+      }
+      $last_row->{$prefix.$entry->trans_type->description}->{data} += 
+          ( $entry->trans_type->direction eq 'out' ? -$entry->qty : $entry->qty );
+  }
+  if ( $last_partid > 0 && ( $allrows || ($row_ind >= $first_nr && $row_ind < $last_nr ))) {
+      $self->make_row_result($last_row,$days,$last_partid);
+      $report->add_data($last_row);
+      $row_ind++ ;
+  } 
+  my $num_rows = @{ $report->{data} } ;
+  #$main::lxdebug->message(LXDebug->DEBUG2(), "count=".$row_ind." rows=".$num_rows);
+
+  if ( ! $allrows ) {
+      $pages->{max}  = SL::DB::Helper::Paginated::ceil($row_ind, $pages->{per_page}) || 1;
+      $pages->{page} = $page < 1 ? 1: $page > $pages->{max} ? $pages->{max}: $page;
+      $pages->{common} = [ grep { $_->{visible} } @{ SL::DB::Helper::Paginated::make_common_pages($pages->{page}, $pages->{max}) } ];
+      $self->{pages} = $pages;
+      $searchparams{action} = "usage";
+      $self->{base_url} = $self->url_for(\%searchparams );
+      #$main::lxdebug->message(LXDebug->DEBUG2(), "page=".$pages->{page}." url=".$self->{base_url});
+
+      $report->set_options('raw_bottom_info_text' => $self->render('inventory/report_bottom', { output => 0 }) );
+  }
+  $report->generate_with_headers();
+
+  $main::lxdebug->leave_sub();
+
+}
+
+sub make_row_result {
+  my ($self,$row,$days,$partid) = @_;
+  my $form     = $main::form;
+  my $myconfig = \%main::myconfig;
+
+  $row->{insum}->{data}  = $row->{stock}->{data} + $row->{incorrection}->{data} + $row->{found}->{data};
+  $row->{outsum}->{data} = $row->{back}->{data} + $row->{outcorrection}->{data} + $row->{disposed}->{data} +
+       $row->{missing}->{data} + $row->{shipped}->{data} + $row->{used}->{data};
+  $row->{consumed}->{data} = $row->{outsum}->{data} - 
+       $row->{outcorrection}->{data} - $row->{incorrection}->{data};
+  $row->{averconsumed}->{data} = $row->{consumed}->{data}*30/$days ;
+  map { $row->{$_}->{data} = $form->format_amount($myconfig,$row->{$_}->{data},2); } $self->getnumcolumns();
+#  $row->{partclass}->{data} = '';
+  $row->{partnumber}->{link} = 'ic.pl?action=edit&id='.$partid;
+#  $row->{partdescription}->{link} = 'ic.pl?action=edit&id='.$partid;
+}
+
 sub action_stock {
   my ($self) = @_;
 
@@ -153,6 +499,10 @@ sub init_warehouses {
   SL::DB::Manager::Warehouse->get_all(query => [ or => [ invalid => 0, invalid => undef ]]);
 }
 
+#sub init_bins {
+#  SL::DB::Manager::Bin->get_all();
+#}
+
 sub init_units {
   SL::DB::Manager::Unit->get_all;
 }
@@ -175,6 +525,14 @@ sub sanitize_target {
 
   $self->warehouse($self->warehouses->[0])       if !$self->warehouse || !$self->warehouse->id;
   $self->bin      ($self->warehouse->bins->[0])  if !$self->bin       || !$self->bin->id;
+#  foreach my $warehouse ( $self->warehouses ) {
+#      $warehouse->{BINS} = [];
+#      foreach my $bin ( $self->bins ) {
+#         if ( $bin->warehouse_id == $warehouse->id ) {
+#             push @{ $warehouse->{BINS} }, $bin;
+#         }
+#      }
+#  }
 }
 
 sub load_part_from_form {
index 5ffa382..fb35908 100644 (file)
@@ -674,6 +674,8 @@ sub generate_journal {
   $form->{title}   = $locale->text("WHJournal");
   $form->{sort}  ||= 'date';
 
+  $form->{report_generator_output_format} = 'HTML' if !$form->{report_generator_output_format};
+
   my %filter;
   my @columns = qw(trans_id date warehouse_from bin_from warehouse_to bin_to partnumber partdescription chargenumber bestbefore trans_type comment qty employee oe_id projectnumber);
 
@@ -709,13 +711,16 @@ sub generate_journal {
     'chargenumber'    => { 'text' => $locale->text('Charge Number'), },
     'bestbefore'      => { 'text' => $locale->text('Best Before'), },
     'qty'             => { 'text' => $locale->text('Qty'), },
+    'unit'            => { 'text' => $locale->text('Part Unit'), },
+    'partunit'        => { 'text' => $locale->text('Unit'), },
     'employee'        => { 'text' => $locale->text('Employee'), },
     'projectnumber'   => { 'text' => $locale->text('Project Number'), },
     'oe_id'           => { 'text' => $locale->text('Document'), },
   );
 
   my $href = build_std_url('action=generate_journal', grep { $form->{$_} } @hidden_variables);
-  map { $column_defs{$_}->{link} = $href . "&sort=${_}&order=" . Q($_ eq $form->{sort} ? 1 - $form->{order} : $form->{order}) } @columns;
+  my $page = $::form->{page} || 1;
+  map { $column_defs{$_}->{link} = $href ."&page=".$page. "&sort=${_}&order=" . Q($_ eq $form->{sort} ? 1 - $form->{order} : $form->{order}) } @columns;
 
   my %column_alignment = map { $_ => 'right' } qw(qty);
 
@@ -747,6 +752,16 @@ sub generate_journal {
                     'purchase_invoice'        => { script => 'ir', title => $locale->text('Purchase Invoice') },
                   );
 
+   my $allrows = 0;
+   $allrows = 1 if $form->{report_generator_output_format} ne 'HTML' ;
+   # manual paginating
+   my $pages = {};
+   $pages->{per_page}        = $::form->{per_page} || 15;
+   my $first_nr = ($page - 1) * $pages->{per_page};
+   my $last_nr  = $first_nr + $pages->{per_page};
+   my $idx       = 0;
   foreach my $entry (@contents) {
     $entry->{qty}        = $form->format_amount_units('amount'     => $entry->{qty},
                                                       'part_unit'  => $entry->{partunit},
@@ -774,9 +789,20 @@ sub generate_journal {
       }
     }
 
-    $report->add_data($row);
+    if ( $allrows || ($idx >= $first_nr && $idx < $last_nr )) {
+       $report->add_data($row);
+    }
+    $idx++;
   }
 
+  if ( ! $allrows ) {
+      $pages->{max}  = SL::DB::Helper::Paginated::ceil($idx, $pages->{per_page}) || 1;
+      $pages->{page} = $page < 1 ? 1: $page > $pages->{max} ? $pages->{max}: $page;
+      $pages->{common} = [ grep { $_->{visible} } @{ SL::DB::Helper::Paginated::make_common_pages($pages->{page}, $pages->{max}) } ];
+
+      $report->set_options('raw_bottom_info_text' => $form->parse_html_template('common/paginate',
+                                                            { 'pages' => $pages , 'base_url' => $href}) );
+  }
   $report->generate_with_headers();
 
   $main::lxdebug->leave_sub();
@@ -822,13 +848,14 @@ sub generate_report {
 
   $form->{title}   = $locale->text("Report about warehouse contents");
   $form->{sort}  ||= 'partnumber';
+  $form->{sort}  ||= 'partunit';
   my $sort_col     = $form->{sort};
 
   my %filter;
   my @columns = qw(warehousedescription bindescription partnumber partdescription chargenumber bestbefore qty stock_value);
 
   # filter stuff
-  map { $filter{$_} = $form->{$_} if ($form->{$_}) } qw(warehouse_id bin_id partnumber description chargenumber bestbefore date include_invalid_warehouses);
+  map { $filter{$_} = $form->{$_} if ($form->{$_}) } qw(warehouse_id bin_id partstypes_id partnumber description chargenumber bestbefore date include_invalid_warehouses);
 
   # show filter stuff also in report
   my @options;
@@ -862,10 +889,11 @@ sub generate_report {
 
   $form->{subtotal} = '' if (!first { $_ eq $sort_col } qw(partnumber partdescription));
 
+  $form->{report_generator_output_format} = 'HTML' if !$form->{report_generator_output_format};
   my $report = SL::ReportGenerator->new(\%myconfig, $form);
 
   my @hidden_variables = map { "l_${_}" } @columns;
-  push @hidden_variables, qw(warehouse_id bin_id partnumber description chargenumber bestbefore qty_op qty qty_unit l_warehousedescription l_bindescription);
+  push @hidden_variables, qw(warehouse_id bin_id partnumber partstypes_id description chargenumber bestbefore qty_op qty qty_unit partunit l_warehousedescription l_bindescription);
   push @hidden_variables, qw(include_empty_bins subtotal include_invalid_warehouses date);
 
   my %column_defs = (
@@ -876,11 +904,13 @@ sub generate_report {
     'chargenumber'         => { 'text' => $locale->text('Charge Number'), },
     'bestbefore'           => { 'text' => $locale->text('Best Before'), },
     'qty'                  => { 'text' => $locale->text('Qty'), },
+    'partunit'             => { 'text' => $locale->text('Unit'), },
     'stock_value'          => { 'text' => $locale->text('Stock value'), },
   );
 
   my $href = build_std_url('action=generate_report', grep { $form->{$_} } @hidden_variables);
-  map { $column_defs{$_}->{link} = $href . "&sort=${_}&order=" . Q($_ eq $sort_col ? 1 - $form->{order} : $form->{order}) } @columns;
+  my $page = $::form->{page} || 1;
+  map { $column_defs{$_}->{link} = $href . "&page=".$page."&sort=${_}&order=" . Q($_ eq $sort_col ? 1 - $form->{order} : $form->{order}) } @columns;
 
   my %column_alignment = map { $_ => 'right' } qw(qty stock_value);
 
@@ -910,13 +940,22 @@ sub generate_report {
 
   my $total_stock_value = 0;
 
+  my $allrows = 0;
+  $allrows = 1 if $form->{report_generator_output_format} ne 'HTML' ;
+
+  # manual paginating
+  my $pages = {};
+  $pages->{per_page}        = $::form->{per_page} || 20;
+  my $first_nr = ($page - 1) * $pages->{per_page};
+  my $last_nr  = $first_nr + $pages->{per_page};
+
   foreach my $entry (@contents) {
     map { $subtotals{$_} += $entry->{$_} } @subtotals_columns;
     $total_stock_value   += $entry->{stock_value} * 1;
-
-    $entry->{qty}         = $form->format_amount_units('amount'     => $entry->{qty},
-                                                       'part_unit'  => $entry->{partunit},
-                                                       'conv_units' => 'convertible');
+    $entry->{qty}         = $form->format_amount(\%myconfig, $entry->{qty});
+#    $entry->{qty}         = $form->format_amount_units('amount'     => $entry->{qty},
+#                                                       'part_unit'  => $entry->{partunit},
+#                                                       'conv_units' => 'convertible');
     $entry->{stock_value} = $form->format_amount(\%myconfig, $entry->{stock_value} * 1, 2);
 
     my $row_set = [ { map { $_ => { 'data' => $entry->{$_}, 'align' => $column_alignment{$_} } } @columns } ];
@@ -926,9 +965,10 @@ sub generate_report {
             || ($entry->{$sort_col} ne $contents[$idx + 1]->{$sort_col}))) {
 
       my $row = { map { $_ => { 'data' => '', 'class' => 'listsubtotal', 'align' => $column_alignment{$_}, } } @columns };
-      $row->{qty}->{data}         = $form->format_amount_units('amount'     => $subtotals{qty} * 1,
-                                                               'part_unit'  => $entry->{partunit},
-                                                               'conv_units' => 'convertible');
+      $row->{qty}->{data}         = $form->format_amount(\%myconfig, $subtotals{qty});
+#      $row->{qty}->{data}         = $form->format_amount_units('amount'     => $subtotals{qty} * 1,
+#                                                               'part_unit'  => $entry->{partunit},
+#                                                               'conv_units' => 'convertible');
       $row->{stock_value}->{data} = $form->format_amount(\%myconfig, $subtotals{stock_value} * 1, 2);
 
       %subtotals                  = map { $_ => 0 } @subtotals_columns;
@@ -936,8 +976,9 @@ sub generate_report {
       push @{ $row_set }, $row;
     }
 
-    $report->add_data($row_set);
-
+    if ( $allrows || ($idx >= $first_nr && $idx < $last_nr )) {
+       $report->add_data($row_set);
+    }
     $idx++;
   }
 
index fa0caef..57d85e4 100644 (file)
@@ -6,6 +6,9 @@
 
 kleinere neue Features und Detailverbesserungen:
 
+  - Weiterer Bericht in der Rubrik Lager: Lagerentnahme
+    Gibt eine Statistik über Lagerbewegungen, pro Monat/Quartal/Jahr.
+
   - Für UStVA Voranmeldung über Elster gibt es die Anbindung über Geierlein (Installation/Config siehe Commit)
   
   - CSV-Import von Artikel hat nun für existierende Artikel folgende Optionen:
index 4b05b81..be38d44 100755 (executable)
@@ -1330,6 +1330,7 @@ $self->{texts} = {
   'Fristsetzung'                => 'Fristsetzung',
   'From'                        => 'Von',
   'From Date'                   => 'Von',
+  'From bin'                    => 'Ausgelagert',
   'From this version on a new feature is available.' => 'Ab dieser Version ist ein neues Feature verfügbar.',
   'From this version on it is necessary to name a default value.' => 'Ab dieser Version benötigt kivitendo eine Standardwährung.',
   'From this version on the partnumber of services, articles and assemblies have to be unique.' => 'Ab dieser Version müssen Artikelnummern eindeutig vergeben werden.',
@@ -1494,6 +1495,7 @@ $self->{texts} = {
   'Internal Phone List'         => 'Interne Telefonliste',
   'Internal comment'            => 'Interne Bemerkungen',
   'Internet'                    => 'Internet',
+  'Into bin'                    => 'Eingelagert',
   'Intra-Community supply'      => 'Gelangensbestätigung',
   'Introduction of clients'     => 'Einführung von Mandanten',
   'Inv. Duedate'                => 'Rg. Fälligkeit',
@@ -1985,12 +1987,15 @@ $self->{texts} = {
   'Part'                        => 'Ware',
   'Part "#1" has chargenumber or best before date set. So it cannot be transfered automatically.' => 'Bei Artikel "#1" ist eine Chargenummer oder ein Mindesthaltbarkeitsdatum vergeben. Deshalb kann dieser Artikel nicht automatisch ausgelagert werden.',
   'Part (database ID)'          => 'Artikel (Datenbank-ID)',
+  'Part Classification'         => '',
   'Part Description'            => 'Artikelbeschreibung',
   'Part Description missing!'   => 'Artikelbezeichnung fehlt!',
   'Part Notes'                  => 'Bemerkungen',
   'Part Number'                 => 'Artikelnummer',
   'Part Number missing!'        => 'Artikelnummer fehlt!',
+  'Part Unit'                   => '',
   'Part picker'                 => 'Artikelauswahl',
+  'Part_br_Description'         => 'Beschreibung',
   'Partial invoices'            => 'Teilrechnungen',
   'Partnumber'                  => 'Artikelnummer',
   'Partnumber must not be set to empty!' => 'Die Artikelnummer darf nicht auf leer ge&auml;ndert werden.',
@@ -3281,6 +3286,8 @@ $self->{texts} = {
   'Updating the client fields in the database "#1" on host "#2:#3" failed.' => 'Die Aktualisierung der Mandantenfelder in der Datenbank "#1" auf Host "#2:#3" schlug fehl.',
   'Uploaded at'                 => 'Hochgeladen um',
   'Uploaded on #1, size #2 kB'  => 'Am #1 hochgeladen, Größe #2 kB',
+  'UsageE'                      => 'Lagerentnahme',
+  'UsageWithout'                => 'Entnommen (ohne Korr.)',
   'Use As New'                  => 'Als neu verwenden',
   'Use Balance Sheet'           => 'Bilanz verwenden',
   'Use Datevautomatik'          => 'Datev-Automatik verwenden',
@@ -3355,6 +3362,7 @@ $self->{texts} = {
   'View/edit all employees sales documents' => 'Bearbeiten/ansehen der Verkaufsdokumente aller Mitarbeiter',
   'Von Konto: '                 => 'von Konto: ',
   'WHJournal'                   => 'Lagerbuchungen',
+  'WHUsage'                     => 'Lagerentnahme',
   'Warehouse'                   => 'Lager',
   'Warehouse (database ID)'     => 'Lager (Datenbank-ID)',
   'Warehouse (name)'            => 'Lager (Name)',
@@ -3472,7 +3480,9 @@ $self->{texts} = {
   'assembly'                    => 'Erzeugnis',
   'assembly_list'               => 'erzeugnisliste',
   'averaged values, in invoice mode only useful when filtered by a part' => 'gemittelte Werte, im Rechnungsmodus nur sinnvoll wenn nach Artikel gefiltert wird',
+  'averconsumed_br'             => 'Ø mtl.',
   'back'                        => 'zurück',
+  'back_br'                     => 'Zurk.',
   'balance'                     => 'Betriebsvermögensvergleich/Bilanzierung',
   'bank_collection_payment_list_#1' => 'bankeinzugszahlungsliste_#1',
   'bank_transfer_payment_list_#1' => 'ueberweisungszahlungsliste_#1',
@@ -3495,9 +3505,11 @@ $self->{texts} = {
   'config/kivitendo.conf: Key "authentication/ldap" is missing.' => 'config/kivitendo.conf: Der Schlüssel "authentication/ldap" fehlt.',
   'config/kivitendo.conf: Missing parameters in "authentication/database". Required parameters are "host", "db" and "user".' => 'config/kivitendo.conf: Fehlende Parameter in "authentication/database". Ben&ouml;tigte Parameter sind "host", "db" und "user".',
   'config/kivitendo.conf: Missing parameters in "authentication/ldap". Required parameters are "host", "attribute" and "base_dn".' => 'config/kivitendo.conf: Fehlende Parameter in "authentication/ldap". Benötigt werden "host", "attribute" und "base_dn".',
+  'consumed'                    => 'Im Zeitraum',
   'contact_list'                => 'ansprechperson_liste',
   'continue'                    => 'weiter',
   'correction'                  => 'Korrektur',
+  'correction_br'               => 'Korr.',
   'cp_greeting to cp_gender migration' => 'Datenumwandlung von Titel nach Geschlecht (cp_greeting to cp_gender)',
   'customer'                    => 'Kunde',
   'customer_list'               => 'kundenliste',
@@ -3511,6 +3523,7 @@ $self->{texts} = {
   'difference_as_skonto'        => 'Differenz als Skonto',
   'direct debit'                => 'Lastschrifteinzug',
   'disposed'                    => 'Entsorgung',
+  'disposed_br'                 => 'Entsgt.',
   'do not include'              => 'Nicht aufnehmen',
   'done'                        => 'erledigt',
   'dunning_list'                => 'mahnungsliste',
@@ -3533,6 +3546,7 @@ $self->{texts} = {
   'for all'                     => 'für alle',
   'for date'                    => 'zum Stichtag',
   'found'                       => 'Gefunden',
+  'found_br'                    => 'Gef.',
   'from (time)'                 => 'von',
   'general_ledger_list'         => 'buchungsjournal',
   'generate cb/ob transactions for selected charts' => 'Buchungen erstellen',
@@ -3572,6 +3586,7 @@ $self->{texts} = {
   'male'                        => 'männlich',
   'mark as paid'                => 'als bezahlt markieren',
   'missing'                     => 'Fehlbestand',
+  'missing_br'                  => 'Fehl.',
   'month'                       => 'Monatliche Abgabe',
   'monthly'                     => 'monatlich',
   'never'                       => 'niemals',
@@ -3650,11 +3665,14 @@ $self->{texts} = {
   'service'                     => 'Dienstleistung',
   'service_list'                => 'dienstleistungsliste',
   'shipped'                     => 'verschickt',
+  'shipped_br'                  => 'Verschk.',
   'singular first char'         => 'S',
   'sort items'                  => 'Positionen sortieren',
   'stock'                       => 'Einlagerung',
+  'stock_br'                    => 'Eingel.',
   'submit'                      => 'abschicken',
   'succeeded'                   => 'erfolgreich',
+  'sum'                         => 'Summe',
   'tax_chartaccno'              => 'Automatikkonto',
   'tax_percent'                 => 'Prozentsatz',
   'tax_rate'                    => 'Prozent',
@@ -3682,6 +3700,7 @@ $self->{texts} = {
   'use program settings'        => 'benutze Programmeinstellungen',
   'use user config'             => 'Verwende Benutzereinstellung',
   'used'                        => 'Verbraucht',
+  'used_br'                     => 'Verbr.',
   'valid from'                  => 'Gültig ab',
   'vendor'                      => 'Lieferant',
   'vendor_invoice_list'         => 'kreditorenbuchungsliste',
@@ -3689,6 +3708,7 @@ $self->{texts} = {
   'waiting for job to be started' => 'warte darauf, dass der Job gestartet wird',
   'warehouse_journal_list'      => 'lagerbuchungsliste',
   'warehouse_report_list'       => 'lagerbestandsliste',
+  'warehouse_usage_list'        => 'Lagerentnahmeliste',
   'with amount'                 => 'mit Betrag',
   'with skonto acc. to pt'      => 'mit Skonto nach ZB',
   'with_skonto_pt'              => 'mit Skonto nach ZB',
diff --git a/menus/user/12-warehouse-usage.yaml b/menus/user/12-warehouse-usage.yaml
new file mode 100644 (file)
index 0000000..06b3c4c
--- /dev/null
@@ -0,0 +1,15 @@
+#
+# opendynamic feature
+#
+---
+#
+# Warenverbrauchsbericht
+#
+- parent: warehouse_reports
+  id: warehouse_reports_whusage
+  name: WHUsage
+  icon: warehouse_usage
+  order: 300
+  access: warehouse_contents | warehouse_management
+  params:
+    action: Inventory/stock_usage
diff --git a/templates/webpages/inventory/report_bottom.html b/templates/webpages/inventory/report_bottom.html
new file mode 100644 (file)
index 0000000..c85ec73
--- /dev/null
@@ -0,0 +1 @@
+[%- PROCESS 'common/paginate.html' pages=SELF.pages, base_url = SELF.base_url %]
diff --git a/templates/webpages/inventory/warehouse_usage.html b/templates/webpages/inventory/warehouse_usage.html
new file mode 100644 (file)
index 0000000..9151556
--- /dev/null
@@ -0,0 +1,126 @@
+[%- USE T8 %]
+[%- USE L %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- WAREHOUSE_FILTER = 1 %]
+[%- PROCESS 'common/select_warehouse_bin.html' %]
+
+<h1>[% title | html %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+<form name="Form" method="post" action="controller.pl">
+
+ <table border="0">
+    <tr>
+     <th class="listheading" align="left" valign="top" colspan="5" nowrap>[% 'Period:' | $T8 %]</th>
+    </tr>
+  <tr>
+    <th align=left><input name=reporttype class=radio type=radio value="custom" checked>[% 'Customized Report' | $T8 %]</th>
+  </tr>
+  <tr>
+    <th colspan=1>[% 'Year' | $T8 %]</th>
+    <td><input name=year size=11 title="[% 'YYYY' | $T8 %]" value="[% year %]" class="initial_focus"></td>
+  </tr>
+  <tr>
+    <td align=right> <b>[% 'Yearly' | $T8 %]</b> </td>
+    <th align=left>[% 'Quarterly' | $T8 %]</th>
+    <th align=left colspan=3>[% 'Monthly' | $T8 %]</th>
+  </tr>
+  <tr>
+    <td align=right>&nbsp; <input name=duetyp class=radio type=radio value="13" checked></td>
+    <td><input name=duetyp class=radio type=radio value="A">&nbsp;1. [% 'Quarter' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="1">&nbsp;[% 'January' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="5">&nbsp;[% 'May' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="9">&nbsp;[% 'September' | $T8 %]</td>
+  </tr>
+  <tr>
+    <td align= right>&nbsp;</td>
+    <td><input name=duetyp class=radio type=radio value="B">&nbsp;2. [% 'Quarter' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="2">&nbsp;[% 'February' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="6">&nbsp;[% 'June' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="10">&nbsp;[% 'October' | $T8 %]</td>
+  </tr>
+  <tr>
+    <td> &nbsp;</td>
+    <td><input name=duetyp class=radio type=radio value="C">&nbsp;3. [% 'Quarter' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="3">&nbsp;[% 'March' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="7">&nbsp;[% 'July' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="11">&nbsp;[% 'November' | $T8 %]</td>
+  </tr>
+  <tr>
+    <td> &nbsp;</td>
+    <td><input name=duetyp class=radio type=radio value="D">&nbsp;4. [% 'Quarter' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="4">&nbsp;[% 'April' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="8">&nbsp;[% 'August' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="12">&nbsp;[% 'December' | $T8 %]</td>
+  </tr>
+  <tr>
+    <td colspan="5"><hr size=3 noshade></td>
+  </tr>
+  <tr>
+    <th align=left><input name=reporttype class=radio type=radio value="free">[% 'Free report period' | $T8 %]</th>
+    <td align=left colspan=4>
+      [% 'From' | $T8 %] [% L.date_tag('fromdate', fromdate) %]
+      [% 'Bis' | $T8 %] [% L.date_tag('todate', todate) %]
+    </td>
+  </tr>
+    <tr>
+     <th class="listheading" align="left" valign="top" colspan="5" nowrap>[% 'Filter' | $T8 %]</th>
+    </tr>
+    <tr>
+     <td colspan="5">
+      <table>
+       <tr>
+        <th align="right" nowrap>[% 'Warehouse' | $T8 %]:</th>
+        <td>
+         <select name="warehouse_id" id="warehouse_id" onchange="kivi.PartsWarehouse.warehouseChanged(this.value, 0)">
+          <option value="">---</option>
+          [%- FOREACH warehouse = WAREHOUSES %]
+          <option value="[% HTML.escape(warehouse.id) %]">[% warehouse.description %]</option>
+          [%- END %]
+         </select>
+        </td>
+       </tr>
+       <tr>
+        <th align="right" nowrap>[% 'Bin' | $T8 %]:</th>
+        <td><select name="bin_id" id="bin_id"></select></td>
+       </tr>
+       <tr>
+        <th align="right" nowrap>[% 'Part Number' | $T8 %]:</th>
+        <td><input name="partnumber" size=20></td>
+       </tr>
+       <tr>
+        <th align="right" nowrap>[% 'Part Description' | $T8 %]:</th>
+        <td><input name="description" size=40></td>
+       </tr>
+[% IF PARTSCLASSIFICATIONS %]
+       <tr>
+        <td>
+           [% L.select_tag('partsclassification',PARTSCLASSIFICATION,title_key="partsclassification") %]
+        </td>
+       </tr>
+[% END %]
+       <tr>
+        <th align="right" nowrap>[% 'Charge Number' | $T8 %]:</th>
+        <td><input name="chargenumber" size=40></td>
+       </tr>
+       [% IF INSTANCE_CONF.get_show_bestbefore %]
+       <tr>
+        <th align="right" nowrap>[% 'Best Before' | $T8 %]:</th>
+        <td>
+          [% L.date_tag('bestbefore') %]
+        </td>
+       </tr>
+       [% END %]
+      </table>
+     </td>
+    </tr>
+   </table>
+  </p>
+
+  <p>
+ <input type="hidden" name="action" value="Inventory/dispatch">
+ <input type="submit" id="action_usage" class="submit" name="action_usage" value="[% 'Continue' | $T8 %]"> 
+ </p>
+ </form>
index 59a98b7..444c0b6 100644 (file)
@@ -1,7 +1,8 @@
 [%- USE T8 %]
 [%- USE HTML %][%- USE LxERP %]
 
- [%- SET default_margin = LxERP.format_amount(1.5) %]
+ [%- SET default_ymargin = LxERP.format_amount(1.5) %]
+ [%- SET default_xmargin = LxERP.format_amount(0.8) %]
 
  <h1>[% HTML.escape(title) %]</h1>
 
 
    <tr>
     <td align="right">[% 'Top' | $T8 %]</td>
-    <td><input name="report_generator_pdf_options_margin_top" size="4" value="[% HTML.escape(default_margin) %]"> cm</td>
+    <td><input name="report_generator_pdf_options_margin_top" size="4" value="[% HTML.escape(default_ymargin) %]"> cm</td>
    </tr>
 
    <tr>
     <td align="right">[% 'Left' | $T8 %]</td>
-    <td><input name="report_generator_pdf_options_margin_left" size="4" value="[% HTML.escape(default_margin) %]"> cm</td>
+    <td><input name="report_generator_pdf_options_margin_left" size="4" value="[% HTML.escape(default_xmargin) %]"> cm</td>
    </tr>
 
    <tr>
     <td align="right">[% 'Bottom' | $T8 %]</td>
-    <td><input name="report_generator_pdf_options_margin_bottom" size="4" value="[% HTML.escape(default_margin) %]"> cm</td>
+    <td><input name="report_generator_pdf_options_margin_bottom" size="4" value="[% HTML.escape(default_ymargin) %]"> cm</td>
    </tr>
 
    <tr>
     <td align="right">[% 'Right' | $T8 %]</td>
-    <td><input name="report_generator_pdf_options_margin_right" size="4" value="[% HTML.escape(default_margin) %]"> cm</td>
+    <td><input name="report_generator_pdf_options_margin_right" size="4" value="[% HTML.escape(default_xmargin) %]"> cm</td>
    </tr>
 
    <tr>