1 #=====================================================================
 
   4 # Based on SQL-Ledger Version 2.1.9
 
   5 # Web http://www.lx-office.org
 
   7 #=====================================================================
 
   8 # SQL-Ledger, Accounting
 
  11 #  Author: Dieter Simader
 
  12 #   Email: dsimader@sql-ledger.org
 
  13 #     Web: http://www.sql-ledger.org
 
  16 # This program is free software; you can redistribute it and/or modify
 
  17 # it under the terms of the GNU General Public License as published by
 
  18 # the Free Software Foundation; either version 2 of the License, or
 
  19 # (at your option) any later version.
 
  21 # This program is distributed in the hope that it will be useful,
 
  22 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 
  23 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
  24 # GNU General Public License for more details.
 
  25 # You should have received a copy of the GNU General Public License
 
  26 # along with this program; if not, write to the Free Software
 
  27 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 
  29 #======================================================================
 
  31 # Inventory Control module
 
  33 #======================================================================
 
  35 use POSIX qw(strftime);
 
  36 use List::Util qw(first max);
 
  37 use List::MoreUtils qw(any);
 
  42 use SL::Helper::Flash qw(flash);
 
  44 use SL::Presenter::Part;
 
  45 use SL::ReportGenerator;
 
  53 our ($form, $locale, %myconfig, $lxdebug, $auth);
 
  55 require "bin/mozilla/io.pl";
 
  56 require "bin/mozilla/common.pl";
 
  57 require "bin/mozilla/reportgenerator.pl";
 
  62 # type=submit $locale->text('Add Part')
 
  63 # type=submit $locale->text('Add Service')
 
  64 # type=submit $locale->text('Add Assembly')
 
  65 # type=submit $locale->text('Edit Part')
 
  66 # type=submit $locale->text('Edit Service')
 
  67 # type=submit $locale->text('Edit Assembly')
 
  68 # $locale->text('Parts')
 
  69 # $locale->text('Services')
 
  70 # $locale->text('Inventory quantity must be zero before you can set this part obsolete!')
 
  71 # $locale->text('Inventory quantity must be zero before you can set this assembly obsolete!')
 
  72 # $locale->text('Part Number missing!')
 
  73 # $locale->text('Service Number missing!')
 
  74 # $locale->text('Assembly Number missing!')
 
  75 # $locale->text('ea');
 
  80   $lxdebug->enter_sub();
 
  82   $auth->assert('part_service_assembly_details');
 
  84   $form->{revers}       = 0;  # switch for backward sorting
 
  85   $form->{lastsort}     = ""; # memory for which table was sort at last time
 
  86   $form->{ndxs_counter} = 0;  # counter for added entries to top100
 
  88   $form->{title} = (ucfirst $form->{searchitems}) . "s";
 
  89   $form->{title} =~ s/ys$/ies/;
 
  90   $form->{title} = $locale->text($form->{title});
 
  92   $form->{CUSTOM_VARIABLES}                  = CVar->get_configs('module' => 'IC');
 
  93   ($form->{CUSTOM_VARIABLES_FILTER_CODE},
 
  94    $form->{CUSTOM_VARIABLES_INCLUSION_CODE}) = CVar->render_search_options('variables'      => $form->{CUSTOM_VARIABLES},
 
  95                                                                            'include_prefix' => 'l_',
 
  96                                                                            'include_value'  => 'Y');
 
  98   setup_ic_search_action_bar();
 
 101   $form->get_lists('partsgroup'    => 'ALL_PARTSGROUPS');
 
 102   print $form->parse_html_template('ic/search');
 
 104   $lxdebug->leave_sub();
 
 108   $::lxdebug->enter_sub();
 
 110   $::auth->assert('part_service_assembly_edit');
 
 112   $::form->{l_soldtotal} = "Y";
 
 113   $::form->{sort}        = "soldtotal";
 
 114   $::form->{lastsort}    = "soldtotal";
 
 116   $::form->{l_qty}       = undef;
 
 117   $::form->{l_linetotal} = undef;
 
 118   $::form->{l_number}    = "Y";
 
 119   $::form->{number}      = "position";
 
 121   unless (   $::form->{bought}
 
 124           || $::form->{quoted}) {
 
 125     $::form->{bought} = $::form->{sold} = 1;
 
 130   $lxdebug->leave_sub();
 
 135 # Warning, deep magic ahead.
 
 136 # This function parses the requested details, sanity checks them, and converts them into a format thats usable for IC->all_parts
 
 138 # flags coming from the form:
 
 140 #  searchitems=part revers=0 lastsort=''
 
 143 # partnumber ean description partsgroup classification serialnumber make model drawing microfiche
 
 144 # transdatefrom transdateto
 
 147 #  itemstatus = active | onhand | short | obsolete | orphaned
 
 148 #  action     = continue | top100
 
 151 #  bought sold onorder ordered rfq quoted
 
 152 #  l_partnumber l_description l_serialnumber l_unit l_listprice l_sellprice l_lastcost
 
 153 #  l_linetotal l_priceupdate l_bin l_rop l_weight l_image l_drawing l_microfiche
 
 154 #  l_partsgroup l_subtotal l_soldtotal l_deliverydate l_pricegroups
 
 157 #  nextsub revers lastsort sort ndxs_counter
 
 159 sub generate_report {
 
 160   $lxdebug->enter_sub();
 
 162   $auth->assert('part_service_assembly_details');
 
 164   my ($revers, $lastsort, $description);
 
 166   my $cvar_configs = CVar->get_configs('module' => 'IC');
 
 168   $form->{title} = $locale->text('Articles');
 
 171     'deliverydate'       => { 'text' => $locale->text('deliverydate'), },
 
 172     'description'        => { 'text' => $locale->text('Part Description'), },
 
 173     'notes'              => { 'text' => $locale->text('Notes'), },
 
 174     'drawing'            => { 'text' => $locale->text('Drawing'), },
 
 175     'ean'                => { 'text' => $locale->text('EAN'), },
 
 176     'image'              => { 'text' => $locale->text('Image'), },
 
 177     'insertdate'         => { 'text' => $locale->text('Insert Date'), },
 
 178     'invnumber'          => { 'text' => $locale->text('Invoice Number'), },
 
 179     'lastcost'           => { 'text' => $locale->text('Last Cost'), },
 
 180     'assembly_lastcost'  => { 'text' => $locale->text('Assembly Last Cost'), },
 
 181     'linetotallastcost'  => { 'text' => $locale->text('Extended'), },
 
 182     'linetotallistprice' => { 'text' => $locale->text('Extended'), },
 
 183     'linetotalsellprice' => { 'text' => $locale->text('Extended'), },
 
 184     'listprice'          => { 'text' => $locale->text('List Price'), },
 
 185     'microfiche'         => { 'text' => $locale->text('Microfiche'), },
 
 186     'name'               => { 'text' => $locale->text('Name'), },
 
 187     'onhand'             => { 'text' => $locale->text('Stocked Qty'), },
 
 188     'ordnumber'          => { 'text' => $locale->text('Order Number'), },
 
 189     'partnumber'         => { 'text' => $locale->text('Part Number'), },
 
 190     'partsgroup'         => { 'text' => $locale->text('Partsgroup'), },
 
 191     'priceupdate'        => { 'text' => $locale->text('Price updated'), },
 
 192     'quonumber'          => { 'text' => $locale->text('Quotation'), },
 
 193     'rop'                => { 'text' => $locale->text('ROP'), },
 
 194     'sellprice'          => { 'text' => $locale->text('Sell Price'), },
 
 195     'serialnumber'       => { 'text' => $locale->text('Serial Number'), },
 
 196     'soldtotal'          => { 'text' => $locale->text('Qty in Selected Records'), },
 
 197     'name'               => { 'text' => $locale->text('Name in Selected Records'), },
 
 198     'transdate'          => { 'text' => $locale->text('Transdate Record'), },
 
 199     'unit'               => { 'text' => $locale->text('Unit'), },
 
 200     'weight'             => { 'text' => $locale->text('Weight'), },
 
 201     'shop'               => { 'text' => $locale->text('Shop article'), },
 
 202     'type_and_classific' => { 'text' => $locale->text('Type'), },
 
 203     'projectnumber'      => { 'text' => $locale->text('Project Number'), },
 
 204     'projectdescription' => { 'text' => $locale->text('Project Description'), },
 
 205     'warehouse'          => { 'text' => $locale->text('Default Warehouse'), },
 
 206     'bin'                => { 'text' => $locale->text('Default Bin'), },
 
 207     'make'               => { 'text' => $locale->text('Make'), },
 
 208     'model'              => { 'text' => $locale->text('Model'), },
 
 211   $revers     = $form->{revers};
 
 212   $lastsort   = $form->{lastsort};
 
 214   # sorting and direction of sorting
 
 215   # ToDO: change this to the simpler field+direction method
 
 216   if (($form->{lastsort} eq "") && ($form->{sort} eq undef)) {
 
 218     $form->{lastsort} = "partnumber";
 
 219     $form->{sort}     = "partnumber";
 
 221     if ($form->{lastsort} eq $form->{sort}) {
 
 222       $form->{revers} = 1 - $form->{revers};
 
 225       $form->{lastsort} = $form->{sort};
 
 229   # special case if we have a serialnumber limit search
 
 230   # serialnumbers are only given in invoices and orders,
 
 231   # so they can only pop up in bought, sold, rfq, and quoted stuff
 
 232   $form->{no_sn_joins} = 'Y' if (   !$form->{bought} && !$form->{sold}
 
 233                                  && !$form->{rfq}    && !$form->{quoted}
 
 234                                  && ($form->{l_serialnumber} || $form->{serialnumber}));
 
 236   # special case for any checkbox of bought | sold | onorder | ordered | rfq | quoted.
 
 237   # if any of these are ticked the behavior changes slightly for lastcost
 
 238   # since all those are aggregation checks for the legder tables this is an internal switch
 
 239   # refered to as ledgerchecks
 
 240   $form->{ledgerchecks} = 'Y' if (   $form->{bought} || $form->{sold} || $form->{onorder}
 
 241                                   || $form->{ordered} || $form->{rfq} || $form->{quoted});
 
 243   # if something should be activated if something else is active, enter it here
 
 245     onhand       => [ qw(l_onhand) ],
 
 246     short        => [ qw(l_onhand) ],
 
 247     onorder      => [ qw(l_ordnumber) ],
 
 248     ordered      => [ qw(l_ordnumber) ],
 
 249     rfq          => [ qw(l_quonumber) ],
 
 250     quoted       => [ qw(l_quonumber) ],
 
 251     bought       => [ qw(l_invnumber) ],
 
 252     sold         => [ qw(l_invnumber) ],
 
 253     ledgerchecks => [ qw(l_name) ],
 
 254     serialnumber => [ qw(l_serialnumber) ],
 
 255     no_sn_joins  => [ qw(bought sold) ],
 
 258   # get name of partsgroup if id is given
 
 260   if ($form->{partsgroup_id}) {
 
 261     my $pg = SL::DB::PartsGroup->new(id => $form->{partsgroup_id})->load;
 
 262     $pg_name = $pg->{'partsgroup'};
 
 265   # these strings get displayed at the top of the results to indicate the user which switches were used
 
 267     active        => $locale->text('Active'),
 
 268     obsolete      => $locale->text('Obsolete'),
 
 269     orphaned      => $locale->text('Orphaned'),
 
 270     onhand        => $locale->text('On Hand'),
 
 271     short         => $locale->text('Short'),
 
 272     onorder       => $locale->text('On Order'),
 
 273     ordered       => $locale->text('Ordered'),
 
 274     rfq           => $locale->text('RFQ'),
 
 275     quoted        => $locale->text('Quoted'),
 
 276     bought        => $locale->text('Bought'),
 
 277     sold          => $locale->text('Sold'),
 
 278     transdatefrom => $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{transdatefrom}, 1),
 
 279     transdateto   => $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{transdateto}, 1),
 
 280     partnumber    => $locale->text('Part Number')      . ": '$form->{partnumber}'",
 
 281     partsgroup    => $locale->text('Partsgroup')       . ": '$form->{partsgroup}'",
 
 282     partsgroup_id => $locale->text('Partsgroup')       . ": '$pg_name'",
 
 283     serialnumber  => $locale->text('Serial Number')    . ": '$form->{serialnumber}'",
 
 284     description   => $locale->text('Part Description') . ": '$form->{description}'",
 
 285     make          => $locale->text('Make')             . ": '$form->{make}'",
 
 286     model         => $locale->text('Model')            . ": '$form->{model}'",
 
 287     customername  => $locale->text('Customer')         . ": '$form->{customername}'",
 
 288     customernumber=> $locale->text('Customer Part Number').": '$form->{customernumber}'",
 
 289     drawing       => $locale->text('Drawing')          . ": '$form->{drawing}'",
 
 290     microfiche    => $locale->text('Microfiche')       . ": '$form->{microfiche}'",
 
 291     l_soldtotal   => $locale->text('Qty in Selected Records'),
 
 292     ean           => $locale->text('EAN')              . ": '$form->{ean}'",
 
 293     insertdatefrom => $locale->text('Insert Date') . ": " . $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{insertdatefrom}, 1),
 
 294     insertdateto   => $locale->text('Insert Date') . ": " . $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{insertdateto}, 1),
 
 295     l_service     => $locale->text('Services'),
 
 296     l_assembly    => $locale->text('Assemblies'),
 
 297     l_part        => $locale->text('Parts'),
 
 300   my @itemstatus_keys = qw(active obsolete orphaned onhand short);
 
 301   my @callback_keys   = qw(onorder ordered rfq quoted bought sold partnumber partsgroup partsgroup_id serialnumber description make model
 
 302                            drawing microfiche l_soldtotal l_deliverydate transdatefrom transdateto insertdatefrom insertdateto ean shop all
 
 303                            l_service l_assembly l_part);
 
 305   # calculate dependencies
 
 306   for (@itemstatus_keys, @callback_keys) {
 
 307     next if ($form->{itemstatus} ne $_ && !$form->{$_});
 
 308     map { $form->{$_} = 'Y' } @{ $dependencies{$_} } if $dependencies{$_};
 
 311   # generate callback and optionstrings
 
 313   for my  $key (@itemstatus_keys, @callback_keys) {
 
 314     next if ($form->{itemstatus} ne $key && !$form->{$key});
 
 315     push @options, $optiontexts{$key};
 
 318   # special case for lastcost
 
 319   if ($form->{ledgerchecks}){
 
 320     # ledgerchecks don't know about sellprice or lastcost. they just return a
 
 321     # price. so rename sellprice to price, and drop lastcost.
 
 322     $column_defs{sellprice}{text} = $locale->text('Price');
 
 323     $form->{l_lastcost} = ""
 
 325   $form->{l_assembly_lastcost} = "Y" if $form->{l_assembly} && $form->{l_lastcost};
 
 327   if ($form->{description}) {
 
 328     $description = $form->{description};
 
 329     $description =~ s/\n/<br>/g;
 
 332   if ($form->{l_linetotal}) {
 
 333     $form->{l_qty} = "Y";
 
 334     $form->{l_linetotalsellprice} = "Y" if $form->{l_sellprice};
 
 335     $form->{l_linetotallastcost}  = $form->{searchitems} eq 'assembly' && !$form->{bom} ? "" : 'Y' if  $form->{l_lastcost};
 
 336     $form->{l_linetotallistprice} = "Y" if $form->{l_listprice};
 
 338   $form->{"l_type_and_classific"} = "Y";
 
 340   if ($form->{l_service} && !$form->{l_assembly} && !$form->{l_part}) {
 
 342     # remove warehouse, bin, weight and rop from list
 
 343     map { $form->{"l_$_"} = "" } qw(bin weight rop warehouse);
 
 345     $form->{l_onhand} = "";
 
 347     # qty is irrelevant unless bought or sold
 
 353         || $form->{quoted}) {
 
 354 #      $form->{l_onhand} = "Y";
 
 356       $form->{l_linetotalsellprice} = "";
 
 357       $form->{l_linetotallastcost}  = "";
 
 361   # soldtotal doesn't make sense with more than one bsooqr option.
 
 362   # so reset it to sold (the most common option), and issue a warning
 
 364   # also it doesn't make sense without bsooqr. disable and issue a warning too
 
 365   my @bsooqr = qw(sold bought onorder ordered rfq quoted);
 
 366   my $bsooqr_mode = grep { $form->{$_} } @bsooqr;
 
 367   if ($form->{l_subtotal} && 1 < $bsooqr_mode) {
 
 368     my $enabled       = first { $form->{$_} } @bsooqr;
 
 369     $form->{$_}       = ''   for @bsooqr;
 
 370     $form->{$enabled} = 'Y';
 
 372     push @options, $::locale->text('Subtotal cannot distinguish betweens record types. Only one of the selected record types will be displayed: #1', $optiontexts{$enabled});
 
 374   if ($form->{l_soldtotal} && !$bsooqr_mode) {
 
 375     delete $form->{l_soldtotal};
 
 377     flash('warning', $::locale->text('Soldtotal does not make sense without any bsooqr options'));
 
 379   if ($form->{l_soldtotal} && ($form->{l_warehouse} || $form->{l_bin})) {
 
 380     delete $form->{"l_$_"} for  qw(bin warehouse);
 
 381     flash('warning', $::locale->text('Sorry, I am too stupid to figure out the default warehouse/bin and the sold qty. I drop the default warehouse/bin option.'));
 
 383   if ($form->{l_name} && !$bsooqr_mode) {
 
 384     delete $form->{l_name};
 
 386     flash('warning', $::locale->text('Name does not make sense without any bsooqr options'));
 
 388   IC->all_parts(\%myconfig, \%$form);
 
 391     partnumber type_and_classific description notes partsgroup warehouse bin
 
 392     make model onhand rop soldtotal unit listprice
 
 393     linetotallistprice sellprice linetotalsellprice lastcost assembly_lastcost linetotallastcost
 
 394     priceupdate weight image drawing microfiche invnumber ordnumber quonumber
 
 395     transdate name serialnumber deliverydate ean projectnumber projectdescription
 
 399   my $pricegroups = SL::DB::Manager::Pricegroup->get_all_sorted;
 
 400   my @pricegroup_columns;
 
 401   my %column_defs_pricegroups;
 
 402   if ($form->{l_pricegroups}) {
 
 403     @pricegroup_columns      = map { "pricegroup_" . $_->id } @{ $pricegroups };
 
 404     %column_defs_pricegroups = map {
 
 405       "pricegroup_" . $_->id => {
 
 406         text    => $::locale->text('Pricegroup') . ' ' . $_->pricegroup,
 
 411   push @columns, @pricegroup_columns;
 
 413   my @includeable_custom_variables = grep { $_->{includeable} } @{ $cvar_configs };
 
 414   my @searchable_custom_variables  = grep { $_->{searchable} }  @{ $cvar_configs };
 
 415   my %column_defs_cvars            = map { +"cvar_$_->{name}" => { 'text' => $_->{description} } } @includeable_custom_variables;
 
 417   push @columns, map { "cvar_$_->{name}" } @includeable_custom_variables;
 
 419   %column_defs = (%column_defs, %column_defs_cvars, %column_defs_pricegroups);
 
 420   map { $column_defs{$_}->{visible} ||= $form->{"l_$_"} ? 1 : 0 } @columns;
 
 421   map { $column_defs{$_}->{align}   = 'right' } qw(onhand sellprice listprice lastcost assembly_lastcost linetotalsellprice linetotallastcost linetotallistprice rop weight soldtotal shop), @pricegroup_columns;
 
 423   my @hidden_variables = (
 
 424     qw(l_subtotal l_linetotal searchitems itemstatus bom l_pricegroups insertdatefrom insertdateto),
 
 425     qw(l_type_and_classific classification_id l_part l_service l_assembly l_assortment),
 
 428     map({ "cvar_$_->{name}" } @searchable_custom_variables),
 
 429     map({'cvar_'. $_->{name} .'_from'} grep({$_->{type} eq 'date'} @searchable_custom_variables)),
 
 430     map({'cvar_'. $_->{name} .'_to'}   grep({$_->{type} eq 'date'} @searchable_custom_variables)),
 
 431     map({'cvar_'. $_->{name} .'_qtyop'} grep({$_->{type} eq 'number'} @searchable_custom_variables)),
 
 432     map({ "l_$_" } @columns),
 
 435   my $callback         = build_std_url('action=generate_report', grep { $form->{$_} } @hidden_variables);
 
 437   my @sort_full        = qw(partnumber description onhand soldtotal deliverydate insertdate shop);
 
 438   my @sort_no_revers   = qw(partsgroup invnumber ordnumber quonumber name image drawing serialnumber);
 
 440   foreach my $col (@sort_full) {
 
 441     $column_defs{$col}->{link} = join '&', $callback, "sort=$col", map { "$_=" . E($form->{$_}) } qw(revers lastsort);
 
 443   map { $column_defs{$_}->{link} = "${callback}&sort=$_" } @sort_no_revers;
 
 445   # add order to callback
 
 446   $form->{callback} = join '&', ($callback, map { "${_}=" . E($form->{$_}) } qw(sort revers));
 
 448   my $report = SL::ReportGenerator->new(\%myconfig, $form);
 
 450   my %attachment_basenames = (
 
 451     'part'     => $locale->text('part_list'),
 
 452     'service'  => $locale->text('service_list'),
 
 453     'assembly' => $locale->text('assembly_list'),
 
 454     'article'  => $locale->text('article_list'),
 
 457   $report->set_options('raw_top_info_text'     => $form->parse_html_template('ic/generate_report_top', { options => \@options }),
 
 458                        'raw_bottom_info_text'  => $form->parse_html_template('ic/generate_report_bottom' ,
 
 459                                                   { PART_CLASSIFICATIONS => SL::DB::Manager::PartClassification->get_all_sorted }),
 
 460                        'output_format'         => 'HTML',
 
 461                        'title'                 => $form->{title},
 
 462                        'attachment_basename'   => 'article_list' . strftime('_%Y%m%d', localtime time),
 
 464   $report->set_options_from_form();
 
 465   $locale->set_numberformat_wo_thousands_separator(\%myconfig) if lc($report->{options}->{output_format}) eq 'csv';
 
 467   $report->set_columns(%column_defs);
 
 468   $report->set_column_order(@columns);
 
 470   $report->set_export_options('generate_report', @hidden_variables, qw(sort revers));
 
 472   $report->set_sort_indicator($form->{sort}, $form->{revers} ? 0 : 1);
 
 474   CVar->add_custom_variables_to_report('module'         => 'IC',
 
 475                                        'trans_id_field' => 'id',
 
 476                                        'configs'        => $cvar_configs,
 
 477                                        'column_defs'    => \%column_defs,
 
 478                                        'data'           => $form->{parts});
 
 480   CVar->add_custom_variables_to_report('module'         => 'IC',
 
 481                                        'sub_module'     => sub { $_[0]->{ioi} },
 
 482                                        'trans_id_field' => 'ioi_id',
 
 483                                        'configs'        => $cvar_configs,
 
 484                                        'column_defs'    => \%column_defs,
 
 485                                        'data'           => $form->{parts});
 
 487   my @subtotal_columns = qw(sellprice listprice lastcost);
 
 488   my %subtotals = map { $_ => 0 } ('onhand', @subtotal_columns);
 
 489   my %totals    = map { $_ => 0 } @subtotal_columns;
 
 491   my $same_item = @{ $form->{parts} } ? $form->{parts}[0]{ $form->{sort} } : undef;
 
 493   my $defaults  = AM->get_defaults();
 
 496   foreach my $ref (@{ $form->{parts} }) {
 
 498     # fresh row, for inserting later
 
 499     my $row = { map { $_ => { 'data' => $ref->{$_} } } @columns };
 
 501     $ref->{exchangerate} ||= 1;
 
 502     $ref->{price_factor} ||= 1;
 
 503     $ref->{sellprice}     *= $ref->{exchangerate} / $ref->{price_factor};
 
 504     $ref->{listprice}     *= $ref->{exchangerate} / $ref->{price_factor};
 
 505     $ref->{lastcost}      *= $ref->{exchangerate} / $ref->{price_factor};
 
 506     $ref->{assembly_lastcost} *= $ref->{exchangerate} / $ref->{price_factor};
 
 508     # use this for assemblies
 
 509     my $soldtotal = $bsooqr_mode ? $ref->{soldtotal} : $ref->{onhand};
 
 511     if ($ref->{assemblyitem}) {
 
 512       $row->{partnumber}{align}   = 'right';
 
 513       $row->{soldtotal}{data}     = 0;
 
 514       $soldtotal                  = 0 if ($form->{sold});
 
 517     my $edit_link               = build_std_url('script=controller.pl', 'action=Part/edit', 'part.id=' . E($ref->{id}));
 
 518     $row->{partnumber}->{link}  = $edit_link;
 
 519     $row->{description}->{link} = $edit_link;
 
 521     foreach (qw(sellprice listprice lastcost assembly_lastcost)) {
 
 522       $row->{$_}{data}            = $form->format_amount(\%myconfig, $ref->{$_}, 2);
 
 523       $row->{"linetotal$_"}{data} = $form->format_amount(\%myconfig, $ref->{onhand} * $ref->{$_}, 2);
 
 525     foreach ( @pricegroup_columns ) {
 
 526       $row->{$_}{data}            = $form->format_amount(\%myconfig, $ref->{"$_"}, 2);
 
 530     map { $row->{$_}{data} = $form->format_amount(\%myconfig, $ref->{$_}); } qw(onhand rop weight soldtotal);
 
 532     $row->{weight}->{data} .= ' ' . $defaults->{weightunit};
 
 534     # 'yes' and 'no' for boolean value shop
 
 535     if ($form->{l_shop}) {
 
 536       $row->{shop}{data} = $row->{shop}{data}? $::locale->text('yes') : $::locale->text('no');
 
 539     if (!$ref->{assemblyitem}) {
 
 540       foreach my $col (@subtotal_columns) {
 
 541         $totals{$col}    += $soldtotal * $ref->{$col};
 
 542         $subtotals{$col} += $soldtotal * $ref->{$col};
 
 545       $subtotals{soldtotal} += $soldtotal;
 
 549     if ($ref->{module} eq 'oe') {
 
 550       # für oe gibt es vier fälle, jeweils nach kunde oder lieferant unterschiedlich:
 
 552       # | ist bestellt  | Von Kunden bestellt |  -> edit_oe_ord_link
 
 553       # | Anfrage       | Angebot             |  -> edit_oe_quo_link
 
 555       my $edit_oe_ord_link = ($::instance_conf->get_feature_experimental_order)
 
 556                            ? build_std_url("script=controller.pl", 'action=Order/edit',
 
 557                                            'type=' . E($ref->{cv} eq 'vendor' ? 'purchase_order' : 'sales_order'),        'id=' . E($ref->{trans_id}), 'callback')
 
 558                            : build_std_url("script=oe.pl",         'action=edit',
 
 559                                            'type=' . E($ref->{cv} eq 'vendor' ? 'purchase_order' : 'sales_order'),        'id=' . E($ref->{trans_id}), 'callback');
 
 561       my $edit_oe_quo_link = ($::instance_conf->get_feature_experimental_order)
 
 562                            ? build_std_url("script=controller.pl", 'action=Order/edit',
 
 563                                            'type=' . E($ref->{cv} eq 'vendor' ? 'request_quotation' : 'sales_quotation'), 'id=' . E($ref->{trans_id}), 'callback')
 
 564                            : build_std_url("script=oe.pl",         'action=edit',
 
 565                                            'type=' . E($ref->{cv} eq 'vendor' ? 'request_quotation' : 'sales_quotation'), 'id=' . E($ref->{trans_id}), 'callback');
 
 567       $row->{ordnumber}{link} = $edit_oe_ord_link;
 
 568       $row->{quonumber}{link} = $edit_oe_quo_link if (!$ref->{ordnumber});
 
 571       $row->{invnumber}{link} = build_std_url("script=$ref->{module}.pl", 'action=edit', 'type=invoice', 'id=' . E($ref->{trans_id}), 'callback') if ($ref->{invnumber});
 
 574     # set properties of images
 
 575     if ($ref->{image} && (lc $report->{options}->{output_format} eq 'html')) {
 
 576       $row->{image}{data}     = '';
 
 577       $row->{image}{raw_data} = '<a href="' . H($ref->{image}) . '"><img src="' . H($ref->{image}) . '" height="32" border="0"></a>';
 
 579     map { $row->{$_}{link} = $ref->{$_} } qw(drawing microfiche);
 
 581     $row->{notes}{data} = SL::HTML::Util->strip($ref->{notes});
 
 582     $row->{type_and_classific}{data} = SL::Presenter::Part::type_abbreviation($ref->{part_type}).
 
 583                                        SL::Presenter::Part::classification_abbreviation($ref->{classification_id});
 
 586     $row->{priceupdate}{data} = SL::DB::Part->new(id => $ref->{id})->load->last_price_update->valid_from->to_kivitendo;
 
 588     $report->add_data($row);
 
 590     my $next_ref = $form->{parts}[$idx + 1];
 
 592     # insert subtotal rows
 
 593     if (($form->{l_subtotal} eq 'Y') &&
 
 595          (!$next_ref->{assemblyitem} && ($same_item ne $next_ref->{ $form->{sort} })))) {
 
 596       my $row = { map { $_ => { 'class' => 'listsubtotal', } } @columns };
 
 598       if ( !$form->{l_assembly} || !$form->{bom}) {
 
 599         $row->{soldtotal}->{data} = $form->format_amount(\%myconfig, $subtotals{soldtotal});
 
 602       map { $row->{"linetotal$_"}->{data} = $form->format_amount(\%myconfig, $subtotals{$_}, 2) } @subtotal_columns;
 
 603       map { $subtotals{$_} = 0 } ('soldtotal', @subtotal_columns);
 
 605       $report->add_data($row);
 
 607       $same_item = $next_ref->{ $form->{sort} };
 
 613   if ($form->{"l_linetotal"} && !$form->{report_generator_csv_options_for_import}) {
 
 614     my $row = { map { $_ => { 'class' => 'listtotal', } } @columns };
 
 616     map { $row->{"linetotal$_"}->{data} = $form->format_amount(\%myconfig, $totals{$_}, 2) } @subtotal_columns;
 
 618     $report->add_separator();
 
 619     $report->add_data($row);
 
 622   setup_ic_generate_report_action_bar();
 
 623   $report->generate_with_headers();
 
 625   $lxdebug->leave_sub();
 
 626 }    #end generate_report
 
 628 sub setup_ic_search_action_bar {
 
 631   for my $bar ($::request->layout->get('actionbar')) {
 
 635         submit    => [ '#form', { action => 'generate_report' } ],
 
 636         accesskey => 'enter',
 
 641         submit => [ '#form', { action => 'top100' } ],
 
 647 sub setup_ic_generate_report_action_bar {
 
 650   for my $bar ($::request->layout->get('actionbar')) {
 
 658           submit    => [ '#new_form', { action => 'Part/add_part' } ],
 
 659           accesskey => 'enter',
 
 663           submit    => [ '#new_form', { action => 'Part/add_service' } ],
 
 667           submit    => [ '#new_form', { action => 'Part/add_assembly' } ],
 
 670           t8('Add Assortment'),
 
 671           submit    => [ '#new_form', { action => 'Part/add_assortment' } ],
 
 673       ], # end of combobox "Add part"