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