f3e7fe5d43f4c5d20d9cfdc25df944b5c03697a8
[kivitendo-erp.git] / 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::Presenter::Part;
45 use SL::ReportGenerator;
46
47 #use SL::PE;
48
49 use strict;
50 #use warnings;
51
52 # global imports
53 our ($form, $locale, %myconfig, $lxdebug, $auth);
54
55 require "bin/mozilla/io.pl";
56 require "bin/mozilla/common.pl";
57 require "bin/mozilla/reportgenerator.pl";
58
59 1;
60
61 # Parserhappy(R):
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');
76
77 # end of main
78
79 sub search {
80   $lxdebug->enter_sub();
81
82   $auth->assert('part_service_assembly_details');
83
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
87
88   $form->{title} = (ucfirst $form->{searchitems}) . "s";
89   $form->{title} =~ s/ys$/ies/;
90   $form->{title} = $locale->text($form->{title});
91
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');
97
98   setup_ic_search_action_bar();
99   $form->header;
100
101   $form->get_lists('partsgroup'    => 'ALL_PARTSGROUPS');
102   print $form->parse_html_template('ic/search');
103
104   $lxdebug->leave_sub();
105 }    #end search()
106
107 sub top100 {
108   $::lxdebug->enter_sub();
109
110   $::auth->assert('part_service_assembly_edit');
111
112   $::form->{l_soldtotal} = "Y";
113   $::form->{sort}        = "soldtotal";
114   $::form->{lastsort}    = "soldtotal";
115
116   $::form->{l_qty}       = undef;
117   $::form->{l_linetotal} = undef;
118   $::form->{l_number}    = "Y";
119   $::form->{number}      = "position";
120
121   unless (   $::form->{bought}
122           || $::form->{sold}
123           || $::form->{rfq}
124           || $::form->{quoted}) {
125     $::form->{bought} = $::form->{sold} = 1;
126   }
127
128   generate_report();
129
130   $lxdebug->leave_sub();
131 }
132
133 #
134 # Report for Wares.
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
137 #
138 # flags coming from the form:
139 # hardcoded:
140 #  searchitems=part revers=0 lastsort=''
141 #
142 # filter:
143 # partnumber ean description partsgroup classification serialnumber make model drawing microfiche
144 # transdatefrom transdateto
145 #
146 # radio:
147 #  itemstatus = active | onhand | short | obsolete | orphaned
148 #  action     = continue | top100
149 #
150 # checkboxes:
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
155 #
156 # hiddens:
157 #  nextsub revers lastsort sort ndxs_counter
158 #
159 sub generate_report {
160   $lxdebug->enter_sub();
161
162   $auth->assert('part_service_assembly_details');
163
164   my ($revers, $lastsort, $description);
165
166   my $cvar_configs = CVar->get_configs('module' => 'IC');
167
168   $form->{title} = $locale->text('Articles');
169
170   my %column_defs = (
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     'assembly_qty'       => { 'text' => $locale->text('Assembly Item Qty'), },
189     'ordnumber'          => { 'text' => $locale->text('Order Number'), },
190     'partnumber'         => { 'text' => $locale->text('Part Number'), },
191     'partsgroup'         => { 'text' => $locale->text('Partsgroup'), },
192     'priceupdate'        => { 'text' => $locale->text('Price updated'), },
193     'quonumber'          => { 'text' => $locale->text('Quotation'), },
194     'rop'                => { 'text' => $locale->text('ROP'), },
195     'sellprice'          => { 'text' => $locale->text('Sell Price'), },
196     'serialnumber'       => { 'text' => $locale->text('Serial Number'), },
197     'soldtotal'          => { 'text' => $locale->text('Qty in Selected Records'), },
198     'name'               => { 'text' => $locale->text('Name in Selected Records'), },
199     'transdate'          => { 'text' => $locale->text('Transdate Record'), },
200     'unit'               => { 'text' => $locale->text('Unit'), },
201     'weight'             => { 'text' => $locale->text('Weight'), },
202     'shop'               => { 'text' => $locale->text('Shop article'), },
203     'type_and_classific' => { 'text' => $locale->text('Type'), },
204     'projectnumber'      => { 'text' => $locale->text('Project Number'), },
205     'projectdescription' => { 'text' => $locale->text('Project Description'), },
206     'warehouse'          => { 'text' => $locale->text('Default Warehouse'), },
207     'bin'                => { 'text' => $locale->text('Default Bin'), },
208     'make'               => { 'text' => $locale->text('Make'), },
209     'model'              => { 'text' => $locale->text('Model'), },
210   );
211
212   $revers     = $form->{revers};
213   $lastsort   = $form->{lastsort};
214
215   # sorting and direction of sorting
216   # ToDO: change this to the simpler field+direction method
217   if (($form->{lastsort} eq "") && ($form->{sort} eq undef)) {
218     $form->{revers}   = 0;
219     $form->{lastsort} = "partnumber";
220     $form->{sort}     = "partnumber";
221   } else {
222     if ($form->{lastsort} eq $form->{sort}) {
223       $form->{revers} = 1 - $form->{revers};
224     } else {
225       $form->{revers} = 0;
226       $form->{lastsort} = $form->{sort};
227     }    #fi
228   }    #fi
229
230   # special case if we have a serialnumber limit search
231   # serialnumbers are only given in invoices and orders,
232   # so they can only pop up in bought, sold, rfq, and quoted stuff
233   $form->{no_sn_joins} = 'Y' if (   !$form->{bought} && !$form->{sold}
234                                  && !$form->{rfq}    && !$form->{quoted}
235                                  && ($form->{l_serialnumber} || $form->{serialnumber}));
236
237   # special case for any checkbox of bought | sold | onorder | ordered | rfq | quoted.
238   # if any of these are ticked the behavior changes slightly for lastcost
239   # since all those are aggregation checks for the legder tables this is an internal switch
240   # refered to as ledgerchecks
241   $form->{ledgerchecks} = 'Y' if (   $form->{bought} || $form->{sold} || $form->{onorder}
242                                   || $form->{ordered} || $form->{rfq} || $form->{quoted});
243
244   # if something should be activated if something else is active, enter it here
245   my %dependencies = (
246     onhand       => [ qw(l_onhand) ],
247     short        => [ qw(l_onhand) ],
248     onorder      => [ qw(l_ordnumber) ],
249     ordered      => [ qw(l_ordnumber) ],
250     rfq          => [ qw(l_quonumber) ],
251     quoted       => [ qw(l_quonumber) ],
252     bought       => [ qw(l_invnumber) ],
253     sold         => [ qw(l_invnumber) ],
254     ledgerchecks => [ qw(l_name) ],
255     serialnumber => [ qw(l_serialnumber) ],
256     no_sn_joins  => [ qw(bought sold) ],
257   );
258
259   # get name of partsgroup if id is given
260   my $pg_name;
261   if ($form->{partsgroup_id}) {
262     my $pg = SL::DB::PartsGroup->new(id => $form->{partsgroup_id})->load;
263     $pg_name = $pg->{'partsgroup'};
264   }
265
266   # these strings get displayed at the top of the results to indicate the user which switches were used
267   my %optiontexts = (
268     active        => $locale->text('Active'),
269     obsolete      => $locale->text('Obsolete'),
270     orphaned      => $locale->text('Orphaned'),
271     onhand        => $locale->text('On Hand'),
272     assembly_qty  => $locale->text('Assembly Item Qty'),
273     short         => $locale->text('Short'),
274     onorder       => $locale->text('On Order'),
275     ordered       => $locale->text('Ordered'),
276     rfq           => $locale->text('RFQ'),
277     quoted        => $locale->text('Quoted'),
278     bought        => $locale->text('Bought'),
279     sold          => $locale->text('Sold'),
280     transdatefrom => $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{transdatefrom}, 1),
281     transdateto   => $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{transdateto}, 1),
282     partnumber    => $locale->text('Part Number')      . ": '$form->{partnumber}'",
283     partsgroup    => $locale->text('Partsgroup')       . ": '$form->{partsgroup}'",
284     partsgroup_id => $locale->text('Partsgroup')       . ": '$pg_name'",
285     serialnumber  => $locale->text('Serial Number')    . ": '$form->{serialnumber}'",
286     description   => $locale->text('Part Description') . ": '$form->{description}'",
287     make          => $locale->text('Make')             . ": '$form->{make}'",
288     model         => $locale->text('Model')            . ": '$form->{model}'",
289     customername  => $locale->text('Customer')         . ": '$form->{customername}'",
290     customernumber=> $locale->text('Customer Part Number').": '$form->{customernumber}'",
291     drawing       => $locale->text('Drawing')          . ": '$form->{drawing}'",
292     microfiche    => $locale->text('Microfiche')       . ": '$form->{microfiche}'",
293     l_soldtotal   => $locale->text('Qty in Selected Records'),
294     ean           => $locale->text('EAN')              . ": '$form->{ean}'",
295     insertdatefrom => $locale->text('Insert Date') . ": " . $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{insertdatefrom}, 1),
296     insertdateto   => $locale->text('Insert Date') . ": " . $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{insertdateto}, 1),
297     l_service     => $locale->text('Services'),
298     l_assembly    => $locale->text('Assemblies'),
299     l_part        => $locale->text('Parts'),
300   );
301
302   my @itemstatus_keys = qw(active obsolete orphaned onhand short);
303   my @callback_keys   = qw(onorder ordered rfq quoted bought sold partnumber partsgroup partsgroup_id serialnumber description make model
304                            drawing microfiche l_soldtotal l_deliverydate transdatefrom transdateto insertdatefrom insertdateto ean shop all
305                            l_service l_assembly l_part);
306
307   # calculate dependencies
308   for (@itemstatus_keys, @callback_keys) {
309     next if ($form->{itemstatus} ne $_ && !$form->{$_});
310     map { $form->{$_} = 'Y' } @{ $dependencies{$_} } if $dependencies{$_};
311   }
312
313   # generate callback and optionstrings
314   my @options;
315   for my  $key (@itemstatus_keys, @callback_keys) {
316     next if ($form->{itemstatus} ne $key && !$form->{$key});
317     push @options, $optiontexts{$key};
318   }
319
320   # special case for lastcost
321   if ($form->{ledgerchecks}){
322     # ledgerchecks don't know about sellprice or lastcost. they just return a
323     # price. so rename sellprice to price, and drop lastcost.
324     $column_defs{sellprice}{text} = $locale->text('Price');
325     $form->{l_lastcost} = ""
326   }
327   $form->{l_assembly_lastcost} = "Y" if $form->{l_assembly} && $form->{l_lastcost};
328
329   if ($form->{description}) {
330     $description = $form->{description};
331     $description =~ s/\n/<br>/g;
332   }
333
334   if ($form->{l_linetotal}) {
335     $form->{l_qty} = "Y";
336     $form->{l_linetotalsellprice} = "Y" if $form->{l_sellprice};
337     $form->{l_linetotallastcost}  = $form->{searchitems} eq 'assembly' && !$form->{bom} ? "" : 'Y' if  $form->{l_lastcost};
338     $form->{l_linetotallistprice} = "Y" if $form->{l_listprice};
339   }
340   $form->{"l_type_and_classific"} = "Y";
341
342   if ($form->{l_service} && !$form->{l_assembly} && !$form->{l_part}) {
343
344     # remove warehouse, bin, weight and rop from list
345     map { $form->{"l_$_"} = "" } qw(bin weight rop warehouse);
346
347     $form->{l_onhand} = "";
348
349     # qty is irrelevant unless bought or sold
350     if (   $form->{bought}
351         || $form->{sold}
352         || $form->{onorder}
353         || $form->{ordered}
354         || $form->{rfq}
355         || $form->{quoted}) {
356 #      $form->{l_onhand} = "Y";
357     } else {
358       $form->{l_linetotalsellprice} = "";
359       $form->{l_linetotallastcost}  = "";
360     }
361   }
362
363   # soldtotal doesn't make sense with more than one bsooqr option.
364   # so reset it to sold (the most common option), and issue a warning
365   # ...
366   # also it doesn't make sense without bsooqr. disable and issue a warning too
367   my @bsooqr = qw(sold bought onorder ordered rfq quoted);
368   my $bsooqr_mode = grep { $form->{$_} } @bsooqr;
369   if ($form->{l_subtotal} && 1 < $bsooqr_mode) {
370     my $enabled       = first { $form->{$_} } @bsooqr;
371     $form->{$_}       = ''   for @bsooqr;
372     $form->{$enabled} = 'Y';
373
374     push @options, $::locale->text('Subtotal cannot distinguish betweens record types. Only one of the selected record types will be displayed: #1', $optiontexts{$enabled});
375   }
376   if ($form->{l_soldtotal} && !$bsooqr_mode) {
377     delete $form->{l_soldtotal};
378
379     flash('warning', $::locale->text('Soldtotal does not make sense without any bsooqr options'));
380   }
381   if ($form->{l_soldtotal} && ($form->{l_warehouse} || $form->{l_bin})) {
382     delete $form->{"l_$_"} for  qw(bin warehouse);
383     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.'));
384   }
385   if ($form->{l_name} && !$bsooqr_mode) {
386     delete $form->{l_name};
387
388     flash('warning', $::locale->text('Name does not make sense without any bsooqr options'));
389   }
390   IC->all_parts(\%myconfig, \%$form);
391
392   my @columns = qw(
393     partnumber type_and_classific description notes partsgroup warehouse bin
394     make model assembly_qty onhand rop soldtotal unit listprice
395     linetotallistprice sellprice linetotalsellprice lastcost assembly_lastcost linetotallastcost
396     priceupdate weight image drawing microfiche invnumber ordnumber quonumber
397     transdate name serialnumber deliverydate ean projectnumber projectdescription
398     insertdate shop
399   );
400
401   my $pricegroups = SL::DB::Manager::Pricegroup->get_all_sorted;
402   my @pricegroup_columns;
403   my %column_defs_pricegroups;
404   if ($form->{l_pricegroups}) {
405     @pricegroup_columns      = map { "pricegroup_" . $_->id } @{ $pricegroups };
406     %column_defs_pricegroups = map {
407       "pricegroup_" . $_->id => {
408         text    => $::locale->text('Pricegroup') . ' ' . $_->pricegroup,
409         visible => 1,
410       },
411     }  @{ $pricegroups };
412   }
413   push @columns, @pricegroup_columns;
414
415   my @includeable_custom_variables = grep { $_->{includeable} } @{ $cvar_configs };
416   my @searchable_custom_variables  = grep { $_->{searchable} }  @{ $cvar_configs };
417   my %column_defs_cvars            = map { +"cvar_$_->{name}" => { 'text' => $_->{description} } } @includeable_custom_variables;
418
419   push @columns, map { "cvar_$_->{name}" } @includeable_custom_variables;
420
421   %column_defs = (%column_defs, %column_defs_cvars, %column_defs_pricegroups);
422   map { $column_defs{$_}->{visible} ||= $form->{"l_$_"} ? 1 : 0 } @columns;
423   map { $column_defs{$_}->{align}   = 'right' } qw(assembly_qty onhand sellprice listprice lastcost assembly_lastcost linetotalsellprice linetotallastcost linetotallistprice rop weight soldtotal shop), @pricegroup_columns;
424
425   my @hidden_variables = (
426     qw(l_subtotal l_linetotal searchitems itemstatus bom l_pricegroups insertdatefrom insertdateto),
427     qw(l_type_and_classific classification_id l_part l_service l_assembly l_assortment),
428     @itemstatus_keys,
429     @callback_keys,
430     map({ "cvar_$_->{name}" } @searchable_custom_variables),
431     map({'cvar_'. $_->{name} .'_from'} grep({$_->{type} eq 'date'} @searchable_custom_variables)),
432     map({'cvar_'. $_->{name} .'_to'}   grep({$_->{type} eq 'date'} @searchable_custom_variables)),
433     map({'cvar_'. $_->{name} .'_qtyop'} grep({$_->{type} eq 'number'} @searchable_custom_variables)),
434     map({ "l_$_" } @columns),
435   );
436
437   my $callback         = build_std_url('action=generate_report', grep { $form->{$_} } @hidden_variables);
438
439   my @sort_full        = qw(partnumber description onhand soldtotal deliverydate insertdate shop);
440   my @sort_no_revers   = qw(partsgroup invnumber ordnumber quonumber name image drawing serialnumber);
441
442   foreach my $col (@sort_full) {
443     $column_defs{$col}->{link} = join '&', $callback, "sort=$col", map { "$_=" . E($form->{$_}) } qw(revers lastsort);
444   }
445   map { $column_defs{$_}->{link} = "${callback}&sort=$_" } @sort_no_revers;
446
447   # add order to callback
448   $form->{callback} = join '&', ($callback, map { "${_}=" . E($form->{$_}) } qw(sort revers));
449
450   my $report = SL::ReportGenerator->new(\%myconfig, $form);
451
452   my %attachment_basenames = (
453     'part'     => $locale->text('part_list'),
454     'service'  => $locale->text('service_list'),
455     'assembly' => $locale->text('assembly_list'),
456     'article'  => $locale->text('article_list'),
457   );
458
459   $report->set_options('raw_top_info_text'     => $form->parse_html_template('ic/generate_report_top', { options => \@options }),
460                        'raw_bottom_info_text'  => $form->parse_html_template('ic/generate_report_bottom' ,
461                                                   { PART_CLASSIFICATIONS => SL::DB::Manager::PartClassification->get_all_sorted }),
462                        'output_format'         => 'HTML',
463                        'title'                 => $form->{title},
464                        'attachment_basename'   => 'article_list' . strftime('_%Y%m%d', localtime time),
465   );
466   $report->set_options_from_form();
467   $locale->set_numberformat_wo_thousands_separator(\%myconfig) if lc($report->{options}->{output_format}) eq 'csv';
468
469   $report->set_columns(%column_defs);
470   $report->set_column_order(@columns);
471
472   $report->set_export_options('generate_report', @hidden_variables, qw(sort revers));
473
474   $report->set_sort_indicator($form->{sort}, $form->{revers} ? 0 : 1);
475
476   CVar->add_custom_variables_to_report('module'         => 'IC',
477                                        'trans_id_field' => 'id',
478                                        'configs'        => $cvar_configs,
479                                        'column_defs'    => \%column_defs,
480                                        'data'           => $form->{parts});
481
482   CVar->add_custom_variables_to_report('module'         => 'IC',
483                                        'sub_module'     => sub { $_[0]->{ioi} },
484                                        'trans_id_field' => 'ioi_id',
485                                        'configs'        => $cvar_configs,
486                                        'column_defs'    => \%column_defs,
487                                        'data'           => $form->{parts});
488
489   my @subtotal_columns = qw(sellprice listprice lastcost);
490   my %subtotals = map { $_ => 0 } ('onhand', @subtotal_columns);
491   my %totals    = map { $_ => 0 } @subtotal_columns;
492   my $idx       = 0;
493   my $same_item = @{ $form->{parts} } ? $form->{parts}[0]{ $form->{sort} } : undef;
494
495   my $defaults  = AM->get_defaults();
496
497   # postprocess parts
498   foreach my $ref (@{ $form->{parts} }) {
499
500     # fresh row, for inserting later
501     my $row = { map { $_ => { 'data' => $ref->{$_} } } @columns };
502
503     $ref->{exchangerate} ||= 1;
504     $ref->{price_factor} ||= 1;
505     $ref->{sellprice}     *= $ref->{exchangerate} / $ref->{price_factor};
506     $ref->{listprice}     *= $ref->{exchangerate} / $ref->{price_factor};
507     $ref->{lastcost}      *= $ref->{exchangerate} / $ref->{price_factor};
508     $ref->{assembly_lastcost} *= $ref->{exchangerate} / $ref->{price_factor};
509
510     # use this for assemblies
511     my $soldtotal = $bsooqr_mode ? $ref->{soldtotal} : $ref->{onhand};
512
513     if ($ref->{assemblyitem}) {
514       $row->{partnumber}{align}   = 'right';
515       $row->{soldtotal}{data}     = 0;
516       $soldtotal                  = 0 if ($form->{sold});
517     }
518
519     my $edit_link               = build_std_url('script=controller.pl', 'action=Part/edit', 'part.id=' . E($ref->{id}));
520     $row->{partnumber}->{link}  = $edit_link;
521     $row->{description}->{link} = $edit_link;
522
523     foreach (qw(sellprice listprice lastcost assembly_lastcost)) {
524       $row->{$_}{data}            = $form->format_amount(\%myconfig, $ref->{$_}, 2);
525       $row->{"linetotal$_"}{data} = $form->format_amount(\%myconfig, $ref->{onhand} * $ref->{$_}, 2);
526     }
527     foreach ( @pricegroup_columns ) {
528       $row->{$_}{data}            = $form->format_amount(\%myconfig, $ref->{"$_"}, 2);
529     };
530
531
532     map { $row->{$_}{data} = $form->format_amount(\%myconfig, $ref->{$_}); } qw(onhand rop weight soldtotal);
533
534     $row->{weight}->{data} .= ' ' . $defaults->{weightunit};
535
536     # 'yes' and 'no' for boolean value shop
537     if ($form->{l_shop}) {
538       $row->{shop}{data} = $row->{shop}{data}? $::locale->text('yes') : $::locale->text('no');
539     }
540
541     if (!$ref->{assemblyitem}) {
542       foreach my $col (@subtotal_columns) {
543         $totals{$col}    += $soldtotal * $ref->{$col};
544         $subtotals{$col} += $soldtotal * $ref->{$col};
545       }
546
547       $subtotals{soldtotal} += $soldtotal;
548     }
549
550     # set module stuff
551     if ($ref->{module} eq 'oe') {
552       # für oe gibt es vier fälle, jeweils nach kunde oder lieferant unterschiedlich:
553       #
554       # | ist bestellt  | Von Kunden bestellt |  -> edit_oe_ord_link
555       # | Anfrage       | Angebot             |  -> edit_oe_quo_link
556
557       my $edit_oe_ord_link = ($::instance_conf->get_feature_experimental_order)
558                            ? build_std_url("script=controller.pl", 'action=Order/edit',
559                                            'type=' . E($ref->{cv} eq 'vendor' ? 'purchase_order' : 'sales_order'),        'id=' . E($ref->{trans_id}), 'callback')
560                            : build_std_url("script=oe.pl",         'action=edit',
561                                            'type=' . E($ref->{cv} eq 'vendor' ? 'purchase_order' : 'sales_order'),        'id=' . E($ref->{trans_id}), 'callback');
562
563       my $edit_oe_quo_link = ($::instance_conf->get_feature_experimental_order)
564                            ? build_std_url("script=controller.pl", 'action=Order/edit',
565                                            'type=' . E($ref->{cv} eq 'vendor' ? 'request_quotation' : 'sales_quotation'), 'id=' . E($ref->{trans_id}), 'callback')
566                            : build_std_url("script=oe.pl",         'action=edit',
567                                            'type=' . E($ref->{cv} eq 'vendor' ? 'request_quotation' : 'sales_quotation'), 'id=' . E($ref->{trans_id}), 'callback');
568
569       $row->{ordnumber}{link} = $edit_oe_ord_link;
570       $row->{quonumber}{link} = $edit_oe_quo_link if (!$ref->{ordnumber});
571
572     } else {
573       $row->{invnumber}{link} = build_std_url("script=$ref->{module}.pl", 'action=edit', 'type=invoice', 'id=' . E($ref->{trans_id}), 'callback') if ($ref->{invnumber});
574     }
575
576     # set properties of images
577     if ($ref->{image} && (lc $report->{options}->{output_format} eq 'html')) {
578       $row->{image}{data}     = '';
579       $row->{image}{raw_data} = '<a href="' . H($ref->{image}) . '"><img src="' . H($ref->{image}) . '" height="32" border="0"></a>';
580     }
581     map { $row->{$_}{link} = $ref->{$_} } qw(drawing microfiche);
582
583     $row->{notes}{data} = SL::HTML::Util->strip($ref->{notes});
584     $row->{type_and_classific}{data} = SL::Presenter::Part::type_abbreviation($ref->{part_type}).
585                                        SL::Presenter::Part::classification_abbreviation($ref->{classification_id});
586
587     # last price update
588     $row->{priceupdate}{data} = SL::DB::Part->new(id => $ref->{id})->load->last_price_update->valid_from->to_kivitendo;
589
590     $report->add_data($row);
591
592     my $next_ref = $form->{parts}[$idx + 1];
593
594     # insert subtotal rows
595     if (($form->{l_subtotal} eq 'Y') &&
596         (!$next_ref ||
597          (!$next_ref->{assemblyitem} && ($same_item ne $next_ref->{ $form->{sort} })))) {
598       my $row = { map { $_ => { 'class' => 'listsubtotal', } } @columns };
599
600       if ( !$form->{l_assembly} || !$form->{bom}) {
601         $row->{soldtotal}->{data} = $form->format_amount(\%myconfig, $subtotals{soldtotal});
602       }
603
604       map { $row->{"linetotal$_"}->{data} = $form->format_amount(\%myconfig, $subtotals{$_}, 2) } @subtotal_columns;
605       map { $subtotals{$_} = 0 } ('soldtotal', @subtotal_columns);
606
607       $report->add_data($row);
608
609       $same_item = $next_ref->{ $form->{sort} };
610     }
611
612     $idx++;
613   }
614
615   if ($form->{"l_linetotal"} && !$form->{report_generator_csv_options_for_import}) {
616     my $row = { map { $_ => { 'class' => 'listtotal', } } @columns };
617
618     map { $row->{"linetotal$_"}->{data} = $form->format_amount(\%myconfig, $totals{$_}, 2) } @subtotal_columns;
619
620     $report->add_separator();
621     $report->add_data($row);
622   }
623
624   setup_ic_generate_report_action_bar();
625   $report->generate_with_headers();
626
627   $lxdebug->leave_sub();
628 }    #end generate_report
629
630 sub setup_ic_search_action_bar {
631   my %params = @_;
632
633   for my $bar ($::request->layout->get('actionbar')) {
634     $bar->add(
635       action => [
636         t8('Search'),
637         submit    => [ '#form', { action => 'generate_report' } ],
638         accesskey => 'enter',
639       ],
640
641       action => [
642         t8('TOP100'),
643         submit => [ '#form', { action => 'top100' } ],
644       ],
645     );
646   }
647 }
648
649 sub setup_ic_generate_report_action_bar {
650   my %params = @_;
651
652   for my $bar ($::request->layout->get('actionbar')) {
653     $bar->add(
654       combobox => [
655         action => [
656           t8('Add'),
657         ],
658         action => [
659           t8('Add Part'),
660           submit    => [ '#new_form', { action => 'Part/add_part' } ],
661           accesskey => 'enter',
662         ],
663         action => [
664           t8('Add Service'),
665           submit    => [ '#new_form', { action => 'Part/add_service' } ],
666         ],
667         action => [
668           t8('Add Assembly'),
669           submit    => [ '#new_form', { action => 'Part/add_assembly' } ],
670         ],
671         action => [
672           t8('Add Assortment'),
673           submit    => [ '#new_form', { action => 'Part/add_assortment' } ],
674         ],
675       ], # end of combobox "Add part"
676     );
677   }
678 }