Telefonnotizen: in Berichten Angebot/Auftrag filtern können.
[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., 51 Franklin Street, Fifth Floor, Boston,
29 # MA 02110-1335, USA.
30 #======================================================================
31 #
32 # Order entry module
33 # Quotation
34 #======================================================================
35
36 package OE;
37
38 use List::Util qw(max first);
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::Project;
46 use SL::DB::ProjectType;
47 use SL::DB::RequirementSpecOrder;
48 use SL::DB::Status;
49 use SL::DB::Tax;
50 use SL::DBUtils;
51 use SL::HTML::Restrict;
52 use SL::IC;
53 use SL::TransNumber;
54 use SL::Util qw(trim);
55 use SL::DB;
56 use SL::YAML;
57 use Text::ParseWords;
58
59 use strict;
60
61 sub transactions {
62   $main::lxdebug->enter_sub();
63
64   my ($self, $myconfig, $form) = @_;
65
66   # connect to database
67   my $dbh = $form->get_standard_dbh;
68
69   my $query;
70   my $ordnumber = 'ordnumber';
71   my $quotation = '0';
72
73   my @values;
74   my $where;
75
76   my ($periodic_invoices_columns, $periodic_invoices_joins);
77
78   my $rate = ($form->{vc} eq 'customer') ? 'buy' : 'sell';
79
80   if ($form->{type} =~ /_quotation$/) {
81     $quotation = '1';
82     $ordnumber = 'quonumber';
83
84   } elsif ($form->{type} eq 'sales_order') {
85     $periodic_invoices_columns = qq| , COALESCE(pcfg.active, 'f') AS periodic_invoices |;
86     $periodic_invoices_joins   = qq| LEFT JOIN periodic_invoices_configs pcfg ON (o.id = pcfg.oe_id) |;
87   }
88
89   my $vc = $form->{vc} eq "customer" ? "customer" : "vendor";
90
91   my %billed_amount;
92   my %billed_netamount;
93   if ($form->{l_remaining_amount} || $form->{l_remaining_netamount}) {
94     $query = <<'';
95       SELECT from_id, ar.amount, ar.netamount FROM (
96         SELECT from_id, to_id
97         FROM record_links
98         WHERE from_table = 'oe' AND to_table = 'ar'
99         UNION
100         SELECT rl1.from_id, rl2.to_id
101         FROM record_links rl1
102         LEFT JOIN record_links rl2 ON (rl1.to_table = rl2.from_table AND rl1.to_id = rl2.from_id)
103         WHERE rl1.from_table = 'oe' AND rl2.to_table = 'ar'
104         UNION
105         SELECT rl1.from_id, rl3.to_id
106         FROM record_links rl1
107         JOIN record_links rl2 ON (rl1.to_table = rl2.from_table AND rl1.to_id = rl2.from_id)
108         JOIN record_links rl3 ON (rl2.to_table = rl3.from_table AND rl2.to_id = rl3.from_id)
109         WHERE rl1.from_table = 'oe' AND rl2.to_table = 'ar' AND rl3.to_table = 'ar'
110       ) rl
111       LEFT JOIN ar ON ar.id = rl.to_id
112
113     for my $ref (@{ selectall_hashref_query($form, $dbh, $query) }) {
114       $billed_amount{   $ref->{from_id}} += $ref->{amount};
115       $billed_netamount{$ref->{from_id}} += $ref->{netamount};
116     }
117   }
118
119   my ($phone_notes_columns, $phone_notes_join);
120   $form->{phone_notes} = trim($form->{phone_notes});
121   if ($form->{phone_notes}) {
122     $phone_notes_columns = qq| , phone_notes.subject AS phone_notes_subject, phone_notes.body AS phone_notes_body |;
123     $phone_notes_join    = qq| JOIN notes phone_notes ON (o.id = phone_notes.trans_id AND phone_notes.trans_module LIKE 'oe') |;
124   }
125
126   $query =
127     qq|SELECT o.id, o.ordnumber, o.transdate, o.reqdate, | .
128     qq|  o.amount, ct.${vc}number, ct.name, o.netamount, o.${vc}_id, o.globalproject_id, | .
129     qq|  o.closed, o.delivered, o.quonumber, o.cusordnumber, o.shippingpoint, o.shipvia, | .
130     qq|  o.transaction_description, | .
131     qq|  o.marge_total, o.marge_percent, | .
132     qq|  o.exchangerate, | .
133     qq|  o.itime::DATE AS insertdate, | .
134     qq|  o.intnotes, | .
135     qq|  department.description as department, | .
136     qq|  ex.$rate AS daily_exchangerate, | .
137     qq|  pt.description AS payment_terms, | .
138     qq|  pr.projectnumber AS globalprojectnumber, | .
139     qq|  e.name AS employee, s.name AS salesman, | .
140     qq|  ct.${vc}number AS vcnumber, ct.country, ct.ustid, ct.business_id,  | .
141     qq|  tz.description AS taxzone | .
142     $periodic_invoices_columns .
143     $phone_notes_columns .
144     qq|  , o.order_probability, o.expected_billing_date, (o.netamount * o.order_probability / 100) AS expected_netamount | .
145     qq|FROM oe o | .
146     qq|JOIN $vc ct ON (o.${vc}_id = ct.id) | .
147     qq|LEFT JOIN contacts cp ON (o.cp_id = cp.cp_id) | .
148     qq|LEFT JOIN employee e ON (o.employee_id = e.id) | .
149     qq|LEFT JOIN employee s ON (o.salesman_id = s.id) | .
150     qq|LEFT JOIN exchangerate ex ON (ex.currency_id = o.currency_id | .
151     qq|  AND ex.transdate = o.transdate) | .
152     qq|LEFT JOIN project pr ON (o.globalproject_id = pr.id) | .
153     qq|LEFT JOIN payment_terms pt ON (pt.id = o.payment_id)| .
154     qq|LEFT JOIN tax_zones tz ON (o.taxzone_id = tz.id) | .
155     qq|LEFT JOIN department   ON (o.department_id = department.id) | .
156     qq|$periodic_invoices_joins | .
157     $phone_notes_join .
158     qq|WHERE (o.quotation = ?) |;
159   push(@values, $quotation);
160
161   if ($form->{department_id}) {
162     $query .= qq| AND o.department_id = ?|;
163     push(@values, $form->{department_id});
164   }
165
166   if ($form->{"project_id"}) {
167     $query .=
168       qq|AND ((globalproject_id = ?) OR EXISTS | .
169       qq|  (SELECT * FROM orderitems oi | .
170       qq|   WHERE oi.project_id = ? AND oi.trans_id = o.id))|;
171     push(@values, conv_i($form->{"project_id"}), conv_i($form->{"project_id"}));
172   }
173
174   if ($form->{"projectnumber"}) {
175     $query .= <<SQL;
176       AND ((pr.projectnumber ILIKE ?) OR EXISTS (
177         SELECT * FROM orderitems oi
178         LEFT JOIN project proi ON proi.id = oi.project_id
179         WHERE proi.projectnumber ILIKE ? AND oi.trans_id = o.id
180       ))
181 SQL
182     push @values, like($form->{"projectnumber"}), like($form->{"projectnumber"});
183   }
184
185   if ($form->{"business_id"}) {
186     $query .= " AND ct.business_id = ?";
187     push(@values, $form->{"business_id"});
188   }
189
190   if ($form->{"${vc}_id"}) {
191     $query .= " AND o.${vc}_id = ?";
192     push(@values, $form->{"${vc}_id"});
193
194   } elsif ($form->{$vc}) {
195     $query .= " AND ct.name ILIKE ?";
196     push(@values, like($form->{$vc}));
197   }
198
199   if ($form->{"cp_name"}) {
200     $query .= " AND (cp.cp_name ILIKE ? OR cp.cp_givenname ILIKE ?)";
201     push(@values, (like($form->{"cp_name"}))x2);
202   }
203
204   if ( !(    ($vc eq 'customer' && ($main::auth->assert('sales_all_edit',    1) || $main::auth->assert('sales_order_view',    1)))
205           || ($vc eq 'vendor'   && ($main::auth->assert('purchase_all_edit', 1) || $main::auth->assert('purchase_order_view', 1))) ) ) {
206     $query .= " AND o.employee_id = (select id from employee where login= ?)";
207     push @values, $::myconfig{login};
208   }
209   if ($form->{employee_id}) {
210     $query .= " AND o.employee_id = ?";
211     push @values, conv_i($form->{employee_id});
212   }
213
214   if ($form->{salesman_id}) {
215     $query .= " AND o.salesman_id = ?";
216     push @values, conv_i($form->{salesman_id});
217   }
218
219   if (!$form->{open} && !$form->{closed}) {
220     $query .= " AND o.id = 0";
221   } elsif (!($form->{open} && $form->{closed})) {
222     $query .= ($form->{open}) ? " AND o.closed = '0'" : " AND o.closed = '1'";
223   }
224
225   if (($form->{"notdelivered"} || $form->{"delivered"}) &&
226       ($form->{"notdelivered"} ne $form->{"delivered"})) {
227     $query .= $form->{"delivered"} ?
228       " AND o.delivered " : " AND NOT o.delivered";
229   }
230
231   if ($form->{$ordnumber}) {
232     $query .= qq| AND o.$ordnumber ILIKE ?|;
233     push(@values, like($form->{$ordnumber}));
234   }
235
236   if ($form->{cusordnumber}) {
237     $query .= qq| AND o.cusordnumber ILIKE ?|;
238     push(@values, like($form->{cusordnumber}));
239   }
240
241   if($form->{transdatefrom}) {
242     $query .= qq| AND o.transdate >= ?|;
243     push(@values, conv_date($form->{transdatefrom}));
244   }
245
246   if($form->{transdateto}) {
247     $query .= qq| AND o.transdate <= ?|;
248     push(@values, conv_date($form->{transdateto}));
249   }
250
251   if($form->{reqdatefrom}) {
252     $query .= qq| AND o.reqdate >= ?|;
253     push(@values, conv_date($form->{reqdatefrom}));
254   }
255
256   if($form->{reqdateto}) {
257     $query .= qq| AND o.reqdate <= ?|;
258     push(@values, conv_date($form->{reqdateto}));
259   }
260
261   if($form->{insertdatefrom}) {
262     $query .= qq| AND o.itime::DATE >= ?|;
263     push(@values, conv_date($form->{insertdatefrom}));
264   }
265
266   if($form->{insertdateto}) {
267     $query .= qq| AND o.itime::DATE <= ?|;
268     push(@values, conv_date($form->{insertdateto}));
269   }
270
271   if ($form->{shippingpoint}) {
272     $query .= qq| AND o.shippingpoint ILIKE ?|;
273     push(@values, like($form->{shippingpoint}));
274   }
275
276   if ($form->{taxzone_id} ne '') { # taxzone_id could be 0
277     $query .= qq| AND tz.id = ?|;
278     push(@values, $form->{taxzone_id});
279   }
280
281   if ($form->{transaction_description}) {
282     $query .= qq| AND o.transaction_description ILIKE ?|;
283     push(@values, like($form->{transaction_description}));
284   }
285
286   if ($form->{periodic_invoices_active} ne $form->{periodic_invoices_inactive}) {
287     my $not  = $form->{periodic_invoices_inactive} ? 'NOT' : '';
288     $query  .= qq| AND ${not} COALESCE(pcfg.active, 'f')|;
289   }
290
291   if ($form->{reqdate_unset_or_old}) {
292     $query .= qq| AND ((o.reqdate IS NULL) OR (o.reqdate < date_trunc('month', current_date)))|;
293   }
294
295   if (($form->{order_probability_value} || '') ne '') {
296     my $op  = $form->{order_probability_value} eq 'le' ? '<=' : '>=';
297     $query .= qq| AND (o.order_probability ${op} ?)|;
298     push @values, trim($form->{order_probability_value});
299   }
300
301   if ($form->{expected_billing_date_from}) {
302     $query .= qq| AND (o.expected_billing_date >= ?)|;
303     push @values, conv_date($form->{expected_billing_date_from});
304   }
305
306   if ($form->{expected_billing_date_to}) {
307     $query .= qq| AND (o.expected_billing_date <= ?)|;
308     push @values, conv_date($form->{expected_billing_date_to});
309   }
310
311   if ($form->{intnotes}) {
312     $query .= qq| AND o.intnotes ILIKE ?|;
313     push(@values, like($form->{intnotes}));
314   }
315
316   if ($form->{phone_notes}) {
317     $query .= qq| AND (phone_notes.subject ILIKE ? OR phone_notes.body ILIKE ?)|;
318     push(@values, like($form->{phone_notes}), like($form->{phone_notes}));
319   }
320
321   if ($form->{parts_partnumber}) {
322     $query .= <<SQL;
323       AND EXISTS (
324         SELECT orderitems.trans_id
325         FROM orderitems
326         LEFT JOIN parts ON (orderitems.parts_id = parts.id)
327         WHERE (orderitems.trans_id = o.id)
328           AND (parts.partnumber ILIKE ?)
329         LIMIT 1
330       )
331 SQL
332     push @values, like($form->{parts_partnumber});
333   }
334
335   if ($form->{parts_description}) {
336     $query .= <<SQL;
337       AND EXISTS (
338         SELECT orderitems.trans_id
339         FROM orderitems
340         WHERE (orderitems.trans_id = o.id)
341           AND (orderitems.description ILIKE ?)
342         LIMIT 1
343       )
344 SQL
345     push @values, like($form->{parts_description});
346   }
347
348   if ($form->{all}) {
349     my @tokens = parse_line('\s+', 0, $form->{all});
350     # ordnumber quonumber customer.name vendor.name transaction_description
351     $query .= qq| AND (
352       o.ordnumber ILIKE ? OR
353       o.quonumber ILIKE ? OR
354       ct.name     ILIKE ? OR
355       o.transaction_description ILIKE ?
356     )| for @tokens;
357     push @values, (like($_))x4 for @tokens;
358   }
359
360   my ($cvar_where, @cvar_values) = CVar->build_filter_query('module'         => 'CT',
361                                                             'trans_id_field' => 'ct.id',
362                                                             'filter'         => $form,
363                                                            );
364   if ($cvar_where) {
365     $query .= qq| AND ($cvar_where)|;
366     push @values, @cvar_values;
367   }
368
369   my $sortdir   = !defined $form->{sortdir} ? 'ASC' : $form->{sortdir} ? 'ASC' : 'DESC';
370   my $sortorder = join(', ', map { "${_} ${sortdir} " } ("o.id", $form->sort_columns("transdate", $ordnumber, "name"), "o.itime"));
371   my %allowed_sort_columns = (
372     "transdate"               => "o.transdate",
373     "reqdate"                 => "o.reqdate",
374     "id"                      => "o.id",
375     "ordnumber"               => "o.ordnumber",
376     "cusordnumber"            => "o.cusordnumber",
377     "quonumber"               => "o.quonumber",
378     "name"                    => "ct.name",
379     "employee"                => "e.name",
380     "salesman"                => "s.name",
381     "shipvia"                 => "o.shipvia",
382     "transaction_description" => "o.transaction_description",
383     "shippingpoint"           => "o.shippingpoint",
384     "insertdate"              => "o.itime",
385     "taxzone"                 => "tz.description",
386     "payment_terms"           => "pt.description",
387     "department"              => "department.description",
388     "intnotes"                => "o.intnotes",
389   );
390   if ($form->{sort} && grep($form->{sort}, keys(%allowed_sort_columns))) {
391     $sortorder = $allowed_sort_columns{$form->{sort}} . " ${sortdir}"  . ", o.itime ${sortdir}";
392   }
393   $query .= qq| ORDER by | . $sortorder;
394
395   my $sth = $dbh->prepare($query);
396   $sth->execute(@values) ||
397     $form->dberror($query . " (" . join(", ", @values) . ")");
398
399   my %id = ();
400   $form->{OE} = [];
401   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
402     $ref->{billed_amount}    = $billed_amount{$ref->{id}};
403     $ref->{billed_netamount} = $billed_netamount{$ref->{id}};
404     if ($ref->{billed_amount} < 0) { # case: credit note(s) higher than invoices
405       $ref->{remaining_amount} = $ref->{amount} + $ref->{billed_amount};
406       $ref->{remaining_netamount} = $ref->{netamount} + $ref->{billed_netamount};
407     } else {
408       $ref->{remaining_amount} = $ref->{amount} - $ref->{billed_amount};
409       $ref->{remaining_netamount} = $ref->{netamount} - $ref->{billed_netamount};
410     }
411     $ref->{exchangerate} ||= $ref->{daily_exchangerate};
412     $ref->{exchangerate} ||= 1;
413     push @{ $form->{OE} }, $ref if $ref->{id} != $id{ $ref->{id} };
414     $id{ $ref->{id} } = $ref->{id};
415   }
416
417   $sth->finish;
418
419   $main::lxdebug->leave_sub();
420 }
421
422 sub transactions_for_todo_list {
423   $main::lxdebug->enter_sub();
424
425   my $self     = shift;
426   my %params   = @_;
427
428   my $myconfig = \%main::myconfig;
429   my $form     = $main::form;
430
431   my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
432
433   my $query    = qq|SELECT id FROM employee WHERE login = ?|;
434   my ($e_id)   = selectrow_query($form, $dbh, $query, $::myconfig{login});
435
436   $query       =
437     qq|SELECT oe.id, oe.transdate, oe.reqdate, oe.quonumber, oe.transaction_description, oe.amount,
438          CASE WHEN (COALESCE(oe.customer_id, 0) = 0) THEN 'vendor' ELSE 'customer' END AS vc,
439          c.name AS customer,
440          v.name AS vendor,
441          e.name AS employee
442        FROM oe
443        LEFT JOIN customer c ON (oe.customer_id = c.id)
444        LEFT JOIN vendor v   ON (oe.vendor_id   = v.id)
445        LEFT JOIN employee e ON (oe.employee_id = e.id)
446        WHERE (COALESCE(quotation, FALSE) = TRUE)
447          AND (COALESCE(closed,    FALSE) = FALSE)
448          AND ((oe.employee_id = ?) OR (oe.salesman_id = ?))
449          AND NOT (oe.reqdate ISNULL)
450          AND (oe.reqdate < current_date)
451        ORDER BY transdate|;
452
453   my $quotations = selectall_hashref_query($form, $dbh, $query, $e_id, $e_id);
454
455   $main::lxdebug->leave_sub();
456
457   return $quotations;
458 }
459
460 sub save {
461   my ($self, $myconfig, $form) = @_;
462   $main::lxdebug->enter_sub();
463
464   my $rc = SL::DB->client->with_transaction(\&_save, $self, $myconfig, $form);
465
466   $::lxdebug->leave_sub;
467
468   return $rc;
469 }
470
471 sub _save {
472   $main::lxdebug->enter_sub();
473
474   my ($self, $myconfig, $form) = @_;
475
476   my $dbh = SL::DB->client->dbh;
477   my $restricter = SL::HTML::Restrict->create;
478
479   my ($query, @values, $sth, $null);
480   my $exchangerate = 0;
481
482   my $all_units = AM->retrieve_units($myconfig, $form);
483   $form->{all_units} = $all_units;
484
485   my $ic_cvar_configs = CVar->get_configs(module => 'IC',
486                                           dbh    => $dbh);
487
488   $form->{employee_id} = (split /--/, $form->{employee})[1] if !$form->{employee_id};
489   unless ($form->{employee_id}) {
490     $form->get_employee($dbh);
491   }
492
493   my $ml = ($form->{type} eq 'sales_order') ? 1 : -1;
494
495   my $number_field         = $form->{type} =~ m{order} ? 'ordnumber' : 'quonumber';
496   my $trans_number         = SL::TransNumber->new(type => $form->{type}, dbh => $dbh, number => $form->{$number_field}, id => $form->{id});
497   $form->{$number_field} ||= $trans_number->create_unique; # set $form->{ordnumber} or $form->{quonumber}
498   my $is_new               = !$form->{id};
499
500   if ($form->{id}) {
501     $query = qq|DELETE FROM custom_variables
502                 WHERE (config_id IN (SELECT id        FROM custom_variable_configs WHERE (module = 'ShipTo')))
503                   AND (trans_id  IN (SELECT shipto_id FROM shipto                  WHERE (module = 'OE') AND (trans_id = ?)))|;
504     do_query($form, $dbh, $query, $form->{id});
505
506     $query = qq|DELETE FROM shipto | .
507              qq|WHERE trans_id = ? AND module = 'OE'|;
508     do_query($form, $dbh, $query, $form->{id});
509
510   } else {
511
512     $query = qq|SELECT nextval('id')|;
513     ($form->{id}) = selectrow_query($form, $dbh, $query);
514
515     $query = qq|INSERT INTO oe (id, ordnumber, employee_id, currency_id, taxzone_id) VALUES (?, '', ?, (SELECT currency_id FROM defaults), ?)|;
516     do_query($form, $dbh, $query, $form->{id}, $form->{employee_id}, $form->{taxzone_id});
517   }
518
519   my $amount    = 0;
520   my $linetotal = 0;
521   my $discount  = 0;
522   my $project_id;
523   my $reqdate;
524   my $taxrate;
525   my $taxbase;
526   my $taxdiff;
527   my $taxamount = 0;
528   my $fxsellprice;
529   my %taxbase;
530   my @taxaccounts;
531   my %taxaccounts;
532   my $netamount = 0;
533   my @processed_orderitems;
534
535   $form->get_lists('price_factors' => 'ALL_PRICE_FACTORS');
536   my %price_factors = map { $_->{id} => $_->{factor} } @{ $form->{ALL_PRICE_FACTORS} };
537   my $price_factor;
538
539   for my $i (1 .. $form->{rowcount}) {
540
541     map({ $form->{"${_}_$i"} = $form->parse_amount($myconfig, $form->{"${_}_$i"}) } qw(qty ship));
542
543     if ($form->{"id_$i"}) {
544
545       # get item baseunit
546       $query = qq|SELECT unit FROM parts WHERE id = ?|;
547       my ($item_unit) = selectrow_query($form, $dbh, $query, $form->{"id_$i"});
548
549       my $basefactor = 1;
550       if (defined($all_units->{$item_unit}->{factor}) &&
551           (($all_units->{$item_unit}->{factor} * 1) != 0)) {
552         $basefactor = $all_units->{$form->{"unit_$i"}}->{factor} / $all_units->{$item_unit}->{factor};
553       }
554       my $baseqty = $form->{"qty_$i"} * $basefactor;
555
556       $form->{"marge_percent_$i"} = $form->parse_amount($myconfig, $form->{"marge_percent_$i"}) * 1;
557       $form->{"marge_absolut_$i"} = $form->parse_amount($myconfig, $form->{"marge_absolut_$i"}) * 1;
558
559       $form->{"lastcost_$i"} = $form->parse_amount($myconfig, $form->{"lastcost_$i"});
560
561       # keep entered selling price
562       my $fxsellprice =
563         $form->parse_amount($myconfig, $form->{"sellprice_$i"});
564
565       my ($dec) = ($fxsellprice =~ /\.(\d+)/);
566       $dec = length $dec;
567       my $decimalplaces = ($dec > 2) ? $dec : 2;
568
569       # undo discount formatting
570       $form->{"discount_$i"} = $form->parse_amount($myconfig, $form->{"discount_$i"}) / 100;
571
572       # deduct discount
573       $form->{"sellprice_$i"} = $fxsellprice * (1 - $form->{"discount_$i"});
574
575       # round linetotal at least to 2 decimal places
576       $price_factor = $price_factors{ $form->{"price_factor_id_$i"} } || 1;
577       $linetotal    = $form->round_amount($form->{"sellprice_$i"} * $form->{"qty_$i"} / $price_factor, 2);
578
579       $form->{"inventory_accno_$i"} *= 1;
580       $form->{"expense_accno_$i"}   *= 1;
581
582       @taxaccounts = split(/ /, $form->{"taxaccounts_$i"});
583       $taxrate     = 0;
584       $taxdiff     = 0;
585
586       map { $taxrate += $form->{"${_}_rate"} } @taxaccounts;
587
588       if ($form->{taxincluded}) {
589         $taxamount = $linetotal * $taxrate / (1 + $taxrate);
590         $taxbase   = $linetotal - $taxamount;
591
592         # we are not keeping a natural price, do not round
593         $form->{"sellprice_$i"} =
594           $form->{"sellprice_$i"} * (1 / (1 + $taxrate));
595       } else {
596         $taxamount = $linetotal * $taxrate;
597         $taxbase   = $linetotal;
598       }
599
600       if ($form->round_amount($taxrate, 7) == 0) {
601         if ($form->{taxincluded}) {
602           foreach my $item (@taxaccounts) {
603             $taxamount = $form->round_amount($linetotal * $form->{"${item}_rate"} / (1 + abs($form->{"${item}_rate"})), 2);
604             $taxaccounts{$item} += $taxamount;
605             $taxdiff            += $taxamount;
606             $taxbase{$item}     += $taxbase;
607           }
608           $taxaccounts{ $taxaccounts[0] } += $taxdiff;
609         } else {
610           foreach my $item (@taxaccounts) {
611             $taxaccounts{$item} += $linetotal * $form->{"${item}_rate"};
612             $taxbase{$item}     += $taxbase;
613           }
614         }
615       } else {
616         foreach my $item (@taxaccounts) {
617           $taxaccounts{$item} += $taxamount * $form->{"${item}_rate"} / $taxrate;
618           $taxbase{$item} += $taxbase;
619         }
620       }
621
622       $netamount += $form->{"sellprice_$i"} * $form->{"qty_$i"} / $price_factor;
623
624       $reqdate = ($form->{"reqdate_$i"}) ? $form->{"reqdate_$i"} : undef;
625
626       # Get pricegroup_id and save it. Unfortunately the interface
627       # also uses ID "0" for signalling that none is selected, but "0"
628       # must not be stored in the database. Therefore we cannot simply
629       # use conv_i().
630       ($null, my $pricegroup_id) = split(/--/, $form->{"sellprice_pg_$i"});
631       $pricegroup_id *= 1;
632       $pricegroup_id  = undef if !$pricegroup_id;
633
634       # force new project, if not set yet
635       if ($::instance_conf->get_order_always_project && !$form->{"globalproject_id"} && ($form->{type} eq 'sales_order')) {
636         require SL::DB::Customer;
637         my $customer = SL::DB::Manager::Customer->find_by(id => $form->{customer_id});
638         die "Can't find customer" unless $customer;
639         die $main::locale->text("Error while creating project with project number of new order number, project number #1 already exists!", $form->{ordnumber})
640           if SL::DB::Manager::Project->find_by(projectnumber => $form->{ordnumber});
641
642         my $new_project = SL::DB::Project->new(
643           projectnumber     => $form->{ordnumber},
644           description       => $customer->name,
645           customer_id       => $customer->id,
646           active            => 1,
647           project_type_id   => $::instance_conf->get_project_type_id,
648           project_status_id => $::instance_conf->get_project_status_id,
649         );
650         $new_project->save;
651         $form->{"globalproject_id"} = $new_project->id;
652       }
653
654       CVar->get_non_editable_ic_cvars(form               => $form,
655                                       dbh                => $dbh,
656                                       row                => $i,
657                                       sub_module         => 'orderitems',
658                                       may_converted_from => ['orderitems', 'invoice']);
659
660       my $position = $i;
661
662       # save detail record in orderitems table
663       if (! $form->{"orderitems_id_$i"}) {
664         $query = qq|SELECT nextval('orderitemsid')|;
665         ($form->{"orderitems_id_$i"}) = selectrow_query($form, $dbh, $query);
666
667         $query = qq|INSERT INTO orderitems (id, position) VALUES (?, ?)|;
668         do_query($form, $dbh, $query, $form->{"orderitems_id_$i"}, conv_i($position));
669       }
670
671       my $orderitems_id = $form->{"orderitems_id_$i"};
672       push @processed_orderitems, $orderitems_id;
673
674        $query = <<SQL;
675          UPDATE orderitems SET
676           trans_id = ?, position = ?, parts_id = ?, description = ?, longdescription = ?, qty = ?, base_qty = ?,
677           sellprice = ?, discount = ?, unit = ?, reqdate = ?, project_id = ?, serialnumber = ?, ship = ?,
678           pricegroup_id = ?, subtotal = ?,
679           marge_percent = ?, marge_total = ?, lastcost = ?, price_factor_id = ?,
680           active_price_source = ?, active_discount_source = ?,
681           price_factor = (SELECT factor FROM price_factors WHERE id = ?), marge_price_factor = ?
682         WHERE id = ?
683 SQL
684       @values = (
685            conv_i($form->{id}), conv_i($position), conv_i($form->{"id_$i"}),
686            $form->{"description_$i"}, $restricter->process($form->{"longdescription_$i"}),
687            $form->{"qty_$i"}, $baseqty,
688            $fxsellprice, $form->{"discount_$i"},
689            $form->{"unit_$i"}, conv_date($reqdate), conv_i($form->{"project_id_$i"}),
690            $form->{"serialnumber_$i"}, $form->{"ship_$i"},
691            $pricegroup_id, $form->{"subtotal_$i"} ? 't' : 'f',
692            $form->{"marge_percent_$i"}, $form->{"marge_absolut_$i"},
693            $form->{"lastcost_$i"}, conv_i($form->{"price_factor_id_$i"}),
694            $form->{"active_price_source_$i"}, $form->{"active_discount_source_$i"},
695            conv_i($form->{"price_factor_id_$i"}), conv_i($form->{"marge_price_factor_$i"}),
696            conv_i($orderitems_id),
697       );
698
699       do_query($form, $dbh, $query, @values);
700
701       $form->{"sellprice_$i"} = $fxsellprice;
702       $form->{"discount_$i"} *= 100;
703
704       CVar->save_custom_variables(module       => 'IC',
705                                   sub_module   => 'orderitems',
706                                   trans_id     => $orderitems_id,
707                                   configs      => $ic_cvar_configs,
708                                   variables    => $form,
709                                   name_prefix  => 'ic_',
710                                   name_postfix => "_$i",
711                                   dbh          => $dbh);
712
713       # link previous items with orderitems
714       # assume we have a new workflow if we link from invoice or order to quotation
715       # unluckily orderitems are used for quotation and orders - therefore one more
716       # check to be sure NOT to link from order to quotation
717       foreach (qw(orderitems)) {
718         if (!$form->{saveasnew} && !$form->{useasnew} && $form->{"converted_from_${_}_id_$i"}
719               && $form->{type} !~ 'quotation') {
720           RecordLinks->create_links('dbh'        => $dbh,
721                                     'mode'       => 'ids',
722                                     'from_table' => $_,
723                                     'from_ids'   => $form->{"converted_from_${_}_id_$i"},
724                                     'to_table'   => 'orderitems',
725                                     'to_id'      => $orderitems_id,
726           );
727         }
728         delete $form->{"converted_from_${_}_id_$i"};
729       }
730     }
731   }
732
733   # search for orphaned ids
734   $query  = sprintf 'SELECT id FROM orderitems WHERE trans_id = ? AND NOT id IN (%s)', join ', ', ("?") x scalar @processed_orderitems;
735   @values = (conv_i($form->{id}), map { conv_i($_) } @processed_orderitems);
736   my @orphaned_ids = map { $_->{id} } selectall_hashref_query($form, $dbh, $query, @values);
737
738   if (scalar @orphaned_ids) {
739     # clean up orderitems
740     $query  = sprintf 'DELETE FROM orderitems WHERE id IN (%s)', join ', ', ("?") x scalar @orphaned_ids;
741     do_query($form, $dbh, $query, @orphaned_ids);
742   }
743
744   $reqdate = ($form->{reqdate}) ? $form->{reqdate} : undef;
745
746   # add up the tax
747   my $tax = 0;
748   map { $tax += $form->round_amount($taxaccounts{$_}, 2) } keys %taxaccounts;
749
750   $amount = $form->round_amount($netamount + $tax, 2, 1);
751   $netamount = $form->round_amount($netamount, 2);
752
753   if ($form->{currency} eq $form->{defaultcurrency}) {
754     $form->{exchangerate} = 1;
755   } else {
756     $exchangerate = $form->check_exchangerate($myconfig, $form->{currency}, $form->{transdate}, ($form->{vc} eq 'customer') ? 'buy' : 'sell');
757   }
758
759   # from inputfield (exchangerate) or hidden (forex)
760   my $exchangerate_from_form = $form->{forex} || $form->parse_amount($myconfig, $form->{exchangerate});
761
762   $form->{exchangerate} = $exchangerate || $exchangerate_from_form;
763
764   my $quotation = $form->{type} =~ /_order$/ ? 'f' : 't';
765
766   # save OE record
767   $query =
768     qq|UPDATE oe SET
769          ordnumber = ?, quonumber = ?, cusordnumber = ?, transdate = ?, vendor_id = ?,
770          customer_id = ?, amount = ?, netamount = ?, reqdate = ?, tax_point = ?, taxincluded = ?,
771          shippingpoint = ?, shipvia = ?, notes = ?, intnotes = ?, currency_id = (SELECT id FROM currencies WHERE name=?), closed = ?,
772          delivered = ?, proforma = ?, quotation = ?, department_id = ?, language_id = ?,
773          taxzone_id = ?, shipto_id = ?, billing_address_id = ?, payment_id = ?, delivery_vendor_id = ?, delivery_customer_id = ?,delivery_term_id = ?,
774          globalproject_id = ?, employee_id = ?, salesman_id = ?, cp_id = ?, transaction_description = ?, marge_total = ?, marge_percent = ?
775          , order_probability = ?, expected_billing_date = ?
776        WHERE id = ?|;
777
778   @values = ($form->{ordnumber} || '', $form->{quonumber},
779              $form->{cusordnumber}, conv_date($form->{transdate}),
780              conv_i($form->{vendor_id}), conv_i($form->{customer_id}),
781              $amount, $netamount, conv_date($reqdate), conv_date($form->{tax_point}),
782              $form->{taxincluded} ? 't' : 'f', $form->{shippingpoint},
783              $form->{shipvia}, $restricter->process($form->{notes}), $form->{intnotes},
784              $form->{currency}, $form->{closed} ? 't' : 'f',
785              $form->{delivered} ? "t" : "f", $form->{proforma} ? 't' : 'f',
786              $quotation, conv_i($form->{department_id}),
787              conv_i($form->{language_id}), conv_i($form->{taxzone_id}),
788              conv_i($form->{shipto_id}), conv_i($form->{billing_address_id}), conv_i($form->{payment_id}),
789              conv_i($form->{delivery_vendor_id}),
790              conv_i($form->{delivery_customer_id}),
791              conv_i($form->{delivery_term_id}),
792              conv_i($form->{globalproject_id}), conv_i($form->{employee_id}),
793              conv_i($form->{salesman_id}), conv_i($form->{cp_id}),
794              $form->{transaction_description},
795              $form->{marge_total} * 1, $form->{marge_percent} * 1,
796              $form->{order_probability} * 1, conv_date($form->{expected_billing_date}),
797              conv_i($form->{id}));
798   do_query($form, $dbh, $query, @values);
799
800   $form->new_lastmtime('oe');
801
802   $form->{ordtotal} = $amount;
803
804   $form->{name} = $form->{ $form->{vc} };
805   $form->{name} =~ s/--\Q$form->{"$form->{vc}_id"}\E//;
806
807   # add shipto
808   if (!$form->{shipto_id}) {
809     $form->add_shipto($dbh, $form->{id}, "OE");
810   }
811
812   # save printed, emailed, queued
813   $form->save_status($dbh);
814
815   # Link this record to the records it was created from.
816   $form->{convert_from_oe_ids} =~ s/^\s+//;
817   $form->{convert_from_oe_ids} =~ s/\s+$//;
818   my @convert_from_oe_ids      =  split m/\s+/, $form->{convert_from_oe_ids};
819   delete $form->{convert_from_oe_ids};
820   if (!$form->{useasnew} && scalar @convert_from_oe_ids) {
821     RecordLinks->create_links('dbh'        => $dbh,
822                               'mode'       => 'ids',
823                               'from_table' => 'oe',
824                               'from_ids'   => \@convert_from_oe_ids,
825                               'to_table'   => 'oe',
826                               'to_id'      => $form->{id},
827       );
828     $self->_close_quotations_rfqs('dbh'     => $dbh,
829                                   'from_id' => \@convert_from_oe_ids,
830                                   'to_id'   => $form->{id});
831   }
832
833   if (($form->{currency} ne $form->{defaultcurrency}) && !$exchangerate) {
834     if ($form->{vc} eq 'customer') {
835       $form->update_exchangerate($dbh, $form->{currency}, $form->{transdate}, $form->{exchangerate}, 0);
836     }
837     if ($form->{vc} eq 'vendor') {
838       $form->update_exchangerate($dbh, $form->{currency}, $form->{transdate}, 0, $form->{exchangerate});
839     }
840   }
841
842   $form->{saved_xyznumber} = $form->{$form->{type} =~ /_quotation$/ ?
843                                        "quonumber" : "ordnumber"};
844
845   Common::webdav_folder($form);
846
847   $self->save_periodic_invoices_config(dbh         => $dbh,
848                                        oe_id       => $form->{id},
849                                        config_yaml => $form->{periodic_invoices_config})
850     if ($form->{type} eq 'sales_order');
851
852   $self->_link_created_sales_order_to_requirement_specs_for_sales_quotations(
853     type               => $form->{type},
854     converted_from_ids => \@convert_from_oe_ids,
855     sales_order_id     => $form->{id},
856     is_new             => $is_new,
857   );
858
859   $self->_set_project_in_linked_requirement_spec(
860     type           => $form->{type},
861     project_id     => $form->{globalproject_id},
862     sales_order_id => $form->{id},
863   );
864
865   $main::lxdebug->leave_sub();
866
867   return 1;
868 }
869
870 sub _link_created_sales_order_to_requirement_specs_for_sales_quotations {
871   my ($self, %params) = @_;
872
873   # If this is a sales order created from a sales quotation and if
874   # that sales quotation was created from a requirement spec document
875   # then link the newly created sales order to the requirement spec
876   # document, too.
877
878   return if !$params{is_new};
879   return if  $params{type} ne 'sales_order';
880   return if !@{ $params{converted_from_ids} };
881
882   my $oe_objects       = SL::DB::Manager::Order->get_all(where => [ id => $params{converted_from_ids} ]);
883   my @sales_quotations = grep { $_->is_type('sales_quotation') } @{ $oe_objects };
884
885   return if !@sales_quotations;
886
887   my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => [ map { $_->id } @sales_quotations ] ]);
888
889   return if !@{ $rs_orders };
890
891   $rs_orders->[0]->db->with_transaction(sub {
892     foreach my $rs_order (@{ $rs_orders }) {
893       SL::DB::RequirementSpecOrder->new(
894         order_id            => $params{sales_order_id},
895         requirement_spec_id => $rs_order->requirement_spec_id,
896         version_id          => $rs_order->version_id,
897       )->save;
898     }
899
900     1;
901   });
902 }
903
904 sub _set_project_in_linked_requirement_spec {
905   my ($self, %params) = @_;
906
907   return if  $params{type} ne 'sales_order';
908   return if !$params{project_id} || !$params{sales_order_id};
909
910   my $query = <<SQL;
911     UPDATE requirement_specs
912     SET project_id = ?
913     WHERE id IN (
914       SELECT so.requirement_spec_id
915       FROM requirement_spec_orders so
916       WHERE so.order_id = ?
917     )
918 SQL
919
920   do_query($::form, $::form->get_standard_dbh, $query, $params{project_id}, $params{sales_order_id});
921 }
922
923 sub save_periodic_invoices_config {
924   my ($self, %params) = @_;
925
926   return if !$params{oe_id};
927
928   my $config = $params{config_yaml} ? SL::YAML::Load($params{config_yaml}) : undef;
929   return if 'HASH' ne ref $config;
930
931   my $obj  = SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $params{oe_id})
932           || SL::DB::PeriodicInvoicesConfig->new(oe_id => $params{oe_id});
933   $obj->update_attributes(%{ $config });
934 }
935
936 sub load_periodic_invoice_config {
937   my $self = shift;
938   my $form = shift;
939
940   delete $form->{periodic_invoices_config};
941
942   if ($form->{id}) {
943     my $config_obj = SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $form->{id});
944
945     if ($config_obj) {
946       my $config = { map { $_ => $config_obj->$_ } qw(active terminated periodicity order_value_periodicity start_date_as_date end_date_as_date first_billing_date_as_date extend_automatically_by ar_chart_id
947                                                       print printer_id copies direct_debit send_email email_recipient_contact_id email_recipient_address email_sender email_subject email_body) };
948       $form->{periodic_invoices_config} = SL::YAML::Dump($config);
949     }
950   }
951 }
952
953 sub _close_quotations_rfqs {
954   $main::lxdebug->enter_sub();
955
956   my $self     = shift;
957   my %params   = @_;
958
959   Common::check_params(\%params, qw(from_id to_id));
960
961   my $myconfig = \%main::myconfig;
962   my $form     = $main::form;
963
964   my $dbh      = $params{dbh} || SL::DB->client->dbh;
965
966   SL::DB->client->with_transaction(sub {
967
968     my $query    = qq|SELECT quotation FROM oe WHERE id = ?|;
969     my $sth      = prepare_query($form, $dbh, $query);
970
971     do_statement($form, $sth, $query, conv_i($params{to_id}));
972
973     my ($quotation) = $sth->fetchrow_array();
974
975     if ($quotation) {
976       return 1;
977     }
978
979     my @close_ids;
980
981     foreach my $from_id (@{ $params{from_id} }) {
982       $from_id = conv_i($from_id);
983       do_statement($form, $sth, $query, $from_id);
984       ($quotation) = $sth->fetchrow_array();
985       push @close_ids, $from_id if ($quotation);
986     }
987
988     $sth->finish();
989
990     if (scalar @close_ids) {
991       $query = qq|UPDATE oe SET closed = TRUE WHERE id IN (| . join(', ', ('?') x scalar @close_ids) . qq|)|;
992       do_query($form, $dbh, $query, @close_ids);
993     }
994     1;
995   }) or do { die SL::DB->client->error };
996
997   $main::lxdebug->leave_sub();
998 }
999
1000 sub delete {
1001   $main::lxdebug->enter_sub();
1002
1003   my ($self, $myconfig, $form) = @_;
1004
1005   my $rc = SL::DB::Order->new->db->with_transaction(sub {
1006     my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $form->{id} ]) };
1007
1008     SL::DB::Order->new(id => $form->{id})->delete;
1009
1010     my $spool = $::lx_office_conf{paths}->{spool};
1011     unlink map { "$spool/$_" } @spoolfiles if $spool;
1012
1013     1;
1014   }) or do { die SL::DB->client->error };
1015
1016   $main::lxdebug->leave_sub();
1017
1018   return $rc;
1019 }
1020
1021 sub retrieve {
1022   my ($self, $myconfig, $form) = @_;
1023   $main::lxdebug->enter_sub();
1024
1025   my $rc = SL::DB->client->with_transaction(\&_retrieve, $self, $myconfig, $form);
1026
1027   $::lxdebug->leave_sub;
1028   return $rc;
1029 }
1030
1031 sub _retrieve {
1032   my ($self, $myconfig, $form) = @_;
1033
1034   # connect to database
1035   my $dbh = SL::DB->client->dbh;
1036
1037   my ($query, $query_add, @values, @ids, $sth);
1038
1039   # translate the ids (given by id_# and trans_id_#) into one array of ids, so we can join them later
1040   map {
1041     push @ids, $form->{"trans_id_$_"}
1042       if ($form->{"multi_id_$_"} and $form->{"trans_id_$_"})
1043   } (1 .. $form->{"rowcount"});
1044
1045   if ($form->{rowcount} && scalar @ids) {
1046     $form->{convert_from_oe_ids} = join ' ', @ids;
1047   }
1048
1049   # if called in multi id mode, and still only got one id, switch back to single id
1050   if ($form->{"rowcount"} and $#ids == 0) {
1051     $form->{"id"} = $ids[0];
1052     undef @ids;
1053     delete $form->{convert_from_oe_ids};
1054   }
1055
1056   # and remember for the rest of the function
1057   my $is_collective_order = scalar @ids;
1058
1059   # If collective order was created from exactly 1 order, we assume the same
1060   # behaviour as a "save as new" from within an order is actually desired, i.e.
1061   # the original order isn't part of a workflow where we want to remember
1062   # record_links, but simply a quick way of generating a new order from an old
1063   # one without having to enter everything again.
1064   # Setting useasnew will prevent the creation of record_links for the items
1065   # when saving the new order.
1066   # This form variable is probably not necessary, could just set saveasnew instead
1067   $form->{useasnew} = 1 if $is_collective_order == 1;
1068
1069   if (!$form->{id}) {
1070     my $extra_days = $form->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval       :
1071                      $form->{type} eq 'sales_order'     ? $::instance_conf->get_delivery_date_interval : 1;
1072     if (   ($form->{type} eq 'sales_order'     &&  !$::instance_conf->get_deliverydate_on)
1073         || ($form->{type} eq 'sales_quotation' &&  !$::instance_conf->get_reqdate_on)) {
1074       $form->{reqdate}   = '';
1075     } else {
1076       $form->{reqdate}   = DateTime->today_local->next_workday(extra_days => $extra_days)->to_kivitendo;
1077     }
1078     $form->{transdate} = DateTime->today_local->to_kivitendo;
1079   }
1080
1081   # get default accounts
1082   $query = qq|SELECT (SELECT c.accno FROM chart c WHERE d.inventory_accno_id = c.id) AS inventory_accno,
1083                      (SELECT c.accno FROM chart c WHERE d.income_accno_id    = c.id) AS income_accno,
1084                      (SELECT c.accno FROM chart c WHERE d.expense_accno_id   = c.id) AS expense_accno,
1085                      (SELECT c.accno FROM chart c WHERE d.fxgain_accno_id    = c.id) AS fxgain_accno,
1086                      (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id    = c.id) AS fxloss_accno,
1087                      (SELECT c.accno FROM chart c WHERE d.rndgain_accno_id   = c.id) AS rndgain_accno,
1088                      (SELECT c.accno FROM chart c WHERE d.rndloss_accno_id   = c.id) AS rndloss_accno
1089               $query_add
1090               FROM defaults d|;
1091   my $ref = selectfirst_hashref_query($form, $dbh, $query);
1092   map { $form->{$_} = $ref->{$_} } keys %$ref;
1093
1094   $form->{currency} = $form->get_default_currency($myconfig);
1095
1096   # set reqdate if this is an invoice->order conversion. If someone knows a better check to ensure
1097   # we come from invoices, feel free.
1098   $form->{reqdate} = $form->{deliverydate}
1099     if (    $form->{deliverydate}
1100         and $form->{callback} =~ /action=ar_transactions/);
1101
1102   my $vc = $form->{vc} eq "customer" ? "customer" : "vendor";
1103
1104   if ($form->{id} or @ids) {
1105
1106     # retrieve order for single id
1107     # NOTE: this query is intended to fetch all information only ONCE.
1108     # so if any of these infos is important (or even different) for any item,
1109     # it will be killed out and then has to be fetched from the item scope query further down
1110     $query =
1111       qq|SELECT o.cp_id, o.ordnumber, o.transdate, o.reqdate,
1112            o.taxincluded, o.shippingpoint, o.shipvia, o.notes, o.intnotes,
1113            (SELECT cu.name FROM currencies cu WHERE cu.id=o.currency_id) AS currency, e.name AS employee, o.employee_id, o.salesman_id,
1114            o.${vc}_id, cv.name AS ${vc}, o.amount AS invtotal,
1115            o.closed, o.reqdate, o.tax_point, o.quonumber, o.department_id, o.cusordnumber,
1116            o.mtime, o.itime,
1117            d.description AS department, o.payment_id, o.language_id, o.taxzone_id,
1118            o.delivery_customer_id, o.delivery_vendor_id, o.proforma, o.shipto_id, o.billing_address_id,
1119            o.globalproject_id, o.delivered, o.transaction_description, o.delivery_term_id,
1120            o.itime::DATE AS insertdate, o.order_probability, o.expected_billing_date
1121          FROM oe o
1122          JOIN ${vc} cv ON (o.${vc}_id = cv.id)
1123          LEFT JOIN employee e ON (o.employee_id = e.id)
1124          LEFT JOIN department d ON (o.department_id = d.id) | .
1125         ($form->{id}
1126          ? "WHERE o.id = ?"
1127          : "WHERE o.id IN (" . join(', ', map("? ", @ids)) . ")"
1128         );
1129     @values = $form->{id} ? ($form->{id}) : @ids;
1130     $sth = prepare_execute_query($form, $dbh, $query, @values);
1131
1132     $ref = $sth->fetchrow_hashref("NAME_lc");
1133
1134     if ($ref) {
1135       map { $form->{$_} = $ref->{$_} } keys %$ref;
1136
1137       $form->{saved_xyznumber} = $form->{$form->{type} =~ /_quotation$/ ? "quonumber" : "ordnumber"};
1138
1139       # set all entries for multiple ids blank that yield different information
1140       while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
1141         map { $form->{$_} = '' if ($ref->{$_} ne $form->{$_}) } keys %$ref;
1142       }
1143     }
1144     $form->{mtime}   ||= $form->{itime};
1145     $form->{lastmtime} = $form->{mtime};
1146
1147     # if not given, fill transdate with current_date
1148     $form->{transdate} = $form->current_date($myconfig)
1149       unless $form->{transdate};
1150
1151     $sth->finish;
1152
1153     if ($form->{delivery_customer_id}) {
1154       $query = qq|SELECT name FROM customer WHERE id = ?|;
1155       ($form->{delivery_customer_string}) = selectrow_query($form, $dbh, $query, $form->{delivery_customer_id});
1156     }
1157
1158     if ($form->{delivery_vendor_id}) {
1159       $query = qq|SELECT name FROM customer WHERE id = ?|;
1160       ($form->{delivery_vendor_string}) = selectrow_query($form, $dbh, $query, $form->{delivery_vendor_id});
1161     }
1162
1163     # shipto and pinted/mailed/queued status makes only sense for single id retrieve
1164     if (!@ids) {
1165       $query = qq|SELECT s.* FROM shipto s WHERE s.trans_id = ? AND s.module = 'OE'|;
1166       $sth = prepare_execute_query($form, $dbh, $query, $form->{id});
1167
1168       $ref = $sth->fetchrow_hashref("NAME_lc");
1169       $form->{$_} = $ref->{$_} for grep { m{^shipto(?!_id$)} } keys %$ref;
1170       $sth->finish;
1171
1172       if ($ref->{shipto_id}) {
1173         my $cvars = CVar->get_custom_variables(
1174           dbh      => $dbh,
1175           module   => 'ShipTo',
1176           trans_id => $ref->{shipto_id},
1177         );
1178         $form->{"shiptocvar_$_->{name}"} = $_->{value} for @{ $cvars };
1179       }
1180
1181       # get printed, emailed and queued
1182       $query = qq|SELECT s.printed, s.emailed, s.spoolfile, s.formname FROM status s WHERE s.trans_id = ?|;
1183       $sth = prepare_execute_query($form, $dbh, $query, $form->{id});
1184
1185       while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
1186         $form->{printed} .= "$ref->{formname} " if $ref->{printed};
1187         $form->{emailed} .= "$ref->{formname} " if $ref->{emailed};
1188         $form->{queued}  .= "$ref->{formname} $ref->{spoolfile} " if $ref->{spoolfile};
1189       }
1190       $sth->finish;
1191       map { $form->{$_} =~ s/ +$//g } qw(printed emailed queued);
1192     }    # if !@ids
1193
1194     my $transdate = $form->{tax_point} ? $dbh->quote($form->{tax_point}) : $form->{transdate} ? $dbh->quote($form->{transdate}) : "current_date";
1195
1196     $form->{taxzone_id} = 0 unless ($form->{taxzone_id});
1197     unshift @values, ($form->{taxzone_id}) x 2;
1198
1199     # retrieve individual items
1200     # this query looks up all information about the items
1201     # stuff different from the whole will not be overwritten, but saved with a suffix.
1202     $query =
1203       qq|SELECT o.id AS orderitems_id,
1204            c1.accno AS inventory_accno, c1.new_chart_id AS inventory_new_chart, date($transdate) - c1.valid_from as inventory_valid,
1205            c2.accno AS income_accno,    c2.new_chart_id AS income_new_chart,    date($transdate) - c2.valid_from as income_valid,
1206            c3.accno AS expense_accno,   c3.new_chart_id AS expense_new_chart,   date($transdate) - c3.valid_from as expense_valid,
1207            oe.ordnumber AS ordnumber_oe, oe.transdate AS transdate_oe, oe.cusordnumber AS cusordnumber_oe,
1208            p.partnumber, p.part_type, p.listprice, o.description, o.qty,
1209            p.classification_id,
1210            o.sellprice, o.parts_id AS id, o.unit, o.discount, p.notes AS partnotes, p.part_type,
1211            o.reqdate, o.project_id, o.serialnumber, o.ship, o.lastcost,
1212            o.ordnumber, o.transdate, o.cusordnumber, o.subtotal, o.longdescription,
1213            o.price_factor_id, o.price_factor, o.marge_price_factor, o.active_price_source, o.active_discount_source,
1214            pr.projectnumber, p.formel,
1215            pg.partsgroup, o.pricegroup_id, (SELECT pricegroup FROM pricegroup WHERE id=o.pricegroup_id) as pricegroup
1216          FROM orderitems o
1217          JOIN parts p ON (o.parts_id = p.id)
1218          JOIN oe ON (o.trans_id = oe.id)
1219          LEFT JOIN chart c1 ON ((SELECT inventory_accno_id                   FROM buchungsgruppen WHERE id=p.buchungsgruppen_id) = c1.id)
1220          LEFT JOIN chart c2 ON ((SELECT tc.income_accno_id  FROM taxzone_charts tc WHERE tc.taxzone_id = ? and tc.buchungsgruppen_id = p.buchungsgruppen_id) = c2.id)
1221          LEFT JOIN chart c3 ON ((SELECT tc.expense_accno_id FROM taxzone_charts tc WHERE tc.taxzone_id = ? and tc.buchungsgruppen_id = p.buchungsgruppen_id) = c3.id)
1222          LEFT JOIN project pr ON (o.project_id = pr.id)
1223          LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id) | .
1224       ($form->{id}
1225        ? qq|WHERE o.trans_id = ?|
1226        : qq|WHERE o.trans_id IN (| . join(", ", map("?", @ids)) . qq|)|) .
1227       qq|ORDER BY o.trans_id, o.position|;
1228
1229     @ids = $form->{id} ? ($form->{id}) : @ids;
1230     $sth = prepare_execute_query($form, $dbh, $query, @values);
1231
1232     while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
1233       # Retrieve custom variables.
1234       my $cvars = CVar->get_custom_variables(dbh        => $dbh,
1235                                              module     => 'IC',
1236                                              sub_module => 'orderitems',
1237                                              trans_id   => $ref->{orderitems_id},
1238                                             );
1239       map { $ref->{"ic_cvar_$_->{name}"} = $_->{value} } @{ $cvars };
1240
1241       # Handle accounts.
1242       if (!$ref->{"part_type"} eq 'part') {
1243         map({ delete($ref->{$_}); } qw(inventory_accno inventory_new_chart inventory_valid));
1244       }
1245       # delete($ref->{"part_inventory_accno_id"});
1246
1247       # in collective order, copy global ordnumber, transdate, cusordnumber into item scope
1248       #   unless already present there
1249       # remove _oe entries afterwards
1250       map { $ref->{$_} = $ref->{"${_}_oe"} if ($ref->{$_} eq '') }
1251         qw|ordnumber transdate cusordnumber|
1252         if (@ids);
1253       map { delete $ref->{$_} } qw|ordnumber_oe transdate_oe cusordnumber_oe|;
1254
1255
1256
1257       while ($ref->{inventory_new_chart} && ($ref->{inventory_valid} >= 0)) {
1258         my $query =
1259           qq|SELECT accno AS inventory_accno, | .
1260           qq|  new_chart_id AS inventory_new_chart, | .
1261           qq|  date($transdate) - valid_from AS inventory_valid | .
1262           qq|FROM chart WHERE id = $ref->{inventory_new_chart}|;
1263         ($ref->{inventory_accno}, $ref->{inventory_new_chart},
1264          $ref->{inventory_valid}) = selectrow_query($form, $dbh, $query);
1265       }
1266
1267       while ($ref->{income_new_chart} && ($ref->{income_valid} >= 0)) {
1268         my $query =
1269           qq|SELECT accno AS income_accno, | .
1270           qq|  new_chart_id AS income_new_chart, | .
1271           qq|  date($transdate) - valid_from AS income_valid | .
1272           qq|FROM chart WHERE id = $ref->{income_new_chart}|;
1273         ($ref->{income_accno}, $ref->{income_new_chart},
1274          $ref->{income_valid}) = selectrow_query($form, $dbh, $query);
1275       }
1276
1277       while ($ref->{expense_new_chart} && ($ref->{expense_valid} >= 0)) {
1278         my $query =
1279           qq|SELECT accno AS expense_accno, | .
1280           qq|  new_chart_id AS expense_new_chart, | .
1281           qq|  date($transdate) - valid_from AS expense_valid | .
1282           qq|FROM chart WHERE id = $ref->{expense_new_chart}|;
1283         ($ref->{expense_accno}, $ref->{expense_new_chart},
1284          $ref->{expense_valid}) = selectrow_query($form, $dbh, $query);
1285       }
1286
1287       # delete orderitems_id in collective orders, so that they get cloned no matter what
1288       # is this correct? or is the following meant?
1289       # remember orderitems_ids in converted_from_orderitems_ids, so that they may be linked
1290       $ref->{converted_from_orderitems_id} = delete $ref->{orderitems_id} if $is_collective_order;
1291
1292       # get tax rates and description
1293       my $accno_id = ($form->{vc} eq "customer") ? $ref->{income_accno} : $ref->{expense_accno};
1294       $query =
1295         qq|SELECT c.accno, t.taxdescription, t.rate, t.id as tax_id, c.accno as taxnumber | .
1296         qq|FROM tax t | .
1297         qq|LEFT JOIN chart c on (c.id = t.chart_id) | .
1298         qq|WHERE t.id IN (SELECT tk.tax_id FROM taxkeys tk | .
1299         qq|               WHERE tk.chart_id = (SELECT id FROM chart WHERE accno = ?) | .
1300         qq|                 AND startdate <= $transdate ORDER BY startdate DESC LIMIT 1) | .
1301         qq|ORDER BY c.accno|;
1302       my $stw = prepare_execute_query($form, $dbh, $query, $accno_id);
1303       $ref->{taxaccounts} = "";
1304       my $i = 0;
1305       while (my $ptr = $stw->fetchrow_hashref("NAME_lc")) {
1306         if (($ptr->{accno} eq "") && ($ptr->{rate} == 0)) {
1307           $i++;
1308           $ptr->{accno} = $i;
1309         }
1310         $ref->{taxaccounts} .= "$ptr->{accno} ";
1311         if (!($form->{taxaccounts} =~ /\Q$ptr->{accno}\E/)) {
1312           $form->{"$ptr->{accno}_rate"}        = $ptr->{rate};
1313           $form->{"$ptr->{accno}_description"} = $ptr->{taxdescription};
1314           $form->{"$ptr->{accno}_taxnumber"}   = $ptr->{taxnumber};
1315           $form->{"$ptr->{accno}_tax_id"}      = $ptr->{tax_id};
1316           $form->{taxaccounts} .= "$ptr->{accno} ";
1317         }
1318
1319       }
1320
1321       chop $ref->{taxaccounts};
1322
1323       push @{ $form->{form_details} }, $ref;
1324       $stw->finish;
1325     }
1326     $sth->finish;
1327
1328   } else {
1329
1330     # get last name used
1331     $form->lastname_used($dbh, $myconfig, $form->{vc})
1332       unless $form->{"$form->{vc}_id"};
1333
1334   }
1335
1336   $form->{exchangerate} = $form->get_exchangerate($dbh, $form->{currency}, $form->{transdate}, ($form->{vc} eq 'customer') ? "buy" : "sell");
1337
1338   Common::webdav_folder($form);
1339
1340   $self->load_periodic_invoice_config($form);
1341
1342   return 1;
1343 }
1344
1345 sub retrieve_simple {
1346   $main::lxdebug->enter_sub();
1347
1348   my $self     = shift;
1349   my %params   = @_;
1350
1351   Common::check_params(\%params, qw(id));
1352
1353   my $myconfig    = \%main::myconfig;
1354   my $form        = $main::form;
1355
1356   my $dbh         = $params{dbh} || $form->get_standard_dbh($myconfig);
1357
1358   my $oe_query    = qq|SELECT * FROM oe         WHERE id = ?|;
1359   my $oi_query    = qq|SELECT * FROM orderitems WHERE trans_id = ? ORDER BY position|;
1360
1361   my $order            = selectfirst_hashref_query($form, $dbh, $oe_query, conv_i($params{id}));
1362   $order->{orderitems} = selectall_hashref_query(  $form, $dbh, $oi_query, conv_i($params{id}));
1363
1364   $main::lxdebug->leave_sub();
1365
1366   return $order;
1367 }
1368
1369 sub order_details {
1370   $main::lxdebug->enter_sub();
1371
1372   my ($self, $myconfig, $form) = @_;
1373
1374   # connect to database
1375   my $dbh = SL::DB->client->dbh;
1376   my $query;
1377   my @values = ();
1378   my $sth;
1379   my $nodiscount;
1380   my $yesdiscount;
1381   my $nodiscount_subtotal = 0;
1382   my $discount_subtotal = 0;
1383   my $item;
1384   my $i;
1385   my @partsgroup = ();
1386   my $partsgroup;
1387   my $position = 0;
1388   my $subtotal_header = 0;
1389   my $subposition = 0;
1390   my %taxaccounts;
1391   my %taxbase;
1392   my $tax_rate;
1393   my $taxamount;
1394
1395   my (@project_ids);
1396
1397   push(@project_ids, $form->{"globalproject_id"}) if ($form->{"globalproject_id"});
1398
1399   $form->get_lists('price_factors' => 'ALL_PRICE_FACTORS');
1400   my %price_factors;
1401
1402   foreach my $pfac (@{ $form->{ALL_PRICE_FACTORS} }) {
1403     $price_factors{$pfac->{id}}  = $pfac;
1404     $pfac->{factor}             *= 1;
1405     $pfac->{formatted_factor}    = $form->format_amount($myconfig, $pfac->{factor});
1406   }
1407
1408   # sort items by partsgroup
1409   for $i (1 .. $form->{rowcount}) {
1410     $partsgroup = "";
1411     if ($form->{"partsgroup_$i"} && $form->{groupitems}) {
1412       $partsgroup = $form->{"partsgroup_$i"};
1413     }
1414     push @partsgroup, [$i, $partsgroup];
1415     push(@project_ids, $form->{"project_id_$i"}) if ($form->{"project_id_$i"});
1416   }
1417
1418   my $projects = [];
1419   my %projects_by_id;
1420   if (@project_ids) {
1421     $projects = SL::DB::Manager::Project->get_all(query => [ id => \@project_ids ]);
1422     %projects_by_id = map { $_->id => $_ } @$projects;
1423   }
1424
1425   if ($projects_by_id{$form->{"globalproject_id"}}) {
1426     $form->{globalprojectnumber} = $projects_by_id{$form->{"globalproject_id"}}->projectnumber;
1427     $form->{globalprojectdescription} = $projects_by_id{$form->{"globalproject_id"}}->description;
1428
1429     for (@{ $projects_by_id{$form->{"globalproject_id"}}->cvars_by_config }) {
1430       $form->{"project_cvar_" . $_->config->name} = $_->value_as_text;
1431     }
1432   }
1433
1434   $form->{discount} = [];
1435
1436   # get some values of parts from db on store them in extra array,
1437   # so that they can be sorted in later
1438   my %prepared_template_arrays = IC->prepare_parts_for_printing(myconfig => $myconfig, form => $form);
1439   my @prepared_arrays          = keys %prepared_template_arrays;
1440   my @separate_totals          = qw(non_separate_subtotal);
1441
1442   $form->{TEMPLATE_ARRAYS} = { };
1443
1444   my $ic_cvar_configs = CVar->get_configs(module => 'IC');
1445   my $project_cvar_configs = CVar->get_configs(module => 'Projects');
1446
1447   my @arrays =
1448     qw(runningnumber number description longdescription qty qty_nofmt ship ship_nofmt unit bin
1449        partnotes serialnumber reqdate sellprice sellprice_nofmt listprice listprice_nofmt netprice netprice_nofmt
1450        discount discount_nofmt p_discount discount_sub discount_sub_nofmt nodiscount_sub nodiscount_sub_nofmt
1451        linetotal linetotal_nofmt nodiscount_linetotal nodiscount_linetotal_nofmt tax_rate projectnumber projectdescription
1452        price_factor price_factor_name partsgroup weight weight_nofmt lineweight lineweight_nofmt optional);
1453
1454   push @arrays, map { "ic_cvar_$_->{name}" } @{ $ic_cvar_configs };
1455   push @arrays, map { "project_cvar_$_->{name}" } @{ $project_cvar_configs };
1456
1457   my @tax_arrays = qw(taxbase tax taxdescription taxrate taxnumber);
1458
1459   map { $form->{TEMPLATE_ARRAYS}->{$_} = [] } (@arrays, @tax_arrays, @prepared_arrays);
1460
1461   my $totalweight = 0;
1462   my $sameitem = "";
1463   foreach $item (sort { $a->[1] cmp $b->[1] } @partsgroup) {
1464     $i = $item->[0];
1465
1466     if ($item->[1] ne $sameitem) {
1467       push(@{ $form->{TEMPLATE_ARRAYS}->{entry_type}  }, 'partsgroup');
1468       push(@{ $form->{TEMPLATE_ARRAYS}->{description} }, qq|$item->[1]|);
1469       $sameitem = $item->[1];
1470
1471       map({ push(@{ $form->{TEMPLATE_ARRAYS}->{$_} }, "") } grep({ $_ ne "description" } (@arrays, @prepared_arrays)));
1472     }
1473
1474     $form->{"qty_$i"} = $form->parse_amount($myconfig, $form->{"qty_$i"});
1475
1476     if ($form->{"id_$i"} != 0) {
1477
1478       # add number, description and qty to $form->{number}, ....
1479
1480       if ($form->{"subtotal_$i"} && !$subtotal_header) {
1481         $subtotal_header = $i;
1482         $position = int($position);
1483         $subposition = 0;
1484         $position++;
1485       } elsif ($subtotal_header) {
1486         $subposition += 1;
1487         $position = int($position);
1488         $position = $position.".".$subposition;
1489       } else {
1490         $position = int($position);
1491         $position++;
1492       }
1493
1494       my $price_factor = $price_factors{$form->{"price_factor_id_$i"}} || { 'factor' => 1 };
1495
1496       push(@{ $form->{TEMPLATE_ARRAYS}->{$_} },                $prepared_template_arrays{$_}[$i - 1]) for @prepared_arrays;
1497
1498       push @{ $form->{TEMPLATE_ARRAYS}->{entry_type} },        'normal';
1499       push @{ $form->{TEMPLATE_ARRAYS}->{runningnumber} },     $position;
1500       push @{ $form->{TEMPLATE_ARRAYS}->{number} },            $form->{"partnumber_$i"};
1501       push @{ $form->{TEMPLATE_ARRAYS}->{description} },       $form->{"description_$i"};
1502       push @{ $form->{TEMPLATE_ARRAYS}->{longdescription} },   $form->{"longdescription_$i"};
1503       push @{ $form->{TEMPLATE_ARRAYS}->{qty} },               $form->format_amount($myconfig, $form->{"qty_$i"});
1504       push @{ $form->{TEMPLATE_ARRAYS}->{qty_nofmt} },         $form->{"qty_$i"};
1505       push @{ $form->{TEMPLATE_ARRAYS}->{ship} },              $form->format_amount($myconfig, $form->{"ship_$i"});
1506       push @{ $form->{TEMPLATE_ARRAYS}->{ship_nofmt} },        $form->{"ship_$i"};
1507       push @{ $form->{TEMPLATE_ARRAYS}->{unit} },              $form->{"unit_$i"};
1508       push @{ $form->{TEMPLATE_ARRAYS}->{bin} },               $form->{"bin_$i"};
1509       push @{ $form->{TEMPLATE_ARRAYS}->{partnotes} },         $form->{"partnotes_$i"};
1510       push @{ $form->{TEMPLATE_ARRAYS}->{serialnumber} },      $form->{"serialnumber_$i"};
1511       push @{ $form->{TEMPLATE_ARRAYS}->{reqdate} },           $form->{"reqdate_$i"};
1512       push @{ $form->{TEMPLATE_ARRAYS}->{sellprice} },         $form->{"sellprice_$i"};
1513       push @{ $form->{TEMPLATE_ARRAYS}->{sellprice_nofmt} },   $form->parse_amount($myconfig, $form->{"sellprice_$i"});
1514       push @{ $form->{TEMPLATE_ARRAYS}->{listprice} },         $form->format_amount($myconfig, $form->{"listprice_$i"}, 2);
1515       push @{ $form->{TEMPLATE_ARRAYS}->{listprice_nofmt} },   $form->{"listprice_$i"};
1516       push @{ $form->{TEMPLATE_ARRAYS}->{price_factor} },      $price_factor->{formatted_factor};
1517       push @{ $form->{TEMPLATE_ARRAYS}->{price_factor_name} }, $price_factor->{description};
1518       push @{ $form->{TEMPLATE_ARRAYS}->{partsgroup} },        $form->{"partsgroup_$i"};
1519       push @{ $form->{TEMPLATE_ARRAYS}->{optional} },          $form->{"optional_$i"};
1520
1521       my $sellprice     = $form->parse_amount($myconfig, $form->{"sellprice_$i"});
1522       my ($dec)         = ($sellprice =~ /\.(\d+)/);
1523       my $decimalplaces = max 2, length($dec);
1524
1525       my $parsed_discount            = $form->parse_amount($myconfig, $form->{"discount_$i"});
1526
1527       my $linetotal_exact            = $form->{"qty_$i"} * $sellprice * (100 - $parsed_discount) / 100 / $price_factor->{factor};
1528       my $linetotal                  = $form->round_amount($linetotal_exact, 2);
1529
1530       my $nodiscount_exact_linetotal = $form->{"qty_$i"} * $sellprice                                  / $price_factor->{factor};
1531       my $nodiscount_linetotal       = $form->round_amount($nodiscount_exact_linetotal,2);
1532
1533       my $discount                   = $nodiscount_linetotal - $linetotal; # is always rounded because $nodiscount_linetotal and $linetotal are rounded
1534
1535       my $discount_round_error       = $discount + ($linetotal_exact - $nodiscount_exact_linetotal); # not used
1536
1537       $form->{"netprice_$i"}   = $form->round_amount($form->{"qty_$i"} ? ($linetotal / $form->{"qty_$i"}) : 0, $decimalplaces);
1538
1539       push @{ $form->{TEMPLATE_ARRAYS}->{netprice} },       ($form->{"netprice_$i"} != 0) ? $form->format_amount($myconfig, $form->{"netprice_$i"}, $decimalplaces) : '';
1540       push @{ $form->{TEMPLATE_ARRAYS}->{netprice_nofmt} }, ($form->{"netprice_$i"} != 0) ? $form->{"netprice_$i"} : '';
1541
1542       $linetotal = ($linetotal != 0) ? $linetotal : '';
1543
1544       push @{ $form->{TEMPLATE_ARRAYS}->{discount} },       ($discount != 0) ? $form->format_amount($myconfig, $discount * -1, 2) : '';
1545       push @{ $form->{TEMPLATE_ARRAYS}->{discount_nofmt} }, ($discount != 0) ? $discount * -1 : '';
1546       push @{ $form->{TEMPLATE_ARRAYS}->{p_discount} },     $form->{"discount_$i"};
1547
1548       if ( $prepared_template_arrays{separate}[$i - 1]  ) {
1549         my $pabbr = $prepared_template_arrays{separate}[$i - 1];
1550         if ( ! $form->{"separate_${pabbr}_subtotal"} ) {
1551             push @separate_totals , "separate_${pabbr}_subtotal";
1552             $form->{"separate_${pabbr}_subtotal"} = 0;
1553         }
1554         $form->{"separate_${pabbr}_subtotal"} += $linetotal;
1555       } else {
1556         $form->{non_separate_subtotal} += $linetotal;
1557       }
1558
1559       $form->{ordtotal}         += $linetotal unless $form->{"optional_$i"};
1560       $form->{nodiscount_total} += $nodiscount_linetotal;
1561       $form->{discount_total}   += $discount;
1562
1563       if ($subtotal_header) {
1564         $discount_subtotal   += $linetotal;
1565         $nodiscount_subtotal += $nodiscount_linetotal;
1566       }
1567
1568       if ($form->{"subtotal_$i"} && $subtotal_header && ($subtotal_header != $i)) {
1569         push @{ $form->{TEMPLATE_ARRAYS}->{discount_sub} },         $form->format_amount($myconfig, $discount_subtotal,   2);
1570         push @{ $form->{TEMPLATE_ARRAYS}->{discount_sub_nofmt} },   $discount_subtotal;
1571         push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_sub} },       $form->format_amount($myconfig, $nodiscount_subtotal, 2);
1572         push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_sub_nofmt} }, $nodiscount_subtotal;
1573
1574         $discount_subtotal   = 0;
1575         $nodiscount_subtotal = 0;
1576         $subtotal_header     = 0;
1577
1578       } else {
1579         push @{ $form->{TEMPLATE_ARRAYS}->{$_} }, "" for qw(discount_sub nodiscount_sub discount_sub_nofmt nodiscount_sub_nofmt);
1580       }
1581
1582       if (!$form->{"discount_$i"}) {
1583         $nodiscount += $linetotal;
1584       }
1585
1586       my $project = $projects_by_id{$form->{"project_id_$i"}} || SL::DB::Project->new;
1587
1588       push @{ $form->{TEMPLATE_ARRAYS}->{linetotal} },                  $form->format_amount($myconfig, $linetotal, 2);
1589       push @{ $form->{TEMPLATE_ARRAYS}->{linetotal_nofmt} },            $linetotal_exact;
1590       push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_linetotal} },       $form->format_amount($myconfig, $nodiscount_linetotal, 2);
1591       push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_linetotal_nofmt} }, $nodiscount_linetotal;
1592       push @{ $form->{TEMPLATE_ARRAYS}->{projectnumber} },              $project->projectnumber;
1593       push @{ $form->{TEMPLATE_ARRAYS}->{projectdescription} },         $project->description;
1594
1595       my $lineweight = $form->{"qty_$i"} * $form->{"weight_$i"};
1596       $totalweight += $lineweight;
1597       push @{ $form->{TEMPLATE_ARRAYS}->{weight} },            $form->format_amount($myconfig, $form->{"weight_$i"}, 3);
1598       push @{ $form->{TEMPLATE_ARRAYS}->{weight_nofmt} },      $form->{"weight_$i"};
1599       push @{ $form->{TEMPLATE_ARRAYS}->{lineweight} },        $form->format_amount($myconfig, $lineweight, 3);
1600       push @{ $form->{TEMPLATE_ARRAYS}->{lineweight_nofmt} },  $lineweight;
1601
1602       my ($taxamount, $taxbase);
1603       my $taxrate = 0;
1604
1605       map { $taxrate += $form->{"${_}_rate"} } split(/ /, $form->{"taxaccounts_$i"});
1606
1607       unless ($form->{"optional_$i"}) {
1608         if ($form->{taxincluded}) {
1609
1610           # calculate tax
1611           $taxamount = $linetotal * $taxrate / (1 + $taxrate);
1612           $taxbase = $linetotal / (1 + $taxrate);
1613         } else {
1614           $taxamount = $linetotal * $taxrate;
1615           $taxbase   = $linetotal;
1616         }
1617       }
1618
1619       if ($taxamount != 0) {
1620         foreach my $accno (split / /, $form->{"taxaccounts_$i"}) {
1621           $taxaccounts{$accno} += $taxamount * $form->{"${accno}_rate"} / $taxrate;
1622           $taxbase{$accno}     += $taxbase;
1623         }
1624       }
1625
1626       $tax_rate = $taxrate * 100;
1627       push(@{ $form->{TEMPLATE_ARRAYS}->{tax_rate} }, qq|$tax_rate|);
1628
1629       if ($form->{"part_type_$i"} eq 'assembly') {
1630         $sameitem = "";
1631
1632         # get parts and push them onto the stack
1633         my $sortorder = "";
1634         if ($form->{groupitems}) {
1635           $sortorder = qq|ORDER BY pg.partsgroup, a.position|;
1636         } else {
1637           $sortorder = qq|ORDER BY a.position|;
1638         }
1639
1640         $query = qq|SELECT p.partnumber, p.description, p.unit, a.qty, | .
1641                  qq|pg.partsgroup | .
1642                  qq|FROM assembly a | .
1643                  qq|  JOIN parts p ON (a.parts_id = p.id) | .
1644                  qq|    LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id) | .
1645                  qq|    WHERE a.bom = '1' | .
1646                  qq|    AND a.id = ? | . $sortorder;
1647         @values = ($form->{"id_$i"});
1648         $sth = $dbh->prepare($query);
1649         $sth->execute(@values) || $form->dberror($query);
1650
1651         while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
1652           if ($form->{groupitems} && $ref->{partsgroup} ne $sameitem) {
1653             map({ push(@{ $form->{TEMPLATE_ARRAYS}->{$_} }, "") } grep({ $_ ne "description" } (@arrays, @prepared_arrays)));
1654             $sameitem = ($ref->{partsgroup}) ? $ref->{partsgroup} : "--";
1655             push(@{ $form->{TEMPLATE_ARRAYS}->{entry_type}  }, 'assembly-item-partsgroup');
1656             push(@{ $form->{TEMPLATE_ARRAYS}->{description} }, $sameitem);
1657           }
1658
1659           push(@{ $form->{TEMPLATE_ARRAYS}->{entry_type}  }, 'assembly-item');
1660           push(@{ $form->{TEMPLATE_ARRAYS}->{description} }, $form->format_amount($myconfig, $ref->{qty} * $form->{"qty_$i"}) . qq|, $ref->{partnumber}, $ref->{description}|);
1661           map({ push(@{ $form->{TEMPLATE_ARRAYS}->{$_} }, "") } grep({ $_ ne "description" } (@arrays, @prepared_arrays)));
1662         }
1663         $sth->finish;
1664       }
1665
1666       CVar->get_non_editable_ic_cvars(form               => $form,
1667                                       dbh                => $dbh,
1668                                       row                => $i,
1669                                       sub_module         => 'orderitems',
1670                                       may_converted_from => ['orderitems', 'invoice']);
1671
1672       push @{ $form->{TEMPLATE_ARRAYS}->{"ic_cvar_$_->{name}"} },
1673         CVar->format_to_template(CVar->parse($form->{"ic_cvar_$_->{name}_$i"}, $_), $_)
1674           for @{ $ic_cvar_configs };
1675
1676       push @{ $form->{TEMPLATE_ARRAYS}->{"project_cvar_" . $_->config->name} }, $_->value_as_text for @{ $project->cvars_by_config };
1677     }
1678   }
1679
1680   $form->{totalweight}       = $form->format_amount($myconfig, $totalweight, 3);
1681   $form->{totalweight_nofmt} = $totalweight;
1682   my $defaults = AM->get_defaults();
1683   $form->{weightunit}        = $defaults->{weightunit};
1684
1685   my $tax = 0;
1686   foreach $item (sort keys %taxaccounts) {
1687     $tax += $taxamount = $form->round_amount($taxaccounts{$item}, 2);
1688
1689     push(@{ $form->{TEMPLATE_ARRAYS}->{taxbase} },        $form->format_amount($myconfig, $taxbase{$item}, 2));
1690     push(@{ $form->{TEMPLATE_ARRAYS}->{taxbase_nofmt} },  $taxbase{$item});
1691     push(@{ $form->{TEMPLATE_ARRAYS}->{tax} },            $form->format_amount($myconfig, $taxamount,      2));
1692     push(@{ $form->{TEMPLATE_ARRAYS}->{tax_nofmt} },      $taxamount);
1693     push(@{ $form->{TEMPLATE_ARRAYS}->{taxrate} },        $form->format_amount($myconfig, $form->{"${item}_rate"} * 100));
1694     push(@{ $form->{TEMPLATE_ARRAYS}->{taxrate_nofmt} },  $form->{"${item}_rate"} * 100);
1695     push(@{ $form->{TEMPLATE_ARRAYS}->{taxnumber} },      $form->{"${item}_taxnumber"});
1696     push(@{ $form->{TEMPLATE_ARRAYS}->{tax_id} },         $form->{"${item}_tax_id"});
1697
1698     if ( $form->{"${item}_tax_id"} ) {
1699       my $tax_obj = SL::DB::Manager::Tax->find_by(id => $form->{"${item}_tax_id"}) or die "Can't find tax with id " . $form->{"${item}_tax_id"};
1700       my $description = $tax_obj ? $tax_obj->translated_attribute('taxdescription',  $form->{language_id}, 0) : '';
1701       push(@{ $form->{TEMPLATE_ARRAYS}->{taxdescription} }, $description . q{ } . 100 * $form->{"${item}_rate"} . q{%});
1702     }
1703   }
1704
1705   $form->{nodiscount_subtotal} = $form->format_amount($myconfig, $form->{nodiscount_total}, 2);
1706   $form->{discount_total}      = $form->format_amount($myconfig, $form->{discount_total}, 2);
1707   $form->{nodiscount}          = $form->format_amount($myconfig, $nodiscount, 2);
1708   $form->{yesdiscount}         = $form->format_amount($myconfig, $form->{nodiscount_total} - $nodiscount, 2);
1709
1710   if($form->{taxincluded}) {
1711     $form->{subtotal}       = $form->format_amount($myconfig, $form->{ordtotal} - $tax, 2);
1712     $form->{subtotal_nofmt} = $form->{ordtotal} - $tax;
1713   } else {
1714     $form->{subtotal}       = $form->format_amount($myconfig, $form->{ordtotal}, 2);
1715     $form->{subtotal_nofmt} = $form->{ordtotal};
1716   }
1717
1718   my $grossamount = ($form->{taxincluded}) ? $form->{ordtotal} : $form->{ordtotal} + $tax;
1719   $form->{ordtotal} = $form->round_amount( $grossamount, 2, 1);
1720   $form->{rounding} = $form->round_amount(
1721     $form->{ordtotal} - $form->round_amount($grossamount, 2),
1722     2
1723   );
1724
1725   # format amounts
1726   $form->{rounding} = $form->format_amount($myconfig, $form->{rounding}, 2);
1727   $form->{quototal} = $form->{ordtotal} = $form->format_amount($myconfig, $form->{ordtotal}, 2);
1728
1729   $form->set_payment_options($myconfig, $form->{$form->{type} =~ /_quotation/ ? 'quodate' : 'orddate'}, $form->{type});
1730
1731   $form->{username} = $myconfig->{name};
1732
1733   $form->{department}    = SL::DB::Manager::Department->find_by(id => $form->{department_id})->description if $form->{department_id};
1734   $form->{delivery_term} = SL::DB::Manager::DeliveryTerm->find_by(id => $form->{delivery_term_id} || undef);
1735   $form->{delivery_term}->description_long($form->{delivery_term}->translated_attribute('description_long', $form->{language_id})) if $form->{delivery_term} && $form->{language_id};
1736
1737   $form->{order} = SL::DB::Manager::Order->find_by(id => $form->{id}) if $form->{id};
1738   $form->{$_} = $form->format_amount($myconfig, $form->{$_}, 2) for @separate_totals;
1739
1740   $main::lxdebug->leave_sub();
1741 }
1742
1743 1;
1744
1745 __END__
1746
1747 =head1 NAME
1748
1749 OE.pm - Order entry module
1750
1751 =head1 DESCRIPTION
1752
1753 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>.
1754
1755 =head1 FUNCTIONS
1756
1757 =over 4
1758
1759 =item retrieve_simple PARAMS
1760
1761 simple OE retrieval by id. does not look up customer, vendor, units or any other stuff. only oe and orderitems.
1762
1763   my $order = retrieve_simple(id => 2);
1764
1765   $order => {
1766     %_OE_CONTENT,
1767     orderitems => [
1768       %_ORDERITEM_ROW_1,
1769       %_ORDERITEM_ROW_2,
1770       ...
1771     ]
1772   }
1773
1774 =back
1775
1776 =cut