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::ReportGenerator;
 
  52 our ($form, $locale, %myconfig, $lxdebug, $auth);
 
  54 require "bin/mozilla/io.pl";
 
  55 require "bin/mozilla/common.pl";
 
  56 require "bin/mozilla/reportgenerator.pl";
 
  61 # type=submit $locale->text('Add Part')
 
  62 # type=submit $locale->text('Add Service')
 
  63 # type=submit $locale->text('Add Assembly')
 
  64 # type=submit $locale->text('Edit Part')
 
  65 # type=submit $locale->text('Edit Service')
 
  66 # type=submit $locale->text('Edit Assembly')
 
  67 # $locale->text('Parts')
 
  68 # $locale->text('Services')
 
  69 # $locale->text('Inventory quantity must be zero before you can set this part obsolete!')
 
  70 # $locale->text('Inventory quantity must be zero before you can set this assembly obsolete!')
 
  71 # $locale->text('Part Number missing!')
 
  72 # $locale->text('Service Number missing!')
 
  73 # $locale->text('Assembly Number missing!')
 
  74 # $locale->text('ea');
 
  79   $lxdebug->enter_sub();
 
  81   $auth->assert('part_service_assembly_details');
 
  83   $form->{revers}       = 0;  # switch for backward sorting
 
  84   $form->{lastsort}     = ""; # memory for which table was sort at last time
 
  85   $form->{ndxs_counter} = 0;  # counter for added entries to top100
 
  87   $form->{title} = (ucfirst $form->{searchitems}) . "s";
 
  88   $form->{title} =~ s/ys$/ies/;
 
  89   $form->{title} = $locale->text($form->{title});
 
  91   $form->{CUSTOM_VARIABLES}                  = CVar->get_configs('module' => 'IC');
 
  92   ($form->{CUSTOM_VARIABLES_FILTER_CODE},
 
  93    $form->{CUSTOM_VARIABLES_INCLUSION_CODE}) = CVar->render_search_options('variables'      => $form->{CUSTOM_VARIABLES},
 
  94                                                                            'include_prefix' => 'l_',
 
  95                                                                            'include_value'  => 'Y');
 
  99   $form->get_lists('partsgroup'    => 'ALL_PARTSGROUPS');
 
 100   print $form->parse_html_template('ic/search');
 
 102   $lxdebug->leave_sub();
 
 105 sub search_update_prices {
 
 106   $lxdebug->enter_sub();
 
 108   $auth->assert('part_service_assembly_edit');
 
 110   my $pricegroups = IC->get_pricegroups(\%myconfig, \%$form);
 
 112   $form->{title} = $locale->text('Update Prices');
 
 116   print $form->parse_html_template('ic/search_update_prices', { PRICE_ROWS => $pricegroups });
 
 118   $lxdebug->leave_sub();
 
 121 sub confirm_price_update {
 
 122   $lxdebug->enter_sub();
 
 124   $auth->assert('part_service_assembly_edit');
 
 127   my $value_found = undef;
 
 129   foreach my $idx (qw(sellprice listprice), (1..$form->{price_rows})) {
 
 130     my $name      = $idx =~ m/\d/ ? $form->{"pricegroup_${idx}"}      : $idx eq 'sellprice' ? $locale->text('Sell Price') : $locale->text('List Price');
 
 131     my $type      = $idx =~ m/\d/ ? $form->{"pricegroup_type_${idx}"} : $form->{"${idx}_type"};
 
 132     my $value_idx = $idx =~ m/\d/ ? "price_${idx}" : $idx;
 
 133     my $value     = $form->parse_amount(\%myconfig, $form->{$value_idx});
 
 135     if ((0 > $value) && ($type eq 'percent')) {
 
 136       push @errors, $locale->text('You cannot adjust the price for pricegroup "#1" by a negative percentage.', $name);
 
 138     } elsif (!$value && ($form->{$value_idx} ne '')) {
 
 139       push @errors, $locale->text('No valid number entered for pricegroup "#1".', $name);
 
 141     } elsif (0 < $value) {
 
 146   push @errors, $locale->text('No prices will be updated because no prices have been entered.') if (!$value_found);
 
 148   my $num_matches = IC->get_num_matches_for_priceupdate();
 
 153     $form->show_generic_error(join('<br>', @errors));
 
 156   $form->{nextsub} = "update_prices";
 
 158   map { delete $form->{$_} } qw(action header);
 
 160   print $form->parse_html_template('ic/confirm_price_update', { HIDDENS     => [ map { name => $_, value => $form->{$_} }, keys %$form ],
 
 161                                                                 num_matches => $num_matches });
 
 163   $lxdebug->leave_sub();
 
 167   $lxdebug->enter_sub();
 
 169   $auth->assert('part_service_assembly_edit');
 
 171   my $num_updated = IC->update_prices(\%myconfig, \%$form);
 
 173   if (-1 != $num_updated) {
 
 174     $form->redirect($locale->text('#1 prices were updated.', $num_updated));
 
 176     $form->error($locale->text('Could not update prices!'));
 
 179   $lxdebug->leave_sub();
 
 183   $::lxdebug->enter_sub();
 
 185   $::auth->assert('part_service_assembly_edit');
 
 187   $::form->{l_soldtotal} = "Y";
 
 188   $::form->{sort}        = "soldtotal";
 
 189   $::form->{lastsort}    = "soldtotal";
 
 191   $::form->{l_qty}       = undef;
 
 192   $::form->{l_linetotal} = undef;
 
 193   $::form->{l_number}    = "Y";
 
 194   $::form->{number}      = "position";
 
 196   unless (   $::form->{bought}
 
 199           || $::form->{quoted}) {
 
 200     $::form->{bought} = $::form->{sold} = 1;
 
 205   $lxdebug->leave_sub();
 
 210 # Warning, deep magic ahead.
 
 211 # This function parses the requested details, sanity checks them, and converts them into a format thats usable for IC->all_parts
 
 213 # flags coming from the form:
 
 215 #  searchitems=part revers=0 lastsort=''
 
 218 # partnumber ean description partsgroup classification serialnumber make model drawing microfiche
 
 219 # transdatefrom transdateto
 
 222 #  itemstatus = active | onhand | short | obsolete | orphaned
 
 223 #  action     = continue | top100
 
 226 #  bought sold onorder ordered rfq quoted
 
 227 #  l_partnumber l_description l_serialnumber l_unit l_listprice l_sellprice l_lastcost
 
 228 #  l_linetotal l_priceupdate l_bin l_rop l_weight l_image l_drawing l_microfiche
 
 229 #  l_partsgroup l_subtotal l_soldtotal l_deliverydate l_pricegroups
 
 232 #  nextsub revers lastsort sort ndxs_counter
 
 234 sub generate_report {
 
 235   $lxdebug->enter_sub();
 
 237   $auth->assert('part_service_assembly_details');
 
 239   my ($revers, $lastsort, $description);
 
 241   my $cvar_configs = CVar->get_configs('module' => 'IC');
 
 243   $form->{title} = $locale->text('Articles');
 
 246     'bin'                => { 'text' => $locale->text('Bin'), },
 
 247     'deliverydate'       => { 'text' => $locale->text('deliverydate'), },
 
 248     'description'        => { 'text' => $locale->text('Part Description'), },
 
 249     'notes'              => { 'text' => $locale->text('Notes'), },
 
 250     'drawing'            => { 'text' => $locale->text('Drawing'), },
 
 251     'ean'                => { 'text' => $locale->text('EAN'), },
 
 252     'image'              => { 'text' => $locale->text('Image'), },
 
 253     'insertdate'         => { 'text' => $locale->text('Insert Date'), },
 
 254     'invnumber'          => { 'text' => $locale->text('Invoice Number'), },
 
 255     'lastcost'           => { 'text' => $locale->text('Last Cost'), },
 
 256     'linetotallastcost'  => { 'text' => $locale->text('Extended'), },
 
 257     'linetotallistprice' => { 'text' => $locale->text('Extended'), },
 
 258     'linetotalsellprice' => { 'text' => $locale->text('Extended'), },
 
 259     'listprice'          => { 'text' => $locale->text('List Price'), },
 
 260     'microfiche'         => { 'text' => $locale->text('Microfiche'), },
 
 261     'name'               => { 'text' => $locale->text('Name'), },
 
 262     'onhand'             => { 'text' => $locale->text('Stocked Qty'), },
 
 263     'ordnumber'          => { 'text' => $locale->text('Order Number'), },
 
 264     'partnumber'         => { 'text' => $locale->text('Part Number'), },
 
 265     'partsgroup'         => { 'text' => $locale->text('Partsgroup'), },
 
 266     'priceupdate'        => { 'text' => $locale->text('Updated'), },
 
 267     'quonumber'          => { 'text' => $locale->text('Quotation'), },
 
 268     'rop'                => { 'text' => $locale->text('ROP'), },
 
 269     'sellprice'          => { 'text' => $locale->text('Sell Price'), },
 
 270     'serialnumber'       => { 'text' => $locale->text('Serial Number'), },
 
 271     'soldtotal'          => { 'text' => $locale->text('Qty in Selected Records'), },
 
 272     'name'               => { 'text' => $locale->text('Name in Selected Records'), },
 
 273     'transdate'          => { 'text' => $locale->text('Transdate'), },
 
 274     'unit'               => { 'text' => $locale->text('Unit'), },
 
 275     'weight'             => { 'text' => $locale->text('Weight'), },
 
 276     'shop'               => { 'text' => $locale->text('Shop article'), },
 
 277     'type_and_classific' => { 'text' => $locale->text('Type'), },
 
 278     'projectnumber'      => { 'text' => $locale->text('Project Number'), },
 
 279     'projectdescription' => { 'text' => $locale->text('Project Description'), },
 
 282   $revers     = $form->{revers};
 
 283   $lastsort   = $form->{lastsort};
 
 285   # sorting and direction of sorting
 
 286   # ToDO: change this to the simpler field+direction method
 
 287   if (($form->{lastsort} eq "") && ($form->{sort} eq undef)) {
 
 289     $form->{lastsort} = "partnumber";
 
 290     $form->{sort}     = "partnumber";
 
 292     if ($form->{lastsort} eq $form->{sort}) {
 
 293       $form->{revers} = 1 - $form->{revers};
 
 296       $form->{lastsort} = $form->{sort};
 
 300   # special case if we have a serialnumber limit search
 
 301   # serialnumbers are only given in invoices and orders,
 
 302   # so they can only pop up in bought, sold, rfq, and quoted stuff
 
 303   $form->{no_sn_joins} = 'Y' if (   !$form->{bought} && !$form->{sold}
 
 304                                  && !$form->{rfq}    && !$form->{quoted}
 
 305                                  && ($form->{l_serialnumber} || $form->{serialnumber}));
 
 307   # special case for any checkbox of bought | sold | onorder | ordered | rfq | quoted.
 
 308   # if any of these are ticked the behavior changes slightly for lastcost
 
 309   # since all those are aggregation checks for the legder tables this is an internal switch
 
 310   # refered to as ledgerchecks
 
 311   $form->{ledgerchecks} = 'Y' if (   $form->{bought} || $form->{sold} || $form->{onorder}
 
 312                                   || $form->{ordered} || $form->{rfq} || $form->{quoted});
 
 314   # if something should be activated if something else is active, enter it here
 
 316     onhand       => [ qw(l_onhand) ],
 
 317     short        => [ qw(l_onhand) ],
 
 318     onorder      => [ qw(l_ordnumber) ],
 
 319     ordered      => [ qw(l_ordnumber) ],
 
 320     rfq          => [ qw(l_quonumber) ],
 
 321     quoted       => [ qw(l_quonumber) ],
 
 322     bought       => [ qw(l_invnumber) ],
 
 323     sold         => [ qw(l_invnumber) ],
 
 324     ledgerchecks => [ qw(l_name) ],
 
 325     serialnumber => [ qw(l_serialnumber) ],
 
 326     no_sn_joins  => [ qw(bought sold) ],
 
 329   # get name of partsgroup if id is given
 
 331   if ($form->{partsgroup_id}) {
 
 332     my $pg = SL::DB::PartsGroup->new(id => $form->{partsgroup_id})->load;
 
 333     $pg_name = $pg->{'partsgroup'};
 
 336   # these strings get displayed at the top of the results to indicate the user which switches were used
 
 338     active        => $locale->text('Active'),
 
 339     obsolete      => $locale->text('Obsolete'),
 
 340     orphaned      => $locale->text('Orphaned'),
 
 341     onhand        => $locale->text('On Hand'),
 
 342     short         => $locale->text('Short'),
 
 343     onorder       => $locale->text('On Order'),
 
 344     ordered       => $locale->text('Ordered'),
 
 345     rfq           => $locale->text('RFQ'),
 
 346     quoted        => $locale->text('Quoted'),
 
 347     bought        => $locale->text('Bought'),
 
 348     sold          => $locale->text('Sold'),
 
 349     transdatefrom => $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{transdatefrom}, 1),
 
 350     transdateto   => $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{transdateto}, 1),
 
 351     partnumber    => $locale->text('Part Number')      . ": '$form->{partnumber}'",
 
 352     partsgroup    => $locale->text('Partsgroup')       . ": '$form->{partsgroup}'",
 
 353     partsgroup_id => $locale->text('Partsgroup')       . ": '$pg_name'",
 
 354     serialnumber  => $locale->text('Serial Number')    . ": '$form->{serialnumber}'",
 
 355     description   => $locale->text('Part Description') . ": '$form->{description}'",
 
 356     make          => $locale->text('Make')             . ": '$form->{make}'",
 
 357     model         => $locale->text('Model')            . ": '$form->{model}'",
 
 358     drawing       => $locale->text('Drawing')          . ": '$form->{drawing}'",
 
 359     microfiche    => $locale->text('Microfiche')       . ": '$form->{microfiche}'",
 
 360     l_soldtotal   => $locale->text('Qty in Selected Records'),
 
 361     ean           => $locale->text('EAN')              . ": '$form->{ean}'",
 
 362     insertdatefrom => $locale->text('Insert Date') . ": " . $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{insertdatefrom}, 1),
 
 363     insertdateto   => $locale->text('Insert Date') . ": " . $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{insertdateto}, 1),
 
 366   my @itemstatus_keys = qw(active obsolete orphaned onhand short);
 
 367   my @callback_keys   = qw(onorder ordered rfq quoted bought sold partnumber partsgroup partsgroup_id serialnumber description make model
 
 368                            drawing microfiche l_soldtotal l_deliverydate transdatefrom transdateto insertdatefrom insertdateto ean shop all);
 
 370   # calculate dependencies
 
 371   for (@itemstatus_keys, @callback_keys) {
 
 372     next if ($form->{itemstatus} ne $_ && !$form->{$_});
 
 373     map { $form->{$_} = 'Y' } @{ $dependencies{$_} } if $dependencies{$_};
 
 376   # generate callback and optionstrings
 
 378   for my  $key (@itemstatus_keys, @callback_keys) {
 
 379     next if ($form->{itemstatus} ne $key && !$form->{$key});
 
 380     push @options, $optiontexts{$key};
 
 383   # special case for lastcost
 
 384   if ($form->{ledgerchecks}){
 
 385     # ledgerchecks don't know about sellprice or lastcost. they just return a
 
 386     # price. so rename sellprice to price, and drop lastcost.
 
 387     $column_defs{sellprice}{text} = $locale->text('Price');
 
 388     $form->{l_lastcost} = ""
 
 391   if ($form->{description}) {
 
 392     $description = $form->{description};
 
 393     $description =~ s/\n/<br>/g;
 
 396   if ($form->{l_linetotal}) {
 
 397     $form->{l_qty} = "Y";
 
 398     $form->{l_linetotalsellprice} = "Y" if $form->{l_sellprice};
 
 399     $form->{l_linetotallastcost}  = $form->{searchitems} eq 'assembly' && !$form->{bom} ? "" : 'Y' if  $form->{l_lastcost};
 
 400     $form->{l_linetotallistprice} = "Y" if $form->{l_listprice};
 
 402   $form->{"l_type_and_classific"} = "Y";
 
 404   if ($form->{l_service} && !$form->{l_assembly} && !$form->{l_part}) {
 
 406     # remove bin, weight and rop from list
 
 407     map { $form->{"l_$_"} = "" } qw(bin weight rop);
 
 409     $form->{l_onhand} = "";
 
 411     # qty is irrelevant unless bought or sold
 
 417         || $form->{quoted}) {
 
 418 #      $form->{l_onhand} = "Y";
 
 420       $form->{l_linetotalsellprice} = "";
 
 421       $form->{l_linetotallastcost}  = "";
 
 425   # soldtotal doesn't make sense with more than one bsooqr option.
 
 426   # so reset it to sold (the most common option), and issue a warning
 
 428   # also it doesn't make sense without bsooqr. disable and issue a warning too
 
 429   my @bsooqr = qw(sold bought onorder ordered rfq quoted);
 
 430   my $bsooqr_mode = grep { $form->{$_} } @bsooqr;
 
 431   if ($form->{l_subtotal} && 1 < $bsooqr_mode) {
 
 432     my $enabled       = first { $form->{$_} } @bsooqr;
 
 433     $form->{$_}       = ''   for @bsooqr;
 
 434     $form->{$enabled} = 'Y';
 
 436     push @options, $::locale->text('Subtotal cannot distinguish betweens record types. Only one of the selected record types will be displayed: #1', $optiontexts{$enabled});
 
 438   if ($form->{l_soldtotal} && !$bsooqr_mode) {
 
 439     delete $form->{l_soldtotal};
 
 441     flash('warning', $::locale->text('Soldtotal does not make sense without any bsooqr options'));
 
 443   if ($form->{l_name} && !$bsooqr_mode) {
 
 444     delete $form->{l_name};
 
 446     flash('warning', $::locale->text('Name does not make sense without any bsooqr options'));
 
 448   IC->all_parts(\%myconfig, \%$form);
 
 451     partnumber type_and_classific description notes partsgroup bin onhand rop soldtotal unit listprice
 
 452     linetotallistprice sellprice linetotalsellprice lastcost linetotallastcost
 
 453     priceupdate weight image drawing microfiche invnumber ordnumber quonumber
 
 454     transdate name serialnumber deliverydate ean projectnumber projectdescription
 
 458   my $pricegroups = SL::DB::Manager::Pricegroup->get_all_sorted;
 
 459   my @pricegroup_columns;
 
 460   my %column_defs_pricegroups;
 
 461   if ($form->{l_pricegroups}) {
 
 462     @pricegroup_columns      = map { "pricegroup_" . $_->id } @{ $pricegroups };
 
 463     %column_defs_pricegroups = map {
 
 464       "pricegroup_" . $_->id => {
 
 465         text    => $::locale->text('Pricegroup') . ' ' . $_->pricegroup,
 
 470   push @columns, @pricegroup_columns;
 
 472   my @includeable_custom_variables = grep { $_->{includeable} } @{ $cvar_configs };
 
 473   my @searchable_custom_variables  = grep { $_->{searchable} }  @{ $cvar_configs };
 
 474   my %column_defs_cvars            = map { +"cvar_$_->{name}" => { 'text' => $_->{description} } } @includeable_custom_variables;
 
 476   push @columns, map { "cvar_$_->{name}" } @includeable_custom_variables;
 
 478   %column_defs = (%column_defs, %column_defs_cvars, %column_defs_pricegroups);
 
 479   map { $column_defs{$_}->{visible} ||= $form->{"l_$_"} ? 1 : 0 } @columns;
 
 480   map { $column_defs{$_}->{align}   = 'right' } qw(onhand sellprice listprice lastcost linetotalsellprice linetotallastcost linetotallistprice rop weight soldtotal shop), @pricegroup_columns;
 
 482   my @hidden_variables = (
 
 483     qw(l_subtotal l_linetotal searchitems itemstatus bom l_pricegroups insertdatefrom insertdateto),
 
 484     qw(l_type_and_classific classification_id),
 
 487     map({ "cvar_$_->{name}" } @searchable_custom_variables),
 
 488     map({'cvar_'. $_->{name} .'_qtyop'} grep({$_->{type} eq 'number'} @searchable_custom_variables)),
 
 489     map({ "l_$_" } @columns),
 
 492   my $callback         = build_std_url('action=generate_report', grep { $form->{$_} } @hidden_variables);
 
 494   my @sort_full        = qw(partnumber description onhand soldtotal deliverydate insertdate shop);
 
 495   my @sort_no_revers   = qw(partsgroup bin priceupdate invnumber ordnumber quonumber name image drawing serialnumber);
 
 497   foreach my $col (@sort_full) {
 
 498     $column_defs{$col}->{link} = join '&', $callback, "sort=$col", map { "$_=" . E($form->{$_}) } qw(revers lastsort);
 
 500   map { $column_defs{$_}->{link} = "${callback}&sort=$_" } @sort_no_revers;
 
 502   # add order to callback
 
 503   $form->{callback} = join '&', ($callback, map { "${_}=" . E($form->{$_}) } qw(sort revers));
 
 505   my $report = SL::ReportGenerator->new(\%myconfig, $form);
 
 507   my %attachment_basenames = (
 
 508     'part'     => $locale->text('part_list'),
 
 509     'service'  => $locale->text('service_list'),
 
 510     'assembly' => $locale->text('assembly_list'),
 
 511     'article'  => $locale->text('article_list'),
 
 514   $report->set_options('raw_top_info_text'     => $form->parse_html_template('ic/generate_report_top', { options => \@options }),
 
 515                        'raw_bottom_info_text'  => $form->parse_html_template('ic/generate_report_bottom' ,
 
 516                                                   { PART_CLASSIFICATIONS => SL::DB::Manager::PartClassification->get_all_sorted }),
 
 517                        'output_format'         => 'HTML',
 
 518                        'title'                 => $form->{title},
 
 519                        'attachment_basename'   => 'article_list' . strftime('_%Y%m%d', localtime time),
 
 521   $report->set_options_from_form();
 
 522   $locale->set_numberformat_wo_thousands_separator(\%myconfig) if lc($report->{options}->{output_format}) eq 'csv';
 
 524   $report->set_columns(%column_defs);
 
 525   $report->set_column_order(@columns);
 
 527   $report->set_export_options('generate_report', @hidden_variables, qw(sort revers));
 
 529   $report->set_sort_indicator($form->{sort}, $form->{revers} ? 0 : 1);
 
 531   CVar->add_custom_variables_to_report('module'         => 'IC',
 
 532                                        'trans_id_field' => 'id',
 
 533                                        'configs'        => $cvar_configs,
 
 534                                        'column_defs'    => \%column_defs,
 
 535                                        'data'           => $form->{parts});
 
 537   CVar->add_custom_variables_to_report('module'         => 'IC',
 
 538                                        'sub_module'     => sub { $_[0]->{ioi} },
 
 539                                        'trans_id_field' => 'ioi_id',
 
 540                                        'configs'        => $cvar_configs,
 
 541                                        'column_defs'    => \%column_defs,
 
 542                                        'data'           => $form->{parts});
 
 544   my @subtotal_columns = qw(sellprice listprice lastcost);
 
 545   my %subtotals = map { $_ => 0 } ('onhand', @subtotal_columns);
 
 546   my %totals    = map { $_ => 0 } @subtotal_columns;
 
 548   my $same_item = @{ $form->{parts} } ? $form->{parts}[0]{ $form->{sort} } : undef;
 
 550   my $defaults  = AM->get_defaults();
 
 553   foreach my $ref (@{ $form->{parts} }) {
 
 555     # fresh row, for inserting later
 
 556     my $row = { map { $_ => { 'data' => $ref->{$_} } } @columns };
 
 558     $ref->{exchangerate} ||= 1;
 
 559     $ref->{price_factor} ||= 1;
 
 560     $ref->{sellprice}     *= $ref->{exchangerate} / $ref->{price_factor};
 
 561     $ref->{listprice}     *= $ref->{exchangerate} / $ref->{price_factor};
 
 562     $ref->{lastcost}      *= $ref->{exchangerate} / $ref->{price_factor};
 
 564     # use this for assemblies
 
 565     my $soldtotal = $bsooqr_mode ? $ref->{soldtotal} : $ref->{onhand};
 
 567     if ($ref->{assemblyitem}) {
 
 568       $row->{partnumber}{align}   = 'right';
 
 569       $row->{soldtotal}{data}     = 0;
 
 570       $soldtotal                  = 0 if ($form->{sold});
 
 573     my $edit_link               = build_std_url('script=controller.pl', 'action=Part/edit', 'part.id=' . E($ref->{id}), 'callback');
 
 574     $row->{partnumber}->{link}  = $edit_link;
 
 575     $row->{description}->{link} = $edit_link;
 
 577     foreach (qw(sellprice listprice lastcost)) {
 
 578       $row->{$_}{data}            = $form->format_amount(\%myconfig, $ref->{$_}, 2);
 
 579       $row->{"linetotal$_"}{data} = $form->format_amount(\%myconfig, $ref->{onhand} * $ref->{$_}, 2);
 
 581     foreach ( @pricegroup_columns ) {
 
 582       $row->{$_}{data}            = $form->format_amount(\%myconfig, $ref->{"$_"}, 2);
 
 586     map { $row->{$_}{data} = $form->format_amount(\%myconfig, $ref->{$_}); } qw(onhand rop weight soldtotal);
 
 588     $row->{weight}->{data} .= ' ' . $defaults->{weightunit};
 
 590     # 'yes' and 'no' for boolean value shop
 
 591     if ($form->{l_shop}) {
 
 592       $row->{shop}{data} = $row->{shop}{data}? $::locale->text('yes') : $::locale->text('no');
 
 595     if (!$ref->{assemblyitem}) {
 
 596       foreach my $col (@subtotal_columns) {
 
 597         $totals{$col}    += $soldtotal * $ref->{$col};
 
 598         $subtotals{$col} += $soldtotal * $ref->{$col};
 
 601       $subtotals{soldtotal} += $soldtotal;
 
 605     if ($ref->{module} eq 'oe') {
 
 606       # für oe gibt es vier fälle, jeweils nach kunde oder lieferant unterschiedlich:
 
 608       # | ist bestellt  | Von Kunden bestellt |  -> edit_oe_ord_link
 
 609       # | Anfrage       | Angebot             |  -> edit_oe_quo_link
 
 611       my $edit_oe_ord_link = build_std_url("script=oe.pl", 'action=edit', 'type=' . E($ref->{cv} eq 'vendor' ? 'purchase_order' : 'sales_order'), 'id=' . E($ref->{trans_id}), 'callback');
 
 612       my $edit_oe_quo_link = build_std_url("script=oe.pl", 'action=edit', 'type=' . E($ref->{cv} eq 'vendor' ? 'request_quotation' : 'sales_quotation'), 'id=' . E($ref->{trans_id}), 'callback');
 
 614       $row->{ordnumber}{link} = $edit_oe_ord_link;
 
 615       $row->{quonumber}{link} = $edit_oe_quo_link if (!$ref->{ordnumber});
 
 618       $row->{invnumber}{link} = build_std_url("script=$ref->{module}.pl", 'action=edit', 'type=invoice', 'id=' . E($ref->{trans_id}), 'callback') if ($ref->{invnumber});
 
 621     # set properties of images
 
 622     if ($ref->{image} && (lc $report->{options}->{output_format} eq 'html')) {
 
 623       $row->{image}{data}     = '';
 
 624       $row->{image}{raw_data} = '<a href="' . H($ref->{image}) . '"><img src="' . H($ref->{image}) . '" height="32" border="0"></a>';
 
 626     map { $row->{$_}{link} = $ref->{$_} } qw(drawing microfiche);
 
 628     $row->{notes}{data} = SL::HTML::Util->strip($ref->{notes});
 
 629     $row->{type_and_classific}{data} = $::request->presenter->type_abbreviation($ref->{part_type}).
 
 630                                        $::request->presenter->classification_abbreviation($ref->{classification_id});
 
 632     $report->add_data($row);
 
 634     my $next_ref = $form->{parts}[$idx + 1];
 
 636     # insert subtotal rows
 
 637     if (($form->{l_subtotal} eq 'Y') &&
 
 639          (!$next_ref->{assemblyitem} && ($same_item ne $next_ref->{ $form->{sort} })))) {
 
 640       my $row = { map { $_ => { 'class' => 'listsubtotal', } } @columns };
 
 642       if ( !$form->{l_assembly} || !$form->{bom}) {
 
 643         $row->{soldtotal}->{data} = $form->format_amount(\%myconfig, $subtotals{soldtotal});
 
 646       map { $row->{"linetotal$_"}->{data} = $form->format_amount(\%myconfig, $subtotals{$_}, 2) } @subtotal_columns;
 
 647       map { $subtotals{$_} = 0 } ('soldtotal', @subtotal_columns);
 
 649       $report->add_data($row);
 
 651       $same_item = $next_ref->{ $form->{sort} };
 
 657   if ($form->{"l_linetotal"} && !$form->{report_generator_csv_options_for_import}) {
 
 658     my $row = { map { $_ => { 'class' => 'listtotal', } } @columns };
 
 660     map { $row->{"linetotal$_"}->{data} = $form->format_amount(\%myconfig, $totals{$_}, 2) } @subtotal_columns;
 
 662     $report->add_separator();
 
 663     $report->add_data($row);
 
 666   $report->generate_with_headers();
 
 668   $lxdebug->leave_sub();
 
 669 }    #end generate_report
 
 671 sub continue { call_sub($form->{"nextsub"}); }
 
 674   my $action = first { $::form->{"action_${_}"} } qw(add);
 
 675   $::form->error($::locale->text('No action defined.')) unless $action;
 
 677   $::form->{dispatched_action} = $action;