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