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");
16 FIXME: This class currently handles three different types of transactions:
17 . simple inventory transactions working on quantities:
18 ST_INVADJUST, ST_LOCTRANSFER, ST_MANUISSUE
19 . balanced low level GL transactions, working on amounts:
20 ST_JOURNAL, ST_COSTUPDATE, used also in purchase invoice costing
21 . unbalanced bank transaction:
22 ST_BANKPAYMENT, ST_BANK_DEPOSIT
24 No reason to keep all this in the same class.
51 var $tax_info; // tax info for the GL transaction
56 // bank transaction related
60 var $person_detail_id;
63 function __construct($type, $trans_no=0)
65 $this->trans_type = $type;
66 $this->order_id = $trans_no;
68 if (in_array($type, array(ST_LOCTRANSFER, ST_INVADJUST, ST_COSTUPDATE, ST_MANUISSUE, ST_MANURECEIVE, ST_JOURNAL)))
69 $this->currency = get_company_pref('curr_default');
73 // --------------- line item functions
75 function add_to_cart($line_no, $stock_id, $qty, $unit_cost, $description=null)
78 if (isset($stock_id) && $stock_id != "" && isset($qty))
80 $this->line_items[$line_no] = new line_item($stock_id, $qty,
81 $unit_cost, $description);
86 // shouldn't come here under normal circumstances
87 display_error("unexpected - adding an invalid item or null quantity", "", true);
93 function find_cart_item($stock_id)
95 foreach($this->line_items as $line_no=>$line) {
96 if ($line->stock_id == $stock_id)
97 return $this->line_items[$line_no];
102 function update_cart_item($line_no, $qty, $unit_cost)
104 $this->line_items[$line_no]->quantity = $qty;
105 $this->line_items[$line_no]->unit_cost = $unit_cost;
108 function remove_from_cart($line_no)
110 array_splice($this->line_items, $line_no, 1);
113 function count_items()
115 return count($this->line_items);
118 function check_qoh($location, $date_, $reverse=false)
122 $low_stock = array();
124 if (!$SysPrefs->allow_negative_stock())
126 foreach ($this->line_items as $line_no => $line_item)
127 if (has_stock_holding($line_item->mb_flag) || is_fixed_asset($line_item->mb_flag))
129 $quantity = $line_item->quantity;
131 $quantity = -$line_item->quantity;
136 if (check_negative_stock($line_item->stock_id, $quantity, $location, $date_))
137 $low_stock[] = $line_item->stock_id;
143 // ----------- GL item functions
145 function add_gl_item($code_id, $dimension_id, $dimension2_id, $amount, $memo='', $act_descr=null, $person_id=null, $date=null)
147 if (isset($code_id) && $code_id != "" && isset($amount) && isset($dimension_id) &&
148 isset($dimension2_id))
150 $this->gl_items[] = new gl_item($code_id, $dimension_id, $dimension2_id, $amount, $memo, $act_descr, $person_id, $date);
155 // shouldn't come here under normal circumstances
156 display_error("unexpected - invalid parameters in add_gl_item($code_id, $dimension_id, $dimension2_id, $amount,...)", "", true);
162 function update_gl_item($index, $code_id, $dimension_id, $dimension2_id, $amount, $memo='', $act_descr=null, $person_id=null)
164 $this->gl_items[$index]->code_id = $code_id;
165 $this->gl_items[$index]->person_id = $person_id;
167 $gl_type = is_subledger_account($code_id, $person_id);
170 $this->gl_items[$index]->person_type_id = $gl_type > 0 ? PT_CUSTOMER : PT_SUPPLIER;
171 $data = get_subaccount_data($code_id, $person_id);
172 $this->gl_items[$index]->person_name = $data['name'];
173 $this->gl_items[$index]->branch_id = $data['id'];
176 $this->gl_items[$index]->person_type_id = $this->gl_items[$index]->person_name = '';
178 $this->gl_items[$index]->dimension_id = $dimension_id;
179 $this->gl_items[$index]->dimension2_id = $dimension2_id;
180 $this->gl_items[$index]->amount = $amount;
181 $this->gl_items[$index]->reference = $memo;
182 if ($act_descr == null)
183 $this->gl_items[$index]->description = get_gl_account_name($code_id);
185 $this->gl_items[$index]->description = $act_descr;
189 function remove_gl_item($index)
191 array_splice($this->gl_items, $index, 1);
194 function count_gl_items()
196 return count($this->gl_items);
199 function gl_items_total()
202 foreach ($this->gl_items as $gl_item)
203 $total += $gl_item->amount;
207 function gl_items_total_debit()
210 foreach ($this->gl_items as $gl_item)
212 if ($gl_item->amount > 0)
213 $total += $gl_item->amount;
218 function gl_items_total_credit()
221 foreach ($this->gl_items as $gl_item)
223 if ($gl_item->amount < 0)
224 $total += $gl_item->amount;
229 // ------------ common functions
231 function clear_items()
233 unset($this->line_items);
234 $this->line_items = array();
236 unset($this->gl_items);
237 $this->gl_items = array();
241 // Check if cart contains virtual subaccount (AP/AR) postings
243 function has_sub_accounts()
245 foreach ($this->gl_items as $gl_item)
247 if (is_subledger_account($gl_item->code_id))
254 // Check if cart contains postings to tax accounts
258 foreach ($this->gl_items as $gl_item)
260 if (is_tax_account($gl_item->code_id))
267 Collect tax info from the GL transaction lines and return as array of values:
268 'tax_date' - tax date
269 'tax_group' - related counterparty tax group
270 'tax_category' - tax category (not set for now)
271 'net_amount' - tax amounts array indexed by tax type id
272 'tax_in', 'tax_out' - tax amounts array indexed by tax type id
273 'tax_reg' - tax register used
275 function collect_tax_info()
278 $subledger_sum = $net_sum = 0;
280 $tax_info['tax_date'] = $this->tran_date;
281 $vat_percent = get_company_pref('partial_vat_percent');
282 $factor = $vat_percent && ($this->vat_category == VC_PARTIAL) ? $vat_percent/100: 1;
284 foreach($this->gl_items as $gl)
286 if ($person_type = is_subledger_account($gl->code_id, $gl->person_id))
288 $tax_info['person_type'] = $person_type < 0 ? PT_SUPPLIER : PT_CUSTOMER;
289 $tax_info['person_id'] = $gl->person_id;
291 if ($tax_info['person_type'] == PT_CUSTOMER)
293 $branch = get_default_branch($gl->person_id);
294 $tax_info['tax_group'] = $branch['tax_group_id'];
296 $supplier = get_supplier($gl->person_id);
297 $tax_info['tax_group'] = $supplier['tax_group_id'];
299 $subledger_sum += $gl->amount;
300 } elseif ($tax_id = is_tax_account($gl->code_id))
302 $tax_type = get_tax_type($tax_id);
303 if ($gl->code_id == $tax_type['purchasing_gl_code']) {
304 if (!isset($tax_info['tax_in'][$tax_id]))
305 $tax_info['tax_in'][$tax_id] = 0;
306 $tax_info['tax_in'][$tax_id] += $gl->amount;
307 $tax_info['tax_reg'] = TR_INPUT;
309 if (!isset($tax_info['tax_out'][$tax_id]))
310 $tax_info['tax_out'][$tax_id] = 0;
311 $tax_info['tax_out'][$tax_id] -= $gl->amount;
312 if (!isset($tax_info['tax_reg'])) // TR_INPUT has priority (EU are posted on both accounts)
313 $tax_info['tax_reg'] = TR_OUTPUT;
315 if ($tax_type['rate'])
317 // assume transaction adjustment for negative tax in/out
318 $sign = (@$tax_info['tax_in'][$tax_id] < 0 || @$tax_info['tax_out'][$tax_id] < 0) ? -1 : 1;
319 // we can have both input and output tax postings in some cases like intra-EU trade.
320 // so just calculate net_amount from the higher in/out tax
321 $tax_info['net_amount'][$tax_id]
322 = $sign*round2(max(abs(@$tax_info['tax_in'][$tax_id]), abs(@$tax_info['tax_out'][$tax_id]))/$tax_type['rate']*100, 2)/$factor;
326 $net_sum += $gl->amount;
328 // if no tax amount posted guess register type from person_type used (e.g. export invoice)
329 if (!isset($tax_info['tax_reg']) && isset($tax_info['person_type']))
330 $tax_info['tax_reg'] = $tax_info['person_type']==PT_CUSTOMER ? TR_OUTPUT : TR_INPUT;
332 if (count_array(@$tax_info['net_amount'])) // guess exempt sales/purchase if any tax has been found
334 $ex_net = abs($net_sum) - @array_sum($tax_info['net_amount']);
336 $tax_info['net_amount_ex'] = $ex_net;
342 function set_currency($curr, $rate=0)
344 $this->currency = $curr;
349 Reduce number of necessary gl posting lines.
353 /* reduce additional postings */
355 foreach($this->gl_items as $n => $gl)
357 $prev = @$codes[$gl->code_id][$gl->person_id][$gl->dimension_id][$gl->dimension2_id][$gl->reference];
358 if (isset($prev)) { // add amount to previous line for the same gl_code dims and memo
359 $this->gl_items[$prev]->amount += $gl->amount;
360 if ($this->gl_items[$prev]->amount == 0) // discard if overall amount==0
362 unset($this->gl_items[$prev], $codes[$gl->code_id][$gl->person_id][$gl->dimension_id][$gl->dimension2_id][$gl->reference]);
364 unset($this->gl_items[$n]);
366 $codes[$gl->code_id][$gl->person_id][$gl->dimension_id][$gl->dimension2_id][$gl->reference] = $n;
370 Write transaction GL postings, creating tax records and updating AP/AR/bank ledger if needed.
372 function write_gl($check_balance = true)
374 $delta = $this->gl_items_total();
375 if ($check_balance && floatcmp($delta, 0) !=0)
377 $this->add_gl_item(get_company_pref($delta>0 ? 'rounding_db_act' : 'rounding_cr_act'),
379 error_log(sprintf( _("Rounding error %s encountered for trans_type:%s,trans_no:%s"), $delta, $this->trans_type, $this->order_id));
382 $bank_trans = $supp_trans = $cust_trans = array();
384 foreach($this->gl_items as $gl)
386 if (!isset($gl->date))
387 $gl->date = $this->tran_date;
389 $total_gl += add_gl_trans($this->trans_type, $this->order_id, $gl->date, $gl->code_id, $gl->dimension_id, $gl->dimension2_id,
390 $gl->reference, $gl->amount, $this->currency, $gl->person_type_id, $gl->person_id, $this->rate);
392 // post to first found bank account using given gl acount code.
393 $is_bank_to = is_bank_account($gl->code_id);
394 if ($is_bank_to && (get_bank_account_currency($is_bank_to) == $this->currency)) // do not register exchange variations in bank trans
396 if (!isset($bank_trans[$is_bank_to]))
397 $bank_trans[$is_bank_to] = 0;
399 $bank_trans[$is_bank_to] += $gl->amount;
400 } elseif ($gl->person_id)
402 $home_currency = get_company_currency();
403 // collect per counterparty amounts (in case more than one posting was done to the account),
404 // do not post exchange variations to AR/AP (journal in not customer/supplier currency)
405 if ($gl->person_type_id==PT_SUPPLIER && (get_supplier_currency($gl->person_id) == $this->currency || $this->currency != $home_currency))
406 $supp_trans[$gl->person_id] = @$supp_trans[$gl->person_id] + $gl->amount;
407 elseif ($gl->person_type_id==PT_CUSTOMER && (get_customer_currency(null, $gl->branch_id) == $this->currency || $this->currency != $home_currency))
408 $cust_trans[$gl->branch_id] = @$cust_trans[$gl->branch_id] + $gl->amount;
412 // post currency roundings if any
413 if ($check_balance && floatcmp($total_gl, 0))
414 add_gl_trans($this->trans_type, $this->order_id, $this->tran_date,
415 get_company_pref($total_gl>0 ? 'rounding_db_act' : 'rounding_cr_act'), 0, 0, _('Exchange rate roundings'), -$total_gl);
417 // update bank ledger if used
418 foreach($bank_trans as $bank_id => $amount)
419 add_bank_trans($this->trans_type, $this->order_id, $bank_id, $this->reference,
420 $this->tran_date, $amount, 0, 0, "", $this->currency);
422 // add AP/AR for journal transaction
423 if ($this->trans_type == ST_JOURNAL)
426 foreach($cust_trans as $branch_id => $amount)
427 if (floatcmp($amount, 0))
428 write_cust_journal($this->trans_type, $this->order_id, $branch_id, $this->tran_date,
429 $this->reference, $amount, $this->rate);
431 foreach($supp_trans as $supp_id => $amount)
432 if (floatcmp($amount, 0))
433 write_supp_journal($this->trans_type, $this->order_id, $supp_id, $this->tran_date,
434 $this->reference, -$amount, $this->rate, $this->source_ref);
437 // generate tax records for journal transaction
438 if ($this->trans_type == ST_JOURNAL && is_array($this->tax_info))
440 foreach($this->tax_info['net_amount'] as $tax_id => $net)
445 // in EU VAT system intra-community goods aquisition is posted to both purchasing and sales tax accounts,
446 // but included only in purchase register. To avoid double registering ELSE is used below!
447 if (isset($this->tax_info['tax_in'][$tax_id]))
449 $tax = $this->tax_info['tax_in'][$tax_id];
452 elseif (isset($this->tax_info['tax_out'][$tax_id]))
454 $tax = $this->tax_info['tax_out'][$tax_id];
457 elseif (isset($this->tax_info['tax_reg'])) // e.g. export
460 $reg = $this->tax_info['tax_reg'];
464 $tax_nominal = $this->tax_info['rate'][$tax_id]/100*$net;
465 add_trans_tax_details($this->trans_type, $this->order_id,
466 $tax_id, $this->tax_info['rate'][$tax_id], 0, $tax_nominal, $net, $this->rate,
468 $this->source_ref, $reg, $this->tax_info['tax_group'], $this->tax_info['tax_category']);
474 //--------------------------------------------------------------------------------------------
479 var $item_description;
487 function __construct($stock_id, $qty, $unit_cost=null, $description=null)
489 $item_row = get_item($stock_id);
491 if ($item_row == null)
492 display_error("invalid item added to order : $stock_id", "");
494 $this->mb_flag = $item_row["mb_flag"];
495 $this->units = $item_row["units"];
497 if ($description == null)
498 $this->item_description = $item_row["description"];
500 $this->item_description = $description;
502 if ($unit_cost == null)
503 $this->unit_cost = $item_row["purchase_cost"];
505 $this->unit_cost = $unit_cost;
507 $this->stock_id = $stock_id;
508 $this->quantity = $qty;
509 //$this->price = $price;
514 //---------------------------------------------------------------------------------------
531 function __construct($code_id=null, $dimension_id=0, $dimension2_id=0, $amount=0, $memo='',
532 $act_descr=null, $person_id=null, $date=null)
534 //echo "adding $index, $code_id, $dimension_id, $amount, $reference<br>";
536 if ($act_descr == null && $code_id)
537 $this->description = get_gl_account_name($code_id);
539 $this->description = $act_descr;
541 $this->code_id = $code_id;
542 $this->person_id = $person_id;
543 $gl_type = is_subledger_account($code_id, $person_id);
546 $this->person_type_id = $gl_type > 0 ? PT_CUSTOMER : PT_SUPPLIER;
547 $data = get_subaccount_data($code_id, $person_id);
548 $this->person_name = $data['name'];
549 $this->branch_id = $data['id'];
551 $this->dimension_id = $dimension_id;
552 $this->dimension2_id = $dimension2_id;
553 $this->amount = round($amount, 2);
554 $this->reference = $memo;