Artikel-Klassifizierung
[kivitendo-erp.git] / bin / mozilla / ic.pl
1 #=====================================================================
2 # LX-Office ERP
3 # Copyright (C) 2004
4 # Based on SQL-Ledger Version 2.1.9
5 # Web http://www.lx-office.org
6 #
7 #=====================================================================
8 # SQL-Ledger, Accounting
9 # Copyright (c) 2001
10 #
11 #  Author: Dieter Simader
12 #   Email: dsimader@sql-ledger.org
13 #     Web: http://www.sql-ledger.org
14 #
15 #
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.
20 #
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,
28 # MA 02110-1335, USA.
29 #======================================================================
30 #
31 # Inventory Control module
32 #
33 #======================================================================
34
35 use POSIX qw(strftime);
36 use List::Util qw(first max);
37 use List::MoreUtils qw(any);
38
39 use SL::AM;
40 use SL::CVar;
41 use SL::IC;
42 use SL::Helper::Flash qw(flash);
43 use SL::HTML::Util;
44 use SL::ReportGenerator;
45
46 #use SL::PE;
47
48 use strict;
49 #use warnings;
50
51 # global imports
52 our ($form, $locale, %myconfig, $lxdebug, $auth);
53
54 require "bin/mozilla/io.pl";
55 require "bin/mozilla/common.pl";
56 require "bin/mozilla/reportgenerator.pl";
57
58 1;
59
60 # Parserhappy(R):
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');
75
76 # end of main
77
78 sub search {
79   $lxdebug->enter_sub();
80
81   $auth->assert('part_service_assembly_details');
82
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
86
87   my %is_xyz     = map { +"is_$_" => ($form->{searchitems} eq $_) } qw(part service assembly assortment);
88
89   $form->{title} = (ucfirst $form->{searchitems}) . "s";
90   $form->{title} =~ s/ys$/ies/;
91   $form->{title} = $locale->text($form->{title});
92   $form->{title} = $locale->text('Assemblies') if ($is_xyz{is_assembly});
93
94   $form->{CUSTOM_VARIABLES}                  = CVar->get_configs('module' => 'IC');
95   ($form->{CUSTOM_VARIABLES_FILTER_CODE},
96    $form->{CUSTOM_VARIABLES_INCLUSION_CODE}) = CVar->render_search_options('variables'      => $form->{CUSTOM_VARIABLES},
97                                                                            'include_prefix' => 'l_',
98                                                                            'include_value'  => 'Y');
99
100   $form->header;
101
102   $form->get_lists('partsgroup'    => 'ALL_PARTSGROUPS');
103   print $form->parse_html_template('ic/search', { %is_xyz, });
104
105   $lxdebug->leave_sub();
106 }    #end search()
107
108 sub search_update_prices {
109   $lxdebug->enter_sub();
110
111   $auth->assert('part_service_assembly_edit');
112
113   my $pricegroups = IC->get_pricegroups(\%myconfig, \%$form);
114
115   $form->{title} = $locale->text('Update Prices');
116
117   $form->header;
118
119   print $form->parse_html_template('ic/search_update_prices', { PRICE_ROWS => $pricegroups });
120
121   $lxdebug->leave_sub();
122 }    #end search()
123
124 sub confirm_price_update {
125   $lxdebug->enter_sub();
126
127   $auth->assert('part_service_assembly_edit');
128
129   my @errors      = ();
130   my $value_found = undef;
131
132   foreach my $idx (qw(sellprice listprice), (1..$form->{price_rows})) {
133     my $name      = $idx =~ m/\d/ ? $form->{"pricegroup_${idx}"}      : $idx eq 'sellprice' ? $locale->text('Sell Price') : $locale->text('List Price');
134     my $type      = $idx =~ m/\d/ ? $form->{"pricegroup_type_${idx}"} : $form->{"${idx}_type"};
135     my $value_idx = $idx =~ m/\d/ ? "price_${idx}" : $idx;
136     my $value     = $form->parse_amount(\%myconfig, $form->{$value_idx});
137
138     if ((0 > $value) && ($type eq 'percent')) {
139       push @errors, $locale->text('You cannot adjust the price for pricegroup "#1" by a negative percentage.', $name);
140
141     } elsif (!$value && ($form->{$value_idx} ne '')) {
142       push @errors, $locale->text('No valid number entered for pricegroup "#1".', $name);
143
144     } elsif (0 < $value) {
145       $value_found = 1;
146     }
147   }
148
149   push @errors, $locale->text('No prices will be updated because no prices have been entered.') if (!$value_found);
150
151   my $num_matches = IC->get_num_matches_for_priceupdate();
152
153   $form->header();
154
155   if (@errors) {
156     $form->show_generic_error(join('<br>', @errors), 'back_button' => 1);
157   }
158
159   $form->{nextsub} = "update_prices";
160
161   map { delete $form->{$_} } qw(action header);
162
163   print $form->parse_html_template('ic/confirm_price_update', { HIDDENS     => [ map { name => $_, value => $form->{$_} }, keys %$form ],
164                                                                 num_matches => $num_matches });
165
166   $lxdebug->leave_sub();
167 }
168
169 sub update_prices {
170   $lxdebug->enter_sub();
171
172   $auth->assert('part_service_assembly_edit');
173
174   my $num_updated = IC->update_prices(\%myconfig, \%$form);
175
176   if (-1 != $num_updated) {
177     $form->redirect($locale->text('#1 prices were updated.', $num_updated));
178   } else {
179     $form->error($locale->text('Could not update prices!'));
180   }
181
182   $lxdebug->leave_sub();
183 }
184
185 sub top100 {
186   $::lxdebug->enter_sub();
187
188   $::auth->assert('part_service_assembly_edit');
189
190   $::form->{l_soldtotal} = "Y";
191   $::form->{sort}        = "soldtotal";
192   $::form->{lastsort}    = "soldtotal";
193
194   $::form->{l_qty}       = undef;
195   $::form->{l_linetotal} = undef;
196   $::form->{l_number}    = "Y";
197   $::form->{number}      = "position";
198
199   unless (   $::form->{bought}
200           || $::form->{sold}
201           || $::form->{rfq}
202           || $::form->{quoted}) {
203     $::form->{bought} = $::form->{sold} = 1;
204   }
205
206   generate_report();
207
208   $lxdebug->leave_sub();
209 }
210
211 #
212 # Report for Wares.
213 # Warning, deep magic ahead.
214 # This function parses the requested details, sanity checks them, and converts them into a format thats usable for IC->all_parts
215 #
216 # flags coming from the form:
217 # hardcoded:
218 #  searchitems=part revers=0 lastsort=''
219 #
220 # filter:
221 # partnumber ean description partsgroup classification serialnumber make model drawing microfiche
222 # transdatefrom transdateto
223 #
224 # radio:
225 #  itemstatus = active | onhand | short | obsolete | orphaned
226 #  action     = continue | top100
227 #
228 # checkboxes:
229 #  bought sold onorder ordered rfq quoted
230 #  l_partnumber l_description l_serialnumber l_unit l_listprice l_sellprice l_lastcost
231 #  l_linetotal l_priceupdate l_bin l_rop l_weight l_image l_drawing l_microfiche
232 #  l_partsgroup l_subtotal l_soldtotal l_deliverydate l_pricegroups
233 #
234 # hiddens:
235 #  nextsub revers lastsort sort ndxs_counter
236 #
237 sub generate_report {
238   $lxdebug->enter_sub();
239
240   $auth->assert('part_service_assembly_details');
241
242   my ($revers, $lastsort, $description);
243
244   my $cvar_configs = CVar->get_configs('module' => 'IC');
245
246   my %titles = (
247     ''         => $locale->text('Articles'),
248     part       => $locale->text('Parts'),
249     service    => $locale->text('Services'),
250     assembly   => $locale->text('Assemblies'),
251     assortment => $locale->text('Assortments'),
252   );
253
254   $form->{title} = $titles{$form->{searchitems}};
255
256   my %column_defs = (
257     'bin'                => { 'text' => $locale->text('Bin'), },
258     'deliverydate'       => { 'text' => $locale->text('deliverydate'), },
259     'description'        => { 'text' => $locale->text('Part Description'), },
260     'notes'              => { 'text' => $locale->text('Notes'), },
261     'drawing'            => { 'text' => $locale->text('Drawing'), },
262     'ean'                => { 'text' => $locale->text('EAN'), },
263     'image'              => { 'text' => $locale->text('Image'), },
264     'insertdate'         => { 'text' => $locale->text('Insert Date'), },
265     'invnumber'          => { 'text' => $locale->text('Invoice Number'), },
266     'lastcost'           => { 'text' => $locale->text('Last Cost'), },
267     'linetotallastcost'  => { 'text' => $locale->text('Extended'), },
268     'linetotallistprice' => { 'text' => $locale->text('Extended'), },
269     'linetotalsellprice' => { 'text' => $locale->text('Extended'), },
270     'listprice'          => { 'text' => $locale->text('List Price'), },
271     'microfiche'         => { 'text' => $locale->text('Microfiche'), },
272     'name'               => { 'text' => $locale->text('Name'), },
273     'onhand'             => { 'text' => $locale->text('Stocked Qty'), },
274     'ordnumber'          => { 'text' => $locale->text('Order Number'), },
275     'partnumber'         => { 'text' => $locale->text('Part Number'), },
276     'partsgroup'         => { 'text' => $locale->text('Partsgroup'), },
277     'priceupdate'        => { 'text' => $locale->text('Updated'), },
278     'quonumber'          => { 'text' => $locale->text('Quotation'), },
279     'rop'                => { 'text' => $locale->text('ROP'), },
280     'sellprice'          => { 'text' => $locale->text('Sell Price'), },
281     'serialnumber'       => { 'text' => $locale->text('Serial Number'), },
282     'soldtotal'          => { 'text' => $locale->text('Qty in Selected Records'), },
283     'name'               => { 'text' => $locale->text('Name in Selected Records'), },
284     'transdate'          => { 'text' => $locale->text('Transdate'), },
285     'unit'               => { 'text' => $locale->text('Unit'), },
286     'weight'             => { 'text' => $locale->text('Weight'), },
287     'shop'               => { 'text' => $locale->text('Shop article'), },
288     'type_and_classific' => { 'text' => $locale->text('Type'), },
289     'projectnumber'      => { 'text' => $locale->text('Project Number'), },
290     'projectdescription' => { 'text' => $locale->text('Project Description'), },
291   );
292
293   $revers     = $form->{revers};
294   $lastsort   = $form->{lastsort};
295
296   # sorting and direction of sorting
297   # ToDO: change this to the simpler field+direction method
298   if (($form->{lastsort} eq "") && ($form->{sort} eq undef)) {
299     $form->{revers}   = 0;
300     $form->{lastsort} = "partnumber";
301     $form->{sort}     = "partnumber";
302   } else {
303     if ($form->{lastsort} eq $form->{sort}) {
304       $form->{revers} = 1 - $form->{revers};
305     } else {
306       $form->{revers} = 0;
307       $form->{lastsort} = $form->{sort};
308     }    #fi
309   }    #fi
310
311   # special case if we have a serialnumber limit search
312   # serialnumbers are only given in invoices and orders,
313   # so they can only pop up in bought, sold, rfq, and quoted stuff
314   $form->{no_sn_joins} = 'Y' if (   !$form->{bought} && !$form->{sold}
315                                  && !$form->{rfq}    && !$form->{quoted}
316                                  && ($form->{l_serialnumber} || $form->{serialnumber}));
317
318   # special case for any checkbox of bought | sold | onorder | ordered | rfq | quoted.
319   # if any of these are ticked the behavior changes slightly for lastcost
320   # since all those are aggregation checks for the legder tables this is an internal switch
321   # refered to as ledgerchecks
322   $form->{ledgerchecks} = 'Y' if (   $form->{bought} || $form->{sold} || $form->{onorder}
323                                   || $form->{ordered} || $form->{rfq} || $form->{quoted});
324
325   # if something should be activated if something else is active, enter it here
326   my %dependencies = (
327     onhand       => [ qw(l_onhand) ],
328     short        => [ qw(l_onhand) ],
329     onorder      => [ qw(l_ordnumber) ],
330     ordered      => [ qw(l_ordnumber) ],
331     rfq          => [ qw(l_quonumber) ],
332     quoted       => [ qw(l_quonumber) ],
333     bought       => [ qw(l_invnumber) ],
334     sold         => [ qw(l_invnumber) ],
335     ledgerchecks => [ qw(l_name) ],
336     serialnumber => [ qw(l_serialnumber) ],
337     no_sn_joins  => [ qw(bought sold) ],
338   );
339
340   # get name of partsgroup if id is given
341   my $pg_name;
342   if ($form->{partsgroup_id}) {
343     my $pg = SL::DB::PartsGroup->new(id => $form->{partsgroup_id})->load;
344     $pg_name = $pg->{'partsgroup'};
345   }
346
347   # these strings get displayed at the top of the results to indicate the user which switches were used
348   my %optiontexts = (
349     active        => $locale->text('Active'),
350     obsolete      => $locale->text('Obsolete'),
351     orphaned      => $locale->text('Orphaned'),
352     onhand        => $locale->text('On Hand'),
353     short         => $locale->text('Short'),
354     onorder       => $locale->text('On Order'),
355     ordered       => $locale->text('Ordered'),
356     rfq           => $locale->text('RFQ'),
357     quoted        => $locale->text('Quoted'),
358     bought        => $locale->text('Bought'),
359     sold          => $locale->text('Sold'),
360     transdatefrom => $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{transdatefrom}, 1),
361     transdateto   => $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{transdateto}, 1),
362     partnumber    => $locale->text('Part Number')      . ": '$form->{partnumber}'",
363     partsgroup    => $locale->text('Partsgroup')       . ": '$form->{partsgroup}'",
364     partsgroup_id => $locale->text('Partsgroup')       . ": '$pg_name'",
365     serialnumber  => $locale->text('Serial Number')    . ": '$form->{serialnumber}'",
366     description   => $locale->text('Part Description') . ": '$form->{description}'",
367     make          => $locale->text('Make')             . ": '$form->{make}'",
368     model         => $locale->text('Model')            . ": '$form->{model}'",
369     drawing       => $locale->text('Drawing')          . ": '$form->{drawing}'",
370     microfiche    => $locale->text('Microfiche')       . ": '$form->{microfiche}'",
371     l_soldtotal   => $locale->text('Qty in Selected Records'),
372     ean           => $locale->text('EAN')              . ": '$form->{ean}'",
373     insertdatefrom => $locale->text('Insert Date') . ": " . $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{insertdatefrom}, 1),
374     insertdateto   => $locale->text('Insert Date') . ": " . $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{insertdateto}, 1),
375   );
376
377   my @itemstatus_keys = qw(active obsolete orphaned onhand short);
378   my @callback_keys   = qw(onorder ordered rfq quoted bought sold partnumber partsgroup partsgroup_id serialnumber description make model
379                            drawing microfiche l_soldtotal l_deliverydate transdatefrom transdateto insertdatefrom insertdateto ean shop all);
380
381   # calculate dependencies
382   for (@itemstatus_keys, @callback_keys) {
383     next if ($form->{itemstatus} ne $_ && !$form->{$_});
384     map { $form->{$_} = 'Y' } @{ $dependencies{$_} } if $dependencies{$_};
385   }
386
387   # generate callback and optionstrings
388   my @options;
389   for my  $key (@itemstatus_keys, @callback_keys) {
390     next if ($form->{itemstatus} ne $key && !$form->{$key});
391     push @options, $optiontexts{$key};
392   }
393
394   # special case for lastcost
395   if ($form->{ledgerchecks}){
396     # ledgerchecks don't know about sellprice or lastcost. they just return a
397     # price. so rename sellprice to price, and drop lastcost.
398     $column_defs{sellprice}{text} = $locale->text('Price');
399     $form->{l_lastcost} = ""
400   }
401
402   if ($form->{description}) {
403     $description = $form->{description};
404     $description =~ s/\n/<br>/g;
405   }
406
407   if ($form->{l_linetotal}) {
408     $form->{l_qty} = "Y";
409     $form->{l_linetotalsellprice} = "Y" if $form->{l_sellprice};
410     $form->{l_linetotallastcost}  = $form->{searchitems} eq 'assembly' && !$form->{bom} ? "" : 'Y' if  $form->{l_lastcost};
411     $form->{l_linetotallistprice} = "Y" if $form->{l_listprice};
412   }
413   $form->{"l_type_and_classific"} = "Y";
414
415   if ($form->{searchitems} eq 'service') {
416
417     # remove bin, weight and rop from list
418     map { $form->{"l_$_"} = "" } qw(bin weight rop);
419
420     $form->{l_onhand} = "";
421
422     # qty is irrelevant unless bought or sold
423     if (   $form->{bought}
424         || $form->{sold}
425         || $form->{onorder}
426         || $form->{ordered}
427         || $form->{rfq}
428         || $form->{quoted}) {
429 #      $form->{l_onhand} = "Y";
430     } else {
431       $form->{l_linetotalsellprice} = "";
432       $form->{l_linetotallastcost}  = "";
433     }
434   }
435
436   # soldtotal doesn't make sense with more than one bsooqr option.
437   # so reset it to sold (the most common option), and issue a warning
438   # ...
439   # also it doesn't make sense without bsooqr. disable and issue a warning too
440   my @bsooqr = qw(sold bought onorder ordered rfq quoted);
441   my $bsooqr_mode = grep { $form->{$_} } @bsooqr;
442   if ($form->{l_subtotal} && 1 < $bsooqr_mode) {
443     my $enabled       = first { $form->{$_} } @bsooqr;
444     $form->{$_}       = ''   for @bsooqr;
445     $form->{$enabled} = 'Y';
446
447     push @options, $::locale->text('Subtotal cannot distinguish betweens record types. Only one of the selected record types will be displayed: #1', $optiontexts{$enabled});
448   }
449   if ($form->{l_soldtotal} && !$bsooqr_mode) {
450     delete $form->{l_soldtotal};
451
452     flash('warning', $::locale->text('Soldtotal does not make sense without any bsooqr options'));
453   }
454   if ($form->{l_name} && !$bsooqr_mode) {
455     delete $form->{l_name};
456
457     flash('warning', $::locale->text('Name does not make sense without any bsooqr options'));
458   }
459   IC->all_parts(\%myconfig, \%$form);
460
461   my @columns = qw(
462     partnumber type_and_classific description notes partsgroup bin onhand rop soldtotal unit listprice
463     linetotallistprice sellprice linetotalsellprice lastcost linetotallastcost
464     priceupdate weight image drawing microfiche invnumber ordnumber quonumber
465     transdate name serialnumber deliverydate ean projectnumber projectdescription
466     insertdate shop
467   );
468
469   my $pricegroups = SL::DB::Manager::Pricegroup->get_all_sorted;
470   my @pricegroup_columns;
471   my %column_defs_pricegroups;
472   if ($form->{l_pricegroups}) {
473     @pricegroup_columns      = map { "pricegroup_" . $_->id } @{ $pricegroups };
474     %column_defs_pricegroups = map {
475       "pricegroup_" . $_->id => {
476         text    => $::locale->text('Pricegroup') . ' ' . $_->pricegroup,
477         visible => 1,
478       },
479     }  @{ $pricegroups };
480   }
481   push @columns, @pricegroup_columns;
482
483   my @includeable_custom_variables = grep { $_->{includeable} } @{ $cvar_configs };
484   my @searchable_custom_variables  = grep { $_->{searchable} }  @{ $cvar_configs };
485   my %column_defs_cvars            = map { +"cvar_$_->{name}" => { 'text' => $_->{description} } } @includeable_custom_variables;
486
487   push @columns, map { "cvar_$_->{name}" } @includeable_custom_variables;
488
489   %column_defs = (%column_defs, %column_defs_cvars, %column_defs_pricegroups);
490   map { $column_defs{$_}->{visible} ||= $form->{"l_$_"} ? 1 : 0 } @columns;
491   map { $column_defs{$_}->{align}   = 'right' } qw(onhand sellprice listprice lastcost linetotalsellprice linetotallastcost linetotallistprice rop weight soldtotal shop), @pricegroup_columns;
492
493   my @hidden_variables = (
494     qw(l_subtotal l_linetotal searchitems itemstatus bom l_pricegroups insertdatefrom insertdateto),
495     qw(l_type_and_classific classification_id),
496     @itemstatus_keys,
497     @callback_keys,
498     map({ "cvar_$_->{name}" } @searchable_custom_variables),
499     map({'cvar_'. $_->{name} .'_qtyop'} grep({$_->{type} eq 'number'} @searchable_custom_variables)),
500     map({ "l_$_" } @columns),
501   );
502
503   my $callback         = build_std_url('action=generate_report', grep { $form->{$_} } @hidden_variables);
504
505   my @sort_full        = qw(partnumber description onhand soldtotal deliverydate insertdate shop);
506   my @sort_no_revers   = qw(partsgroup bin priceupdate invnumber ordnumber quonumber name image drawing serialnumber);
507
508   foreach my $col (@sort_full) {
509     $column_defs{$col}->{link} = join '&', $callback, "sort=$col", map { "$_=" . E($form->{$_}) } qw(revers lastsort);
510   }
511   map { $column_defs{$_}->{link} = "${callback}&sort=$_" } @sort_no_revers;
512
513   # add order to callback
514   $form->{callback} = join '&', ($callback, map { "${_}=" . E($form->{$_}) } qw(sort revers));
515
516   my $report = SL::ReportGenerator->new(\%myconfig, $form);
517
518   my %attachment_basenames = (
519     'part'     => $locale->text('part_list'),
520     'service'  => $locale->text('service_list'),
521     'assembly' => $locale->text('assembly_list'),
522     'article'  => $locale->text('article_list'),
523   );
524
525   $report->set_options('raw_top_info_text'     => $form->parse_html_template('ic/generate_report_top', { options => \@options }),
526                        'raw_bottom_info_text'  => $form->parse_html_template('ic/generate_report_bottom' ,
527                                                   { PART_CLASSIFICATIONS => SL::DB::Manager::PartClassification->get_all_sorted }),
528                        'output_format'         => 'HTML',
529                        'title'                 => $form->{title},
530                        'attachment_basename'   => $attachment_basenames{$form->{searchitems}} . strftime('_%Y%m%d', localtime time),
531   );
532   $report->set_options_from_form();
533   $locale->set_numberformat_wo_thousands_separator(\%myconfig) if lc($report->{options}->{output_format}) eq 'csv';
534
535   $report->set_columns(%column_defs);
536   $report->set_column_order(@columns);
537
538   $report->set_export_options('generate_report', @hidden_variables, qw(sort revers));
539
540   $report->set_sort_indicator($form->{sort}, $form->{revers} ? 0 : 1);
541
542   CVar->add_custom_variables_to_report('module'         => 'IC',
543                                        'trans_id_field' => 'id',
544                                        'configs'        => $cvar_configs,
545                                        'column_defs'    => \%column_defs,
546                                        'data'           => $form->{parts});
547
548   CVar->add_custom_variables_to_report('module'         => 'IC',
549                                        'sub_module'     => sub { $_[0]->{ioi} },
550                                        'trans_id_field' => 'ioi_id',
551                                        'configs'        => $cvar_configs,
552                                        'column_defs'    => \%column_defs,
553                                        'data'           => $form->{parts});
554
555   my @subtotal_columns = qw(sellprice listprice lastcost);
556   my %subtotals = map { $_ => 0 } ('onhand', @subtotal_columns);
557   my %totals    = map { $_ => 0 } @subtotal_columns;
558   my $idx       = 0;
559   my $same_item = @{ $form->{parts} } ? $form->{parts}[0]{ $form->{sort} } : undef;
560
561   my $defaults  = AM->get_defaults();
562
563   # postprocess parts
564   foreach my $ref (@{ $form->{parts} }) {
565
566     # fresh row, for inserting later
567     my $row = { map { $_ => { 'data' => $ref->{$_} } } @columns };
568
569     $ref->{exchangerate} ||= 1;
570     $ref->{price_factor} ||= 1;
571     $ref->{sellprice}     *= $ref->{exchangerate} / $ref->{price_factor};
572     $ref->{listprice}     *= $ref->{exchangerate} / $ref->{price_factor};
573     $ref->{lastcost}      *= $ref->{exchangerate} / $ref->{price_factor};
574
575     # use this for assemblies
576     my $soldtotal = $bsooqr_mode ? $ref->{soldtotal} : $ref->{onhand};
577
578     if ($ref->{assemblyitem}) {
579       $row->{partnumber}{align}   = 'right';
580       $row->{soldtotal}{data}     = 0;
581       $soldtotal                  = 0 if ($form->{sold});
582     }
583
584     my $edit_link               = build_std_url('script=controller.pl', 'action=Part/edit', 'part.id=' . E($ref->{id}), 'callback');
585     $row->{partnumber}->{link}  = $edit_link;
586     $row->{description}->{link} = $edit_link;
587
588     foreach (qw(sellprice listprice lastcost)) {
589       $row->{$_}{data}            = $form->format_amount(\%myconfig, $ref->{$_}, 2);
590       $row->{"linetotal$_"}{data} = $form->format_amount(\%myconfig, $ref->{onhand} * $ref->{$_}, 2);
591     }
592     foreach ( @pricegroup_columns ) {
593       $row->{$_}{data}            = $form->format_amount(\%myconfig, $ref->{"$_"}, 2);
594     };
595
596
597     map { $row->{$_}{data} = $form->format_amount(\%myconfig, $ref->{$_}); } qw(onhand rop weight soldtotal);
598
599     $row->{weight}->{data} .= ' ' . $defaults->{weightunit};
600
601     # 'yes' and 'no' for boolean value shop
602     if ($form->{l_shop}) {
603       $row->{shop}{data} = $row->{shop}{data}? $::locale->text('yes') : $::locale->text('no');
604     }
605
606     if (!$ref->{assemblyitem}) {
607       foreach my $col (@subtotal_columns) {
608         $totals{$col}    += $soldtotal * $ref->{$col};
609         $subtotals{$col} += $soldtotal * $ref->{$col};
610       }
611
612       $subtotals{soldtotal} += $soldtotal;
613     }
614
615     # set module stuff
616     if ($ref->{module} eq 'oe') {
617       # für oe gibt es vier fälle, jeweils nach kunde oder lieferant unterschiedlich:
618       #
619       # | ist bestellt  | Von Kunden bestellt |  -> edit_oe_ord_link
620       # | Anfrage       | Angebot             |  -> edit_oe_quo_link
621
622       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');
623       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');
624
625       $row->{ordnumber}{link} = $edit_oe_ord_link;
626       $row->{quonumber}{link} = $edit_oe_quo_link if (!$ref->{ordnumber});
627
628     } else {
629       $row->{invnumber}{link} = build_std_url("script=$ref->{module}.pl", 'action=edit', 'type=invoice', 'id=' . E($ref->{trans_id}), 'callback') if ($ref->{invnumber});
630     }
631
632     # set properties of images
633     if ($ref->{image} && (lc $report->{options}->{output_format} eq 'html')) {
634       $row->{image}{data}     = '';
635       $row->{image}{raw_data} = '<a href="' . H($ref->{image}) . '"><img src="' . H($ref->{image}) . '" height="32" border="0"></a>';
636     }
637     map { $row->{$_}{link} = $ref->{$_} } qw(drawing microfiche);
638
639     $row->{notes}{data} = SL::HTML::Util->strip($ref->{notes});
640     $row->{type_and_classific}{data} = $::request->presenter->type_abbreviation($ref->{part_type}).
641                                        $::request->presenter->classification_abbreviation($ref->{classification_id});
642
643     $report->add_data($row);
644
645     my $next_ref = $form->{parts}[$idx + 1];
646
647     # insert subtotal rows
648     if (($form->{l_subtotal} eq 'Y') &&
649         (!$next_ref ||
650          (!$next_ref->{assemblyitem} && ($same_item ne $next_ref->{ $form->{sort} })))) {
651       my $row = { map { $_ => { 'class' => 'listsubtotal', } } @columns };
652
653       if (($form->{searchitems} ne 'assembly') || !$form->{bom}) {
654         $row->{soldtotal}->{data} = $form->format_amount(\%myconfig, $subtotals{soldtotal});
655       }
656
657       map { $row->{"linetotal$_"}->{data} = $form->format_amount(\%myconfig, $subtotals{$_}, 2) } @subtotal_columns;
658       map { $subtotals{$_} = 0 } ('soldtotal', @subtotal_columns);
659
660       $report->add_data($row);
661
662       $same_item = $next_ref->{ $form->{sort} };
663     }
664
665     $idx++;
666   }
667
668   if ($form->{"l_linetotal"} && !$form->{report_generator_csv_options_for_import}) {
669     my $row = { map { $_ => { 'class' => 'listtotal', } } @columns };
670
671     map { $row->{"linetotal$_"}->{data} = $form->format_amount(\%myconfig, $totals{$_}, 2) } @subtotal_columns;
672
673     $report->add_separator();
674     $report->add_data($row);
675   }
676
677   $report->generate_with_headers();
678
679   $lxdebug->leave_sub();
680 }    #end generate_report
681
682 sub ajax_autocomplete {
683   $main::lxdebug->enter_sub();
684
685   my $form     = $main::form;
686   my %myconfig = %main::myconfig;
687
688   $form->{column}          = 'description'     unless $form->{column} =~ /^partnumber|description$/;
689   $form->{$form->{column}} = $form->{q}           || '';
690   $form->{limit}           = ($form->{limit} * 1) || 10;
691   $form->{searchitems}   ||= '';
692
693   my @results = IC->all_parts(\%myconfig, $form);
694
695   print $form->ajax_response_header(),
696         $form->parse_html_template('ic/ajax_autocomplete');
697
698   $main::lxdebug->leave_sub();
699 }
700
701 sub back_to_record {
702   _check_io_auth();
703
704
705   delete @{$::form}{qw(action action_add action_back_to_record back_sub description item notes partnumber sellprice taxaccount2 unit vc)};
706
707   $::auth->restore_form_from_session($::form->{previousform}, clobber => 1);
708   $::form->{rowcount}--;
709   $::form->{action}   = 'display_form';
710   $::form->{callback} = $::form->{script} . '?' . join('&', map { $::form->escape($_) . '=' . $::form->escape($::form->{$_}) } sort keys %{ $::form });
711   $::form->redirect;
712 }
713
714 sub continue { call_sub($form->{"nextsub"}); }
715
716 sub dispatcher {
717   my $action = first { $::form->{"action_${_}"} } qw(add back_to_record);
718   $::form->error($::locale->text('No action defined.')) unless $action;
719
720   $::form->{dispatched_action} = $action;
721   call_sub($action);
722 }