Additional fixes for journal handling. Added reg_type in trans_tax_details.
[fa-stable.git] / includes / ui / items_cart.inc
1 <?php
2 /**********************************************************************
3     Copyright (C) FrontAccounting, LLC.
4         Released under the terms of the GNU General Public License, GPL, 
5         as published by the Free Software Foundation, either version 3 
6         of the License, or (at your option) any later version.
7     This program is distributed in the hope that it will be useful,
8     but WITHOUT ANY WARRANTY; without even the implied warranty of
9     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
10     See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
11 ***********************************************************************/
12 include_once($path_to_root . "/includes/prefs/sysprefs.inc");
13 include_once($path_to_root . "/inventory/includes/inventory_db.inc");
14
15 class items_cart
16 {
17         var $trans_type;
18         var $line_items;
19         var $gl_items;
20
21         var     $order_id;
22
23         var $from_loc;
24         var $to_loc;
25         var $tran_date;
26         var $doc_date;
27         var $event_date;
28         var $transfer_type;
29         var $increase;
30         var $memo_;
31         var $branch_id;
32         var $reference;
33         var $original_amount;
34         var $currency;
35         var $rate;
36         var $source_ref;
37         var $vat_category;
38
39         var $tax_info;  // tax info for the GL transaction
40
41         function items_cart($type, $trans_no=0)
42         {
43                 $this->trans_type = $type;
44                 $this->order_id = $trans_no;
45                 $this->clear_items();
46                 if (in_array($type, array(ST_LOCTRANSFER, ST_INVADJUST, ST_COSTUPDATE, ST_MANUISSUE, ST_MANURECEIVE, ST_JOURNAL)))
47                         $this->currency = get_company_pref('curr_default');
48                 $this->rate = 1;
49         }
50
51         // --------------- line item functions
52
53         function add_to_cart($line_no, $stock_id, $qty, $standard_cost, $description=null)
54         {
55
56                 if (isset($stock_id) && $stock_id != "" && isset($qty))
57                 {
58                         $this->line_items[$line_no] = new line_item($stock_id, $qty,
59                                 $standard_cost, $description);
60                         return true;
61                 }
62                 else
63                 {
64                         // shouldn't come here under normal circumstances
65                         display_error("unexpected - adding an invalid item or null quantity", "", true);
66                 }
67
68                 return false;
69         }
70
71         function find_cart_item($stock_id)
72         {
73                 foreach($this->line_items as $line_no=>$line) {
74                         if ($line->stock_id == $stock_id)
75                                 return $this->line_items[$line_no];
76                 }
77                 return null;
78         }
79
80         function update_cart_item($line_no, $qty, $standard_cost)
81         {
82                 $this->line_items[$line_no]->quantity = $qty;
83                 $this->line_items[$line_no]->standard_cost = $standard_cost;
84         }
85
86         function remove_from_cart($line_no)
87         {
88                 array_splice($this->line_items, $line_no, 1);
89         }
90
91         function count_items()
92         {
93                 return count($this->line_items);
94         }
95
96         function check_qoh($location, $date_, $reverse=false)
97         {
98                 foreach ($this->line_items as $line_no => $line_item)
99                 {
100                         $item_ret = $line_item->check_qoh($location, $date_, $reverse);
101                         if ($item_ret != null)
102                                 return $line_no;
103                 }
104                 return -1;
105         }
106
107         // ----------- GL item functions
108
109         function add_gl_item($code_id, $dimension_id, $dimension2_id, $amount, $memo='', $act_descr=null, $person_id=null)
110         {
111                 if (isset($code_id) && $code_id != "" && isset($amount) && isset($dimension_id)  &&
112                         isset($dimension2_id))
113                 {
114                         $this->gl_items[] = new gl_item($code_id, $dimension_id, $dimension2_id, $amount, $memo, $act_descr, $person_id);
115                         return true;
116                 }
117                 else
118                 {
119                         // shouldn't come here under normal circumstances
120                         display_error("unexpected - invalid parameters in add_gl_item($code_id, $dimension_id, $dimension2_id, $amount,...)", "", true);
121                 }
122
123                 return false;
124         }
125
126         function update_gl_item($index, $code_id, $dimension_id, $dimension2_id, $amount, $memo='', $act_descr=null, $person_id=null)
127         {
128             $this->gl_items[$index]->code_id = $code_id;
129             $this->gl_items[$index]->person_id = $person_id;
130
131                 $gl_type = is_subledger_account($code_id, $person_id);
132                 if ($gl_type)
133                 {
134                         $this->gl_items[$index]->person_type_id = $gl_type > 0 ? PT_CUSTOMER : PT_SUPPLIER;
135                         $data = get_subaccount_data($code_id, $person_id);
136                         $this->gl_items[$index]->person_name = $data['name'];
137                         $this->gl_items[$index]->branch_id = $data['id'];
138                 } else
139                 {
140                         $this->gl_items[$index]->person_type_id = $this->gl_items[$index]->person_name = '';
141                 }
142                 $this->gl_items[$index]->dimension_id = $dimension_id;
143                 $this->gl_items[$index]->dimension2_id = $dimension2_id;
144                 $this->gl_items[$index]->amount = $amount;
145                 $this->gl_items[$index]->reference = $memo;
146                 if ($act_descr == null)
147                         $this->gl_items[$index]->description = get_gl_account_name($code_id);
148                 else
149                         $this->gl_items[$index]->description = $act_descr;
150
151         }
152
153         function remove_gl_item($index)
154         {
155                 array_splice($this->gl_items, $index, 1);
156         }
157
158         function count_gl_items()
159         {
160                 return count($this->gl_items);
161         }
162
163         function gl_items_total()
164         {
165                 $total = 0;
166                 foreach ($this->gl_items as $gl_item)
167                         $total += $gl_item->amount;
168                 return $total;
169         }
170
171         function gl_items_total_debit()
172         {
173                 $total = 0;
174                 foreach ($this->gl_items as $gl_item)
175                 {
176                         if ($gl_item->amount > 0)
177                                 $total += $gl_item->amount;
178                 }
179                 return $total;
180         }
181
182         function gl_items_total_credit()
183         {
184                 $total = 0;
185                 foreach ($this->gl_items as $gl_item)
186                 {
187                         if ($gl_item->amount < 0)
188                                 $total += $gl_item->amount;
189                 }
190                 return $total;
191         }
192
193         // ------------ common functions
194
195         function clear_items()
196         {
197         unset($this->line_items);
198                 $this->line_items = array();
199
200         unset($this->gl_items);
201                 $this->gl_items = array();
202
203         }
204         //
205         //      Check if cart contains virtual subaccount (AP/AR) postings
206         //
207         function has_sub_accounts()
208         {
209                 foreach ($this->gl_items as $gl_item)
210                 {
211                         if ($gl_item->person_id)
212                                 return true;
213                 }
214                 return false;
215         }
216
217         //
218         //      Check if cart contains postings to tax accounts
219         //
220         function has_taxes()
221         {
222                 foreach ($this->gl_items as $gl_item)
223                 {
224                         if (is_tax_account($gl_item->code_id))
225                                 return true;
226                 }
227                 return false;
228         }
229
230         /*
231                 Collect tax info from the GL transaction lines and return as array of values:
232                         'tax_date'              - tax date
233                         'tax_group'             - related counterparty tax group
234                         'tax_category'  - tax category (not set for now)
235                         'net_amount' - tax amounts array indexed by tax type id
236                         'tax_in', 'tax_out' - tax amounts array indexed by tax type id
237                         'tax_reg' - tax register used
238         */
239         function collect_tax_info()
240         {
241                 $tax_info = array();
242                 $subledger_sum = $net_sum = 0;
243
244                 $tax_info['tax_date'] = $this->tran_date;
245                 $vat_percent = get_company_pref('partial_vat_percent');
246                 $factor = $vat_percent && ($this->vat_category == VC_PARTIAL) ? $vat_percent/100: 1;
247
248                 foreach($this->gl_items as $gl)
249                 {
250                         if ($person_type = is_subledger_account($gl->code_id, $gl->person_id))
251                         {
252                                 $tax_info['person_type'] = $person_type < 0 ? PT_SUPPLIER : PT_CUSTOMER;
253                                 $tax_info['person_id'] = $gl->person_id;
254
255                                 if ($tax_info['person_type'] == PT_CUSTOMER)
256                                 {
257                                         $branch = get_default_branch($gl->person_id);
258                                         $tax_info['tax_group'] = $branch['tax_group_id'];
259                                 } else {
260                                         $supplier = get_supplier($gl->person_id);
261                                         $tax_info['tax_group'] = $supplier['tax_group_id'];
262                                 }
263                                 $subledger_sum += $gl->amount;
264                         } elseif ($tax_id = is_tax_account($gl->code_id))
265                         {
266                                 $tax_type = get_tax_type($tax_id);
267                                 if ($gl->code_id == $tax_type['purchasing_gl_code']) {
268                                         if (!isset($tax_info['tax_in'][$tax_id]))
269                                                 $tax_info['tax_in'][$tax_id] = 0;
270                                         $tax_info['tax_in'][$tax_id] += $gl->amount;
271                                         $tax_info['tax_reg'] = TR_INPUT;
272                                 } else {
273                                         if (!isset($tax_info['tax_out'][$tax_id]))
274                                                 $tax_info['tax_out'][$tax_id] = 0;
275                                         $tax_info['tax_out'][$tax_id] -= $gl->amount;
276                                         if (!isset($tax_info['tax_reg'])) // TR_INPUT has priority (EU are posted on both accounts)
277                                                 $tax_info['tax_reg'] = TR_OUTPUT;
278                                 }
279                                 if ($tax_type['rate'])
280                                 {
281                                         // assume transaction adjustment for negative tax in/out
282                                         $sign = (@$tax_info['tax_in'][$tax_id] < 0 || @$tax_info['tax_out'][$tax_id] < 0) ? -1 : 1;
283                                         // we can have both input and output tax postings in some cases like intra-EU trade.
284                                         // so just calculate net_amount from the higher in/out tax
285                                         $tax_info['net_amount'][$tax_id]
286                                                 = $sign*round2(max(abs(@$tax_info['tax_in'][$tax_id]), abs(@$tax_info['tax_out'][$tax_id]))/$tax_type['rate']*100, 2)/$factor;
287
288                                 }
289                         } else
290                                 $net_sum += $gl->amount;
291                 }
292                 // if no tax amount posted guess register type from person_type used (e.g. export invoice)
293                 if (!isset($tax_info['tax_reg']) && isset($tax_info['person_type']))
294                         $tax_info['tax_reg'] = $tax_info['person_type']==PT_CUSTOMER ? TR_OUTPUT : TR_INPUT;
295
296                 if (count(@$tax_info['net_amount']))    // guess exempt sales/purchase if any tax has been found
297                 {
298                         $ex_net = abs($net_sum) - @array_sum($tax_info['net_amount']);
299                         if ($ex_net != 0)
300                                 $tax_info['net_amount_ex'] = $ex_net;
301                 }
302
303                 return $tax_info;
304         }
305
306         function set_currency($curr, $rate=0)
307         {
308                 $this->currency = $curr;
309                 $this->rate = $rate;
310         }
311
312         /*
313                 Reduce number of necessary gl posting lines.
314         */
315         function reduce_gl()
316         {
317                 /* reduce additional postings */
318                 $codes = array();
319                 foreach($this->gl_items as $n => $gl)
320                 {
321                         $prev = @$codes[$gl->code_id][$gl->person_id][$gl->dimension_id][$gl->dimension2_id][$gl->reference];
322                         if (isset($prev)) { // add amount to previous line for the same gl_code dims and memo
323                                 $this->gl_items[$prev]->amount += $gl->amount;
324                                 if ($this->gl_items[$prev]->amount == 0) // discard if overall amount==0
325                                 {
326                                         unset($this->gl_items[$prev], $codes[$gl->code_id][$gl->person_id][$gl->dimension_id][$gl->dimension2_id][$gl->reference]);
327                                 }
328                                 unset($this->gl_items[$n]);
329                         } else
330                                 $codes[$gl->code_id][$gl->person_id][$gl->dimension_id][$gl->dimension2_id][$gl->reference] = $n;
331                 }
332         }
333         /*
334                 Write transaction GL postings, creating tax records and updating AP/AR/bank ledger if needed.
335         */
336         function write_gl($check_balance = true)
337         {
338                 $delta = $this->gl_items_total();
339                 if ($check_balance && floatcmp($delta, 0) !=0)
340                 {
341                         $this->add_gl_item(get_company_pref($delta>0 ? 'rounding_db_act' : 'rounding_cr_act'),
342                                 0, 0, -$delta, '');
343                         error_log(sprintf( _("Rounding error %s encountered for trans_type:%s,trans_no:%s"), $delta, $this->trans_type, $this->order_id));
344                 }
345
346                 $bank_trans = $supp_trans = $cust_trans = array();
347                 $total_gl = 0;
348                 foreach($this->gl_items as $gl)
349                 {
350                         $total_gl += add_gl_trans($this->trans_type, $this->order_id, $this->tran_date, $gl->code_id, $gl->dimension_id, $gl->dimension2_id, 
351                                 $gl->reference, $gl->amount, $this->currency, $gl->person_type_id, $gl->person_id, "", $this->rate);
352
353                         // post to first found bank account using given gl acount code.
354                         $is_bank_to = is_bank_account($gl->code_id);
355                 if ($is_bank_to && (get_bank_account_currency($is_bank_to) == $this->currency)) // do not register exchange variations in bank trans
356                 {
357                         if (!isset($bank_trans[$is_bank_to]))
358                                 $bank_trans[$is_bank_to] = 0;
359
360                         $bank_trans[$is_bank_to] += $gl->amount;
361                 } elseif ($gl->person_id)
362                 {
363                         $home_currency = get_company_currency();
364                                 // collect per counterparty amounts (in case more than one posting was done to the account),
365                                 // do not post exchange variations to AR/AP (journal in not customer/supplier currency)
366                         if ($gl->person_type_id==PT_SUPPLIER && (get_supplier_currency($gl->person_id) == $this->currency || $this->currency != $home_currency))
367                                         $supp_trans[$gl->person_id] = @$supp_trans[$gl->person_id] + $gl->amount;
368                         else
369                         if ($gl->person_type_id==PT_CUSTOMER && (get_customer_currency(null, $gl->branch_id) == $this->currency || $this->currency != $home_currency))
370                                         $cust_trans[$gl->branch_id] = @$cust_trans[$gl->branch_id] + $gl->amount;
371                 }
372
373                 }
374                 // post currency roundings if any
375                 if ($check_balance && floatcmp($total_gl, 0))
376                         add_gl_trans($this->trans_type, $this->order_id, $this->tran_date, 
377                                 get_company_pref($total_gl>0 ? 'rounding_db_act' : 'rounding_cr_act'), 0, 0, _('Exchange rate roundings'), -$total_gl);
378
379                 // update bank ledger if used
380                 foreach($bank_trans as $bank_id => $amount)
381                         add_bank_trans($this->trans_type, $this->order_id, $bank_id, $this->reference,
382                                 $this->tran_date, $amount, 0, "", $this->currency,
383                                 "Cannot insert a destination bank transaction");
384
385                 // add AP/AR for journal transaction
386                 if ($this->trans_type == ST_JOURNAL)
387                 {
388                         // update AR
389                         foreach($cust_trans as $branch_id => $amount)
390                                 if (floatcmp($amount, 0))
391                                         write_cust_journal($this->trans_type, $this->order_id, $branch_id, $this->tran_date,
392                                                 $this->reference, -$amount, $this->rate);
393                         // update AP
394                         foreach($supp_trans as $supp_id => $amount)
395                                 if (floatcmp($amount, 0))
396                                         write_supp_journal($this->trans_type, $this->order_id, $supp_id, $this->tran_date,
397                                                 $this->reference, -$amount, $this->rate, $this->source_ref);
398                 }
399
400                 // generate tax records for journal transaction
401                 if ($this->trans_type == ST_JOURNAL && is_array($this->tax_info))
402                 {
403                         foreach($this->tax_info['net_amount'] as $tax_id => $net)
404                         {
405                                 if (!$net)
406                                         continue;
407
408                                 // in EU VAT system intra-community goods aquisition is posted to both purchasing and sales tax accounts,
409                                 // but included only in purchase register. To avoid double registering ELSE is used below!
410                                 if (isset($this->tax_info['tax_in'][$tax_id]))
411                                 {
412                                         $tax = $this->tax_info['tax_in'][$tax_id];
413                                         $reg = TR_INPUT;
414                                 }
415                                 elseif (isset($this->tax_info['tax_out'][$tax_id]))
416                                 {
417                                         $tax = $this->tax_info['tax_out'][$tax_id];
418                                         $reg = TR_OUTPUT;
419                                 }
420                                 elseif (isset($this->tax_info['tax_reg'])) // e.g. export
421                                 {
422                                         $tax = 0;
423                                         $reg = $this->tax_info['tax_reg'];
424                                 } else
425                                         continue;
426
427                                 $tax_nominal = $this->tax_info['rate'][$tax_id]/100*$net;
428                                 add_trans_tax_details($this->trans_type, $this->order_id,
429                                         $tax_id, $this->tax_info['rate'][$tax_id], 0, $tax_nominal, $net, $this->rate,
430                                         $this->tran_date,
431                                         $this->source_ref, $reg);
432                         }
433                 }
434         }
435 }
436
437 //--------------------------------------------------------------------------------------------
438
439 class line_item
440 {
441         var $stock_id;
442         var $item_description;
443         var $units;
444         var $mb_flag;
445
446         var $quantity;
447         var $price;
448         var $standard_cost;
449
450         function line_item ($stock_id, $qty, $standard_cost=null, $description=null)
451         {
452                 $item_row = get_item($stock_id);
453
454                 if ($item_row == null)
455                         display_error("invalid item added to order : $stock_id", "");
456
457                 $this->mb_flag = $item_row["mb_flag"];
458                 $this->units = $item_row["units"];
459
460                 if ($description == null)
461                         $this->item_description = $item_row["description"];
462                 else
463                         $this->item_description = $description;
464
465                 if ($standard_cost == null)
466                         $this->standard_cost = $item_row["actual_cost"];
467                 else
468                         $this->standard_cost = $standard_cost;
469
470                 $this->stock_id = $stock_id;
471                 $this->quantity = $qty;
472                 //$this->price = $price;
473                 $this->price = 0;
474         }
475
476         function check_qoh($location, $date_, $reverse)
477         {
478                 global $SysPrefs;
479                 
480         if (!$SysPrefs->allow_negative_stock())
481         {
482                         if (has_stock_holding($this->mb_flag))
483                         {
484                                 $quantity = $this->quantity;
485                                 if ($reverse)
486                                         $quantity = -$this->quantity;
487
488                                 if ($quantity >= 0)
489                                         return null;
490
491                                 $qoh = get_qoh_on_date($this->stock_id, $location, $date_);
492                         if ($quantity + $qoh < 0)
493                         {
494                                 return $this;
495                         }
496                 }
497         }
498
499         return null;
500         }
501 }
502
503 //---------------------------------------------------------------------------------------
504
505 class gl_item
506 {
507
508         var $code_id;
509         var $dimension_id;
510         var $dimension2_id;
511         var $amount;
512         var $reference;
513         var $description;
514         var $person_id;
515         var $person_type_id;
516         var $person_name;
517         var $branch_id;
518
519         function gl_item($code_id=null, $dimension_id=0, $dimension2_id=0, $amount=0, $memo='',
520                 $act_descr=null, $person_id=null)
521         {
522                 //echo "adding $index, $code_id, $dimension_id, $amount, $reference<br>";
523
524                 if ($act_descr == null && $code_id)
525                         $this->description = get_gl_account_name($code_id);
526                 else
527                         $this->description = $act_descr;
528
529                 $this->code_id = $code_id;
530                 $this->person_id = $person_id;
531                 $gl_type = is_subledger_account($code_id, $person_id);
532                 if ($gl_type)
533                 {
534                         $this->person_type_id = $gl_type > 0 ? PT_CUSTOMER : PT_SUPPLIER;
535                         $data = get_subaccount_data($code_id, $person_id);
536                         $this->person_name = $data['name'];
537                         $this->branch_id = $data['id'];
538                 }
539                 $this->dimension_id = $dimension_id;
540                 $this->dimension2_id = $dimension2_id;
541                 $this->amount = round($amount, 2);
542                 $this->reference = $memo;
543         }
544 }