is: top100 gegrillt. standardfunktion kann das genauso.
[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., 675 Mass Ave, Cambridge, MA 02139, USA.
28 #======================================================================
29 #
30 # Inventory Control module
31 #
32 #======================================================================
33
34 use POSIX qw(strftime);
35 use List::Util qw(first max);
36 use List::MoreUtils qw(any);
37
38 use SL::AM;
39 use SL::CVar;
40 use SL::IC;
41 use SL::Helper::Flash qw(flash);
42 use SL::HTML::Util;
43 use SL::ReportGenerator;
44
45 #use SL::PE;
46
47 use strict;
48 #use warnings;
49
50 # global imports
51 our ($form, $locale, %myconfig, $lxdebug, $auth);
52
53 require "bin/mozilla/io.pl";
54 require "bin/mozilla/common.pl";
55 require "bin/mozilla/reportgenerator.pl";
56
57 1;
58
59 # Parserhappy(R):
60 # type=submit $locale->text('Add Part')
61 # type=submit $locale->text('Add Service')
62 # type=submit $locale->text('Add Assembly')
63 # type=submit $locale->text('Edit Part')
64 # type=submit $locale->text('Edit Service')
65 # type=submit $locale->text('Edit Assembly')
66 # $locale->text('Parts')
67 # $locale->text('Services')
68 # $locale->text('Inventory quantity must be zero before you can set this part obsolete!')
69 # $locale->text('Inventory quantity must be zero before you can set this assembly obsolete!')
70 # $locale->text('Part Number missing!')
71 # $locale->text('Service Number missing!')
72 # $locale->text('Assembly Number missing!')
73 # $locale->text('ea');
74
75 # end of main
76
77 sub add {
78   $lxdebug->enter_sub();
79
80   $auth->assert('part_service_assembly_edit');
81
82   my $title                = 'Add ' . ucfirst $form->{item};
83   $form->{title}           = $locale->text($title);
84   $form->{callback}        = "$form->{script}?action=add&item=$form->{item}" unless $form->{callback};
85   $form->{unit_changeable} = 1;
86
87   IC->get_pricegroups(\%myconfig, \%$form);
88   &link_part;
89   &display_form;
90
91   $lxdebug->leave_sub();
92 }
93
94 sub search {
95   $lxdebug->enter_sub();
96
97   $auth->assert('part_service_assembly_details');
98
99   $form->{revers}       = 0;  # switch for backward sorting
100   $form->{lastsort}     = ""; # memory for which table was sort at last time
101   $form->{ndxs_counter} = 0;  # counter for added entries to top100
102
103   my %is_xyz     = map { +"is_$_" => ($form->{searchitems} eq $_) } qw(part service assembly);
104
105   $form->{title} = (ucfirst $form->{searchitems}) . "s";
106   $form->{title} = $locale->text($form->{title});
107   $form->{title} = $locale->text('Assemblies') if ($is_xyz{is_assembly});
108
109   $form->{CUSTOM_VARIABLES}                  = CVar->get_configs('module' => 'IC');
110   ($form->{CUSTOM_VARIABLES_FILTER_CODE},
111    $form->{CUSTOM_VARIABLES_INCLUSION_CODE}) = CVar->render_search_options('variables'      => $form->{CUSTOM_VARIABLES},
112                                                                            'include_prefix' => 'l_',
113                                                                            'include_value'  => 'Y');
114
115   $form->header;
116
117   $form->get_lists('partsgroup'    => 'ALL_PARTSGROUPS');
118   print $form->parse_html_template('ic/search', { %is_xyz, });
119
120   $lxdebug->leave_sub();
121 }    #end search()
122
123 sub search_update_prices {
124   $lxdebug->enter_sub();
125
126   $auth->assert('part_service_assembly_edit');
127
128   my $pricegroups = IC->get_pricegroups(\%myconfig, \%$form);
129
130   $form->{title} = $locale->text('Update Prices');
131
132   $form->header;
133
134   print $form->parse_html_template('ic/search_update_prices', { PRICE_ROWS => $pricegroups });
135
136   $lxdebug->leave_sub();
137 }    #end search()
138
139 sub confirm_price_update {
140   $lxdebug->enter_sub();
141
142   $auth->assert('part_service_assembly_edit');
143
144   my @errors      = ();
145   my $value_found = undef;
146
147   foreach my $idx (qw(sellprice listprice), (1..$form->{price_rows})) {
148     my $name      = $idx =~ m/\d/ ? $form->{"pricegroup_${idx}"}      : $idx eq 'sellprice' ? $locale->text('Sell Price') : $locale->text('List Price');
149     my $type      = $idx =~ m/\d/ ? $form->{"pricegroup_type_${idx}"} : $form->{"${idx}_type"};
150     my $value_idx = $idx =~ m/\d/ ? "price_${idx}" : $idx;
151     my $value     = $form->parse_amount(\%myconfig, $form->{$value_idx});
152
153     if ((0 > $value) && ($type eq 'percent')) {
154       push @errors, $locale->text('You cannot adjust the price for pricegroup "#1" by a negative percentage.', $name);
155
156     } elsif (!$value && ($form->{$value_idx} ne '')) {
157       push @errors, $locale->text('No valid number entered for pricegroup "#1".', $name);
158
159     } elsif (0 < $value) {
160       $value_found = 1;
161     }
162   }
163
164   push @errors, $locale->text('No prices will be updated because no prices have been entered.') if (!$value_found);
165
166   my $num_matches = IC->get_num_matches_for_priceupdate();
167
168   $form->header();
169
170   if (@errors) {
171     $form->show_generic_error(join('<br>', @errors), 'back_button' => 1);
172   }
173
174   $form->{nextsub} = "update_prices";
175
176   map { delete $form->{$_} } qw(action header);
177
178   print $form->parse_html_template('ic/confirm_price_update', { HIDDENS     => [ map { name => $_, value => $form->{$_} }, keys %$form ],
179                                                                 num_matches => $num_matches });
180
181   $lxdebug->leave_sub();
182 }
183
184 sub update_prices {
185   $lxdebug->enter_sub();
186
187   $auth->assert('part_service_assembly_edit');
188
189   my $num_updated = IC->update_prices(\%myconfig, \%$form);
190
191   if (-1 != $num_updated) {
192     $form->redirect($locale->text('#1 prices were updated.', $num_updated));
193   } else {
194     $form->error($locale->text('Could not update prices!'));
195   }
196
197   $lxdebug->leave_sub();
198 }
199
200 sub top100 {
201   $::lxdebug->enter_sub();
202
203   $::auth->assert('part_service_assembly_edit');
204
205   $::form->{l_soldtotal} = "Y";
206   $::form->{sort}        = "soldtotal";
207   $::form->{lastsort}    = "soldtotal";
208
209   $::form->{l_qty}       = undef;
210   $::form->{l_linetotal} = undef;
211   $::form->{l_number}    = "Y";
212   $::form->{number}      = "position";
213
214   unless (   $::form->{bought}
215           || $::form->{sold}
216           || $::form->{rfq}
217           || $::form->{quoted}) {
218     $::form->{bought} = $::form->{sold} = 1;
219   }
220
221   generate_report();
222
223   $lxdebug->leave_sub();
224 }
225
226 #
227 # Report for Wares.
228 # Warning, deep magic ahead.
229 # This function parses the requested details, sanity checks them, and converts them into a format thats usable for IC->all_parts
230 #
231 # flags coming from the form:
232 # hardcoded:
233 #  searchitems=part revers=0 lastsort=''
234 #
235 # filter:
236 # partnumber ean description partsgroup serialnumber make model drawing microfiche
237 # transdatefrom transdateto
238 #
239 # radio:
240 #  itemstatus = active | onhand | short | obsolete | orphaned
241 #  action     = continue | top100
242 #
243 # checkboxes:
244 #  bought sold onorder ordered rfq quoted
245 #  l_partnumber l_description l_serialnumber l_unit l_listprice l_sellprice l_lastcost
246 #  l_linetotal l_priceupdate l_bin l_rop l_weight l_image l_drawing l_microfiche
247 #  l_partsgroup l_subtotal l_soldtotal l_deliverydate l_pricegroups
248 #
249 # hiddens:
250 #  nextsub revers lastsort sort ndxs_counter
251 #
252 sub generate_report {
253   $lxdebug->enter_sub();
254
255   $auth->assert('part_service_assembly_details');
256
257   my ($revers, $lastsort, $description);
258
259   my $cvar_configs = CVar->get_configs('module' => 'IC');
260
261   $form->{title} = (ucfirst $form->{searchitems}) . "s";
262   $form->{title} =~ s/ys$/ies/;
263   $form->{title} = $locale->text($form->{title});
264
265   my %column_defs = (
266     'bin'                => { 'text' => $locale->text('Bin'), },
267     'deliverydate'       => { 'text' => $locale->text('deliverydate'), },
268     'description'        => { 'text' => $locale->text('Part Description'), },
269     'notes'              => { 'text' => $locale->text('Notes'), },
270     'drawing'            => { 'text' => $locale->text('Drawing'), },
271     'ean'                => { 'text' => $locale->text('EAN'), },
272     'image'              => { 'text' => $locale->text('Image'), },
273     'insertdate'         => { 'text' => $locale->text('Insert Date'), },
274     'invnumber'          => { 'text' => $locale->text('Invoice Number'), },
275     'lastcost'           => { 'text' => $locale->text('Last Cost'), },
276     'linetotallastcost'  => { 'text' => $locale->text('Extended'), },
277     'linetotallistprice' => { 'text' => $locale->text('Extended'), },
278     'linetotalsellprice' => { 'text' => $locale->text('Extended'), },
279     'listprice'          => { 'text' => $locale->text('List Price'), },
280     'microfiche'         => { 'text' => $locale->text('Microfiche'), },
281     'name'               => { 'text' => $locale->text('Name'), },
282     'onhand'             => { 'text' => $locale->text('Stocked Qty'), },
283     'ordnumber'          => { 'text' => $locale->text('Order Number'), },
284     'partnumber'         => { 'text' => $locale->text('Part Number'), },
285     'partsgroup'         => { 'text' => $locale->text('Group'), },
286     'priceupdate'        => { 'text' => $locale->text('Updated'), },
287     'quonumber'          => { 'text' => $locale->text('Quotation'), },
288     'rop'                => { 'text' => $locale->text('ROP'), },
289     'sellprice'          => { 'text' => $locale->text('Sell Price'), },
290     'serialnumber'       => { 'text' => $locale->text('Serial Number'), },
291     'soldtotal'          => { 'text' => $locale->text('Qty in Selected Records'), },
292     'name'               => { 'text' => $locale->text('Name in Selected Records'), },
293     'transdate'          => { 'text' => $locale->text('Transdate'), },
294     'unit'               => { 'text' => $locale->text('Unit'), },
295     'weight'             => { 'text' => $locale->text('Weight'), },
296     'shop'               => { 'text' => $locale->text('Shop article'), },
297     'projectnumber'      => { 'text' => $locale->text('Project Number'), },
298     'projectdescription' => { 'text' => $locale->text('Project Description'), },
299   );
300
301   $revers     = $form->{revers};
302   $lastsort   = $form->{lastsort};
303
304   # sorting and direction of sorting
305   # ToDO: change this to the simpler field+direction method
306   if (($form->{lastsort} eq "") && ($form->{sort} eq undef)) {
307     $form->{revers}   = 0;
308     $form->{lastsort} = "partnumber";
309     $form->{sort}     = "partnumber";
310   } else {
311     if ($form->{lastsort} eq $form->{sort}) {
312       $form->{revers} = 1 - $form->{revers};
313     } else {
314       $form->{revers} = 0;
315       $form->{lastsort} = $form->{sort};
316     }    #fi
317   }    #fi
318
319   # special case if we have a serialnumber limit search
320   # serialnumbers are only given in invoices and orders,
321   # so they can only pop up in bought, sold, rfq, and quoted stuff
322   $form->{no_sn_joins} = 'Y' if (   !$form->{bought} && !$form->{sold}
323                                  && !$form->{rfq}    && !$form->{quoted}
324                                  && ($form->{l_serialnumber} || $form->{serialnumber}));
325
326   # special case for any checkbox of bought | sold | onorder | ordered | rfq | quoted.
327   # if any of these are ticked the behavior changes slightly for lastcost
328   # since all those are aggregation checks for the legder tables this is an internal switch
329   # refered to as ledgerchecks
330   $form->{ledgerchecks} = 'Y' if (   $form->{bought} || $form->{sold} || $form->{onorder}
331                                   || $form->{ordered} || $form->{rfq} || $form->{quoted});
332
333   # if something should be activated if something else is active, enter it here
334   my %dependencies = (
335     onhand       => [ qw(l_onhand) ],
336     short        => [ qw(l_onhand) ],
337     onorder      => [ qw(l_ordnumber) ],
338     ordered      => [ qw(l_ordnumber) ],
339     rfq          => [ qw(l_quonumber) ],
340     quoted       => [ qw(l_quonumber) ],
341     bought       => [ qw(l_invnumber) ],
342     sold         => [ qw(l_invnumber) ],
343     ledgerchecks => [ qw(l_name) ],
344     serialnumber => [ qw(l_serialnumber) ],
345     no_sn_joins  => [ qw(bought sold) ],
346   );
347
348   # get name of partsgroup if id is given
349   my $pg_name;
350   if ($form->{partsgroup_id}) {
351     my $pg = SL::DB::PartsGroup->new(id => $form->{partsgroup_id})->load;
352     $pg_name = $pg->{'partsgroup'};
353   }
354
355   # these strings get displayed at the top of the results to indicate the user which switches were used
356   my %optiontexts = (
357     active        => $locale->text('Active'),
358     obsolete      => $locale->text('Obsolete'),
359     orphaned      => $locale->text('Orphaned'),
360     onhand        => $locale->text('On Hand'),
361     short         => $locale->text('Short'),
362     onorder       => $locale->text('On Order'),
363     ordered       => $locale->text('Ordered'),
364     rfq           => $locale->text('RFQ'),
365     quoted        => $locale->text('Quoted'),
366     bought        => $locale->text('Bought'),
367     sold          => $locale->text('Sold'),
368     transdatefrom => $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{transdatefrom}, 1),
369     transdateto   => $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{transdateto}, 1),
370     partnumber    => $locale->text('Part Number')      . ": '$form->{partnumber}'",
371     partsgroup    => $locale->text('Group')            . ": '$form->{partsgroup}'",
372     partsgroup_id => $locale->text('Group')            . ": '$pg_name'",
373     serialnumber  => $locale->text('Serial Number')    . ": '$form->{serialnumber}'",
374     description   => $locale->text('Part Description') . ": '$form->{description}'",
375     make          => $locale->text('Make')             . ": '$form->{make}'",
376     model         => $locale->text('Model')            . ": '$form->{model}'",
377     drawing       => $locale->text('Drawing')          . ": '$form->{drawing}'",
378     microfiche    => $locale->text('Microfiche')       . ": '$form->{microfiche}'",
379     l_soldtotal   => $locale->text('Qty in Selected Records'),
380     ean           => $locale->text('EAN')              . ": '$form->{ean}'",
381     insertdatefrom => $locale->text('Insert Date') . ": " . $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{insertdatefrom}, 1),
382     insertdateto   => $locale->text('Insert Date') . ": " . $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{insertdateto}, 1),
383   );
384
385   my @itemstatus_keys = qw(active obsolete orphaned onhand short);
386   my @callback_keys   = qw(onorder ordered rfq quoted bought sold partnumber partsgroup partsgroup_id serialnumber description make model
387                            drawing microfiche l_soldtotal l_deliverydate transdatefrom transdateto insertdatefrom insertdateto ean shop);
388
389   # calculate dependencies
390   for (@itemstatus_keys, @callback_keys) {
391     next if ($form->{itemstatus} ne $_ && !$form->{$_});
392     map { $form->{$_} = 'Y' } @{ $dependencies{$_} } if $dependencies{$_};
393   }
394
395   # generate callback and optionstrings
396   my @options;
397   for my  $key (@itemstatus_keys, @callback_keys) {
398     next if ($form->{itemstatus} ne $key && !$form->{$key});
399     push @options, $optiontexts{$key};
400   }
401
402   # special case for lastcost
403   if ($form->{ledgerchecks}){
404     # ledgerchecks don't know about sellprice or lastcost. they just return a
405     # price. so rename sellprice to price, and drop lastcost.
406     $column_defs{sellprice}{text} = $locale->text('Price');
407     $form->{l_lastcost} = ""
408   }
409
410   if ($form->{description}) {
411     $description = $form->{description};
412     $description =~ s/\n/<br>/g;
413   }
414
415   if ($form->{l_linetotal}) {
416     $form->{l_qty} = "Y";
417     $form->{l_linetotalsellprice} = "Y" if $form->{l_sellprice};
418     $form->{l_linetotallastcost}  = $form->{searchitems} eq 'assembly' && !$form->{bom} ? "" : 'Y' if  $form->{l_lastcost};
419     $form->{l_linetotallistprice} = "Y" if $form->{l_listprice};
420   }
421
422   if ($form->{searchitems} eq 'service') {
423
424     # remove bin, weight and rop from list
425     map { $form->{"l_$_"} = "" } qw(bin weight rop);
426
427     $form->{l_onhand} = "";
428
429     # qty is irrelevant unless bought or sold
430     if (   $form->{bought}
431         || $form->{sold}
432         || $form->{onorder}
433         || $form->{ordered}
434         || $form->{rfq}
435         || $form->{quoted}) {
436 #      $form->{l_onhand} = "Y";
437     } else {
438       $form->{l_linetotalsellprice} = "";
439       $form->{l_linetotallastcost}  = "";
440     }
441   }
442
443   # soldtotal doesn't make sense with more than one bsooqr option.
444   # so reset it to sold (the most common option), and issue a warning
445   # ...
446   # also it doesn't make sense without bsooqr. disable and issue a warning too
447   my @bsooqr = qw(sold bought onorder ordered rfq quoted);
448   my $bsooqr_mode = grep { $form->{$_} } @bsooqr;
449   if ($form->{l_subtotal} && 1 < $bsooqr_mode) {
450     my $enabled       = first { $form->{$_} } @bsooqr;
451     $form->{$_}       = ''   for @bsooqr;
452     $form->{$enabled} = 'Y';
453
454     push @options, $::locale->text('Subtotal cannot distinguish betweens record types. Only one of the selected record types will be displayed: #1', $optiontexts{$enabled});
455   }
456   if ($form->{l_soldtotal} && !$bsooqr_mode) {
457     delete $form->{l_soldtotal};
458
459     flash('warning', $::locale->text('Soldtotal does not make sense without any bsooqr options'));
460   }
461   if ($form->{l_name} && !$bsooqr_mode) {
462     delete $form->{l_name};
463
464     flash('warning', $::locale->text('Name does not make sense without any bsooqr options'));
465   }
466   IC->all_parts(\%myconfig, \%$form);
467
468   my @columns = qw(
469     partnumber description notes partsgroup bin onhand rop soldtotal unit listprice
470     linetotallistprice sellprice linetotalsellprice lastcost linetotallastcost
471     priceupdate weight image drawing microfiche invnumber ordnumber quonumber
472     transdate name serialnumber deliverydate ean projectnumber projectdescription
473     insertdate shop
474   );
475
476   my $pricegroups = SL::DB::Manager::Pricegroup->get_all(sort => 'id');
477   my @pricegroup_columns;
478   my %column_defs_pricegroups;
479   if ($form->{l_pricegroups}) {
480     @pricegroup_columns      = map { "pricegroup_" . $_->id } @{ $pricegroups };
481     %column_defs_pricegroups = map {
482       "pricegroup_" . $_->id => {
483         text    => $::locale->text('Pricegroup') . ' ' . $_->pricegroup,
484         visible => 1,
485       },
486     }  @{ $pricegroups };
487   }
488   push @columns, @pricegroup_columns;
489
490   my @includeable_custom_variables = grep { $_->{includeable} } @{ $cvar_configs };
491   my @searchable_custom_variables  = grep { $_->{searchable} }  @{ $cvar_configs };
492   my %column_defs_cvars            = map { +"cvar_$_->{name}" => { 'text' => $_->{description} } } @includeable_custom_variables;
493
494   push @columns, map { "cvar_$_->{name}" } @includeable_custom_variables;
495
496   %column_defs = (%column_defs, %column_defs_cvars, %column_defs_pricegroups);
497   map { $column_defs{$_}->{visible} ||= $form->{"l_$_"} ? 1 : 0 } @columns;
498   map { $column_defs{$_}->{align}   = 'right' } qw(onhand sellprice listprice lastcost linetotalsellprice linetotallastcost linetotallistprice rop weight soldtotal shop), @pricegroup_columns;
499
500   my @hidden_variables = (
501     qw(l_subtotal l_linetotal searchitems itemstatus bom l_pricegroups insertdatefrom insertdateto),
502     @itemstatus_keys,
503     @callback_keys,
504     map({ "cvar_$_->{name}" } @searchable_custom_variables),
505     map({'cvar_'. $_->{name} .'_qtyop'} grep({$_->{type} eq 'number'} @searchable_custom_variables)),
506     map({ "l_$_" } @columns),
507   );
508
509   my $callback         = build_std_url('action=generate_report', grep { $form->{$_} } @hidden_variables);
510
511   my @sort_full        = qw(partnumber description onhand soldtotal deliverydate insertdate shop);
512   my @sort_no_revers   = qw(partsgroup bin priceupdate invnumber ordnumber quonumber name image drawing serialnumber);
513
514   foreach my $col (@sort_full) {
515     $column_defs{$col}->{link} = join '&', $callback, "sort=$col", map { "$_=" . E($form->{$_}) } qw(revers lastsort);
516   }
517   map { $column_defs{$_}->{link} = "${callback}&sort=$_" } @sort_no_revers;
518
519   # add order to callback
520   $form->{callback} = join '&', ($callback, map { "${_}=" . E($form->{$_}) } qw(sort revers));
521
522   my $report = SL::ReportGenerator->new(\%myconfig, $form);
523
524   my %attachment_basenames = (
525     'part'     => $locale->text('part_list'),
526     'service'  => $locale->text('service_list'),
527     'assembly' => $locale->text('assembly_list'),
528   );
529
530   $report->set_options('raw_top_info_text'     => $form->parse_html_template('ic/generate_report_top', { options => \@options }),
531                        'raw_bottom_info_text'  => $form->parse_html_template('ic/generate_report_bottom'),
532                        'output_format'         => 'HTML',
533                        'title'                 => $form->{title},
534                        'attachment_basename'   => $attachment_basenames{$form->{searchitems}} . strftime('_%Y%m%d', localtime time),
535   );
536   $report->set_options_from_form();
537   $locale->set_numberformat_wo_thousands_separator(\%myconfig) if lc($report->{options}->{output_format}) eq 'csv';
538
539   $report->set_columns(%column_defs);
540   $report->set_column_order(@columns);
541
542   $report->set_export_options('generate_report', @hidden_variables, qw(sort revers));
543
544   $report->set_sort_indicator($form->{sort}, $form->{revers} ? 0 : 1);
545
546   CVar->add_custom_variables_to_report('module'         => 'IC',
547                                        'trans_id_field' => 'id',
548                                        'configs'        => $cvar_configs,
549                                        'column_defs'    => \%column_defs,
550                                        'data'           => $form->{parts});
551
552   CVar->add_custom_variables_to_report('module'         => 'IC',
553                                        'sub_module'     => sub { $_[0]->{ioi} },
554                                        'trans_id_field' => 'ioi_id',
555                                        'configs'        => $cvar_configs,
556                                        'column_defs'    => \%column_defs,
557                                        'data'           => $form->{parts});
558
559   my @subtotal_columns = qw(sellprice listprice lastcost);
560   my %subtotals = map { $_ => 0 } ('onhand', @subtotal_columns);
561   my %totals    = map { $_ => 0 } @subtotal_columns;
562   my $idx       = 0;
563   my $same_item = @{ $form->{parts} } ? $form->{parts}[0]{ $form->{sort} } : undef;
564
565   my $defaults  = AM->get_defaults();
566
567   # postprocess parts
568   foreach my $ref (@{ $form->{parts} }) {
569
570     # fresh row, for inserting later
571     my $row = { map { $_ => { 'data' => $ref->{$_} } } @columns };
572
573     $ref->{exchangerate} ||= 1;
574     $ref->{price_factor} ||= 1;
575     $ref->{sellprice}     *= $ref->{exchangerate} / $ref->{price_factor};
576     $ref->{listprice}     *= $ref->{exchangerate} / $ref->{price_factor};
577     $ref->{lastcost}      *= $ref->{exchangerate} / $ref->{price_factor};
578
579     # use this for assemblies
580     my $soldtotal = $bsooqr_mode ? $ref->{soldtotal} : $ref->{onhand};
581
582     if ($ref->{assemblyitem}) {
583       $row->{partnumber}{align}   = 'right';
584       $row->{soldtotal}{data}     = 0;
585       $soldtotal                  = 0 if ($form->{sold});
586     }
587
588     my $edit_link               = build_std_url('action=edit', 'id=' . E($ref->{id}), 'callback');
589     $row->{partnumber}->{link}  = $edit_link;
590     $row->{description}->{link} = $edit_link;
591
592     foreach (qw(sellprice listprice lastcost)) {
593       $row->{$_}{data}            = $form->format_amount(\%myconfig, $ref->{$_}, 2);
594       $row->{"linetotal$_"}{data} = $form->format_amount(\%myconfig, $ref->{onhand} * $ref->{$_}, 2);
595     }
596     foreach ( @pricegroup_columns ) {
597       $row->{$_}{data}            = $form->format_amount(\%myconfig, $ref->{"$_"}, 2);
598     };
599
600
601     map { $row->{$_}{data} = $form->format_amount(\%myconfig, $ref->{$_}); } qw(onhand rop weight soldtotal);
602
603     $row->{weight}->{data} .= ' ' . $defaults->{weightunit};
604
605     # 'yes' and 'no' for boolean value shop
606     if ($form->{l_shop}) {
607       $row->{shop}{data} = $row->{shop}{data}? $::locale->text('yes') : $::locale->text('no');
608     }
609
610     if (!$ref->{assemblyitem}) {
611       foreach my $col (@subtotal_columns) {
612         $totals{$col}    += $soldtotal * $ref->{$col};
613         $subtotals{$col} += $soldtotal * $ref->{$col};
614       }
615
616       $subtotals{soldtotal} += $soldtotal;
617     }
618
619     # set module stuff
620     if ($ref->{module} eq 'oe') {
621       # für oe gibt es vier fälle, jeweils nach kunde oder lieferant unterschiedlich:
622       #
623       # | ist bestellt  | Von Kunden bestellt |  -> edit_oe_ord_link
624       # | Anfrage       | Angebot             |  -> edit_oe_quo_link
625
626       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');
627       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');
628
629       $row->{ordnumber}{link} = $edit_oe_ord_link;
630       $row->{quonumber}{link} = $edit_oe_quo_link if (!$ref->{ordnumber});
631
632     } else {
633       $row->{invnumber}{link} = build_std_url("script=$ref->{module}.pl", 'action=edit', 'type=invoice', 'id=' . E($ref->{trans_id}), 'callback') if ($ref->{invnumber});
634     }
635
636     # set properties of images
637     if ($ref->{image} && (lc $report->{options}->{output_format} eq 'html')) {
638       $row->{image}{data}     = '';
639       $row->{image}{raw_data} = '<a href="' . H($ref->{image}) . '"><img src="' . H($ref->{image}) . '" height="32" border="0"></a>';
640     }
641     map { $row->{$_}{link} = $ref->{$_} } qw(drawing microfiche);
642
643     $row->{notes}{data} = SL::HTML::Util->strip($ref->{notes});
644
645     $report->add_data($row);
646
647     my $next_ref = $form->{parts}[$idx + 1];
648
649     # insert subtotal rows
650     if (($form->{l_subtotal} eq 'Y') &&
651         (!$next_ref ||
652          (!$next_ref->{assemblyitem} && ($same_item ne $next_ref->{ $form->{sort} })))) {
653       my $row = { map { $_ => { 'class' => 'listsubtotal', } } @columns };
654
655       if (($form->{searchitems} ne 'assembly') || !$form->{bom}) {
656         $row->{soldtotal}->{data} = $form->format_amount(\%myconfig, $subtotals{soldtotal});
657       }
658
659       map { $row->{"linetotal$_"}->{data} = $form->format_amount(\%myconfig, $subtotals{$_}, 2) } @subtotal_columns;
660       map { $subtotals{$_} = 0 } ('soldtotal', @subtotal_columns);
661
662       $report->add_data($row);
663
664       $same_item = $next_ref->{ $form->{sort} };
665     }
666
667     $idx++;
668   }
669
670   if ($form->{"l_linetotal"} && !$form->{report_generator_csv_options_for_import}) {
671     my $row = { map { $_ => { 'class' => 'listtotal', } } @columns };
672
673     map { $row->{"linetotal$_"}->{data} = $form->format_amount(\%myconfig, $totals{$_}, 2) } @subtotal_columns;
674
675     $report->add_separator();
676     $report->add_data($row);
677   }
678
679   $report->generate_with_headers();
680
681   $lxdebug->leave_sub();
682 }    #end generate_report
683
684 sub parts_subtotal {
685   $lxdebug->enter_sub();
686
687   $auth->assert('part_service_assembly_edit');
688
689   my (%column_data);
690   my ($column_index, $subtotalonhand, $subtotalsellprice, $subtotallastcost, $subtotallistprice) = @_;
691
692   map { $column_data{$_} = "<td>&nbsp;</td>" } @{ $column_index };
693   $$subtotalonhand = 0 if ($form->{searchitems} eq 'assembly' && $form->{bom});
694
695   $column_data{onhand} =
696       "<th class=listsubtotal align=right>"
697     . $form->format_amount(\%myconfig, $$subtotalonhand)
698     . "</th>";
699
700   $column_data{linetotalsellprice} =
701       "<th class=listsubtotal align=right>"
702     . $form->format_amount(\%myconfig, $$subtotalsellprice, 2)
703     . "</th>";
704   $column_data{linetotallistprice} =
705       "<th class=listsubtotal align=right>"
706     . $form->format_amount(\%myconfig, $$subtotallistprice, 2)
707     . "</th>";
708   $column_data{linetotallastcost} =
709       "<th class=listsubtotal align=right>"
710     . $form->format_amount(\%myconfig, $$subtotallastcost, 2)
711     . "</th>";
712
713   $$subtotalonhand    = 0;
714   $$subtotalsellprice = 0;
715   $$subtotallistprice = 0;
716   $$subtotallastcost  = 0;
717
718   print "<tr class=listsubtotal>";
719
720   map { print "\n$column_data{$_}" } @{ $column_index };
721
722   print qq|
723   </tr>
724 |;
725
726   $lxdebug->leave_sub();
727 }
728
729 sub edit {
730   $lxdebug->enter_sub();
731
732   $auth->assert('part_service_assembly_details');
733
734   # show history button
735   $form->{javascript} = qq|<script type="text/javascript" src="js/show_history.js"></script>|;
736   #/show hhistory button
737   IC->get_part(\%myconfig, \%$form);
738
739   $form->{"original_partnumber"} = $form->{"partnumber"};
740
741   my $title      = 'Edit ' . ucfirst $form->{item};
742   $form->{title} = $locale->text($title);
743
744   &link_part;
745   &display_form;
746
747   $lxdebug->leave_sub();
748 }
749
750 sub link_part {
751   $lxdebug->enter_sub();
752
753   $auth->assert('part_service_assembly_details');
754
755   IC->create_links("IC", \%myconfig, \%$form);
756
757   # currencies
758   map({ $form->{selectcurrency} .= "<option>$_\n" } $::form->get_all_currencies());
759
760   # parts and assemblies have the same links
761   my $item = $form->{item};
762   if ($form->{item} eq 'assembly') {
763     $item = 'part';
764   }
765
766   # build the popup menus
767   $form->{taxaccounts} = "";
768   foreach my $key (keys %{ $form->{IC_links} }) {
769     foreach my $ref (@{ $form->{IC_links}{$key} }) {
770
771       # if this is a tax field
772       if ($key =~ /IC_tax/) {
773         if ($key =~ /\Q$item\E/) {
774           $form->{taxaccounts} .= "$ref->{accno} ";
775           $form->{"IC_tax_$ref->{accno}_description"} =
776             "$ref->{accno}--$ref->{description}";
777
778           if ($form->{id}) {
779             if ($form->{amount}{ $ref->{accno} }) {
780               $form->{"IC_tax_$ref->{accno}"} = "checked";
781             }
782           } else {
783             $form->{"IC_tax_$ref->{accno}"} = "checked";
784           }
785         }
786       } else {
787
788         $form->{"select$key"} .=
789           "<option $ref->{selected}>$ref->{accno}--$ref->{description}\n";
790         if ($form->{amount}{$key} eq $ref->{accno}) {
791           $form->{$key} = "$ref->{accno}--$ref->{description}";
792         }
793
794       }
795     }
796   }
797   chop $form->{taxaccounts};
798
799   if (($form->{item} eq "part") || ($form->{item} eq "assembly")) {
800     $form->{selectIC_income}  = $form->{selectIC_sale};
801     $form->{selectIC_expense} = $form->{selectIC_cogs};
802     $form->{IC_income}        = $form->{IC_sale};
803     $form->{IC_expense}       = $form->{IC_cogs};
804   }
805
806   delete $form->{IC_links};
807   delete $form->{amount};
808
809   $form->get_partsgroup(\%myconfig, { all => 1 });
810
811   $form->{partsgroup} = "$form->{partsgroup}--$form->{partsgroup_id}";
812
813   if (@{ $form->{all_partsgroup} }) {
814     $form->{selectpartsgroup} = qq|<option>\n|;
815     map { $form->{selectpartsgroup} .= qq|<option value="$_->{partsgroup}--$_->{id}">$_->{partsgroup}\n| } @{ $form->{all_partsgroup} };
816   }
817
818   if ($form->{item} eq 'assembly') {
819
820     foreach my $i (1 .. $form->{assembly_rows}) {
821       if ($form->{"partsgroup_id_$i"}) {
822         $form->{"partsgroup_$i"} =
823           qq|$form->{"partsgroup_$i"}--$form->{"partsgroup_id_$i"}|;
824       }
825     }
826     $form->get_partsgroup(\%myconfig);
827
828     if (@{ $form->{all_partsgroup} }) {
829       $form->{selectassemblypartsgroup} = qq|<option>\n|;
830
831       map {
832         $form->{selectassemblypartsgroup} .=
833           qq|<option value="$_->{partsgroup}--$_->{id}">$_->{partsgroup}\n|
834       } @{ $form->{all_partsgroup} };
835     }
836   }
837   $lxdebug->leave_sub();
838 }
839
840 sub form_header {
841   $lxdebug->enter_sub();
842
843   $auth->assert('part_service_assembly_details');
844
845   $form->{pg_keys}          = sub { "$_[0]->{partsgroup}--$_[0]->{id}" };
846   $form->{description_area} = ($form->{rows} = $form->numtextrows($form->{description}, 40)) > 1;
847   $form->{notes_rows}       =  max 4, $form->numtextrows($form->{notes}, 40), $form->numtextrows($form->{formel}, 40);
848
849   map { $form->{"is_$_"}  = ($form->{item} eq $_) } qw(part service assembly);
850   map { $form->{$_}       =~ s/"/&quot;/g;        } qw(unit);
851
852   $form->get_lists('price_factors' => 'ALL_PRICE_FACTORS',
853                    'partsgroup'    => 'all_partsgroup',
854                    'vendors'       => 'ALL_VENDORS',
855                    'warehouses'    => { 'key'    => 'WAREHOUSES',
856                                         'bins'   => 'BINS', });
857   # leerer wert für Lager und Lagerplatz korrekt einstellt
858   # ID 0 sollte in Ordnung sein, da der Zähler sowieso höher ist
859   my $no_default_bin_entry = { 'id' => '0', description => '--', 'BINS' => [ { id => '0', description => ''} ] };
860   push @ { $form->{WAREHOUSES} }, $no_default_bin_entry;
861   if (my $max = scalar @{ $form->{WAREHOUSES} }) {
862     my ($default_warehouse_id, $default_bin_id);
863     if ($form->{action} eq 'add') { # default only for new entries
864       $default_warehouse_id = $::instance_conf->get_warehouse_id;
865       $default_bin_id       = $::instance_conf->get_bin_id;
866     }
867     $form->{warehouse_id} ||= $default_warehouse_id || $form->{WAREHOUSES}->[$max -1]->{id};
868     $form->{bin_id}       ||= $default_bin_id       ||  $form->{WAREHOUSES}->[$max -1]->{BINS}->[0]->{id};
869   }
870
871   $form->{LANGUAGES}        = SL::DB::Manager::Language->get_all_sorted;
872   $form->{translations_map} = { map { ($_->{language_id} => $_) } @{ $form->{translations} || [] } };
873
874   IC->retrieve_buchungsgruppen(\%myconfig, $form);
875   @{ $form->{BUCHUNGSGRUPPEN} } = grep { $_->{id} eq $form->{buchungsgruppen_id} || ($form->{id} && $form->{orphaned}) || !$form->{id} } @{ $form->{BUCHUNGSGRUPPEN} };
876
877   if (($form->{partnumber} ne '') && !SL::TransNumber->new(number => $form->{partnumber}, type => $form->{item}, id => $form->{id})->is_unique) {
878     flash('info', $::locale->text('This partnumber is not unique. You should change it.'));
879   }
880
881   my $units = AM->retrieve_units(\%myconfig, $form);
882   $form->{ALL_UNITS} = [ map +{ name => $_ }, sort { $units->{$a}{sortkey} <=> $units->{$b}{sortkey} } keys %$units ];
883
884   $form->{defaults} = AM->get_defaults();
885
886   $form->{CUSTOM_VARIABLES} = CVar->get_custom_variables('module' => 'IC', 'trans_id' => $form->{id});
887
888   my ($null, $partsgroup_id) = split /--/, $form->{partsgroup};
889
890   CVar->render_inputs('variables' => $form->{CUSTOM_VARIABLES}, show_disabled_message => 1, partsgroup_id => $partsgroup_id)
891     if (scalar @{ $form->{CUSTOM_VARIABLES} });
892
893   $::request->layout->use_javascript("${_}.js") for qw(ckeditor/ckeditor ckeditor/adapters/jquery kivi.PriceRule);
894   $::request->layout->add_javascripts_inline("\$(function(){kivi.PriceRule.load_price_rules_for_part(@{[ $::form->{id} * 1 ]})});") if $::form->{id};
895   $form->header;
896   #print $form->parse_html_template('ic/form_header', { ALL_PRICE_FACTORS => $form->{ALL_PRICE_FACTORS},
897   #                                                     ALL_UNITS         => $form->{ALL_UNITS},
898   #                                                     BUCHUNGSGRUPPEN   => $form->{BUCHUNGSGRUPPEN},
899   #                                                     payment_terms     => $form->{payment_terms},
900   #                                                     all_partsgroup    => $form->{all_partsgroup}});
901
902   $form->{show_edit_buttons} = $main::auth->check_right($::myconfig{login}, 'part_service_assembly_edit');
903
904   print $form->parse_html_template('ic/form_header');
905   $lxdebug->leave_sub();
906 }
907
908 sub form_footer {
909   $lxdebug->enter_sub();
910
911   $auth->assert('part_service_assembly_details');
912
913   print $form->parse_html_template('ic/form_footer');
914
915   $lxdebug->leave_sub();
916 }
917
918 sub makemodel_row {
919   $lxdebug->enter_sub();
920   my ($numrows) = @_;
921   #hli
922   my @mm_data = grep { any { $_ ne '' } @$_{qw(make model)} } map +{ make => $form->{"make_$_"}, model => $form->{"model_$_"}, lastcost => $form->{"lastcost_$_"}, lastupdate => $form->{"lastupdate_$_"}, sortorder => $form->{"sortorder_$_"} }, 1 .. $numrows;
923   delete @{$form}{grep { m/^make_\d+/ || m/^model_\d+/ } keys %{ $form }};
924   print $form->parse_html_template('ic/makemodel', { MM_DATA => [ @mm_data, {} ], mm_rows => scalar @mm_data + 1 });
925
926   $lxdebug->leave_sub();
927 }
928
929 sub assembly_row {
930   $lxdebug->enter_sub();
931   my ($numrows) = @_;
932   my (@column_index);
933   my ($nochange, $callback, $previousform, $linetotal, $line_purchase_price, $href);
934
935   @column_index = qw(runningnumber qty unit bom partnumber description partsgroup lastcost total);
936
937   if ($form->{previousform}) {
938     $nochange     = 1;
939     @column_index = qw(qty unit bom partnumber description partsgroup total);
940   } else {
941
942     # change callback
943     $form->{old_callback} = $form->{callback};
944     $callback             = $form->{callback};
945     $form->{callback}     = "$form->{script}?action=display_form";
946
947     # delete action
948     map { delete $form->{$_} } qw(action header);
949
950     # save form variables in a previousform variable
951     my %form_to_save = map   { ($_ => m/^ (?: listprice | sellprice | lastcost ) $/x ? $form->format_amount(\%myconfig, $form->{$_}) : $form->{$_}) }
952                        keys %{ $form };
953     $previousform    = $::auth->save_form_in_session(form => \%form_to_save);
954
955     $form->{callback} = $callback;
956     $form->{assemblytotal} = 0;
957     $form->{assembly_purchase_price_total} = 0;
958     $form->{weight}        = 0;
959   }
960
961   my %header = (
962    runningnumber => { text =>  $locale->text('No.'),              nowrap => 1, width => '5%',  align => 'left',},
963    qty           => { text =>  $locale->text('Qty'),              nowrap => 1, width => '10%', align => 'left',},
964    unit          => { text =>  $locale->text('Unit'),             nowrap => 1, width => '5%',  align => 'left',},
965    partnumber    => { text =>  $locale->text('Part Number'),      nowrap => 1, width => '20%', align => 'left',},
966    description   => { text =>  $locale->text('Part Description'), nowrap => 1, width => '50%', align => 'left',},
967    lastcost      => { text =>  $locale->text('Purchase Prices'),  nowrap => 1, width => '50%', align => 'right',},
968    total         => { text =>  $locale->text('Sale Prices'),      nowrap => 1,                 align => 'right',},
969    bom           => { text =>  $locale->text('BOM'),                                           align => 'center',},
970    partsgroup    => { text =>  $locale->text('Group'),                                         align => 'left',},
971   );
972
973   my @ROWS;
974
975   for my $i (1 .. $numrows) {
976     my (%row, @row_hiddens);
977
978     $form->{"partnumber_$i"} =~ s/\"/&quot;/g;
979
980     $linetotal           = $form->round_amount($form->{"sellprice_$i"} * $form->{"qty_$i"} / ($form->{"price_factor_$i"} || 1), 4);
981     $line_purchase_price = $form->round_amount($form->{"lastcost_$i"} *  $form->{"qty_$i"} / ($form->{"price_factor_$i"} || 1), 4);
982     $form->{assemblytotal}                  += $linetotal;
983     $form->{assembly_purchase_price_total}  += $line_purchase_price;
984     $form->{"qty_$i"}    = $form->format_amount(\%myconfig, $form->{"qty_$i"});
985     $linetotal           = $form->format_amount(\%myconfig, $linetotal, 2);
986     $line_purchase_price = $form->format_amount(\%myconfig, $line_purchase_price, 2);
987     $href                = build_std_url("action=edit", qq|id=$form->{"id_$i"}|, "rowcount=$numrows", "currow=$i", "previousform=$previousform");
988     map { $row{$_}{data} = "" } qw(qty unit partnumber description bom partsgroup runningnumber);
989
990     # last row
991     if (($i >= 1) && ($i == $numrows)) {
992       if (!$form->{previousform}) {
993         $row{partnumber}{data}  = qq|<input name="partnumber_$i" size=15 value="$form->{"partnumber_$i"}">|;
994         $row{qty}{data}         = qq|<input name="qty_$i" size=5 value="$form->{"qty_$i"}">|;
995         $row{description}{data} = qq|<input name="description_$i" size=40 value="$form->{"description_$i"}">|;
996         $row{partsgroup}{data}  = qq|<input name="partsgroup_$i" size=10 value="$form->{"partsgroup_$i"}">|;
997       }
998     # other rows
999     } else {
1000       if ($form->{previousform}) {
1001         push @row_hiddens,          qw(qty bom);
1002         $row{partnumber}{data}    = $form->{"partnumber_$i"};
1003         $row{qty}{data}           = $form->{"qty_$i"};
1004         $row{bom}{data}           = $form->{"bom_$i"} ? "x" : "&nbsp;";
1005         $row{qty}{align}          = 'right';
1006       } else {
1007         $row{partnumber}{data}    = qq|$form->{"partnumber_$i"}|;
1008         $row{partnumber}{link}     = $href;
1009         $row{qty}{data}           = qq|<input name="qty_$i" size=5 value="$form->{"qty_$i"}">|;
1010         $row{runningnumber}{data} = qq|<input name="runningnumber_$i" size=3 value="$i">|;
1011         $row{bom}{data}   = sprintf qq|<input name="bom_$i" type=checkbox class=checkbox value=1 %s>|,
1012                                        $form->{"bom_$i"} ? 'checked' : '';
1013       }
1014       push @row_hiddens,        qw(unit description partnumber partsgroup);
1015       $row{unit}{data}        = $form->{"unit_$i"};
1016       #Bei der Artikelbeschreibung und Warengruppe können Sonderzeichen verwendet
1017       #werden, die den HTML Code stören. Daher sollen diese im Template escaped werden
1018       #dies geschieht, wenn die Variable escape gesetzt ist
1019       $row{description}{data}   = $form->{"description_$i"};
1020       $row{description}{escape} = 1;
1021       $row{partsgroup}{data}    = $form->{"partsgroup_$i"};
1022       $row{partsgroup}{escape}  = 1;
1023       $row{bom}{align}          = 'center';
1024     }
1025
1026     $row{lastcost}{data}      = $line_purchase_price;
1027     $row{total}{data}         = $linetotal;
1028     $row{lastcost}{align}     = 'right';
1029     $row{total}{align}        = 'right';
1030     $row{deliverydate}{align} = 'right';
1031
1032     push @row_hiddens, qw(id sellprice lastcost weight price_factor_id price_factor);
1033     $row{hiddens} = [ map +{ name => "${_}_$i", value => $form->{"${_}_$i"} }, @row_hiddens ];
1034
1035     push @ROWS, \%row;
1036   }
1037
1038   print $form->parse_html_template('ic/assembly_row', { COLUMNS => \@column_index, ROWS => \@ROWS, HEADER => \%header });
1039
1040   $lxdebug->leave_sub();
1041 }
1042
1043 sub update {
1044   $lxdebug->enter_sub();
1045
1046   $auth->assert('part_service_assembly_edit');
1047
1048   # update checks whether pricegroups, makemodels or assembly items have been changed/added
1049   # new items might have been added (and the original form might have been stored and restored)
1050   # so at the end the ic form is run through check_form in io.pl
1051   # The various combination of events can lead to problems with the order of parse_amount and format_amount
1052   # Currently check_form parses some variables in assembly mode, but not in article or service mode
1053   # This will only ever really be sanely resolved with a rewrite...
1054
1055   # parse pricegroups. and no, don't rely on check_form for this...
1056   map { $form->{"price_$_"} = $form->parse_amount(\%myconfig, $form->{"price_$_"}) } 1 .. $form->{price_rows};
1057
1058   unless ($form->{item} eq 'assembly') {
1059     # for assemblies check_form will parse sellprice and listprice, but not for parts or services
1060     $form->{$_} = $form->parse_amount(\%myconfig, $form->{$_}) for qw(sellprice listprice ve gv);
1061   };
1062
1063   if ($form->{item} eq 'part') {
1064     $form->{$_} = $form->parse_amount(\%myconfig, $form->{$_}) for qw(weight rop);
1065   }
1066
1067   # same for makemodel lastcosts
1068   # but parse_amount not necessary for assembly component lastcosts
1069   unless ($form->{item} eq "assembly") {
1070     map { $form->{"lastcost_$_"} = $form->parse_amount(\%myconfig, $form->{"lastcost_$_"}) } 1 .. $form->{"makemodel_rows"};
1071     $form->{lastcost} = $form->parse_amount(\%myconfig, $form->{lastcost});
1072   }
1073
1074   if ($form->{item} eq "assembly") {
1075     my $i = $form->{assembly_rows};
1076
1077     # if last row is empty check the form otherwise retrieve item
1078     if (   ($form->{"partnumber_$i"} eq "")
1079         && ($form->{"description_$i"} eq "")
1080         && ($form->{"partsgroup_$i"}  eq "")) {
1081       # no new assembly item was added
1082
1083       &check_form;
1084
1085     } else {
1086       # search db for newly added assemblyitems, via partnumber or description
1087       IC->assembly_item(\%myconfig, \%$form);
1088
1089       # form->{item_list} contains the possible matches, next check whether the
1090       # match is unique or we need to call the page to select the item
1091       my $rows = scalar @{ $form->{item_list} };
1092
1093       if ($rows) {
1094         $form->{"qty_$i"} = 1 unless ($form->{"qty_$i"});
1095
1096         if ($rows > 1) {
1097           $form->{makemodel_rows}--;
1098           select_item(mode => 'IC', pre_entered_qty => $form->parse_amount(\%myconfig, $form->{"qty_$i"}));
1099           $::dispatcher->end_request;
1100         } else {
1101           map { $form->{item_list}[$i]{$_} =~ s/\"/&quot;/g }
1102             qw(partnumber description unit partsgroup);
1103           map { $form->{"${_}_$i"} = $form->{item_list}[0]{$_} }
1104             keys %{ $form->{item_list}[0] };
1105           $form->{"runningnumber_$i"} = $form->{assembly_rows};
1106           $form->{assembly_rows}++;
1107
1108           &check_form;
1109
1110         }
1111
1112       } else {
1113
1114         $form->{rowcount} = $i;
1115         $form->{assembly_rows}++;
1116
1117         &new_item;
1118
1119       }
1120     }
1121
1122   } elsif (($form->{item} eq 'part') || ($form->{item} eq 'service')) {
1123     &check_form;
1124   }
1125
1126   $lxdebug->leave_sub();
1127 }
1128
1129 sub save {
1130   $lxdebug->enter_sub();
1131
1132   $auth->assert('part_service_assembly_edit');
1133   $::form->mtime_ischanged('parts');
1134   my ($parts_id, %newform, $amount, $callback);
1135
1136   # check if there is a part number - commented out, cause there is an automatic allocation of numbers
1137   # $form->isblank("partnumber", $locale->text(ucfirst $form->{item}." Part Number missing!"));
1138
1139   # check if there is a description
1140   $form->isblank("description", $locale->text("Part Description missing!"));
1141
1142   $form->error($locale->text("Inventory quantity must be zero before you can set this $form->{item} obsolete!"))
1143     if $form->{obsolete} && $form->{onhand} * 1 && $form->{item} ne 'service';
1144
1145   if (!$form->{buchungsgruppen_id}) {
1146     $form->error($locale->text("Parts must have an entry type.") . " " .
1147      $locale->text("If you see this message, you most likely just setup your LX-Office and haven't added any entry types. If this is the case, the option is accessible for administrators in the System menu.")
1148     );
1149   }
1150
1151   $form->error($locale->text('Description must not be empty!')) unless $form->{description};
1152   $form->error($locale->text('Partnumber must not be set to empty!')) if $form->{id} && !$form->{partnumber};
1153
1154   # undef warehouse_id if the empty value is selected
1155   if ( ($form->{warehouse_id} == 0) && ($form->{bin_id} == 0) ) {
1156     undef $form->{warehouse_id};
1157     undef $form->{bin_id};
1158   }
1159   # save part
1160   if (IC->save(\%myconfig, \%$form) == 3) {
1161     $form->error($locale->text('Partnumber not unique!'));
1162   }
1163   # saving the history
1164   if(!exists $form->{addition}) {
1165     $form->{snumbers}  = qq|partnumber_| . $form->{partnumber};
1166     $form->{what_done} = "part";
1167     $form->{addition}  = "SAVED";
1168     $form->save_history;
1169   }
1170   # /saving the history
1171   $parts_id = $form->{id};
1172
1173   my $i;
1174   # load previous variables
1175   if ($form->{previousform}) {
1176
1177     # save the new form variables before splitting previousform
1178     map { $newform{$_} = $form->{$_} } keys %$form;
1179
1180     # don't trample on previous variables
1181     map { delete $form->{$_} } keys %newform;
1182
1183     my $ic_cvar_configs = CVar->get_configs(module => 'IC');
1184     my @ic_cvar_fields  = map { "cvar_$_->{name}" } @{ $ic_cvar_configs };
1185
1186     # restore original values
1187     $::auth->restore_form_from_session($newform{previousform}, form => $form);
1188     $form->{taxaccounts} = $newform{taxaccount2};
1189
1190     if ($form->{item} eq 'assembly') {
1191
1192       # undo number formatting
1193       map { $form->{$_} = $form->parse_amount(\%myconfig, $form->{$_}) }
1194         qw(weight listprice sellprice rop);
1195
1196       $form->{assembly_rows}--;
1197       if ($newform{currow}) {
1198         $i = $newform{currow};
1199       } else {
1200         $i = $form->{assembly_rows};
1201       }
1202       $form->{"qty_$i"} = 1 unless ($form->{"qty_$i"} > 0);
1203
1204       $form->{sellprice} -= $form->{"sellprice_$i"} * $form->{"qty_$i"};
1205       $form->{weight}    -= $form->{"weight_$i"} * $form->{"qty_$i"};
1206
1207       # change/add values for assembly item
1208       map { $form->{"${_}_$i"} = $newform{$_} } qw(partnumber description bin unit weight listprice sellprice inventory_accno income_accno expense_accno price_factor_id);
1209       map { $form->{"ic_${_}_$i"} = $newform{$_} } @ic_cvar_fields;
1210
1211       # das ist __voll__ bekloppt, dass so auszurechnen jb 22.5.09
1212       #$form->{sellprice} += $form->{"sellprice_$i"} * $form->{"qty_$i"};
1213       $form->{weight}    += $form->{"weight_$i"} * $form->{"qty_$i"};
1214
1215     } else {
1216
1217       # set values for last invoice/order item
1218       $i = $form->{rowcount};
1219       $form->{"qty_$i"} = 1 unless ($form->{"qty_$i"} > 0);
1220
1221       map { $form->{"${_}_$i"} = $newform{$_} } qw(partnumber description bin unit listprice inventory_accno income_accno expense_accno sellprice lastcost price_factor_id);
1222       map { $form->{"ic_${_}_$i"} = $newform{$_} } @ic_cvar_fields;
1223
1224       $form->{"longdescription_$i"} = $newform{notes};
1225
1226       $form->{"sellprice_$i"} = $newform{lastcost} if ($form->{vendor_id});
1227
1228       if ($form->{exchangerate} != 0) {
1229         $form->{"sellprice_$i"} /= $form->{exchangerate};
1230       }
1231
1232       map { $form->{"taxaccounts_$i"} .= "$_ " } split / /, $newform{taxaccount};
1233       chop $form->{"taxaccounts_$i"};
1234       foreach my $item (qw(description rate taxnumber)) {
1235         my $index = $form->{"taxaccounts_$i"} . "_$item";
1236         $form->{$index} = $newform{$index};
1237       }
1238
1239       # credit remaining calculation
1240       $amount = $form->{"sellprice_$i"} * (1 - $form->{"discount_$i"} / 100) * $form->{"qty_$i"};
1241
1242       map { $form->{"${_}_base"} += $amount } (split / /, $form->{"taxaccounts_$i"});
1243       map { $amount += ($form->{"${_}_base"} * $form->{"${_}_rate"}) } split / /, $form->{"taxaccounts_$i"} if !$form->{taxincluded};
1244
1245       $form->{creditremaining} -= $amount;
1246
1247       # redo number formatting, because invoice parse them!
1248       map { $form->{"${_}_$i"} = $form->format_amount(\%myconfig, $form->{"${_}_$i"}) } qw(weight listprice sellprice lastcost rop);
1249     }
1250
1251     $form->{"id_$i"} = $parts_id;
1252
1253     # Get the actual price factor (not just the ID) for the marge calculation.
1254     $form->get_lists('price_factors' => 'ALL_PRICE_FACTORS');
1255     foreach my $pfac (@{ $form->{ALL_PRICE_FACTORS} }) {
1256       next if ($pfac->{id} != $newform{price_factor_id});
1257       $form->{"marge_price_factor_$i"} = $pfac->{factor};
1258       last;
1259     }
1260     delete $form->{ALL_PRICE_FACTORS};
1261
1262     delete $form->{action};
1263
1264     # restore original callback
1265     $callback = $form->unescape($form->{callback});
1266     $form->{callback} = $form->unescape($form->{old_callback});
1267     delete $form->{old_callback};
1268
1269     $form->{makemodel_rows}--;
1270
1271     # put callback together
1272     foreach my $key (keys %$form) {
1273
1274       # do single escape for Apache 2.0
1275       my $value = $form->escape($form->{$key}, 1);
1276       $callback .= qq|&$key=$value|;
1277     }
1278     $form->{callback} = $callback;
1279   }
1280
1281   # redirect
1282   $form->redirect;
1283
1284   $lxdebug->leave_sub();
1285 }
1286
1287 sub save_as_new {
1288   $lxdebug->enter_sub();
1289
1290   $auth->assert('part_service_assembly_edit');
1291
1292   # saving the history
1293   if(!exists $form->{addition}) {
1294     $form->{snumbers}  = qq|partnumber_| . $form->{partnumber};
1295     $form->{addition}  = "SAVED AS NEW";
1296     $form->{what_done} = "part";
1297     $form->save_history;
1298   }
1299   # /saving the history
1300   $form->{id} = 0;
1301   if ($form->{"original_partnumber"} &&
1302       ($form->{"partnumber"} eq $form->{"original_partnumber"})) {
1303     $form->{partnumber} = "";
1304   }
1305   &save;
1306   $lxdebug->leave_sub();
1307 }
1308
1309 sub delete {
1310   $lxdebug->enter_sub();
1311
1312   $auth->assert('part_service_assembly_edit');
1313
1314   # saving the history
1315   if(!exists $form->{addition}) {
1316     $form->{snumbers}  = qq|partnumber_| . $form->{partnumber};
1317     $form->{addition}  = "DELETED";
1318     $form->{what_done} = "part";
1319     $form->save_history;
1320   }
1321   # /saving the history
1322   my $rc = IC->delete(\%myconfig, \%$form);
1323
1324   # redirect
1325   $form->redirect($locale->text('Item deleted!')) if ($rc > 0);
1326   $form->error($locale->text('Cannot delete item!'));
1327
1328   $lxdebug->leave_sub();
1329 }
1330
1331 sub price_row {
1332   $lxdebug->enter_sub();
1333
1334   $auth->assert('part_service_assembly_details');
1335
1336   my ($numrows) = @_;
1337
1338   my @PRICES = map +{
1339     pricegroup    => $form->{"pricegroup_$_"},
1340     pricegroup_id => $form->{"pricegroup_id_$_"},
1341     price         => $form->{"price_$_"},
1342   }, 1 .. $numrows;
1343
1344   print $form->parse_html_template('ic/price_row', { PRICES => \@PRICES });
1345
1346   $lxdebug->leave_sub();
1347 }
1348
1349 sub ajax_autocomplete {
1350   $main::lxdebug->enter_sub();
1351
1352   my $form     = $main::form;
1353   my %myconfig = %main::myconfig;
1354
1355   $form->{column}          = 'description'     unless $form->{column} =~ /^partnumber|description$/;
1356   $form->{$form->{column}} = $form->{q}           || '';
1357   $form->{limit}           = ($form->{limit} * 1) || 10;
1358   $form->{searchitems}   ||= '';
1359
1360   my @results = IC->all_parts(\%myconfig, $form);
1361
1362   print $form->ajax_response_header(),
1363         $form->parse_html_template('ic/ajax_autocomplete');
1364
1365   $main::lxdebug->leave_sub();
1366 }
1367
1368 sub display_form {
1369   $::lxdebug->enter_sub;
1370
1371   $auth->assert('part_service_assembly_edit');
1372
1373   relink_accounts();
1374
1375   $::form->language_payment(\%::myconfig);
1376
1377   Common::webdav_folder($::form);
1378
1379   form_header();
1380   price_row($::form->{price_rows});
1381   makemodel_row(++$::form->{makemodel_rows}) if $::form->{item} =~ /^(part|service)$/;
1382   assembly_row(++$::form->{assembly_rows})   if $::form->{item} eq 'assembly';
1383
1384   form_footer();
1385
1386   $::lxdebug->leave_sub;
1387 }
1388
1389 sub back_to_record {
1390   _check_io_auth();
1391
1392
1393   delete @{$::form}{qw(action action_add action_back_to_record back_sub description item notes partnumber sellprice taxaccount2 unit vc)};
1394
1395   $::auth->restore_form_from_session($::form->{previousform}, clobber => 1);
1396   $::form->{rowcount}--;
1397   $::form->{action}   = 'display_form';
1398   $::form->{callback} = $::form->{script} . '?' . join('&', map { $::form->escape($_) . '=' . $::form->escape($::form->{$_}) } sort keys %{ $::form });
1399   $::form->redirect;
1400 }
1401
1402 sub continue { call_sub($form->{"nextsub"}); }
1403
1404 sub dispatcher {
1405   my $action = first { $::form->{"action_${_}"} } qw(add back_to_record);
1406   $::form->error($::locale->text('No action defined.')) unless $action;
1407
1408   $::form->{dispatched_action} = $action;
1409   call_sub($action);
1410 }