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