fdeecc473b8af30c9ab252a78e84c027c73b0ce8
[kivitendo-erp.git] / SL / IC.pm
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 #  Contributors:
16 #
17 # This program is free software; you can redistribute it and/or modify
18 # it under the terms of the GNU General Public License as published by
19 # the Free Software Foundation; either version 2 of the License, or
20 # (at your option) any later version.
21 #
22 # This program is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25 # GNU General Public License for more details.
26 # You should have received a copy of the GNU General Public License
27 # along with this program; if not, write to the Free Software
28 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
29 #======================================================================
30 #
31 # Inventory Control backend
32 #
33 #======================================================================
34
35 package IC;
36
37 use Data::Dumper;
38 use YAML;
39
40 use SL::DBUtils;
41
42 sub get_part {
43   $main::lxdebug->enter_sub();
44
45   my ($self, $myconfig, $form) = @_;
46
47   # connect to db
48   my $dbh = $form->dbconnect($myconfig);
49
50   my $sth;
51
52   my $query =
53     qq|SELECT p.*,
54          c1.accno AS inventory_accno,
55          c2.accno AS income_accno,
56          c3.accno AS expense_accno,
57          pg.partsgroup
58        FROM parts p
59        LEFT JOIN chart c1 ON (p.inventory_accno_id = c1.id)
60        LEFT JOIN chart c2 ON (p.income_accno_id = c2.id)
61        LEFT JOIN chart c3 ON (p.expense_accno_id = c3.id)
62        LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
63        WHERE p.id = ? |;
64   my $ref = selectfirst_hashref_query($form, $dbh, $query, conv_i($form->{id}));
65
66   # copy to $form variables
67   map { $form->{$_} = $ref->{$_} } (keys %{$ref});
68
69   $form->{onhand} *= 1;
70
71   my %oid = ('Pg'     => 'a.oid',
72              'Oracle' => 'a.rowid');
73
74   # part or service item
75   $form->{item} = ($form->{inventory_accno}) ? 'part' : 'service';
76   if ($form->{assembly}) {
77     $form->{item} = 'assembly';
78
79     # retrieve assembly items
80     $query =
81       qq|SELECT p.id, p.partnumber, p.description,
82            p.sellprice, p.lastcost, p.weight, a.qty, a.bom, p.unit,
83            pg.partsgroup, p.price_factor_id, pfac.factor AS price_factor
84          FROM parts p
85          JOIN assembly a ON (a.parts_id = p.id)
86          LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
87          LEFT JOIN price_factors pfac ON pfac.id = p.price_factor_id
88          WHERE (a.id = ?)
89          ORDER BY $oid{$myconfig->{dbdriver}}|;
90     $sth = prepare_execute_query($form, $dbh, $query, conv_i($form->{id}));
91
92     $form->{assembly_rows} = 0;
93     while (my $ref = $sth->fetchrow_hashref(NAME_lc)) {
94       $form->{assembly_rows}++;
95       foreach my $key (keys %{$ref}) {
96         $form->{"${key}_$form->{assembly_rows}"} = $ref->{$key};
97       }
98     }
99     $sth->finish;
100
101   }
102
103   # setup accno hash for <option checked> {amount} is used in create_links
104   $form->{amount}{IC}         = $form->{inventory_accno};
105   $form->{amount}{IC_income}  = $form->{income_accno};
106   $form->{amount}{IC_sale}    = $form->{income_accno};
107   $form->{amount}{IC_expense} = $form->{expense_accno};
108   $form->{amount}{IC_cogs}    = $form->{expense_accno};
109
110   my @pricegroups          = ();
111   my @pricegroups_not_used = ();
112
113   # get prices
114   $query =
115     qq|SELECT p.parts_id, p.pricegroup_id, p.price,
116          (SELECT pg.pricegroup
117           FROM pricegroup pg
118           WHERE pg.id = p.pricegroup_id) AS pricegroup
119        FROM prices p
120        WHERE (parts_id = ?)
121        ORDER BY pricegroup|;
122   $sth = prepare_execute_query($form, $dbh, $query, conv_i($form->{id}));
123
124   #for pricegroups
125   my $i = 1;
126   while (($form->{"klass_$i"}, $form->{"pricegroup_id_$i"},
127           $form->{"price_$i"}, $form->{"pricegroup_$i"})
128          = $sth->fetchrow_array()) {
129     $form->{"price_$i"} = $form->round_amount($form->{"price_$i"}, 5);
130     $form->{"price_$i"} = $form->format_amount($myconfig, $form->{"price_$i"}, -2);
131     push @pricegroups, $form->{"pricegroup_id_$i"};
132     $i++;
133   }
134
135   $sth->finish;
136
137   # get pricegroups
138   $query = qq|SELECT id, pricegroup FROM pricegroup|;
139   $form->{PRICEGROUPS} = selectall_hashref_query($form, $dbh, $query);
140
141   #find not used pricegroups
142   while ($tmp = pop(@{ $form->{PRICEGROUPS} })) {
143     my $in_use = 0;
144     foreach my $item (@pricegroups) {
145       if ($item eq $tmp->{id}) {
146         $in_use = 1;
147         last;
148       }
149     }
150     push(@pricegroups_not_used, $tmp) unless ($in_use);
151   }
152
153   # if not used pricegroups are avaible
154   if (@pricegroups_not_used) {
155
156     foreach $name (@pricegroups_not_used) {
157       $form->{"klass_$i"} = "$name->{id}";
158       $form->{"price_$i"} = $form->round_amount($form->{sellprice}, 5);
159       $form->{"price_$i"} = $form->format_amount($myconfig, $form->{"price_$i"}, -2);
160       $form->{"pricegroup_id_$i"} = "$name->{id}";
161       $form->{"pricegroup_$i"}    = "$name->{pricegroup}";
162       $i++;
163     }
164   }
165
166   #correct rows
167   $form->{price_rows} = $i - 1;
168
169   unless ($form->{item} eq 'service') {
170
171     # get makes
172     if ($form->{makemodel}) {
173       $query = qq|SELECT m.make, m.model FROM makemodel m | .
174                qq|WHERE m.parts_id = ?|;
175       @values = ($form->{id});
176       $sth = $dbh->prepare($query);
177       $sth->execute(@values) || $form->dberror("$query (" . join(', ', @values) . ")");
178
179       my $i = 1;
180       while (($form->{"make_$i"}, $form->{"model_$i"}) = $sth->fetchrow_array)
181       {
182         $i++;
183       }
184       $sth->finish;
185       $form->{makemodel_rows} = $i - 1;
186
187     }
188   }
189
190   # get translations
191   $form->{language_values} = "";
192   $query = qq|SELECT language_id, translation, longdescription
193               FROM translation
194               WHERE parts_id = ?|;
195   my $trq = prepare_execute_query($form, $dbh, $query, conv_i($form->{id}));
196   while (my $tr = $trq->fetchrow_hashref(NAME_lc)) {
197     $form->{language_values} .= "---+++---" . join('--++--', @{$tr}{qw(language_id translation longdescription)});
198   }
199   $trq->finish;
200
201   # now get accno for taxes
202   $query =
203     qq|SELECT c.accno
204        FROM chart c, partstax pt
205        WHERE (pt.chart_id = c.id) AND (pt.parts_id = ?)|;
206   $sth = prepare_execute_query($form, $dbh, $query, conv_i($form->{id}));
207   while (($key) = $sth->fetchrow_array) {
208     $form->{amount}{$key} = $key;
209   }
210
211   $sth->finish;
212
213   # is it an orphan
214   my @referencing_tables = qw(invoice orderitems inventory rmaitems);
215   my %column_map         = ( );
216   my $parts_id           = conv_i($form->{id});
217
218   $form->{orphaned}      = 1;
219
220   foreach my $table (@referencing_tables) {
221     my $column  = $column_map{$table} || 'parts_id';
222     $query      = qq|SELECT $column FROM $table WHERE $column = ? LIMIT 1|;
223     my ($found) = selectrow_query($form, $dbh, $query, $parts_id);
224
225     if ($found) {
226       $form->{orphaned} = 0;
227       last;
228     }
229   }
230
231   $form->{"unit_changeable"} = $form->{orphaned};
232
233   $dbh->disconnect;
234
235   $main::lxdebug->leave_sub();
236 }
237
238 sub get_pricegroups {
239   $main::lxdebug->enter_sub();
240
241   my ($self, $myconfig, $form) = @_;
242
243   my $dbh = $form->dbconnect($myconfig);
244
245   # get pricegroups
246   my $query = qq|SELECT id, pricegroup FROM pricegroup|;
247   my $pricegroups = selectall_hashref_query($form, $dbh, $query);
248
249   my $i = 1;
250   foreach $pg (@{ $pricegroups }) {
251     $form->{"klass_$i"} = "$pg->{id}";
252     $form->{"price_$i"} = $form->format_amount($myconfig, $form->{"price_$i"}, -2);
253     $form->{"pricegroup_id_$i"} = "$pg->{id}";
254     $form->{"pricegroup_$i"}    = "$pg->{pricegroup}";
255     $i++;
256   }
257
258   #correct rows
259   $form->{price_rows} = $i - 1;
260
261   $dbh->disconnect;
262
263   $main::lxdebug->leave_sub();
264
265   return $pricegroups;
266 }
267
268 sub retrieve_buchungsgruppen {
269   $main::lxdebug->enter_sub();
270
271   my ($self, $myconfig, $form) = @_;
272
273   my ($query, $sth);
274
275   my $dbh = $form->dbconnect($myconfig);
276
277   # get buchungsgruppen
278   $query = qq|SELECT id, description FROM buchungsgruppen ORDER BY sortkey|;
279   $form->{BUCHUNGSGRUPPEN} = selectall_hashref_query($form, $dbh, $query);
280
281   $main::lxdebug->leave_sub();
282 }
283
284 sub save {
285   $main::lxdebug->enter_sub();
286
287   my ($self, $myconfig, $form) = @_;
288   my @values;
289   # connect to database, turn off AutoCommit
290   my $dbh = $form->dbconnect_noauto($myconfig);
291
292   # save the part
293   # make up a unique handle and store in partnumber field
294   # then retrieve the record based on the unique handle to get the id
295   # replace the partnumber field with the actual variable
296   # add records for makemodel
297
298   # if there is a $form->{id} then replace the old entry
299   # delete all makemodel entries and add the new ones
300
301   # undo amount formatting
302   map { $form->{$_} = $form->parse_amount($myconfig, $form->{$_}) }
303     qw(rop weight listprice sellprice gv lastcost);
304
305   my $makemodel = (($form->{make_1}) || ($form->{model_1})) ? 1 : 0;
306
307   $form->{assembly} = ($form->{item} eq 'assembly') ? 1 : 0;
308
309   my ($query, $sth);
310
311   my $priceupdate = ', priceupdate = current_date';
312
313   if ($form->{id}) {
314
315     # get old price
316     $query = qq|SELECT sellprice, weight FROM parts WHERE id = ?|;
317     my ($sellprice, $weight) = selectrow_query($form, $dbh, $query, conv_i($form->{id}));
318
319     # if item is part of an assembly adjust all assemblies
320     $query = qq|SELECT id, qty FROM assembly WHERE parts_id = ?|;
321     $sth = prepare_execute_query($form, $dbh, $query, conv_i($form->{id}));
322     while (my ($id, $qty) = $sth->fetchrow_array) {
323       &update_assembly($dbh, $form, $id, $qty, $sellprice * 1, $weight * 1);
324     }
325     $sth->finish;
326
327     if ($form->{item} ne 'service') {
328       # delete makemodel records
329       do_query($form, $dbh, qq|DELETE FROM makemodel WHERE parts_id = ?|, conv_i($form->{id}));
330     }
331
332     if ($form->{item} eq 'assembly') {
333       # delete assembly records
334       do_query($form, $dbh, qq|DELETE FROM assembly WHERE id = ?|, conv_i($form->{id}));
335     }
336
337     # delete tax records
338     do_query($form, $dbh, qq|DELETE FROM partstax WHERE parts_id = ?|, conv_i($form->{id}));
339
340     # delete translations
341     do_query($form, $dbh, qq|DELETE FROM translation WHERE parts_id = ?|, conv_i($form->{id}));
342
343     # Check whether or not the prices have changed. If they haven't
344     # then 'priceupdate' should not be updated.
345     my $previous_values = selectfirst_hashref_query($form, $dbh, qq|SELECT * FROM parts WHERE id = ?|, conv_i($form->{id})) || {};
346     if (   ($previous_values->{sellprice} == $form->{sellprice})
347         && ($previous_values->{lastcost}  == $form->{lastcost})
348         && ($previous_values->{listprice} == $form->{listprice})) {
349       $priceupdate = '';
350     }
351
352   } else {
353     my ($count) = selectrow_query($form, $dbh, qq|SELECT COUNT(*) FROM parts WHERE partnumber = ?|, $form->{partnumber});
354     if ($count) {
355       $main::lxdebug->leave_sub();
356       return 3;
357     }
358
359     ($form->{id}) = selectrow_query($form, $dbh, qq|SELECT nextval('id')|);
360     do_query($form, $dbh, qq|INSERT INTO parts (id, partnumber) VALUES (?, '')|, $form->{id});
361
362     $form->{orphaned} = 1;
363     if ($form->{partnumber} eq "" && $form->{"item"} eq "service") {
364       $form->{partnumber} = $form->update_defaults($myconfig, "servicenumber");
365     }
366     if ($form->{partnumber} eq "" && $form->{"item"} ne "service") {
367       $form->{partnumber} = $form->update_defaults($myconfig, "articlenumber");
368     }
369
370   }
371   my $partsgroup_id = 0;
372
373   if ($form->{partsgroup}) {
374     ($partsgroup, $partsgroup_id) = split(/--/, $form->{partsgroup});
375   }
376
377   my ($subq_inventory, $subq_expense, $subq_income);
378   if ($form->{"item"} eq "part") {
379     $subq_inventory =
380       qq|(SELECT bg.inventory_accno_id
381           FROM buchungsgruppen bg
382           WHERE bg.id = | . conv_i($form->{"buchungsgruppen_id"}, 'NULL') . qq|)|;
383   } else {
384     $subq_inventory = "NULL";
385   }
386
387   if ($form->{"item"} ne "assembly") {
388     $subq_expense =
389       qq|(SELECT bg.expense_accno_id_0
390           FROM buchungsgruppen bg
391           WHERE bg.id = | . conv_i($form->{"buchungsgruppen_id"}, 'NULL') . qq|)|;
392   } else {
393     $subq_expense = "NULL";
394   }
395
396   $query =
397     qq|UPDATE parts SET
398          partnumber = ?,
399          description = ?,
400          makemodel = ?,
401          alternate = 'f',
402          assembly = ?,
403          listprice = ?,
404          sellprice = ?,
405          lastcost = ?,
406          weight = ?,
407          unit = ?,
408          notes = ?,
409          formel = ?,
410          rop = ?,
411          bin = ?,
412          buchungsgruppen_id = ?,
413          payment_id = ?,
414          inventory_accno_id = $subq_inventory,
415          income_accno_id = (SELECT bg.income_accno_id_0 FROM buchungsgruppen bg WHERE bg.id = ?),
416          expense_accno_id = $subq_expense,
417          obsolete = ?,
418          image = ?,
419          drawing = ?,
420          shop = ?,
421          ve = ?,
422          gv = ?,
423          ean = ?,
424          not_discountable = ?,
425          microfiche = ?,
426          partsgroup_id = ?,
427          price_factor_id = ?
428          $priceupdate
429        WHERE id = ?|;
430   @values = ($form->{partnumber},
431              $form->{description},
432              $makemodel ? 't' : 'f',
433              $form->{assembly} ? 't' : 'f',
434              $form->{listprice},
435              $form->{sellprice},
436              $form->{lastcost},
437              $form->{weight},
438              $form->{unit},
439              $form->{notes},
440              $form->{formel},
441              $form->{rop},
442              $form->{bin},
443              conv_i($form->{buchungsgruppen_id}),
444              conv_i($form->{payment_id}),
445              conv_i($form->{buchungsgruppen_id}),
446              $form->{obsolete} ? 't' : 'f',
447              $form->{image},
448              $form->{drawing},
449              $form->{shop} ? 't' : 'f',
450              conv_i($form->{ve}),
451              conv_i($form->{gv}),
452              $form->{ean},
453              $form->{not_discountable} ? 't' : 'f',
454              $form->{microfiche},
455              conv_i($partsgroup_id),
456              conv_i($form->{price_factor_id}),
457              conv_i($form->{id})
458   );
459   do_query($form, $dbh, $query, @values);
460
461   # delete translation records
462   do_query($form, $dbh, qq|DELETE FROM translation WHERE parts_id = ?|, conv_i($form->{id}));
463
464   if ($form->{language_values} ne "") {
465     foreach $item (split(/---\+\+\+---/, $form->{language_values})) {
466       my ($language_id, $translation, $longdescription) = split(/--\+\+--/, $item);
467       if ($translation ne "") {
468         $query = qq|INSERT into translation (parts_id, language_id, translation, longdescription)
469                     VALUES ( ?, ?, ?, ? )|;
470         @values = (conv_i($form->{id}), conv_i($language_id), $translation, $longdescription);
471         do_query($form, $dbh, $query, @values);
472       }
473     }
474   }
475
476   # delete price records
477   do_query($form, $dbh, qq|DELETE FROM prices WHERE parts_id = ?|, conv_i($form->{id}));
478
479   # insert price records only if different to sellprice
480   for my $i (1 .. $form->{price_rows}) {
481     if ($form->{"price_$i"} eq "0") {
482       $form->{"price_$i"} = $form->{sellprice};
483     }
484     if (
485         (   $form->{"price_$i"}
486          || $form->{"klass_$i"}
487          || $form->{"pricegroup_id_$i"})
488         and $form->{"price_$i"} != $form->{sellprice}
489       ) {
490       #$klass = $form->parse_amount($myconfig, $form->{"klass_$i"});
491       $price = $form->parse_amount($myconfig, $form->{"price_$i"});
492       $pricegroup_id =
493         $form->parse_amount($myconfig, $form->{"pricegroup_id_$i"});
494       $query = qq|INSERT INTO prices (parts_id, pricegroup_id, price) | .
495                qq|VALUES(?, ?, ?)|;
496       @values = (conv_i($form->{id}), conv_i($pricegroup_id), $price);
497       do_query($form, $dbh, $query, @values);
498     }
499   }
500
501   # insert makemodel records
502   unless ($form->{item} eq 'service') {
503     for my $i (1 .. $form->{makemodel_rows}) {
504       if (($form->{"make_$i"}) || ($form->{"model_$i"})) {
505
506         $query = qq|INSERT INTO makemodel (parts_id, make, model) | .
507                              qq|VALUES (?, ?, ?)|;
508                     @values = (conv_i($form->{id}), conv_i($form->{"make_$i"}), $form->{"model_$i"});
509
510         do_query($form, $dbh, $query, @values);
511       }
512     }
513   }
514
515   # insert taxes
516   foreach $item (split(/ /, $form->{taxaccounts})) {
517     if ($form->{"IC_tax_$item"}) {
518       $query =
519         qq|INSERT INTO partstax (parts_id, chart_id)
520            VALUES (?, (SELECT id FROM chart WHERE accno = ?))|;
521                         @values = (conv_i($form->{id}), $item);
522       do_query($form, $dbh, $query, @values);
523     }
524   }
525
526   # add assembly records
527   if ($form->{item} eq 'assembly') {
528
529     for my $i (1 .. $form->{assembly_rows}) {
530       $form->{"qty_$i"} = $form->parse_amount($myconfig, $form->{"qty_$i"});
531
532       if ($form->{"qty_$i"} != 0) {
533         $form->{"bom_$i"} *= 1;
534         $query = qq|INSERT INTO assembly (id, parts_id, qty, bom) | .
535                              qq|VALUES (?, ?, ?, ?)|;
536                     @values = (conv_i($form->{id}), conv_i($form->{"id_$i"}), conv_i($form->{"qty_$i"}), $form->{"bom_$i"} ? 't' : 'f');
537         do_query($form, $dbh, $query, @values);
538       }
539     }
540
541     @a = localtime;
542     $a[5] += 1900;
543     $a[4]++;
544     my $shippingdate = "$a[5]-$a[4]-$a[3]";
545
546     $form->get_employee($dbh);
547
548   }
549
550   #set expense_accno=inventory_accno if they are different => bilanz
551   $vendor_accno =
552     ($form->{expense_accno} != $form->{inventory_accno})
553     ? $form->{inventory_accno}
554     : $form->{expense_accno};
555
556   # get tax rates and description
557   $accno_id =
558     ($form->{vc} eq "customer") ? $form->{income_accno} : $vendor_accno;
559   $query =
560     qq|SELECT c.accno, c.description, t.rate, t.taxnumber
561        FROM chart c, tax t
562        WHERE (c.id = t.chart_id) AND (t.taxkey IN (SELECT taxkey_id FROM chart where accno = ?))
563        ORDER BY c.accno|;
564   $stw = prepare_execute_query($form, $dbh, $query, $accno_id);
565
566   $form->{taxaccount} = "";
567   while ($ptr = $stw->fetchrow_hashref(NAME_lc)) {
568     $form->{taxaccount} .= "$ptr->{accno} ";
569     if (!($form->{taxaccount2} =~ /\Q$ptr->{accno}\E/)) {
570       $form->{"$ptr->{accno}_rate"}        = $ptr->{rate};
571       $form->{"$ptr->{accno}_description"} = $ptr->{description};
572       $form->{"$ptr->{accno}_taxnumber"}   = $ptr->{taxnumber};
573       $form->{taxaccount2} .= " $ptr->{accno} ";
574     }
575   }
576
577   # commit
578   my $rc = $dbh->commit;
579   $dbh->disconnect;
580
581   $main::lxdebug->leave_sub();
582
583   return $rc;
584 }
585
586 sub update_assembly {
587   $main::lxdebug->enter_sub();
588
589   my ($dbh, $form, $id, $qty, $sellprice, $weight) = @_;
590
591   my $query = qq|SELECT id, qty FROM assembly WHERE parts_id = ?|;
592   my $sth = prepare_execute_query($form, $dbh, $query, conv_i($id));
593
594   while (my ($pid, $aqty) = $sth->fetchrow_array) {
595     &update_assembly($dbh, $form, $pid, $aqty * $qty, $sellprice, $weight);
596   }
597   $sth->finish;
598
599   $query =
600     qq|UPDATE parts SET sellprice = sellprice + ?, weight = weight + ?
601        WHERE id = ?|;
602   @values = ($qty * ($form->{sellprice} - $sellprice),
603              $qty * ($form->{weight} - $weight), conv_i($id));
604   do_query($form, $dbh, $query, @values);
605
606   $main::lxdebug->leave_sub();
607 }
608
609 sub retrieve_assemblies {
610   $main::lxdebug->enter_sub();
611
612   my ($self, $myconfig, $form) = @_;
613
614   # connect to database
615   my $dbh = $form->dbconnect($myconfig);
616
617   my $where = qq|NOT p.obsolete|;
618   my @values;
619
620   if ($form->{partnumber}) {
621     $where .= qq| AND (p.partnumber ILIKE ?)|;
622     push(@values, '%' . $form->{partnumber} . '%');
623   }
624
625   if ($form->{description}) {
626     $where .= qq| AND (p.description ILIKE ?)|;
627     push(@values, '%' . $form->{description} . '%');
628   }
629
630   # retrieve assembly items
631   my $query =
632     qq|SELECT p.id, p.partnumber, p.description,
633          p.bin, p.onhand, p.rop,
634          (SELECT sum(p2.inventory_accno_id)
635           FROM parts p2, assembly a
636           WHERE (p2.id = a.parts_id) AND (a.id = p.id)) AS inventory
637        FROM parts p
638        WHERE NOT p.obsolete AND p.assembly $where|;
639
640   $form->{assembly_items} = selectall_hashref_query($form, $dbh, $query, @values);
641
642   $dbh->disconnect;
643
644   $main::lxdebug->leave_sub();
645 }
646
647 sub delete {
648   $main::lxdebug->enter_sub();
649
650   my ($self, $myconfig, $form) = @_;
651   my @values = (conv_i($form->{id}));
652   # connect to database, turn off AutoCommit
653   my $dbh = $form->dbconnect_noauto($myconfig);
654
655   my %columns = ( "assembly" => "id", "parts" => "id" );
656
657   for my $table (qw(prices partstax makemodel inventory assembly license translation parts)) {
658     my $column = defined($columns{$table}) ? $columns{$table} : "parts_id";
659     do_query($form, $dbh, qq|DELETE FROM $table WHERE $column = ?|, @values);
660   }
661
662   # commit
663   my $rc = $dbh->commit;
664   $dbh->disconnect;
665
666   $main::lxdebug->leave_sub();
667
668   return $rc;
669 }
670
671 sub assembly_item {
672   $main::lxdebug->enter_sub();
673
674   my ($self, $myconfig, $form) = @_;
675
676   my $i = $form->{assembly_rows};
677   my $var;
678   my $where = qq|1 = 1|;
679   my @values;
680
681   my %columns = ("partnumber" => "p", "description" => "p", "partsgroup" => "pg");
682
683   while (my ($column, $table) = each(%columns)) {
684     next unless ($form->{"${column}_$i"});
685     $where .= qq| AND ${table}.${column} ILIKE ?|;
686     push(@values, '%' . $form->{"${column}_$i"} . '%');
687   }
688
689   if ($form->{id}) {
690     $where .= qq| AND NOT (p.id = ?)|;
691     push(@values, conv_i($form->{id}));
692   }
693
694   if ($partnumber) {
695     $where .= qq| ORDER BY p.partnumber|;
696   } else {
697     $where .= qq| ORDER BY p.description|;
698   }
699
700   # connect to database
701   my $dbh = $form->dbconnect($myconfig);
702
703   my $query =
704     qq|SELECT p.id, p.partnumber, p.description, p.sellprice, p.weight, p.onhand, p.unit, pg.partsgroup,
705               p.price_factor_id, pfac.factor AS price_factor
706        FROM parts p
707        LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
708        LEFT JOIN price_factors pfac ON pfac.id = p.price_factor_id
709        WHERE $where|;
710   $form->{item_list} = selectall_hashref_query($form, $dbh, $query, @values);
711
712   $dbh->disconnect;
713
714   $main::lxdebug->leave_sub();
715 }
716
717 #
718 # Report for Wares.
719 # Warning, deep magic ahead.
720 # This function gets all parts from the database according to the filters specified
721 #
722 # specials:
723 #   sort revers  - sorting field + direction
724 #   top100
725 #
726 # simple filter strings (every one of those also has a column flag prefixed with 'l_' associated):
727 #   partnumber ean description partsgroup microfiche drawing
728 #
729 # column flags:
730 #   l_partnumber l_description l_listprice l_sellprice l_lastcost l_priceupdate l_weight l_unit l_bin l_rop l_image l_drawing l_microfiche l_partsgroup
731 #
732 # exclusives:
733 #   itemstatus  = active | onhand | short | obsolete | orphaned
734 #   searchitems = part | assembly | service
735 #
736 # joining filters:
737 #   make model                               - makemodel
738 #   serialnumber transdatefrom transdateto   - invoice/orderitems
739 #
740 # binary flags:
741 #   bought sold onorder ordered rfq quoted   - aggreg joins with invoices/orders
742 #   l_linetotal l_subtotal                   - aggreg joins to display totals (complicated) - NOT IMPLEMENTED here, implementation at frontend
743 #   l_soldtotal                              - aggreg join to display total of sold quantity
744 #   onhand                                   - as above, but masking the simple itemstatus results (doh!)
745 #   short                                    - NOT IMPLEMENTED as form filter, only as itemstatus option
746 #   l_serialnumber                           - belonges to serialnumber filter
747 #   l_deliverydate                           - displays deliverydate is sold etc. flags are active
748 #   l_soldtotal                              - aggreg join to display total of sold quantity, works as long as there's no bullshit in soldtotal
749 #
750 # not working:
751 #   onhand                                   - as above, but masking the simple itemstatus results (doh!)
752 #   masking of onhand in bsooqr mode         - ToDO: fixme
753 #
754 # disabled sanity checks and changes:
755 #  - searchitems = assembly will no longer disable bought
756 #  - searchitems = service will no longer disable make and model, although services don't have make/model, it doesn't break the query
757 #  - itemstatus = orphaned will no longer disable onhand short bought sold onorder ordered rfq quoted transdate[from|to]
758 #  - itemstatus = obsolete will no longer disable onhand, short
759 #  - allow sorting by ean
760 #  - serialnumber filter also works if l_serialnumber isn't ticked
761 #  - onhand doesn't get masked by it's oi or invoice counterparts atm. ToDO: fix this
762 #  - sorting will now change sorting if the requested sorting column isn't checked and doesn't get checked as a side effect
763 #
764 sub all_parts {
765   $main::lxdebug->enter_sub();
766
767   my ($self, $myconfig, $form) = @_;
768   my $dbh = $form->get_standard_dbh($myconfig);
769
770   $form->{parts}     = +{ };
771   $form->{soldtotal} = undef if $form->{l_soldtotal}; # security fix. top100 insists on putting strings in there...
772
773   my @simple_filters       = qw(partnumber ean description partsgroup microfiche drawing onhand);
774   my @makemodel_filters    = qw(make model);
775   my @invoice_oi_filters   = qw(serialnumber soldtotal);
776   my @apoe_filters         = qw(transdate);
777   my @all_columns          = (@simple_filters, @makemodel_filters, @apoe_filters, qw(serialnumber));
778   my @simple_l_switches    = (@all_columns, qw(listprice sellprice lastcost priceupdate weight unit bin rop image));
779   my @oe_flags             = qw(bought sold onorder ordered rfq quoted);
780   my @qsooqr_flags         = qw(invnumber ordnumber quonumber trans_id name module);
781   my @deliverydate_flags   = qw(deliverydate);
782 #  my @other_flags          = qw(onhand); # ToDO: implement these
783 #  my @inactive_flags       = qw(l_subtotal short l_linetotal);
784
785   my %joins = (
786     partsgroup => 'LEFT JOIN partsgroup pg      ON (pg.id       = p.partsgroup_id)',
787     makemodel  => 'LEFT JOIN makemodel mm       ON (mm.parts_id = p.id)',
788     pfac       => 'LEFT JOIN price_factors pfac ON (pfac.id     = p.price_factor_id)',
789     invoice_oi =>
790       q|LEFT JOIN (
791          SELECT parts_id, description, serialnumber, trans_id, unit, sellprice, qty,          assemblyitem, 'invoice'    AS ioi FROM invoice UNION
792          SELECT parts_id, description, serialnumber, trans_id, unit, sellprice, qty, FALSE AS assemblyitem, 'orderitems' AS ioi FROM orderitems
793        ) AS ioi ON ioi.parts_id = p.id|,
794     apoe       =>
795       q|LEFT JOIN (
796          SELECT id, transdate, 'ir' AS module, ordnumber, quonumber,         invnumber, FALSE AS quotation, NULL AS customer_id,         vendor_id, NULL AS deliverydate, 'invoice'    AS ioi FROM ap UNION
797          SELECT id, transdate, 'is' AS module, ordnumber, quonumber,         invnumber, FALSE AS quotation,         customer_id, NULL AS vendor_id,         deliverydate, 'invoice'    AS ioi FROM ar UNION
798          SELECT id, transdate, 'oe' AS module, ordnumber, quonumber, NULL AS invnumber,          quotation,         customer_id,         vendor_id, NULL AS deliverydate, 'orderitems' AS ioi FROM oe
799        ) AS apoe ON ((ioi.trans_id = apoe.id) AND (ioi.ioi = apoe.ioi))|,
800     cv         =>
801       q|LEFT JOIN (
802            SELECT id, name, 'customer' AS cv FROM customer UNION
803            SELECT id, name, 'vendor'   AS cv FROM vendor
804          ) AS cv ON cv.id = apoe.customer_id OR cv.id = apoe.vendor_id|,
805   );
806   my @join_order = qw(partsgroup makemodel invoice_oi apoe cv pfac);
807   my %joins_needed;
808
809   if (($form->{searchitems} eq 'assembly') && $form->{l_lastcost}) {
810     @simple_l_switches = grep { $_ ne 'lastcost' } @simple_l_switches;
811   }
812
813   #===== switches and simple filters ========#
814
815   my @select_tokens = qw(id factor);
816   my @where_tokens  = qw(1=1);
817   my @group_tokens  = ();
818
819   # special case transdate
820   if (grep { $form->{$_} } qw(transdatefrom transdateto)) {
821     $form->{"l_transdate"} = 1;
822     push @select_tokens, 'transdate';
823     for (qw(transdatefrom transdateto)) {
824       next unless $form->{$_};
825       push @where_tokens, sprintf "transdate %s ?", /from$/ ? '>=' : '<=';
826       push @bind_vars,    $form->{$_};
827     }
828   }
829
830   my %simple_filter_table_prefix = (
831      description  => 'p.',
832   );
833
834   foreach (@simple_filters, @makemodel_filters, @invoice_oi_filters) {
835     next unless $form->{$_};
836     $form->{"l_$_"} = '1'; # show the column
837     push @where_tokens, "$simple_filter_table_prefix{$_}$_ ILIKE ?";
838     push @bind_vars,    "%$form->{$_}%";
839   }
840
841   foreach (@simple_l_switches) {
842     next unless $form->{"l_$_"};
843     push @select_tokens, $_;
844   }
845
846   for ($form->{searchitems}) {
847     push @where_tokens, 'p.inventory_accno_id > 0'     if /part/;
848     push @where_tokens, 'p.inventory_accno_id IS NULL' if /service/;
849     push @where_tokens, 'NOT p.assembly'               if /service/;
850     push @where_tokens, '    p.assembly'               if /assembly/;
851   }
852
853   for ($form->{itemstatus}) {
854     push @where_tokens, 'p.id NOT IN
855         (SELECT DISTINCT parts_id FROM invoice UNION
856          SELECT DISTINCT parts_id FROM assembly UNION
857          SELECT DISTINCT parts_id FROM orderitems)'    if /orphaned/;
858     push @where_tokens, 'p.onhand = 0'                 if /orphaned/;
859     push @where_tokens, 'NOT p.obsolete'               if /active/;
860     push @where_tokens, '    p.obsolete',              if /obsolete/;
861     push @where_tokens, 'p.onhand > 0',                if /onhand/;
862     push @where_tokens, 'p.onhand < p.rop',            if /short/;
863   }
864
865   my $q_assembly_lastcost =
866     qq|(SELECT SUM(a_lc.qty * p_lc.lastcost / COALESCE(pfac_lc.factor, 1))
867         FROM assembly a_lc
868         LEFT JOIN parts p_lc            ON (a_lc.parts_id        = p_lc.id)
869         LEFT JOIN price_factors pfac_lc ON (p_lc.price_factor_id = pfac_lc.id)
870         WHERE (a_lc.id = p.id)) AS lastcost|;
871
872   my @sort_cols = (@simple_filters, qw(id bin priceupdate onhand invnumber ordnumber quonumber name serialnumber soldtotal deliverydate));
873   $form->{sort} = 'id' unless grep { $form->{"l_$_"} } grep { $form->{sort} eq $_ } @sort_cols;
874
875   my $sort_order = ($form->{revers} ? ' DESC' : ' ASC');
876
877   # special case: sorting by partnumber
878   # since partnumbers are expected to be prefixed integers, a special sorting is implemented sorting first lexically by prefix and then by suffix.
879   # and yes, that expression is designed to hold that array of regexes only once, so the map is kinda messy, sorry about that.
880   # ToDO: implement proper functional sorting
881   $form->{sort} = join ', ', map { push @select_tokens, $_; ($table_prefix{$_} = "substring(partnumber,'[") . $_ } qw|^[:digit:]]+') [:digit:]]+')::INTEGER|
882     if $form->{sort} eq 'partnumber';
883
884   my $order_clause = " ORDER BY $form->{sort} $sort_order";
885
886   my $limit_clause = " LIMIT 100" if $form->{top100};
887
888   #=== joins and complicated filters ========#
889
890   my $bsooqr = $form->{bought}  || $form->{sold}
891             || $form->{ordered} || $form->{onorder}
892             || $form->{quoted}  || $form->{rfq};
893
894   my @bsooqr;
895   push @select_tokens, @qsooqr_flags                                          if $bsooqr;
896   push @select_tokens, @deliverydate_flags                                    if $bsooqr && $form->{l_deliverydate};
897   push @select_tokens, $q_assembly_lastcost                                   if ($form->{searchitems} eq 'assembly') && $form->{l_lastcost};
898   push @bsooqr_tokens, q|module = 'ir' AND NOT ioi.assemblyitem|              if $form->{bought};
899   push @bsooqr_tokens, q|module = 'is' AND NOT ioi.assemblyitem|              if $form->{sold};
900   push @bsooqr_tokens, q|module = 'oe' AND NOT quotation AND cv = 'customer'| if $form->{ordered};
901   push @bsooqr_tokens, q|module = 'oe' AND NOT quotation AND cv = 'vendor'|   if $form->{onorder};
902   push @bsooqr_tokens, q|module = 'oe' AND     quotation AND cv = 'customer'| if $form->{quoted};
903   push @bsooqr_tokens, q|module = 'oe' AND     quotation AND cv = 'vendor'|   if $form->{rfq};
904   push @where_tokens, join ' OR ', map { "($_)" } @bsooqr_tokens              if $bsooqr;
905
906   $joins_needed{partsgroup}  = 1;
907   $joins_needed{pfac}        = 1;
908   $joins_needed{makemodel}   = 1 if grep { $form->{$_} || $form->{"l_$_"} } @makemodel_filters;
909   $joins_needed{cv}          = 1 if $bsooqr;
910   $joins_needed{apoe}        = 1 if $joins_needed{cv}   || grep { $form->{$_} || $form->{"l_$_"} } @apoe_filters;
911   $joins_needed{invoice_oi}  = 1 if $joins_needed{apoe} || grep { $form->{$_} || $form->{"l_$_"} } @invoice_oi_filters;
912
913   # special case for description search.
914   # up in the simple filter section the description filter got interpreted as something like: WHERE description ILIKE '%$form->{description}%'
915   # now we'd like to search also for the masked description entered in orderitems and invoice, so...
916   # find the old entries in of @where_tokens and @bind_vars, and adjust them
917   if ($joins_needed{invoice_oi}) {
918     for (my ($wi, $bi) = (0)x2; $wi <= $#where_tokens; $bi++ if $where_tokens[$wi++] =~ /\?/) {
919       next unless $where_tokens[$wi] =~ /^description ILIKE/;
920       splice @where_tokens, $wi, 1, 'p.description ILIKE ? OR ioi.description ILIKE ?';
921       splice @bind_vars,    $bi, 0, $bind_vars[$bi];
922       last;
923     }
924   }
925
926   # now the master trick: soldtotal.
927   if ($form->{l_soldtotal}) {
928     push @where_tokens, 'ioi.qty >= 0';
929     push @group_tokens, @select_tokens;
930      map { s/.*\sAS\s+//si } @group_tokens;
931     push @select_tokens, 'SUM(ioi.qty)';
932   }
933
934   #============= build query ================#
935
936   %table_prefix = (
937      %table_prefix,
938      deliverydate => 'apoe.', serialnumber => 'ioi.',
939      transdate    => 'apoe.', trans_id     => 'ioi.',
940      module       => 'apoe.', name         => 'cv.',
941      ordnumber    => 'apoe.', make         => 'mm.',
942      quonumber    => 'apoe.', model        => 'mm.',
943      invnumber    => 'apoe.', partsgroup   => 'pg.',
944      lastcost     => ' ',
945      factor       => 'pfac.',
946      'SUM(ioi.qty)' => ' ',
947   );
948
949   $table_prefix{$q_assembly_lastcost} = ' ';
950
951   my %renamed_columns = (
952     'factor'       => 'price_factor',
953     'SUM(ioi.qty)' => 'soldtotal',
954   );
955
956   map { $table_prefix{$_} = 'ioi.' } qw(description serialnumber qty unit) if $joins_needed{invoice_oi};
957   map { $renamed_columns{$_} = ' AS ' . $renamed_columns{$_} } keys %renamed_columns;
958
959   my $select_clause = join ', ',    map { ($table_prefix{$_} || "p.") . $_ . $renamed_columns{$_} } @select_tokens;
960   my $join_clause   = join ' ',     @joins{ grep $joins_needed{$_}, @join_order };
961   my $where_clause  = join ' AND ', map { "($_)" } @where_tokens;
962   my $group_clause  = ' GROUP BY ' . join ', ',    map { ($table_prefix{$_} || "p.") . $_ } @group_tokens if scalar @group_tokens;
963
964   my $query = qq|SELECT DISTINCT $select_clause FROM parts p $join_clause WHERE $where_clause $group_clause $order_clause $limit_clause|;
965
966   $form->{parts} = selectall_hashref_query($form, $dbh, $query, @bind_vars);
967
968   map { $_->{onhand} *= 1 } @{ $form->{parts} };
969
970   my @assemblies;
971   # include individual items for assemblies
972   if ($form->{searchitems} eq 'assembly' && $form->{bom}) {
973     $query =
974       qq|SELECT p.id, p.partnumber, p.description, a.qty AS onhand,
975            p.unit, p.bin,
976            p.sellprice, p.listprice, p.lastcost,
977            p.rop, p.weight, p.priceupdate,
978            p.image, p.drawing, p.microfiche
979          FROM parts p, assembly a
980          WHERE (p.id = a.parts_id) AND (a.id = ?)|;
981     $sth = prepare_query($form, $dbh, $query);
982
983     foreach $item (@{ $form->{parts} }) {
984       push(@assemblies, $item);
985       do_statement($form, $sth, $query, conv_i($item->{id}));
986
987       while ($ref = $sth->fetchrow_hashref(NAME_lc)) {
988         $ref->{assemblyitem} = 1;
989         push(@assemblies, $ref);
990       }
991       $sth->finish;
992     }
993
994     # copy assemblies to $form->{parts}
995     $form->{parts} = \@assemblies;
996   }
997
998   $main::lxdebug->leave_sub();
999 }
1000
1001 sub update_prices {
1002   $main::lxdebug->enter_sub();
1003
1004   my ($self, $myconfig, $form) = @_;
1005   my @where_values;
1006   my $where = '1 = 1';
1007   my $var;
1008
1009   my $group;
1010   my $limit;
1011
1012   if ($item ne 'make') {
1013     foreach my $item (qw(partnumber drawing microfiche make model pg.partsgroup)) {
1014       my $column = $item;
1015       $column =~ s/.*\.//;
1016       next unless ($form->{$column});
1017       $where .= qq| AND $item ILIKE ?|;
1018       push(@where_values, '%' . $form->{$column} . '%');
1019     }
1020   }
1021
1022   # special case for description
1023   if ($form->{description}
1024       && !(   $form->{bought}  || $form->{sold} || $form->{onorder}
1025            || $form->{ordered} || $form->{rfq} || $form->{quoted})) {
1026     $where .= qq| AND (p.description ILIKE ?)|;
1027     push(@where_values, '%' . $form->{description} . '%');
1028   }
1029
1030   # special case for serialnumber
1031   if ($form->{l_serialnumber} && $form->{serialnumber}) {
1032     $where .= qq| AND serialnumber ILIKE ?|;
1033     push(@where_values, '%' . $form->{serialnumber} . '%');
1034   }
1035
1036
1037   # items which were never bought, sold or on an order
1038   if ($form->{itemstatus} eq 'orphaned') {
1039     $form->{onhand}  = $form->{short}   = 0;
1040     $form->{bought}  = $form->{sold}    = 0;
1041     $form->{onorder} = $form->{ordered} = 0;
1042     $form->{rfq}     = $form->{quoted}  = 0;
1043
1044     $form->{transdatefrom} = $form->{transdateto} = "";
1045
1046     $where .=
1047       qq| AND (p.onhand = 0)
1048           AND p.id NOT IN
1049             (
1050               SELECT DISTINCT parts_id FROM invoice
1051               UNION
1052               SELECT DISTINCT parts_id FROM assembly
1053               UNION
1054               SELECT DISTINCT parts_id FROM orderitems
1055             )|;
1056   }
1057
1058   if ($form->{itemstatus} eq 'active') {
1059     $where .= qq| AND p.obsolete = '0'|;
1060   }
1061
1062   if ($form->{itemstatus} eq 'obsolete') {
1063     $where .= qq| AND p.obsolete = '1'|;
1064     $form->{onhand} = $form->{short} = 0;
1065   }
1066
1067   if ($form->{itemstatus} eq 'onhand') {
1068     $where .= qq| AND p.onhand > 0|;
1069   }
1070
1071   if ($form->{itemstatus} eq 'short') {
1072     $where .= qq| AND p.onhand < p.rop|;
1073   }
1074
1075   foreach my $column (qw(make model)) {
1076     next unless ($form->{$colum});
1077     $where .= qq| AND p.id IN (SELECT DISTINCT parts_id FROM makemodel WHERE $column ILIKE ?|;
1078     push(@where_values, '%' . $form->{$column} . '%');
1079   }
1080
1081   # connect to database
1082   my $dbh = $form->dbconnect_noauto($myconfig);
1083
1084   for my $column (qw(sellprice listprice)) {
1085     next if ($form->{$column} eq "");
1086
1087     my $value = $form->parse_amount($myconfig, $form->{$column});
1088     my $operator = '+';
1089
1090     if ($form->{"${column}_type"} eq "percent") {
1091       $value = ($value / 100) + 1;
1092       $operator = '*';
1093     }
1094
1095     $query =
1096       qq|UPDATE parts SET $column = $column $operator ?
1097          WHERE id IN
1098            (SELECT p.id
1099             FROM parts p
1100             LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
1101             WHERE $where)|;
1102     do_query($from, $dbh, $query, $value, @where_values);
1103   }
1104
1105   my $q_add =
1106     qq|UPDATE prices SET price = price + ?
1107        WHERE parts_id IN
1108          (SELECT p.id
1109           FROM parts p
1110           LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
1111           WHERE $where) AND (pricegroup_id = ?)|;
1112   my $sth_add = prepare_query($form, $dbh, $q_add);
1113
1114   my $q_multiply =
1115     qq|UPDATE prices SET price = price * ?
1116        WHERE parts_id IN
1117          (SELECT p.id
1118           FROM parts p
1119           LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
1120           WHERE $where) AND (pricegroup_id = ?)|;
1121   my $sth_multiply = prepare_query($form, $dbh, $q_multiply);
1122
1123   for my $i (1 .. $form->{price_rows}) {
1124     next if ($form->{"price_$i"} eq "");
1125
1126     my $value = $form->parse_amount($myconfig, $form->{"price_$i"});
1127
1128     if ($form->{"pricegroup_type_$i"} eq "percent") {
1129       do_statement($form, $sth_multiply, $q_multiply, ($value / 100) + 1, @where_values, conv_i($form->{"pricegroup_id_$i"}));
1130     } else {
1131       do_statement($form, $sth_add, $q_add, $value, @where_values, conv_i($form->{"pricegroup_id_$i"}));
1132     }
1133   }
1134
1135   $sth_add->finish();
1136   $sth_multiply->finish();
1137
1138   my $rc= $dbh->commit;
1139   $dbh->disconnect;
1140
1141   $main::lxdebug->leave_sub();
1142
1143   return $rc;
1144 }
1145
1146 sub create_links {
1147   $main::lxdebug->enter_sub();
1148
1149   my ($self, $module, $myconfig, $form) = @_;
1150
1151   # connect to database
1152   my $dbh = $form->dbconnect($myconfig);
1153
1154   my @values = ('%' . $module . '%');
1155
1156   if ($form->{id}) {
1157     $query =
1158       qq|SELECT c.accno, c.description, c.link, c.id,
1159            p.inventory_accno_id, p.income_accno_id, p.expense_accno_id
1160          FROM chart c, parts p
1161          WHERE (c.link LIKE ?) AND (p.id = ?)
1162          ORDER BY c.accno|;
1163     push(@values, conv_i($form->{id}));
1164
1165   } else {
1166     $query =
1167       qq|SELECT c.accno, c.description, c.link, c.id,
1168            d.inventory_accno_id, d.income_accno_id, d.expense_accno_id
1169          FROM chart c, defaults d
1170          WHERE c.link LIKE ?
1171          ORDER BY c.accno|;
1172   }
1173
1174   my $sth = prepare_execute_query($form, $dbh, $query, @values);
1175   while (my $ref = $sth->fetchrow_hashref(NAME_lc)) {
1176     foreach my $key (split(/:/, $ref->{link})) {
1177       if ($key =~ /\Q$module\E/) {
1178         if (   ($ref->{id} eq $ref->{inventory_accno_id})
1179             || ($ref->{id} eq $ref->{income_accno_id})
1180             || ($ref->{id} eq $ref->{expense_accno_id})) {
1181           push @{ $form->{"${module}_links"}{$key} },
1182             { accno       => $ref->{accno},
1183               description => $ref->{description},
1184               selected    => "selected" };
1185           $form->{"${key}_default"} = "$ref->{accno}--$ref->{description}";
1186             } else {
1187           push @{ $form->{"${module}_links"}{$key} },
1188             { accno       => $ref->{accno},
1189               description => $ref->{description},
1190               selected    => "" };
1191         }
1192       }
1193     }
1194   }
1195   $sth->finish;
1196
1197   # get buchungsgruppen
1198   $form->{BUCHUNGSGRUPPEN} = selectall_hashref_query($form, $dbh, qq|SELECT id, description FROM buchungsgruppen|);
1199
1200   # get payment terms
1201   $form->{payment_terms} = selectall_hashref_query($form, $dbh, qq|SELECT id, description FROM payment_terms ORDER BY sortkey|);
1202
1203   if (!$form->{id}) {
1204     ($form->{priceupdate}) = selectrow_query($form, $dbh, qq|SELECT current_date|);
1205   }
1206
1207   $dbh->disconnect;
1208   $main::lxdebug->leave_sub();
1209 }
1210
1211 # get partnumber, description, unit, sellprice and soldtotal with choice through $sortorder for Top100
1212 sub get_parts {
1213   $main::lxdebug->enter_sub();
1214
1215   my ($self, $myconfig, $form, $sortorder) = @_;
1216   my $dbh   = $form->dbconnect($myconfig);
1217   my $order = qq| p.partnumber|;
1218   my $where = qq|1 = 1|;
1219   my @values;
1220
1221   if ($sortorder eq "all") {
1222     $where .= qq| AND (partnumber ILIKE ?) AND (description ILIKE ?)|;
1223     push(@values, '%' . $form->{partnumber} . '%', '%' . $form->{description} . '%');
1224
1225   } elsif ($sortorder eq "partnumber") {
1226     $where .= qq| AND (partnumber ILIKE ?)|;
1227     push(@values, '%' . $form->{partnumber} . '%');
1228
1229   } elsif ($sortorder eq "description") {
1230     $where .= qq| AND (description ILIKE ?)|;
1231     push(@values, '%' . $form->{description} . '%');
1232     $order = "description";
1233
1234   }
1235
1236   my $query =
1237     qq|SELECT id, partnumber, description, unit, sellprice
1238        FROM parts
1239        WHERE $where ORDER BY $order|;
1240
1241   my $sth = prepare_execute_query($form, $dbh, $query, @values);
1242
1243   my $j = 0;
1244   while (my $ref = $sth->fetchrow_hashref(NAME_lc)) {
1245     if (($ref->{partnumber} eq "*") && ($ref->{description} eq "")) {
1246       next;
1247     }
1248
1249     $j++;
1250     $form->{"id_$j"}          = $ref->{id};
1251     $form->{"partnumber_$j"}  = $ref->{partnumber};
1252     $form->{"description_$j"} = $ref->{description};
1253     $form->{"unit_$j"}        = $ref->{unit};
1254     $form->{"sellprice_$j"}   = $ref->{sellprice};
1255     $form->{"soldtotal_$j"}   = get_soldtotal($dbh, $ref->{id});
1256   }    #while
1257   $form->{rows} = $j;
1258   $sth->finish;
1259   $dbh->disconnect;
1260
1261   $main::lxdebug->leave_sub();
1262
1263   return $self;
1264 }    #end get_parts()
1265
1266 # gets sum of sold part with part_id
1267 sub get_soldtotal {
1268   $main::lxdebug->enter_sub();
1269
1270   my ($dbh, $id) = @_;
1271
1272   my $query = qq|SELECT sum(qty) FROM invoice WHERE parts_id = ?|;
1273   my ($sum) = selectrow_query($form, $dbh, $query, conv_i($id));
1274   $sum ||= 0;
1275
1276   $main::lxdebug->leave_sub();
1277
1278   return $sum;
1279 }    #end get_soldtotal
1280
1281 sub retrieve_languages {
1282   $main::lxdebug->enter_sub();
1283
1284   my ($self, $myconfig, $form) = @_;
1285
1286   # connect to database
1287   my $dbh = $form->dbconnect($myconfig);
1288
1289   my @values;
1290   my $where;
1291
1292   if ($form->{language_values} ne "") {
1293     $query =
1294       qq|SELECT l.id, l.description, tr.translation, tr.longdescription
1295          FROM language l
1296          LEFT OUTER JOIN translation tr ON (tr.language_id = l.id) AND (tr.parts_id = ?)
1297          ORDER BY lower(l.description)|;
1298     @values = (conv_i($form->{id}));
1299
1300   } else {
1301     $query = qq|SELECT id, description
1302                 FROM language
1303                 ORDER BY lower(description)|;
1304   }
1305
1306   my $languages = selectall_hashref_query($form, $dbh, $query, @values);
1307
1308   $dbh->disconnect;
1309
1310   $main::lxdebug->leave_sub();
1311
1312   return $languages;
1313 }
1314
1315 sub follow_account_chain {
1316   $main::lxdebug->enter_sub(2);
1317
1318   my ($self, $form, $dbh, $transdate, $accno_id, $accno) = @_;
1319
1320   my @visited_accno_ids = ($accno_id);
1321
1322   my ($query, $sth);
1323
1324   $query =
1325     qq|SELECT c.new_chart_id, date($transdate) >= c.valid_from AS is_valid, | .
1326     qq|  cnew.accno | .
1327     qq|FROM chart c | .
1328     qq|LEFT JOIN chart cnew ON c.new_chart_id = cnew.id | .
1329     qq|WHERE (c.id = ?) AND NOT c.new_chart_id ISNULL AND (c.new_chart_id > 0)|;
1330   $sth = prepare_query($form, $dbh, $query);
1331
1332   while (1) {
1333     do_statement($form, $sth, $query, $accno_id);
1334     $ref = $sth->fetchrow_hashref();
1335     last unless ($ref && $ref->{"is_valid"} &&
1336                  !grep({ $_ == $ref->{"new_chart_id"} } @visited_accno_ids));
1337     $accno_id = $ref->{"new_chart_id"};
1338     $accno = $ref->{"accno"};
1339     push(@visited_accno_ids, $accno_id);
1340   }
1341
1342   $main::lxdebug->leave_sub(2);
1343
1344   return ($accno_id, $accno);
1345 }
1346
1347 sub retrieve_accounts {
1348   $main::lxdebug->enter_sub(2);
1349
1350   my ($self, $myconfig, $form, $parts_id, $index) = @_;
1351
1352   my ($query, $sth, $dbh);
1353
1354   $form->{"taxzone_id"} *= 1;
1355
1356   $dbh = $form->get_standard_dbh($myconfig);
1357
1358   my $transdate = "";
1359   if ($form->{type} eq "invoice") {
1360     if (($form->{vc} eq "vendor") || !$form->{deliverydate}) {
1361       $transdate = $form->{invdate};
1362     } else {
1363       $transdate = $form->{deliverydate};
1364     }
1365   } elsif (($form->{type} eq "credit_note") || ($form->{script} eq 'ir.pl')) {
1366     $transdate = $form->{invdate};
1367   } else {
1368     $transdate = $form->{transdate};
1369   }
1370
1371   if ($transdate eq "") {
1372     $transdate = "current_date";
1373   } else {
1374     $transdate = $dbh->quote($transdate);
1375   }
1376
1377   $query =
1378     qq|SELECT | .
1379     qq|  p.inventory_accno_id AS is_part, | .
1380     qq|  bg.inventory_accno_id, | .
1381     qq|  bg.income_accno_id_$form->{taxzone_id} AS income_accno_id, | .
1382     qq|  bg.expense_accno_id_$form->{taxzone_id} AS expense_accno_id, | .
1383     qq|  c1.accno AS inventory_accno, | .
1384     qq|  c2.accno AS income_accno, | .
1385     qq|  c3.accno AS expense_accno | .
1386     qq|FROM parts p | .
1387     qq|LEFT JOIN buchungsgruppen bg ON p.buchungsgruppen_id = bg.id | .
1388     qq|LEFT JOIN chart c1 ON bg.inventory_accno_id = c1.id | .
1389     qq|LEFT JOIN chart c2 ON bg.income_accno_id_$form->{taxzone_id} = c2.id | .
1390     qq|LEFT JOIN chart c3 ON bg.expense_accno_id_$form->{taxzone_id} = c3.id | .
1391     qq|WHERE p.id = ?|;
1392   my $ref = selectfirst_hashref_query($form, $dbh, $query, $parts_id);
1393
1394   return $main::lxdebug->leave_sub(2) if (!$ref);
1395
1396   $ref->{"inventory_accno_id"} = undef unless ($ref->{"is_part"});
1397
1398   my %accounts;
1399   foreach my $type (qw(inventory income expense)) {
1400     next unless ($ref->{"${type}_accno_id"});
1401     ($accounts{"${type}_accno_id"}, $accounts{"${type}_accno"}) =
1402       $self->follow_account_chain($form, $dbh, $transdate,
1403                                   $ref->{"${type}_accno_id"},
1404                                   $ref->{"${type}_accno"});
1405   }
1406
1407   map({ $form->{"${_}_accno_$index"} = $accounts{"${_}_accno"} }
1408       qw(inventory income expense));
1409
1410   my $inc_exp = $form->{"vc"} eq "customer" ? "income" : "expense";
1411   my $accno_id = $accounts{"${inc_exp}_accno_id"};
1412
1413   $query =
1414     qq|SELECT c.accno, t.taxdescription AS description, t.rate, t.taxnumber | .
1415     qq|FROM tax t | .
1416     qq|LEFT JOIN chart c ON c.id = t.chart_id | .
1417     qq|WHERE t.id IN | .
1418     qq|  (SELECT tk.tax_id | .
1419     qq|   FROM taxkeys tk | .
1420     qq|   WHERE tk.chart_id = ? AND startdate <= | . quote_db_date($transdate) .
1421     qq|   ORDER BY startdate DESC LIMIT 1) |;
1422   $ref = selectfirst_hashref_query($form, $dbh, $query, $accno_id);
1423
1424   unless ($ref) {
1425     $main::lxdebug->leave_sub(2);
1426     return;
1427   }
1428
1429   $form->{"taxaccounts_$index"} = $ref->{"accno"};
1430   if ($form->{"taxaccounts"} !~ /$ref->{accno}/) {
1431     $form->{"taxaccounts"} .= "$ref->{accno} ";
1432   }
1433   map({ $form->{"$ref->{accno}_${_}"} = $ref->{$_}; }
1434       qw(rate description taxnumber));
1435
1436 #   $main::lxdebug->message(0, "formvars: rate " . $form->{"$ref->{accno}_rate"} .
1437 #                           " description " . $form->{"$ref->{accno}_description"} .
1438 #                           " taxnumber " . $form->{"$ref->{accno}_taxnumber"} .
1439 #                           " || taxaccounts_$index " . $form->{"taxaccounts_$index"} .
1440 #                           " || taxaccounts " . $form->{"taxaccounts"});
1441
1442   $main::lxdebug->leave_sub(2);
1443 }
1444
1445 sub get_basic_part_info {
1446   $main::lxdebug->enter_sub();
1447
1448   my $self     = shift;
1449   my %params   = @_;
1450
1451   Common::check_params(\%params, qw(id));
1452
1453   my @ids      = 'ARRAY' eq ref $params{id} ? @{ $params{id} } : ($params{id});
1454
1455   if (!scalar @ids) {
1456     $main::lxdebug->leave_sub();
1457     return ();
1458   }
1459
1460   my $myconfig = \%main::myconfig;
1461   my $form     = $main::form;
1462
1463   my $dbh      = $form->get_standard_dbh($myconfig);
1464
1465   my $query    = qq|SELECT id, partnumber, description, unit FROM parts WHERE id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
1466
1467   my $info     = selectall_hashref_query($form, $dbh, $query, map { conv_i($_) } @ids);
1468
1469   if ('' eq ref $params{id}) {
1470     $info = $info->[0] || { };
1471
1472     $main::lxdebug->leave_sub();
1473     return $info;
1474   }
1475
1476   my %info_map = map { $_->{id} => $_ } @{ $info };
1477
1478   $main::lxdebug->leave_sub();
1479
1480   return %info_map;
1481 }
1482
1483 sub prepare_parts_for_printing {
1484   $main::lxdebug->enter_sub();
1485
1486   my $self     = shift;
1487   my %params   = @_;
1488
1489   my $myconfig = \%main::myconfig;
1490   my $form     = $main::form;
1491
1492   my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
1493
1494   my $prefix   = $params{prefix} || 'id_';
1495   my $rowcount = defined $params{rowcount} ? $params{rowcount} : $form->{rowcount};
1496
1497   my @part_ids = keys %{ { map { $_ => 1 } grep { $_ } map { $form->{"${prefix}${_}"} } (1 .. $rowcount) } };
1498
1499   if (!@part_ids) {
1500     $main::lxdebug->leave_sub();
1501     return;
1502   }
1503
1504   my $placeholders = join ', ', ('?') x scalar(@part_ids);
1505   my $query        = qq|SELECT mm.parts_id, mm.model, v.name AS make
1506                         FROM makemodel mm
1507                         LEFT JOIN vendor v ON (mm.make = cast (v.id as text))
1508                         WHERE mm.parts_id IN ($placeholders)|;
1509
1510   my %makemodel    = ();
1511
1512   my $sth          = prepare_execute_query($form, $dbh, $query, @part_ids);
1513
1514   while (my $ref = $sth->fetchrow_hashref()) {
1515     $makemodel{$ref->{parts_id}} ||= [];
1516     push @{ $makemodel{$ref->{parts_id}} }, $ref;
1517   }
1518
1519   $sth->finish();
1520
1521   my @columns = qw(ean);
1522
1523   $query      = qq|SELECT id, | . join(', ', @columns) . qq|
1524                    FROM parts
1525                    WHERE id IN ($placeholders)|;
1526
1527   my %data    = selectall_as_map($form, $dbh, $query, 'id', \@columns, @part_ids);
1528
1529   map { $form->{$_} = [] } (qw(make model), @columns);
1530
1531   foreach my $i (1 .. $rowcount) {
1532     my $id = $form->{"${prefix}${i}"};
1533
1534     next if (!$id);
1535
1536     foreach (@columns) {
1537       push @{ $form->{$_} }, $data{$id}->{$_};
1538     }
1539
1540     push @{ $form->{make} },  [];
1541     push @{ $form->{model} }, [];
1542
1543     next if (!$makemodel{$id});
1544
1545     foreach my $ref (@{ $makemodel{$id} }) {
1546       map { push @{ $form->{$_}->[-1] }, $ref->{$_} } qw(make model);
1547     }
1548   }
1549
1550   $main::lxdebug->leave_sub();
1551 }
1552
1553
1554 1;