3d92746b55db2bfcf5beb45bc65a4dd2bc5eb427
[kivitendo-erp.git] / SL / OE.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) 1999-2003
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 # Order entry module
32 # Quotation
33 #======================================================================
34
35 package OE;
36
37 use List::Util qw(max first);
38 use YAML;
39
40 use SL::AM;
41 use SL::Common;
42 use SL::CVar;
43 use SL::DB::Order;
44 use SL::DB::PeriodicInvoicesConfig;
45 use SL::DB::Status;
46 use SL::DB::Tax;
47 use SL::DBUtils;
48 use SL::IC;
49 use SL::TransNumber;
50
51 use strict;
52
53 sub transactions {
54   $main::lxdebug->enter_sub();
55
56   my ($self, $myconfig, $form) = @_;
57
58   # connect to database
59   my $dbh = $form->get_standard_dbh;
60
61   my $query;
62   my $ordnumber = 'ordnumber';
63   my $quotation = '0';
64
65   my @values;
66   my $where;
67
68   my ($periodic_invoices_columns, $periodic_invoices_joins);
69
70   my $rate = ($form->{vc} eq 'customer') ? 'buy' : 'sell';
71
72   if ($form->{type} =~ /_quotation$/) {
73     $quotation = '1';
74     $ordnumber = 'quonumber';
75
76   } elsif ($form->{type} eq 'sales_order') {
77     $periodic_invoices_columns = qq| , COALESCE(pcfg.active, 'f') AS periodic_invoices |;
78     $periodic_invoices_joins   = qq| LEFT JOIN periodic_invoices_configs pcfg ON (o.id = pcfg.oe_id) |;
79   }
80
81   my $vc = $form->{vc} eq "customer" ? "customer" : "vendor";
82
83   $query =
84     qq|SELECT o.id, o.ordnumber, o.transdate, o.reqdate, | .
85     qq|  o.amount, ct.${vc}number, ct.name, o.netamount, o.${vc}_id, o.globalproject_id, | .
86     qq|  o.closed, o.delivered, o.quonumber, o.shippingpoint, o.shipvia, | .
87     qq|  o.transaction_description, | .
88     qq|  o.marge_total, o.marge_percent, | .
89     qq|  ex.$rate AS exchangerate, | .
90     qq|  pr.projectnumber AS globalprojectnumber, | .
91     qq|  e.name AS employee, s.name AS salesman, | .
92     qq|  ct.${vc}number AS vcnumber, ct.country, ct.ustid, ct.business_id,  | .
93     qq|  tz.description AS taxzone | .
94     $periodic_invoices_columns .
95     qq|FROM oe o | .
96     qq|JOIN $vc ct ON (o.${vc}_id = ct.id) | .
97     qq|LEFT JOIN employee e ON (o.employee_id = e.id) | .
98     qq|LEFT JOIN employee s ON (o.salesman_id = s.id) | .
99     qq|LEFT JOIN exchangerate ex ON (ex.currency_id = o.currency_id | .
100     qq|  AND ex.transdate = o.transdate) | .
101     qq|LEFT JOIN project pr ON (o.globalproject_id = pr.id) | .
102     qq|LEFT JOIN tax_zones tz ON (o.taxzone_id = tz.id) | .
103     qq|$periodic_invoices_joins | .
104     qq|WHERE (o.quotation = ?) |;
105   push(@values, $quotation);
106
107   my ($null, $split_department_id) = split /--/, $form->{department};
108   my $department_id = $form->{department_id} || $split_department_id;
109   if ($department_id) {
110     $query .= qq| AND o.department_id = ?|;
111     push(@values, $department_id);
112   }
113
114   if ($form->{"project_id"}) {
115     $query .=
116       qq|AND ((globalproject_id = ?) OR EXISTS | .
117       qq|  (SELECT * FROM orderitems oi | .
118       qq|   WHERE oi.project_id = ? AND oi.trans_id = o.id))|;
119     push(@values, conv_i($form->{"project_id"}), conv_i($form->{"project_id"}));
120   }
121
122   if ($form->{"projectnumber"}) {
123     $query .= <<SQL;
124       AND ((pr.projectnumber ILIKE ?) OR EXISTS (
125         SELECT * FROM orderitems oi
126         LEFT JOIN project proi ON proi.id = oi.project_id
127         WHERE proi.projectnumber ILIKE ? AND oi.trans_id = o.id
128       ))
129 SQL
130     push @values, "%" . $form->{"projectnumber"} . "%", "%" . $form->{"projectnumber"} . "%" ;
131   }
132
133   if ($form->{"business_id"}) {
134     $query .= " AND ct.business_id = ?";
135     push(@values, $form->{"business_id"});
136   }
137
138   if ($form->{"${vc}_id"}) {
139     $query .= " AND o.${vc}_id = ?";
140     push(@values, $form->{"${vc}_id"});
141
142   } elsif ($form->{$vc}) {
143     $query .= " AND ct.name ILIKE ?";
144     push(@values, '%' . $form->{$vc} . '%');
145   }
146
147   if (!$main::auth->assert('sales_all_edit', 1)) {
148     $query .= " AND o.employee_id = (select id from employee where login= ?)";
149     push @values, $form->{login};
150   }
151   if ($form->{employee_id}) {
152     $query .= " AND o.employee_id = ?";
153     push @values, conv_i($form->{employee_id});
154   }
155
156   if ($form->{salesman_id}) {
157     $query .= " AND o.salesman_id = ?";
158     push @values, conv_i($form->{salesman_id});
159   }
160
161   if (!$form->{open} && !$form->{closed}) {
162     $query .= " AND o.id = 0";
163   } elsif (!($form->{open} && $form->{closed})) {
164     $query .= ($form->{open}) ? " AND o.closed = '0'" : " AND o.closed = '1'";
165   }
166
167   if (($form->{"notdelivered"} || $form->{"delivered"}) &&
168       ($form->{"notdelivered"} ne $form->{"delivered"})) {
169     $query .= $form->{"delivered"} ?
170       " AND o.delivered " : " AND NOT o.delivered";
171   }
172
173   if ($form->{$ordnumber}) {
174     $query .= qq| AND o.$ordnumber ILIKE ?|;
175     push(@values, '%' . $form->{$ordnumber} . '%');
176   }
177
178   if($form->{transdatefrom}) {
179     $query .= qq| AND o.transdate >= ?|;
180     push(@values, conv_date($form->{transdatefrom}));
181   }
182
183   if($form->{transdateto}) {
184     $query .= qq| AND o.transdate <= ?|;
185     push(@values, conv_date($form->{transdateto}));
186   }
187
188   if($form->{reqdatefrom}) {
189     $query .= qq| AND o.reqdate >= ?|;
190     push(@values, conv_date($form->{reqdatefrom}));
191   }
192
193   if($form->{reqdateto}) {
194     $query .= qq| AND o.reqdate <= ?|;
195     push(@values, conv_date($form->{reqdateto}));
196   }
197
198   if ($form->{shippingpoint}) {
199     $query .= qq| AND o.shippingpoint ILIKE ?|;
200     push(@values, '%' . $form->{shippingpoint} . '%');
201   }
202
203   if ($form->{taxzone_id} ne '') { # taxzone_id could be 0
204     $query .= qq| AND tz.id = ?|;
205     push(@values, $form->{taxzone_id});
206   }
207
208   if ($form->{transaction_description}) {
209     $query .= qq| AND o.transaction_description ILIKE ?|;
210     push(@values, '%' . $form->{transaction_description} . '%');
211   }
212
213   if ($form->{periodic_invoices_active} ne $form->{periodic_invoices_inactive}) {
214     my $not  = $form->{periodic_invoices_inactive} ? 'NOT' : '';
215     $query  .= qq| AND ${not} COALESCE(pcfg.active, 'f')|;
216   }
217
218   my $sortdir   = !defined $form->{sortdir} ? 'ASC' : $form->{sortdir} ? 'ASC' : 'DESC';
219   my $sortorder = join(', ', map { "${_} ${sortdir} " } ("o.id", $form->sort_columns("transdate", $ordnumber, "name")));
220   my %allowed_sort_columns = (
221     "transdate"               => "o.transdate",
222     "reqdate"                 => "o.reqdate",
223     "id"                      => "o.id",
224     "ordnumber"               => "o.ordnumber",
225     "quonumber"               => "o.quonumber",
226     "name"                    => "ct.name",
227     "employee"                => "e.name",
228     "salesman"                => "s.name",
229     "shipvia"                 => "o.shipvia",
230     "transaction_description" => "o.transaction_description",
231     "shippingpoint"           => "o.shippingpoint",
232     "taxzone"                 => "tz.description",
233   );
234   if ($form->{sort} && grep($form->{sort}, keys(%allowed_sort_columns))) {
235     $sortorder = $allowed_sort_columns{$form->{sort}} . " ${sortdir}";
236   }
237   $query .= qq| ORDER by | . $sortorder;
238
239   my $sth = $dbh->prepare($query);
240   $sth->execute(@values) ||
241     $form->dberror($query . " (" . join(", ", @values) . ")");
242
243   my %id = ();
244   $form->{OE} = [];
245   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
246     $ref->{exchangerate} = 1 unless $ref->{exchangerate};
247     push @{ $form->{OE} }, $ref if $ref->{id} != $id{ $ref->{id} };
248     $id{ $ref->{id} } = $ref->{id};
249   }
250
251   $sth->finish;
252
253   $main::lxdebug->leave_sub();
254 }
255
256 sub transactions_for_todo_list {
257   $main::lxdebug->enter_sub();
258
259   my $self     = shift;
260   my %params   = @_;
261
262   my $myconfig = \%main::myconfig;
263   my $form     = $main::form;
264
265   my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
266
267   my $query    = qq|SELECT id FROM employee WHERE login = ?|;
268   my ($e_id)   = selectrow_query($form, $dbh, $query, $form->{login});
269
270   $query       =
271     qq|SELECT oe.id, oe.transdate, oe.reqdate, oe.quonumber, oe.transaction_description, oe.amount,
272          CASE WHEN (COALESCE(oe.customer_id, 0) = 0) THEN 'vendor' ELSE 'customer' END AS vc,
273          c.name AS customer,
274          v.name AS vendor,
275          e.name AS employee
276        FROM oe
277        LEFT JOIN customer c ON (oe.customer_id = c.id)
278        LEFT JOIN vendor v   ON (oe.vendor_id   = v.id)
279        LEFT JOIN employee e ON (oe.employee_id = e.id)
280        WHERE (COALESCE(quotation, FALSE) = TRUE)
281          AND (COALESCE(closed,    FALSE) = FALSE)
282          AND ((oe.employee_id = ?) OR (oe.salesman_id = ?))
283          AND NOT (oe.reqdate ISNULL)
284          AND (oe.reqdate < current_date)
285        ORDER BY transdate|;
286
287   my $quotations = selectall_hashref_query($form, $dbh, $query, $e_id, $e_id);
288
289   $main::lxdebug->leave_sub();
290
291   return $quotations;
292 }
293
294 sub save {
295   $main::lxdebug->enter_sub();
296
297   my ($self, $myconfig, $form) = @_;
298
299   # connect to database, turn off autocommit
300   my $dbh = $form->get_standard_dbh;
301
302   my ($query, @values, $sth, $null);
303   my $exchangerate = 0;
304
305   my $all_units = AM->retrieve_units($myconfig, $form);
306   $form->{all_units} = $all_units;
307
308   my $ic_cvar_configs = CVar->get_configs(module => 'IC',
309                                           dbh    => $dbh);
310
311   $form->{employee_id} = (split /--/, $form->{employee})[1] if !$form->{employee_id};
312   unless ($form->{employee_id}) {
313     $form->get_employee($dbh);
314   }
315
316   my $ml = ($form->{type} eq 'sales_order') ? 1 : -1;
317
318   my $number_field         = $form->{type} =~ m{order} ? 'ordnumber' : 'quonumber';
319   my $trans_number         = SL::TransNumber->new(type => $form->{type}, dbh => $dbh, number => $form->{$number_field}, id => $form->{id});
320   $form->{$number_field} ||= $trans_number->create_unique;
321
322   if ($form->{id}) {
323     $query = qq|DELETE FROM custom_variables
324                 WHERE (config_id IN (SELECT id FROM custom_variable_configs WHERE module = 'IC'))
325                   AND (sub_module = 'orderitems')
326                   AND (trans_id IN (SELECT id FROM orderitems WHERE trans_id = ?))|;
327     do_query($form, $dbh, $query, $form->{id});
328
329     $query = qq|DELETE FROM orderitems WHERE trans_id = ?|;
330     do_query($form, $dbh, $query, $form->{id});
331
332     $query = qq|DELETE FROM shipto | .
333              qq|WHERE trans_id = ? AND module = 'OE'|;
334     do_query($form, $dbh, $query, $form->{id});
335
336   } else {
337
338     $query = qq|SELECT nextval('id')|;
339     ($form->{id}) = selectrow_query($form, $dbh, $query);
340
341     $query = qq|INSERT INTO oe (id, ordnumber, employee_id, currency_id) VALUES (?, '', ?, (SELECT currency_id FROM defaults))|;
342     do_query($form, $dbh, $query, $form->{id}, $form->{employee_id});
343   }
344
345   my $amount    = 0;
346   my $linetotal = 0;
347   my $discount  = 0;
348   my $project_id;
349   my $reqdate;
350   my $taxrate;
351   my $taxbase;
352   my $taxdiff;
353   my $taxamount = 0;
354   my $fxsellprice;
355   my %taxbase;
356   my @taxaccounts;
357   my %taxaccounts;
358   my $netamount = 0;
359
360   $form->get_lists('price_factors' => 'ALL_PRICE_FACTORS');
361   my %price_factors = map { $_->{id} => $_->{factor} } @{ $form->{ALL_PRICE_FACTORS} };
362   my $price_factor;
363
364   for my $i (1 .. $form->{rowcount}) {
365
366     map({ $form->{"${_}_$i"} = $form->parse_amount($myconfig, $form->{"${_}_$i"}) } qw(qty ship));
367
368     if ($form->{"id_$i"}) {
369
370       # get item baseunit
371       $query = qq|SELECT unit FROM parts WHERE id = ?|;
372       my ($item_unit) = selectrow_query($form, $dbh, $query, $form->{"id_$i"});
373
374       my $basefactor = 1;
375       if (defined($all_units->{$item_unit}->{factor}) &&
376           (($all_units->{$item_unit}->{factor} * 1) != 0)) {
377         $basefactor = $all_units->{$form->{"unit_$i"}}->{factor} / $all_units->{$item_unit}->{factor};
378       }
379       my $baseqty = $form->{"qty_$i"} * $basefactor;
380
381       $form->{"marge_percent_$i"} = $form->parse_amount($myconfig, $form->{"marge_percent_$i"}) * 1;
382       $form->{"marge_absolut_$i"} = $form->parse_amount($myconfig, $form->{"marge_absolut_$i"}) * 1;
383
384       $form->{"lastcost_$i"} = $form->parse_amount($myconfig, $form->{"lastcost_$i"});
385
386       # set values to 0 if nothing entered
387       $form->{"discount_$i"} = $form->parse_amount($myconfig, $form->{"discount_$i"}) / 100;
388
389       $form->{"sellprice_$i"} = $form->parse_amount($myconfig, $form->{"sellprice_$i"});
390       $fxsellprice = $form->{"sellprice_$i"};
391
392       my ($dec) = ($form->{"sellprice_$i"} =~ /\.(\d+)/);
393       $dec = length($dec);
394       my $decimalplaces = ($dec > 2) ? $dec : 2;
395
396       $discount = $form->round_amount($form->{"sellprice_$i"} * $form->{"discount_$i"}, $decimalplaces);
397       $form->{"sellprice_$i"} = $form->round_amount($form->{"sellprice_$i"} - $discount, $decimalplaces);
398
399       $form->{"inventory_accno_$i"} *= 1;
400       $form->{"expense_accno_$i"}   *= 1;
401
402       $price_factor = $price_factors{ $form->{"price_factor_id_$i"} } || 1;
403       $linetotal    = $form->round_amount($form->{"sellprice_$i"} * $form->{"qty_$i"} / $price_factor, 2);
404
405       @taxaccounts = split(/ /, $form->{"taxaccounts_$i"});
406       $taxrate     = 0;
407       $taxdiff     = 0;
408
409       map { $taxrate += $form->{"${_}_rate"} } @taxaccounts;
410
411       if ($form->{taxincluded}) {
412         $taxamount = $linetotal * $taxrate / (1 + $taxrate);
413         $taxbase   = $linetotal - $taxamount;
414
415         # we are not keeping a natural price, do not round
416         $form->{"sellprice_$i"} =
417           $form->{"sellprice_$i"} * (1 / (1 + $taxrate));
418       } else {
419         $taxamount = $linetotal * $taxrate;
420         $taxbase   = $linetotal;
421       }
422
423       if ($form->round_amount($taxrate, 7) == 0) {
424         if ($form->{taxincluded}) {
425           foreach my $item (@taxaccounts) {
426             $taxamount = $form->round_amount($linetotal * $form->{"${item}_rate"} / (1 + abs($form->{"${item}_rate"})), 2);
427             $taxaccounts{$item} += $taxamount;
428             $taxdiff            += $taxamount;
429             $taxbase{$item}     += $taxbase;
430           }
431           $taxaccounts{ $taxaccounts[0] } += $taxdiff;
432         } else {
433           foreach my $item (@taxaccounts) {
434             $taxaccounts{$item} += $linetotal * $form->{"${item}_rate"};
435             $taxbase{$item}     += $taxbase;
436           }
437         }
438       } else {
439         foreach my $item (@taxaccounts) {
440           $taxaccounts{$item} += $taxamount * $form->{"${item}_rate"} / $taxrate;
441           $taxbase{$item} += $taxbase;
442         }
443       }
444
445       $netamount += $form->{"sellprice_$i"} * $form->{"qty_$i"} / $price_factor;
446
447       $reqdate = ($form->{"reqdate_$i"}) ? $form->{"reqdate_$i"} : undef;
448
449       # Get pricegroup_id and save it. Unfortunately the interface
450       # also uses ID "0" for signalling that none is selected, but "0"
451       # must not be stored in the database. Therefore we cannot simply
452       # use conv_i().
453       ($null, my $pricegroup_id) = split(/--/, $form->{"sellprice_pg_$i"});
454       $pricegroup_id *= 1;
455       $pricegroup_id  = undef if !$pricegroup_id;
456
457       # save detail record in orderitems table
458       my $orderitems_id = $form->{"orderitems_id_$i"};
459       ($orderitems_id)  = selectfirst_array_query($form, $dbh, qq|SELECT nextval('orderitemsid')|) if (!$orderitems_id);
460
461       @values = ();
462       $query = qq|INSERT INTO orderitems (
463                     id, trans_id, parts_id, description, longdescription, qty, base_qty,
464                     sellprice, discount, unit, reqdate, project_id, serialnumber, ship,
465                     pricegroup_id, ordnumber, transdate, cusordnumber, subtotal,
466                     marge_percent, marge_total, lastcost, price_factor_id, price_factor, marge_price_factor)
467                   VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
468                           (SELECT factor FROM price_factors WHERE id = ?), ?)|;
469       push(@values,
470            conv_i($orderitems_id), conv_i($form->{id}), conv_i($form->{"id_$i"}),
471            $form->{"description_$i"}, $form->{"longdescription_$i"},
472            $form->{"qty_$i"}, $baseqty,
473            $fxsellprice, $form->{"discount_$i"},
474            $form->{"unit_$i"}, conv_date($reqdate), conv_i($form->{"project_id_$i"}),
475            $form->{"serialnumber_$i"}, $form->{"ship_$i"}, $pricegroup_id,
476            $form->{"ordnumber_$i"}, conv_date($form->{"transdate_$i"}),
477            $form->{"cusordnumber_$i"}, $form->{"subtotal_$i"} ? 't' : 'f',
478            $form->{"marge_percent_$i"}, $form->{"marge_absolut_$i"},
479            $form->{"lastcost_$i"},
480            conv_i($form->{"price_factor_id_$i"}), conv_i($form->{"price_factor_id_$i"}),
481            conv_i($form->{"marge_price_factor_$i"}));
482       do_query($form, $dbh, $query, @values);
483
484       $form->{"sellprice_$i"} = $fxsellprice;
485       $form->{"discount_$i"} *= 100;
486
487       CVar->save_custom_variables(module       => 'IC',
488                                   sub_module   => 'orderitems',
489                                   trans_id     => $orderitems_id,
490                                   configs      => $ic_cvar_configs,
491                                   variables    => $form,
492                                   name_prefix  => 'ic_',
493                                   name_postfix => "_$i",
494                                   dbh          => $dbh);
495     }
496   }
497
498   $reqdate = ($form->{reqdate}) ? $form->{reqdate} : undef;
499
500   # add up the tax
501   my $tax = 0;
502   map { $tax += $form->round_amount($taxaccounts{$_}, 2) } keys %taxaccounts;
503
504   $amount = $form->round_amount($netamount + $tax, 2);
505   $netamount = $form->round_amount($netamount, 2);
506
507   if ($form->{currency} eq $form->{defaultcurrency}) {
508     $form->{exchangerate} = 1;
509   } else {
510     $exchangerate = $form->check_exchangerate($myconfig, $form->{currency}, $form->{transdate}, ($form->{vc} eq 'customer') ? 'buy' : 'sell');
511   }
512
513   $form->{exchangerate} = $exchangerate || $form->parse_amount($myconfig, $form->{exchangerate});
514
515   my $quotation = $form->{type} =~ /_order$/ ? 'f' : 't';
516
517   ($null, $form->{department_id}) = split(/--/, $form->{department}) if $form->{department};
518
519   # save OE record
520   $query =
521     qq|UPDATE oe SET
522          ordnumber = ?, quonumber = ?, cusordnumber = ?, transdate = ?, vendor_id = ?,
523          customer_id = ?, amount = ?, netamount = ?, reqdate = ?, taxincluded = ?,
524          shippingpoint = ?, shipvia = ?, notes = ?, intnotes = ?, currency_id = (SELECT id FROM currencies WHERE name=?), closed = ?,
525          delivered = ?, proforma = ?, quotation = ?, department_id = ?, language_id = ?,
526          taxzone_id = ?, shipto_id = ?, payment_id = ?, delivery_vendor_id = ?, delivery_customer_id = ?,delivery_term_id = ?,
527          globalproject_id = ?, employee_id = ?, salesman_id = ?, cp_id = ?, transaction_description = ?, marge_total = ?, marge_percent = ?
528        WHERE id = ?|;
529
530   @values = ($form->{ordnumber} || '', $form->{quonumber},
531              $form->{cusordnumber}, conv_date($form->{transdate}),
532              conv_i($form->{vendor_id}), conv_i($form->{customer_id}),
533              $amount, $netamount, conv_date($reqdate),
534              $form->{taxincluded} ? 't' : 'f', $form->{shippingpoint},
535              $form->{shipvia}, $form->{notes}, $form->{intnotes},
536              $form->{currency}, $form->{closed} ? 't' : 'f',
537              $form->{delivered} ? "t" : "f", $form->{proforma} ? 't' : 'f',
538              $quotation, conv_i($form->{department_id}),
539              conv_i($form->{language_id}), conv_i($form->{taxzone_id}),
540              conv_i($form->{shipto_id}), conv_i($form->{payment_id}),
541              conv_i($form->{delivery_vendor_id}),
542              conv_i($form->{delivery_customer_id}),
543              conv_i($form->{delivery_term_id}),
544              conv_i($form->{globalproject_id}), conv_i($form->{employee_id}),
545              conv_i($form->{salesman_id}), conv_i($form->{cp_id}),
546              $form->{transaction_description},
547              $form->{marge_total} * 1, $form->{marge_percent} * 1,
548              conv_i($form->{id}));
549   do_query($form, $dbh, $query, @values);
550
551   $form->{ordtotal} = $amount;
552
553   # add shipto
554   $form->{name} = $form->{ $form->{vc} };
555   $form->{name} =~ s/--\Q$form->{"$form->{vc}_id"}\E//;
556
557   if (!$form->{shipto_id}) {
558     $form->add_shipto($dbh, $form->{id}, "OE");
559   }
560
561   # save printed, emailed, queued
562   $form->save_status($dbh);
563
564   # Link this record to the records it was created from.
565   $form->{convert_from_oe_ids} =~ s/^\s+//;
566   $form->{convert_from_oe_ids} =~ s/\s+$//;
567   my @convert_from_oe_ids      =  split m/\s+/, $form->{convert_from_oe_ids};
568   delete $form->{convert_from_oe_ids};
569
570   if (scalar @convert_from_oe_ids) {
571     RecordLinks->create_links('dbh'        => $dbh,
572                               'mode'       => 'ids',
573                               'from_table' => 'oe',
574                               'from_ids'   => \@convert_from_oe_ids,
575                               'to_table'   => 'oe',
576                               'to_id'      => $form->{id},
577       );
578
579     $self->_close_quotations_rfqs('dbh'     => $dbh,
580                                   'from_id' => \@convert_from_oe_ids,
581                                   'to_id'   => $form->{id});
582   }
583
584   if (($form->{currency} ne $form->{defaultcurrency}) && !$exchangerate) {
585     if ($form->{vc} eq 'customer') {
586       $form->update_exchangerate($dbh, $form->{currency}, $form->{transdate}, $form->{exchangerate}, 0);
587     }
588     if ($form->{vc} eq 'vendor') {
589       $form->update_exchangerate($dbh, $form->{currency}, $form->{transdate}, 0, $form->{exchangerate});
590     }
591   }
592
593   $form->{saved_xyznumber} = $form->{$form->{type} =~ /_quotation$/ ?
594                                        "quonumber" : "ordnumber"};
595
596   Common::webdav_folder($form);
597
598   my $rc = $dbh->commit;
599
600   $self->save_periodic_invoices_config(dbh         => $dbh,
601                                        oe_id       => $form->{id},
602                                        config_yaml => $form->{periodic_invoices_config})
603     if ($form->{type} eq 'sales_order');
604
605   $main::lxdebug->leave_sub();
606
607   return $rc;
608 }
609
610 sub save_periodic_invoices_config {
611   my ($self, %params) = @_;
612
613   return if !$params{oe_id};
614
615   my $config = $params{config_yaml} ? YAML::Load($params{config_yaml}) : undef;
616   return if 'HASH' ne ref $config;
617
618   my $obj  = SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $params{oe_id})
619           || SL::DB::PeriodicInvoicesConfig->new(oe_id => $params{oe_id});
620   $obj->update_attributes(%{ $config });
621 }
622
623 sub load_periodic_invoice_config {
624   my $self = shift;
625   my $form = shift;
626
627   delete $form->{periodic_invoices_config};
628
629   if ($form->{id}) {
630     my $config_obj = SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $form->{id});
631
632     if ($config_obj) {
633       my $config = { map { $_ => $config_obj->$_ } qw(active terminated periodicity start_date_as_date end_date_as_date extend_automatically_by ar_chart_id
634                                                       print printer_id copies) };
635       $form->{periodic_invoices_config} = YAML::Dump($config);
636     }
637   }
638 }
639
640 sub _close_quotations_rfqs {
641   $main::lxdebug->enter_sub();
642
643   my $self     = shift;
644   my %params   = @_;
645
646   Common::check_params(\%params, qw(from_id to_id));
647
648   my $myconfig = \%main::myconfig;
649   my $form     = $main::form;
650
651   my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
652
653   my $query    = qq|SELECT quotation FROM oe WHERE id = ?|;
654   my $sth      = prepare_query($form, $dbh, $query);
655
656   do_statement($form, $sth, $query, conv_i($params{to_id}));
657
658   my ($quotation) = $sth->fetchrow_array();
659
660   if ($quotation) {
661     $main::lxdebug->leave_sub();
662     return;
663   }
664
665   my @close_ids;
666
667   foreach my $from_id (@{ $params{from_id} }) {
668     $from_id = conv_i($from_id);
669     do_statement($form, $sth, $query, $from_id);
670     ($quotation) = $sth->fetchrow_array();
671     push @close_ids, $from_id if ($quotation);
672   }
673
674   $sth->finish();
675
676   if (scalar @close_ids) {
677     $query = qq|UPDATE oe SET closed = TRUE WHERE id IN (| . join(', ', ('?') x scalar @close_ids) . qq|)|;
678     do_query($form, $dbh, $query, @close_ids);
679
680     $dbh->commit() unless ($params{dbh});
681   }
682
683   $main::lxdebug->leave_sub();
684 }
685
686 sub delete {
687   $main::lxdebug->enter_sub();
688
689   my ($self, $myconfig, $form) = @_;
690
691   my $rc = SL::DB::Order->new->db->with_transaction(sub {
692     my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $form->{id} ]) };
693
694     SL::DB::Order->new(id => $form->{id})->delete;
695
696     my $spool = $::lx_office_conf{paths}->{spool};
697     unlink map { "$spool/$_" } @spoolfiles if $spool;
698
699     1;
700   });
701
702   $main::lxdebug->leave_sub();
703
704   return $rc;
705 }
706
707 sub retrieve {
708   $main::lxdebug->enter_sub();
709
710   my ($self, $myconfig, $form) = @_;
711
712   # connect to database
713   my $dbh = $form->get_standard_dbh;
714
715   my ($query, $query_add, @values, @ids, $sth);
716
717   # translate the ids (given by id_# and trans_id_#) into one array of ids, so we can join them later
718   map {
719     push @ids, $form->{"trans_id_$_"}
720       if ($form->{"multi_id_$_"} and $form->{"trans_id_$_"})
721   } (1 .. $form->{"rowcount"});
722
723   if ($form->{rowcount} && scalar @ids) {
724     $form->{convert_from_oe_ids} = join ' ', @ids;
725   }
726
727   # if called in multi id mode, and still only got one id, switch back to single id
728   if ($form->{"rowcount"} and $#ids == 0) {
729     $form->{"id"} = $ids[0];
730     undef @ids;
731   }
732
733   # and remember for the rest of the function
734   my $is_collective_order = scalar @ids;
735
736   if (!$form->{id}) {
737     my $wday         = (localtime(time))[6];
738     my $next_workday = $wday == 5 ? 3 : $wday == 6 ? 2 : 1;
739     $query_add       = qq|, current_date AS transdate, date(current_date + interval '${next_workday} days') AS reqdate|;
740   }
741
742   # get default accounts
743   $query = qq|SELECT (SELECT c.accno FROM chart c WHERE d.inventory_accno_id = c.id) AS inventory_accno,
744                      (SELECT c.accno FROM chart c WHERE d.income_accno_id    = c.id) AS income_accno,
745                      (SELECT c.accno FROM chart c WHERE d.expense_accno_id   = c.id) AS expense_accno,
746                      (SELECT c.accno FROM chart c WHERE d.fxgain_accno_id    = c.id) AS fxgain_accno,
747                      (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id    = c.id) AS fxloss_accno
748               $query_add
749               FROM defaults d|;
750   my $ref = selectfirst_hashref_query($form, $dbh, $query);
751   map { $form->{$_} = $ref->{$_} } keys %$ref;
752
753   $form->{currency} = $form->get_default_currency($myconfig);
754
755   # set reqdate if this is an invoice->order conversion. If someone knows a better check to ensure
756   # we come from invoices, feel free.
757   $form->{reqdate} = $form->{deliverydate}
758     if (    $form->{deliverydate}
759         and $form->{callback} =~ /action=ar_transactions/);
760
761   my $vc = $form->{vc} eq "customer" ? "customer" : "vendor";
762
763   if ($form->{id} or @ids) {
764
765     # retrieve order for single id
766     # NOTE: this query is intended to fetch all information only ONCE.
767     # so if any of these infos is important (or even different) for any item,
768     # it will be killed out and then has to be fetched from the item scope query further down
769     $query =
770       qq|SELECT o.cp_id, o.ordnumber, o.transdate, o.reqdate,
771            o.taxincluded, o.shippingpoint, o.shipvia, o.notes, o.intnotes,
772            (SELECT cu.name FROM currencies cu WHERE cu.id=o.currency_id) AS currency, e.name AS employee, o.employee_id, o.salesman_id,
773            o.${vc}_id, cv.name AS ${vc}, o.amount AS invtotal,
774            o.closed, o.reqdate, o.quonumber, o.department_id, o.cusordnumber,
775            d.description AS department, o.payment_id, o.language_id, o.taxzone_id,
776            o.delivery_customer_id, o.delivery_vendor_id, o.proforma, o.shipto_id,
777            o.globalproject_id, o.delivered, o.transaction_description, o.delivery_term_id
778          FROM oe o
779          JOIN ${vc} cv ON (o.${vc}_id = cv.id)
780          LEFT JOIN employee e ON (o.employee_id = e.id)
781          LEFT JOIN department d ON (o.department_id = d.id) | .
782         ($form->{id}
783          ? "WHERE o.id = ?"
784          : "WHERE o.id IN (" . join(', ', map("? ", @ids)) . ")"
785         );
786     @values = $form->{id} ? ($form->{id}) : @ids;
787     $sth = prepare_execute_query($form, $dbh, $query, @values);
788
789     $ref = $sth->fetchrow_hashref("NAME_lc");
790
791     if ($ref) {
792       map { $form->{$_} = $ref->{$_} } keys %$ref;
793
794       $form->{saved_xyznumber} = $form->{$form->{type} =~ /_quotation$/ ? "quonumber" : "ordnumber"};
795
796       # set all entries for multiple ids blank that yield different information
797       while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
798         map { $form->{$_} = '' if ($ref->{$_} ne $form->{$_}) } keys %$ref;
799       }
800     }
801
802     # if not given, fill transdate with current_date
803     $form->{transdate} = $form->current_date($myconfig)
804       unless $form->{transdate};
805
806     $sth->finish;
807
808     if ($form->{delivery_customer_id}) {
809       $query = qq|SELECT name FROM customer WHERE id = ?|;
810       ($form->{delivery_customer_string}) = selectrow_query($form, $dbh, $query, $form->{delivery_customer_id});
811     }
812
813     if ($form->{delivery_vendor_id}) {
814       $query = qq|SELECT name FROM customer WHERE id = ?|;
815       ($form->{delivery_vendor_string}) = selectrow_query($form, $dbh, $query, $form->{delivery_vendor_id});
816     }
817
818     # shipto and pinted/mailed/queued status makes only sense for single id retrieve
819     if (!@ids) {
820       $query = qq|SELECT s.* FROM shipto s WHERE s.trans_id = ? AND s.module = 'OE'|;
821       $sth = prepare_execute_query($form, $dbh, $query, $form->{id});
822
823       $ref = $sth->fetchrow_hashref("NAME_lc");
824       delete($ref->{id});
825       map { $form->{$_} = $ref->{$_} } keys %$ref;
826       $sth->finish;
827
828       # get printed, emailed and queued
829       $query = qq|SELECT s.printed, s.emailed, s.spoolfile, s.formname FROM status s WHERE s.trans_id = ?|;
830       $sth = prepare_execute_query($form, $dbh, $query, $form->{id});
831
832       while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
833         $form->{printed} .= "$ref->{formname} " if $ref->{printed};
834         $form->{emailed} .= "$ref->{formname} " if $ref->{emailed};
835         $form->{queued}  .= "$ref->{formname} $ref->{spoolfile} " if $ref->{spoolfile};
836       }
837       $sth->finish;
838       map { $form->{$_} =~ s/ +$//g } qw(printed emailed queued);
839     }    # if !@ids
840
841     my $transdate = $form->{transdate} ? $dbh->quote($form->{transdate}) : "current_date";
842
843     $form->{taxzone_id} = 0 unless ($form->{taxzone_id});
844
845     # retrieve individual items
846     # this query looks up all information about the items
847     # stuff different from the whole will not be overwritten, but saved with a suffix.
848     $query =
849       qq|SELECT o.id AS orderitems_id,
850            c1.accno AS inventory_accno, c1.new_chart_id AS inventory_new_chart, date($transdate) - c1.valid_from as inventory_valid,
851            c2.accno AS income_accno,    c2.new_chart_id AS income_new_chart,    date($transdate) - c2.valid_from as income_valid,
852            c3.accno AS expense_accno,   c3.new_chart_id AS expense_new_chart,   date($transdate) - c3.valid_from as expense_valid,
853            oe.ordnumber AS ordnumber_oe, oe.transdate AS transdate_oe, oe.cusordnumber AS cusordnumber_oe,
854            p.partnumber, p.assembly, p.listprice, o.description, o.qty,
855            o.sellprice, o.parts_id AS id, o.unit, o.discount, p.notes AS partnotes, p.inventory_accno_id AS part_inventory_accno_id,
856            o.reqdate, o.project_id, o.serialnumber, o.ship, o.lastcost,
857            o.ordnumber, o.transdate, o.cusordnumber, o.subtotal, o.longdescription,
858            o.price_factor_id, o.price_factor, o.marge_price_factor,
859            pr.projectnumber, p.formel,
860            pg.partsgroup, o.pricegroup_id, (SELECT pricegroup FROM pricegroup WHERE id=o.pricegroup_id) as pricegroup
861          FROM orderitems o
862          JOIN parts p ON (o.parts_id = p.id)
863          JOIN oe ON (o.trans_id = oe.id)
864          LEFT JOIN chart c1 ON ((SELECT inventory_accno_id                   FROM buchungsgruppen WHERE id=p.buchungsgruppen_id) = c1.id)
865          LEFT JOIN chart c2 ON ((SELECT income_accno_id_$form->{taxzone_id}  FROM buchungsgruppen WHERE id=p.buchungsgruppen_id) = c2.id)
866          LEFT JOIN chart c3 ON ((SELECT expense_accno_id_$form->{taxzone_id} FROM buchungsgruppen WHERE id=p.buchungsgruppen_id) = c3.id)
867          LEFT JOIN project pr ON (o.project_id = pr.id)
868          LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id) | .
869       ($form->{id}
870        ? qq|WHERE o.trans_id = ?|
871        : qq|WHERE o.trans_id IN (| . join(", ", map("?", @ids)) . qq|)|) .
872       qq|ORDER BY o.oid|;
873
874     @ids = $form->{id} ? ($form->{id}) : @ids;
875     $sth = prepare_execute_query($form, $dbh, $query, @values);
876
877     while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
878       # Retrieve custom variables.
879       my $cvars = CVar->get_custom_variables(dbh        => $dbh,
880                                              module     => 'IC',
881                                              sub_module => 'orderitems',
882                                              trans_id   => $ref->{orderitems_id},
883                                             );
884       map { $ref->{"ic_cvar_$_->{name}"} = $_->{value} } @{ $cvars };
885
886       # Handle accounts.
887       if (!$ref->{"part_inventory_accno_id"}) {
888         map({ delete($ref->{$_}); } qw(inventory_accno inventory_new_chart inventory_valid));
889       }
890       delete($ref->{"part_inventory_accno_id"});
891
892       # in collective order, copy global ordnumber, transdate, cusordnumber into item scope
893       #   unless already present there
894       # remove _oe entries afterwards
895       map { $ref->{$_} = $ref->{"${_}_oe"} if ($ref->{$_} eq '') }
896         qw|ordnumber transdate cusordnumber|
897         if (@ids);
898       map { delete $ref->{$_} } qw|ordnumber_oe transdate_oe cusordnumber_oe|;
899
900
901
902       while ($ref->{inventory_new_chart} && ($ref->{inventory_valid} >= 0)) {
903         my $query =
904           qq|SELECT accno AS inventory_accno, | .
905           qq|  new_chart_id AS inventory_new_chart, | .
906           qq|  date($transdate) - valid_from AS inventory_valid | .
907           qq|FROM chart WHERE id = $ref->{inventory_new_chart}|;
908         ($ref->{inventory_accno}, $ref->{inventory_new_chart},
909          $ref->{inventory_valid}) = selectrow_query($form, $dbh, $query);
910       }
911
912       while ($ref->{income_new_chart} && ($ref->{income_valid} >= 0)) {
913         my $query =
914           qq|SELECT accno AS income_accno, | .
915           qq|  new_chart_id AS income_new_chart, | .
916           qq|  date($transdate) - valid_from AS income_valid | .
917           qq|FROM chart WHERE id = $ref->{income_new_chart}|;
918         ($ref->{income_accno}, $ref->{income_new_chart},
919          $ref->{income_valid}) = selectrow_query($form, $dbh, $query);
920       }
921
922       while ($ref->{expense_new_chart} && ($ref->{expense_valid} >= 0)) {
923         my $query =
924           qq|SELECT accno AS expense_accno, | .
925           qq|  new_chart_id AS expense_new_chart, | .
926           qq|  date($transdate) - valid_from AS expense_valid | .
927           qq|FROM chart WHERE id = $ref->{expense_new_chart}|;
928         ($ref->{expense_accno}, $ref->{expense_new_chart},
929          $ref->{expense_valid}) = selectrow_query($form, $dbh, $query);
930       }
931
932       # delete orderitems_id in collective orders, so that they get cloned no matter what
933       delete $ref->{orderitems_id} if (@ids);
934
935       # get tax rates and description
936       my $accno_id = ($form->{vc} eq "customer") ? $ref->{income_accno} : $ref->{expense_accno};
937       $query =
938         qq|SELECT c.accno, t.taxdescription, t.rate, t.taxnumber | .
939         qq|FROM tax t LEFT JOIN chart c on (c.id = t.chart_id) | .
940         qq|WHERE t.id IN (SELECT tk.tax_id FROM taxkeys tk | .
941         qq|               WHERE tk.chart_id = (SELECT id FROM chart WHERE accno = ?) | .
942         qq|                 AND startdate <= $transdate ORDER BY startdate DESC LIMIT 1) | .
943         qq|ORDER BY c.accno|;
944       my $stw = prepare_execute_query($form, $dbh, $query, $accno_id);
945       $ref->{taxaccounts} = "";
946       my $i = 0;
947       while (my $ptr = $stw->fetchrow_hashref("NAME_lc")) {
948         if (($ptr->{accno} eq "") && ($ptr->{rate} == 0)) {
949           $i++;
950           $ptr->{accno} = $i;
951         }
952         $ref->{taxaccounts} .= "$ptr->{accno} ";
953         if (!($form->{taxaccounts} =~ /\Q$ptr->{accno}\E/)) {
954           $form->{"$ptr->{accno}_rate"}        = $ptr->{rate};
955           $form->{"$ptr->{accno}_description"} = $ptr->{taxdescription};
956           $form->{"$ptr->{accno}_taxnumber"}   = $ptr->{taxnumber};
957           $form->{taxaccounts} .= "$ptr->{accno} ";
958         }
959
960       }
961
962       chop $ref->{taxaccounts};
963
964       push @{ $form->{form_details} }, $ref;
965       $stw->finish;
966     }
967     $sth->finish;
968
969   } else {
970
971     # get last name used
972     $form->lastname_used($dbh, $myconfig, $form->{vc})
973       unless $form->{"$form->{vc}_id"};
974
975   }
976
977   $form->{exchangerate} = $form->get_exchangerate($dbh, $form->{currency}, $form->{transdate}, ($form->{vc} eq 'customer') ? "buy" : "sell");
978
979   Common::webdav_folder($form);
980
981   $self->load_periodic_invoice_config($form);
982
983   my $rc = $dbh->commit;
984
985   $main::lxdebug->leave_sub();
986
987   return $rc;
988 }
989
990 sub retrieve_simple {
991   $main::lxdebug->enter_sub();
992
993   my $self     = shift;
994   my %params   = @_;
995
996   Common::check_params(\%params, qw(id));
997
998   my $myconfig    = \%main::myconfig;
999   my $form        = $main::form;
1000
1001   my $dbh         = $params{dbh} || $form->get_standard_dbh($myconfig);
1002
1003   my $oe_query    = qq|SELECT * FROM oe         WHERE id = ?|;
1004   my $oi_query    = qq|SELECT * FROM orderitems WHERE trans_id = ?|;
1005
1006   my $order            = selectfirst_hashref_query($form, $dbh, $oe_query, conv_i($params{id}));
1007   $order->{orderitems} = selectall_hashref_query(  $form, $dbh, $oi_query, conv_i($params{id}));
1008
1009   $main::lxdebug->leave_sub();
1010
1011   return $order;
1012 }
1013
1014 sub order_details {
1015   $main::lxdebug->enter_sub();
1016
1017   my ($self, $myconfig, $form) = @_;
1018
1019   # connect to database
1020   my $dbh = $form->get_standard_dbh;
1021   my $query;
1022   my @values = ();
1023   my $sth;
1024   my $nodiscount;
1025   my $yesdiscount;
1026   my $nodiscount_subtotal = 0;
1027   my $discount_subtotal = 0;
1028   my $item;
1029   my $i;
1030   my @partsgroup = ();
1031   my $partsgroup;
1032   my $position = 0;
1033   my $subtotal_header = 0;
1034   my $subposition = 0;
1035   my %taxaccounts;
1036   my %taxbase;
1037   my $tax_rate;
1038   my $taxamount;
1039
1040   my (@project_ids);
1041
1042   push(@project_ids, $form->{"globalproject_id"}) if ($form->{"globalproject_id"});
1043
1044   $form->get_lists('price_factors' => 'ALL_PRICE_FACTORS',
1045                    'departments'   => 'ALL_DEPARTMENTS');
1046   my %price_factors;
1047
1048   foreach my $pfac (@{ $form->{ALL_PRICE_FACTORS} }) {
1049     $price_factors{$pfac->{id}}  = $pfac;
1050     $pfac->{factor}             *= 1;
1051     $pfac->{formatted_factor}    = $form->format_amount($myconfig, $pfac->{factor});
1052   }
1053
1054   # lookup department
1055   foreach my $dept (@{ $form->{ALL_DEPARTMENTS} }) {
1056     next unless $dept->{id} eq $form->{department_id};
1057     $form->{department} = $dept->{description};
1058     last;
1059   }
1060
1061   # sort items by partsgroup
1062   for $i (1 .. $form->{rowcount}) {
1063     $partsgroup = "";
1064     if ($form->{"partsgroup_$i"} && $form->{groupitems}) {
1065       $partsgroup = $form->{"partsgroup_$i"};
1066     }
1067     push @partsgroup, [$i, $partsgroup];
1068     push(@project_ids, $form->{"project_id_$i"}) if ($form->{"project_id_$i"});
1069   }
1070
1071   my $projects = [];
1072   my %projects_by_id;
1073   if (@project_ids) {
1074     $projects = SL::DB::Manager::Project->get_all(query => [ id => \@project_ids ]);
1075     %projects_by_id = map { $_->id => $_ } @$projects;
1076   }
1077
1078   if ($projects_by_id{$form->{"globalproject_id"}}) {
1079     $form->{globalprojectnumber} = $projects_by_id{$form->{"globalproject_id"}}->projectnumber;
1080     $form->{globalprojectdescription} = $projects_by_id{$form->{"globalproject_id"}}->description;
1081
1082     for (@{ $projects_by_id{$form->{"globalproject_id"}}->cvars_by_config }) {
1083       $form->{"project_cvar_" . $_->config->name} = $_->value_as_text;
1084     }
1085   }
1086
1087   $form->{discount} = [];
1088
1089   $form->{TEMPLATE_ARRAYS} = { };
1090   IC->prepare_parts_for_printing(myconfig => $myconfig, form => $form);
1091
1092   my $ic_cvar_configs = CVar->get_configs(module => 'IC');
1093   my $project_cvar_configs = CVar->get_configs(module => 'Projects');
1094
1095   my @arrays =
1096     qw(runningnumber number description longdescription qty ship unit bin
1097        partnotes serialnumber reqdate sellprice listprice netprice
1098        discount p_discount discount_sub nodiscount_sub
1099        linetotal  nodiscount_linetotal tax_rate projectnumber projectdescription
1100        price_factor price_factor_name partsgroup weight lineweight);
1101
1102   push @arrays, map { "ic_cvar_$_->{name}" } @{ $ic_cvar_configs };
1103   push @arrays, map { "project_cvar_$_->{name}" } @{ $project_cvar_configs };
1104
1105   my @tax_arrays = qw(taxbase tax taxdescription taxrate taxnumber);
1106
1107   map { $form->{TEMPLATE_ARRAYS}->{$_} = [] } (@arrays, @tax_arrays);
1108
1109   my $totalweight = 0;
1110   my $sameitem = "";
1111   foreach $item (sort { $a->[1] cmp $b->[1] } @partsgroup) {
1112     $i = $item->[0];
1113
1114     if ($item->[1] ne $sameitem) {
1115       push(@{ $form->{TEMPLATE_ARRAYS}->{description} }, qq|$item->[1]|);
1116       $sameitem = $item->[1];
1117
1118       map({ push(@{ $form->{TEMPLATE_ARRAYS}->{$_} }, "") } grep({ $_ ne "description" } @arrays));
1119     }
1120
1121     $form->{"qty_$i"} = $form->parse_amount($myconfig, $form->{"qty_$i"});
1122
1123     if ($form->{"id_$i"} != 0) {
1124
1125       # add number, description and qty to $form->{number}, ....
1126
1127       if ($form->{"subtotal_$i"} && !$subtotal_header) {
1128         $subtotal_header = $i;
1129         $position = int($position);
1130         $subposition = 0;
1131         $position++;
1132       } elsif ($subtotal_header) {
1133         $subposition += 1;
1134         $position = int($position);
1135         $position = $position.".".$subposition;
1136       } else {
1137         $position = int($position);
1138         $position++;
1139       }
1140
1141       my $price_factor = $price_factors{$form->{"price_factor_id_$i"}} || { 'factor' => 1 };
1142
1143       push @{ $form->{TEMPLATE_ARRAYS}->{runningnumber} },     $position;
1144       push @{ $form->{TEMPLATE_ARRAYS}->{number} },            $form->{"partnumber_$i"};
1145       push @{ $form->{TEMPLATE_ARRAYS}->{description} },       $form->{"description_$i"};
1146       push @{ $form->{TEMPLATE_ARRAYS}->{longdescription} },   $form->{"longdescription_$i"};
1147       push @{ $form->{TEMPLATE_ARRAYS}->{qty} },               $form->format_amount($myconfig, $form->{"qty_$i"});
1148       push @{ $form->{TEMPLATE_ARRAYS}->{qty_nofmt} },         $form->{"qty_$i"};
1149       push @{ $form->{TEMPLATE_ARRAYS}->{ship} },              $form->format_amount($myconfig, $form->{"ship_$i"});
1150       push @{ $form->{TEMPLATE_ARRAYS}->{ship_nofmt} },        $form->{"ship_$i"};
1151       push @{ $form->{TEMPLATE_ARRAYS}->{unit} },              $form->{"unit_$i"};
1152       push @{ $form->{TEMPLATE_ARRAYS}->{bin} },               $form->{"bin_$i"};
1153       push @{ $form->{TEMPLATE_ARRAYS}->{partnotes} },         $form->{"partnotes_$i"};
1154       push @{ $form->{TEMPLATE_ARRAYS}->{serialnumber} },      $form->{"serialnumber_$i"};
1155       push @{ $form->{TEMPLATE_ARRAYS}->{reqdate} },           $form->{"reqdate_$i"};
1156       push @{ $form->{TEMPLATE_ARRAYS}->{sellprice} },         $form->{"sellprice_$i"};
1157       push @{ $form->{TEMPLATE_ARRAYS}->{sellprice_nofmt} },   $form->parse_amount($myconfig, $form->{"sellprice_$i"});
1158       push @{ $form->{TEMPLATE_ARRAYS}->{listprice} },         $form->{"listprice_$i"};
1159       push @{ $form->{TEMPLATE_ARRAYS}->{price_factor} },      $price_factor->{formatted_factor};
1160       push @{ $form->{TEMPLATE_ARRAYS}->{price_factor_name} }, $price_factor->{description};
1161       push @{ $form->{TEMPLATE_ARRAYS}->{partsgroup} },        $form->{"partsgroup_$i"};
1162
1163       my $sellprice     = $form->parse_amount($myconfig, $form->{"sellprice_$i"});
1164       my ($dec)         = ($sellprice =~ /\.(\d+)/);
1165       my $decimalplaces = max 2, length($dec);
1166
1167       my $parsed_discount            = $form->parse_amount($myconfig, $form->{"discount_$i"});
1168
1169       my $linetotal_exact            = $form->{"qty_$i"} * $sellprice * (100 - $parsed_discount) / 100 / $price_factor->{factor};
1170       my $linetotal                  = $form->round_amount($linetotal_exact, 2);
1171
1172       my $nodiscount_exact_linetotal = $form->{"qty_$i"} * $sellprice                                  / $price_factor->{factor};
1173       my $nodiscount_linetotal       = $form->round_amount($nodiscount_exact_linetotal,2);
1174
1175       my $discount                   = $nodiscount_linetotal - $linetotal; # is always rounded because $nodiscount_linetotal and $linetotal are rounded
1176
1177       my $discount_round_error       = $discount + ($linetotal_exact - $nodiscount_exact_linetotal); # not used
1178
1179       $form->{"netprice_$i"}   = $form->round_amount($form->{"qty_$i"} ? ($linetotal / $form->{"qty_$i"}) : 0, 2);
1180
1181       push @{ $form->{TEMPLATE_ARRAYS}->{netprice} },       ($form->{"netprice_$i"} != 0) ? $form->format_amount($myconfig, $form->{"netprice_$i"}, $decimalplaces) : '';
1182       push @{ $form->{TEMPLATE_ARRAYS}->{netprice_nofmt} }, ($form->{"netprice_$i"} != 0) ? $form->{"netprice_$i"} : '';
1183
1184       $linetotal = ($linetotal != 0) ? $linetotal : '';
1185
1186       push @{ $form->{TEMPLATE_ARRAYS}->{discount} },       ($discount != 0) ? $form->format_amount($myconfig, $discount * -1, 2) : '';
1187       push @{ $form->{TEMPLATE_ARRAYS}->{discount_nofmt} }, ($discount != 0) ? $discount * -1 : '';
1188       push @{ $form->{TEMPLATE_ARRAYS}->{p_discount} },     $form->{"discount_$i"};
1189
1190       $form->{ordtotal}         += $linetotal;
1191       $form->{nodiscount_total} += $nodiscount_linetotal;
1192       $form->{discount_total}   += $discount;
1193
1194       if ($subtotal_header) {
1195         $discount_subtotal   += $linetotal;
1196         $nodiscount_subtotal += $nodiscount_linetotal;
1197       }
1198
1199       if ($form->{"subtotal_$i"} && $subtotal_header && ($subtotal_header != $i)) {
1200         push @{ $form->{TEMPLATE_ARRAYS}->{discount_sub} },         $form->format_amount($myconfig, $discount_subtotal,   2);
1201         push @{ $form->{TEMPLATE_ARRAYS}->{discount_sub_nofmt} },   $discount_subtotal;
1202         push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_sub} },       $form->format_amount($myconfig, $nodiscount_subtotal, 2);
1203         push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_sub_nofmt} }, $nodiscount_subtotal;
1204
1205         $discount_subtotal   = 0;
1206         $nodiscount_subtotal = 0;
1207         $subtotal_header     = 0;
1208
1209       } else {
1210         push @{ $form->{TEMPLATE_ARRAYS}->{$_} }, "" for qw(discount_sub nodiscount_sub discount_sub_nofmt nodiscount_sub_nofmt);
1211       }
1212
1213       if (!$form->{"discount_$i"}) {
1214         $nodiscount += $linetotal;
1215       }
1216
1217       my $project = $projects_by_id{$form->{"project_id_$i"}} || SL::DB::Project->new;
1218
1219       push @{ $form->{TEMPLATE_ARRAYS}->{linetotal} },                  $form->format_amount($myconfig, $linetotal, 2);
1220       push @{ $form->{TEMPLATE_ARRAYS}->{linetotal_nofmt} },            $linetotal_exact;
1221       push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_linetotal} },       $form->format_amount($myconfig, $nodiscount_linetotal, 2);
1222       push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_linetotal_nofmt} }, $nodiscount_linetotal;
1223       push @{ $form->{TEMPLATE_ARRAYS}->{projectnumber} },              $project->projectnumber;
1224       push @{ $form->{TEMPLATE_ARRAYS}->{projectdescription} },         $project->description;
1225
1226       my $lineweight = $form->{"qty_$i"} * $form->{"weight_$i"};
1227       $totalweight += $lineweight;
1228       push @{ $form->{TEMPLATE_ARRAYS}->{weight} },            $form->format_amount($myconfig, $form->{"weight_$i"}, 3);
1229       push @{ $form->{TEMPLATE_ARRAYS}->{weight_nofmt} },      $form->{"weight_$i"};
1230       push @{ $form->{TEMPLATE_ARRAYS}->{lineweight} },        $form->format_amount($myconfig, $lineweight, 3);
1231       push @{ $form->{TEMPLATE_ARRAYS}->{lineweight_nofmt} },  $lineweight;
1232
1233       my ($taxamount, $taxbase);
1234       my $taxrate = 0;
1235
1236       map { $taxrate += $form->{"${_}_rate"} } split(/ /, $form->{"taxaccounts_$i"});
1237
1238       if ($form->{taxincluded}) {
1239
1240         # calculate tax
1241         $taxamount = $linetotal * $taxrate / (1 + $taxrate);
1242         $taxbase = $linetotal / (1 + $taxrate);
1243       } else {
1244         $taxamount = $linetotal * $taxrate;
1245         $taxbase   = $linetotal;
1246       }
1247
1248       if ($taxamount != 0) {
1249         foreach my $accno (split / /, $form->{"taxaccounts_$i"}) {
1250           $taxaccounts{$accno} += $taxamount * $form->{"${accno}_rate"} / $taxrate;
1251           $taxbase{$accno}     += $taxbase;
1252         }
1253       }
1254
1255       $tax_rate = $taxrate * 100;
1256       push(@{ $form->{TEMPLATE_ARRAYS}->{tax_rate} }, qq|$tax_rate|);
1257
1258       if ($form->{"assembly_$i"}) {
1259         $sameitem = "";
1260
1261         # get parts and push them onto the stack
1262         my $sortorder = "";
1263         if ($form->{groupitems}) {
1264           $sortorder = qq|ORDER BY pg.partsgroup, a.oid|;
1265         } else {
1266           $sortorder = qq|ORDER BY a.oid|;
1267         }
1268
1269         $query = qq|SELECT p.partnumber, p.description, p.unit, a.qty, | .
1270                  qq|pg.partsgroup | .
1271                  qq|FROM assembly a | .
1272                  qq|  JOIN parts p ON (a.parts_id = p.id) | .
1273                  qq|    LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id) | .
1274                  qq|    WHERE a.bom = '1' | .
1275                  qq|    AND a.id = ? | . $sortorder;
1276         @values = ($form->{"id_$i"});
1277         $sth = $dbh->prepare($query);
1278         $sth->execute(@values) || $form->dberror($query);
1279
1280         while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
1281           if ($form->{groupitems} && $ref->{partsgroup} ne $sameitem) {
1282             map({ push(@{ $form->{TEMPLATE_ARRAYS}->{$_} }, "") } grep({ $_ ne "description" } @arrays));
1283             $sameitem = ($ref->{partsgroup}) ? $ref->{partsgroup} : "--";
1284             push(@{ $form->{TEMPLATE_ARRAYS}->{description} }, $sameitem);
1285           }
1286
1287           push(@{ $form->{TEMPLATE_ARRAYS}->{description} }, $form->format_amount($myconfig, $ref->{qty} * $form->{"qty_$i"}) . qq|, $ref->{partnumber}, $ref->{description}|);
1288           map({ push(@{ $form->{TEMPLATE_ARRAYS}->{$_} }, "") } grep({ $_ ne "description" } @arrays));
1289         }
1290         $sth->finish;
1291       }
1292
1293       push @{ $form->{TEMPLATE_ARRAYS}->{"ic_cvar_$_->{name}"} },
1294         CVar->format_to_template(CVar->parse($form->{"ic_cvar_$_->{name}_$i"}, $_), $_)
1295           for @{ $ic_cvar_configs };
1296
1297       push @{ $form->{TEMPLATE_ARRAYS}->{"project_cvar_" . $_->config->name} }, $_->value_as_text for @{ $project->cvars_by_config };
1298     }
1299   }
1300
1301   $form->{totalweight}       = $form->format_amount($myconfig, $totalweight, 3);
1302   $form->{totalweight_nofmt} = $totalweight;
1303   my $defaults = AM->get_defaults();
1304   $form->{weightunit}        = $defaults->{weightunit};
1305
1306   my $tax = 0;
1307   foreach $item (sort keys %taxaccounts) {
1308     $tax += $taxamount = $form->round_amount($taxaccounts{$item}, 2);
1309
1310     push(@{ $form->{TEMPLATE_ARRAYS}->{taxbase} },        $form->format_amount($myconfig, $taxbase{$item}, 2));
1311     push(@{ $form->{TEMPLATE_ARRAYS}->{taxbase_nofmt} },  $taxbase{$item});
1312     push(@{ $form->{TEMPLATE_ARRAYS}->{tax} },            $form->format_amount($myconfig, $taxamount,      2));
1313     push(@{ $form->{TEMPLATE_ARRAYS}->{tax_nofmt} },      $taxamount);
1314     push(@{ $form->{TEMPLATE_ARRAYS}->{taxrate} },        $form->format_amount($myconfig, $form->{"${item}_rate"} * 100));
1315     push(@{ $form->{TEMPLATE_ARRAYS}->{taxrate_nofmt} },  $form->{"${item}_rate"} * 100);
1316     push(@{ $form->{TEMPLATE_ARRAYS}->{taxnumber} },      $form->{"${item}_taxnumber"});
1317
1318     my $tax_obj     = SL::DB::Manager::Tax->find_by(taxnumber => $form->{"${item}_taxnumber"});
1319     my $description = $tax_obj->translated_attribute('taxdescription',  $form->{language_id}, 0) if $tax_obj;
1320     push(@{ $form->{TEMPLATE_ARRAYS}->{taxdescription} }, $description . q{ } . 100 * $form->{"${item}_rate"} . q{%});
1321   }
1322
1323   $form->{nodiscount_subtotal} = $form->format_amount($myconfig, $form->{nodiscount_total}, 2);
1324   $form->{discount_total}      = $form->format_amount($myconfig, $form->{discount_total}, 2);
1325   $form->{nodiscount}          = $form->format_amount($myconfig, $nodiscount, 2);
1326   $form->{yesdiscount}         = $form->format_amount($myconfig, $form->{nodiscount_total} - $nodiscount, 2);
1327
1328   if($form->{taxincluded}) {
1329     $form->{subtotal}       = $form->format_amount($myconfig, $form->{ordtotal} - $tax, 2);
1330     $form->{subtotal_nofmt} = $form->{ordtotal} - $tax;
1331   } else {
1332     $form->{subtotal}       = $form->format_amount($myconfig, $form->{ordtotal}, 2);
1333     $form->{subtotal_nofmt} = $form->{ordtotal};
1334   }
1335
1336   $form->{ordtotal} = ($form->{taxincluded}) ? $form->{ordtotal} : $form->{ordtotal} + $tax;
1337
1338   # format amounts
1339   $form->{quototal} = $form->{ordtotal} = $form->format_amount($myconfig, $form->{ordtotal}, 2);
1340
1341   if ($form->{type} =~ /_quotation/) {
1342     $form->set_payment_options($myconfig, $form->{quodate});
1343   } else {
1344     $form->set_payment_options($myconfig, $form->{orddate});
1345   }
1346
1347   $form->{username} = $myconfig->{name};
1348
1349   $dbh->disconnect;
1350
1351   $form->{delivery_term} = SL::DB::Manager::DeliveryTerm->find_by(id => $form->{delivery_term_id} || undef);
1352   $form->{delivery_term}->description_long($form->{delivery_term}->translated_attribute('description_long', $form->{language_id})) if $form->{delivery_term} && $form->{language_id};
1353
1354   $main::lxdebug->leave_sub();
1355 }
1356
1357 sub project_description {
1358   $main::lxdebug->enter_sub();
1359
1360   my ($self, $dbh, $id) = @_;
1361
1362   my $query = qq|SELECT description FROM project WHERE id = ?|;
1363   my ($value) = selectrow_query($main::form, $dbh, $query, $id);
1364
1365   $main::lxdebug->leave_sub();
1366
1367   return $value;
1368 }
1369
1370 1;
1371
1372 __END__
1373
1374 =head1 NAME
1375
1376 OE.pm - Order entry module
1377
1378 =head1 DESCRIPTION
1379
1380 OE.pm is part of the OE module. OE is responsible for sales and purchase orders, as well as sales quotations and purchase requests. This file abstracts the database tables C<oe> and C<orderitems>.
1381
1382 =head1 FUNCTIONS
1383
1384 =over 4
1385
1386 =item retrieve_simple PARAMS
1387
1388 simple OE retrieval by id. does not look up customer, vendor, units or any other stuff. only oe and orderitems.
1389
1390   my $order = retrieve_simple(id => 2);
1391
1392   $order => {
1393     %_OE_CONTENT,
1394     orderitems => [
1395       %_ORDERITEM_ROW_1,
1396       %_ORDERITEM_ROW_2,
1397       ...
1398     ]
1399   }
1400
1401 =back
1402
1403 =cut