<?php
/**
 * phpaga
 *
 * billing functionality.
 *
 * This file contains the necessary routines for billing and invoice management.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @version $Id: bills.php 886 2007-11-05 12:04:26Z florian $
 *
 * Copyright (c) 2002, Florian Lanthaler <florian@phpaga.net>
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *    * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *    * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in
 *      the documentation and/or other materials provided with the
 *      distribution.
 *
 *    * Neither the name of Florian Lanthaler nor the names of his
 *      contributors may be used to endorse or promote products derived
 *      from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * This class allows the management of billing details as shown on an invoice
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

class phpaga_bill_details
{

    var $curr_name;
    var $curr_decimals;
    var $details = array();

    /**
     * Constructor
     *
     * @param int   $curr_id   Currency ID
     *
     * @return void
     *
     * @author Florian Lanthaler <florian@phpaga.net>
     * @since phpaga 0.1
     */

    function phpaga_bill_details($curr_id)
    {
        if (!isset($curr_id) || !is_numeric($curr_id))
        {
            $this->curr_name = "";
            $this->curr_name = 0;
        } else {
            $currInfo = phpaga_currencies_getCurrencyInfo($curr_id);
            $this->curr_name = $currInfo["curr_name"];
            $this->curr_decimals = $currInfo["curr_decimals"];
        }

    }

    /**
     * Adds a detail line.
     *
     * @param string   $text      Text to be displayed
     * @param real     $amount    Amount
     *
     * @return void
     *
     * @author Florian Lanthaler <florian@phpaga.net>
     * @since phpaga 0.1
     */

    function add_detail($text = "", $amount = "")
    {
        $this->details[] = array("text" => $text,
                                 "amount" =>
                                 number_format($amount,
                                               $this->curr_decimals,
                                               PHPAGA_SEPARATOR_DECIMALS,
                                               PHPAGA_SEPARATOR_THOUSANDS).
                                 " ".
                                 $this->curr_name);
    }

    /**
     * Adds a separator line.
     *
     * @return void
     *
     * @author Florian Lanthaler <florian@phpaga.net>
     * @since phpaga 0.1
     */

    function add_separator()
    {
        $this->details[] = array("text" => "", "amount" => "");
    }

}


/**
 * Gets a list of installed and enabled invoice types (methods) from
 * the database and returns them as an array.
 *
 * @return  array  $invoiceTypeList  Array of invoice types. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_bills_getInvoiceTypeList()
{

    $list = null;

    $d = new PhPagaDbData;

    $r = $d->queryAll('SELECT bmt_id, bmt_description FROM billing_methods
                       WHERE bmt_status IS NULL OR bmt_status = 0
                       ORDER BY bmt_description');

    if (PhPagaError::isError($r))
        return $r;

    foreach ($r as $p) {
        phpaga_sanitizeArray($p);
        $list[$p['bmt_id']] = $p['bmt_description'];
    }

    return $list;
}


/**
 * Checks if the billing information is valid.
 *
 * @param  array   $billInfo    Array containing billoing information
 * @return array   $valid       True. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_bills_checkInfo($billInfo)
{

    $errors = array();

    if (!is_array($billInfo))
        $billInfo = array ($billInfo);

    if (!isset($billInfo['bill_number']) || !strlen ($billInfo['bill_number']))
        $errors[] = PHPAGA_ERR_BILL_INVALID_NUMBER;

    if (!isset($billInfo['bill_date']) || !phpaga_checkdate ($billInfo['bill_date'], false))
        $errors[] = PHPAGA_ERR_BILL_INVALID_DATE;

    if (isset($billInfo['bill_date_due']) && strlen($billInfo['bill_date_due']) && !phpaga_checkdate($billInfo['bill_date_due'], false))
        $errors[] = PHPAGA_ERR_BILL_INVALID_DUEDATE;

    if (!isset($billInfo['cpn_id']) || !is_numeric($billInfo['cpn_id']))
        $errors[] = PHPAGA_ERR_BILL_INVALID_CPNID;

    if (!isset($billInfo['bmt_id']) || !is_numeric($billInfo['bmt_id']))
        $errors[] = PHPAGA_ERR_BILL_INVALID_BMTID;

    if (!isset($billInfo['curr_id']) || !is_numeric($billInfo['curr_id']))
        $errors[] = PHPAGA_ERR_BILL_INVALID_CURRID;

    if (!isset($billInfo['bill_startsum']) || !is_numeric($billInfo['bill_startsum']))
        $errors[] = PHPAGA_ERR_BILL_INVALID_STARTSUM;

    if (isset($billInfo['rbill_end']) && strlen($billInfo['rbill_end']) && !phpaga_checkdate($billInfo['rbill_end'], false))
        $errors[] = PHPAGA_ERR_BILL_INVALID_RBILLENDDATE;

    if (count($errors))
        return PhPagaError::raiseErrorByCode($errors);

    return true;
}


/**
 * Creates and inserts a new invoice.
 *
 * @param  array  $billInfo     Array containing billing information
 *
 * @return int    $result       ID of the freshly inserted invoice. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_bills_add($billInfo)
{

    $billID = null;

    $result = phpaga_bills_checkInfo($billInfo);

    if (PhPagaError::isError($result))
        return $result;

    if (!isset($billInfo['bill_payterm']) || !strlen($billInfo['bill_payterm']))
        $billInfo['bill_payterm'] = null;

    if (!isset($billInfo['bill_date_due']) || !strlen($billInfo['bill_date_due']))
        $billInfo['bill_date_due'] = null;

    if (!isset($billInfo['pe_id_recipient']) || !is_numeric($billInfo['pe_id_recipient']))
        $billInfo['pe_id_recipient'] = null;

    $d = new PhPagaDbData;

    $billID = $d->nextId('bills_bill_id');

    $fields = array('bill_id' => $billID,
                    'bill_number' => $billInfo['bill_number'],
                    'bill_date' => $billInfo['bill_date'],
                    'bill_date_due' => $billInfo['bill_date_due'],
                    'bill_payterm' => $billInfo['bill_payterm'],
                    'cpn_id' => $billInfo['cpn_id'],
                    'pe_id_recipient' => $billInfo['pe_id_recipient'],
                    'bill_sent' => PHPAGA_NO,
                    'bill_paydate' => null,
                    'bill_startsum' => $billInfo['bill_startsum'],
                    'bill_endsum' => $billInfo['bill_endsum'],
                    'bmt_id' => $billInfo['bmt_id'],
                    'curr_id' => $billInfo['curr_id']);

    if (isset($billInfo['rbill_id']) && is_numeric($billInfo['rbill_id']))
        $fields['rbill_id'] = $billInfo['rbill_id'];

    $r = $d->insert('bills', $fields);

    if (PhPagaError::isError($r))
        return $r;

    /* Insert line items */

    $fields = array('bill_id',
                    'lit_id',
                    'op_id',
                    'mat_id',
                    'lit_prodcode',
                    'lit_desc',
                    'lit_qty',
                    'lit_netprice');

    $materials = array();
    $val = array();

    foreach ($billInfo['lineitems'] as $lit) {

        if (!is_numeric($lit['op_id']) || ($lit['op_id'] < 0))
            $lit['op_id'] = null;

        if (!is_numeric($lit['mat_id']) || ($lit['mat_id'] < 0))
            $lit['mat_id'] = null;
        else
            $materials[] = $lit['mat_id'];

        $val[] = array($billID,
                       $d->nextId('lineitems_lit_id'),
                       $lit['op_id'],
                       $lit['mat_id'],
                       $lit['lit_prodcode'],
                       $lit['lit_desc'],
                       $lit['lit_qty'],
                       $lit['lit_netprice']);

    }

    $r = $d->insertMultiple('lineitems', $fields, $val);

    if (PhPagaError::isError($r))
        return $r;

    /* If there are any related operations then update them */

    if (isset($billInfo['operations_listin']) && strlen($billInfo['operations_listin'])) {

        $r = $d->update('operations', array('bill_id' => $billID), 'op_id IN ('.$billInfo['operations_listin'].')', null);

        if (PhPagaError::isError($r))
            return $r;
    }

    /* If there are any related materials then update them */

    if (count($materials)) {

        $r = $d->update('material', array('bill_id' => $billID), 'mat_id IN ('.implode(', ', $materials).')', null);

        if (PhPagaError::isError($r))
            return $r;
    }

    /* If there are any related expenses then update them */

    if (isset($billInfo['expenses']) && is_array($billInfo['expenses']) && count($billInfo['expenses'])) {

        $expids = array();
        foreach($billInfo['expenses'] as $e)
            if (isset($e['exp_id']) && is_numeric($e['exp_id']))
                $expids[] = $e['exp_id'];

        if (count($expids)) {

            $r = $d->update('expenses', array('bill_id' => $billID), 'exp_id IN ('.implode(', ', $expids).')', null);

            if (PhPagaError::isError($r))
                return $r;
        }
    }

    /* Is this a recurring invoice? */

    if (isset($billInfo['rbill_type']) && is_numeric($billInfo['rbill_type']) && ($billInfo['rbill_type'] > 0)) {

        $rbillID = $d->nextId('recurrbills_rbill_id');

        $fields = array('rbill_id' => $rbillID,
                        'bill_id' => $billID,
                        'rbill_type' => $billInfo['rbill_type'],
                        'rbill_start' => $billInfo['bill_date'],
                        'rbill_end' => (isset($billInfo['rbill_end']) && strlen($billInfo['rbill_end'])) ? $billInfo['rbill_end'] : null);

        $r = $d->insert('recurrbills', $fields);

        if (PhPagaError::isError($r))
            return $r;

        $r = $d->update('bills', array('rbill_id' => $rbillID), 'bill_id = ?', $billID);

        if (PhPagaError::isError($r))
            return $r;
    }

    return $billID;
}


/**
 * Deletes an invoice and resets the operations and expenses
 * associated with it.
 *
 * @param  int     $billID      ID of the invoice
 *
 * @return bool    $result      True if successful, PhPagaError otherwise.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_bills_delete($billID)
{

    if (!isset($billID) || !is_numeric($billID))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_BILL_INVALID_ID);

    $d = new PhPagaDbData;

    $r = $d->beginTransaction();
    if (PhPagaError::isError($r))
        return $r;

    $r = $d->update('operations', array('bill_id' => null), 'bill_id = ?', $billID);

    if (PhPagaError::isError($r)) {
        $d->rollback();
        return $r;
    }

    $r = $d->update('material', array('bill_id' => null), 'bill_id = ?', $billID);

    if (PhPagaError::isError($r)) {
        $d->rollback();
        return $r;
    }

    $r = $d->update('expenses', array('bill_id' => null), 'bill_id = ?', $billID);

    if (PhPagaError::isError($r)) {
        $d->rollback();
        return $r;
    }

    $r = $d->execute('DELETE FROM lineitems WHERE bill_id = ?', $billID);

    if (PhPagaError::isError($r)) {
        $d->rollback();
        return $r;
    }

    $r = $d->execute('DELETE FROM bills WHERE bill_id = ?', $billID);

    if (PhPagaError::isError($r)) {
        $d->rollback();
        return $r;
    }

    $r = $d->commit();

    return $r;
}


/**
 * Generates a new invoice number (numeric or alphanumeric) for the
 * current year.
 *
 * @return  string $invoiceNumber    An invoice number. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_bills_getNewInvoiceNumber()
{

    $invoiceNumber = null;
    $billnums = array();

    $d = new PhPagaDbData;
    $r = $d->queryAll('SELECT bill_number FROM bills WHERE YEAR(bill_date) = YEAR(NOW())');

    if (PhPagaError::isError($r))
        return $r;

    if (!count($r))
        $invoiceNumber = 1;
    else {

        foreach($r as $t)
            $billnums[] = $t['bill_number'];

        natsort($billnums);
        $invoiceNumber = array_pop($billnums);

        /* This works also with alphanumeric strings */
        $invoiceNumber++;
    }

    return $invoiceNumber;
}


/**
 * Includes a billing method plugin.
 *
 * @param  int     $bmt_id   Billing method ID
 *
 * @return bool    $result   True. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_bills_includebmtPlugin($bmt_id)
{
    if (!isset($bmt_id) || !is_numeric($bmt_id))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_BILL_INVALID_BMTID);

    /* no need to include the same plugin twice */
    if (defined('PHPAGA_BILL_PLUGININCLUDED') && (PHPAGA_BILL_PLUGININCLUDED == $bmt_id))
        return true;

    /* impossible to include different plugins contemporarily */
    if (defined('PHPAGA_BILL_PLUGININCLUDED') && (PHPAGA_BILL_PLUGININCLUDED != $bmt_id))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_BMTPLUGIN_DIFFINCLUDED);

    if (function_exists('phpaga_bill_calculate'))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_BMTPLUGIN_ALREADYINCLUDED);

    $d = new PhPagaDbData;

    $filename = $d->queryOneVal('SELECT bmt_filename FROM billing_methods WHERE bmt_id = ?', $bmt_id);

    if (PhPagaError::isError($filename))
        return $filename;

    if (!strlen($filename))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_BILL_INVALID_BMTID);

    if (!is_file(PHPAGA_BMTPATH.$filename) || !is_readable(PHPAGA_BMTPATH.$filename))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_FILE_READ);
    else {
        include_once(PHPAGA_BMTPATH.$filename);
        define('PHPAGA_BILL_PLUGININCLUDED', $bmt_id);
        return true;
    }
}


/**
 * Search bills matching search criteria.
 *
 * @param int    $count     Number of records matching the query
 * @param array  $billInfo  Invoice information
 * @param int    $offset    Offset
 * @param int    $limit     Limit
 *
 * @return array $rows      Array containing the results. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_bills_search(&$count, $billInfo, $offset = null, $limit = null)
{
    $count = 0;
    $count_bills = 0;
    $rows = array();
    $where = array();
    $where_clause = null;
    $qparam = array();

    $countquery = 'SELECT COUNT(bill_id) AS count_bill FROM bills ';

    $query = 'SELECT bill_id, bill_number, bill_date, bill_date_due,
        bills.cpn_id, cpn_name, bill_sent, bill_payterm,
        bill_paydate, bill_startsum, bill_endsum, bills.bmt_id, bills.rbill_id,
        bills.curr_id, curr_name, bmt_description, pe_id_recipient, pe_lastname, pe_firstname
        FROM bills
        LEFT JOIN companies ON companies.cpn_id = bills.cpn_id
        LEFT JOIN currencies ON currencies.curr_id = bills.curr_id
        LEFT JOIN billing_methods ON billing_methods.bmt_id = bills.bmt_id
        LEFT JOIN persons ON persons.pe_id = pe_id_recipient ';

    if (isset($billInfo['bill_id']) && is_numeric($billInfo['bill_id'])) {
        $where[] = 'bills.bill_id = ?';
        $qparam[] = $billInfo['bill_id'];
    }

    if (isset($billInfo['rbill_id']) && is_numeric($billInfo['rbill_id'])) {
        $where[] = 'bills.rbill_id = ?';
        $qparam[] = $billInfo['rbill_id'];
    }

    if (isset($billInfo['curr_id']) && is_numeric($billInfo['curr_id'])) {
        $where[] = 'bills.curr_id = ?';
        $qparam[] = $billInfo['curr_id'];
    }

    if (isset($billInfo['cpn_id']) && is_numeric($billInfo['cpn_id'])) {
        $where[] = 'bills.cpn_id = ?';
        $qparam[] = $billInfo['cpn_id'];
    }

    if (isset($billInfo['pe_id_recipient']) && is_numeric($billInfo['pe_id_recipient'])) {
        $where[] = 'bills.pe_id_recipient = ?';
        $qparam[] = $billInfo['pe_id_recipient'];
    }

    if (isset($billInfo['bmt_id']) && is_numeric($billInfo['bmt_id'])) {
        $where[] = 'bills.bmt_id = ?';
        $qparam[] = $billInfo['bmt_id'];
    }

    if (isset($billInfo['bill_paid']) && is_numeric($billInfo['bill_paid'])) {
        switch ($billInfo['bill_paid']) {
            case PHPAGA_BILL_NOTPAID:
                $where[] = 'bills.bill_paydate IS NULL';
                break;

            case PHPAGA_BILL_PAID:
                $where[] = 'bills.bill_paydate IS NOT NULL';
                break;

            default:
                break;
        }
    }

    if (isset($billInfo['bill_sent']) && is_numeric($billInfo['bill_sent'])) {
        switch ($billInfo['bill_sent']) {
            case PHPAGA_BILL_NOTSENT:
                $where[] = 'bills.bill_sent IS NULL';
                break;

            case PHPAGA_BILL_SENT:
                $where[] = 'bills.bill_sent IS NOT NULL';
                break;

            default:
                break;
        }
    }

    if (isset($billInfo['bill_number']) && strlen($billInfo['bill_number'])) {
        $where[] = 'bills.bill_number = ?';
        $qparam[] = $billInfo['bill_number'];
    }

    if (isset($billInfo['bill_datefrom']) && phpaga_checkdate($billInfo['bill_datefrom'], false)) {
        $where[] = 'bills.bill_date >= ?';
        $qparam[] = $billInfo['bill_datefrom'];
    }

    if (isset($billInfo['bill_dateto']) && phpaga_checkdate($billInfo['bill_dateto'], false)) {
        $where[] = 'bills.bill_date <= ?';
        $qparam[] = $billInfo['bill_dateto'];
    }

    if (isset($billInfo['bill_dateduefrom']) && phpaga_checkdate($billInfo['bill_dateduefrom'], false)) {
        $where[] = 'bills.bill_date_due >= ?';
        $qparam[] = $billInfo['bill_dateduefrom'];
    }

    if (isset($billInfo['bill_datedueto']) && phpaga_checkdate($billInfo['bill_datedueto'], false)) {
        $where[] = 'bills.bill_date_due <= ?';
        $qparam[] = $billInfo['bill_datedueto'];
    }

    if (isset($billInfo['bill_year']) && is_numeric($billInfo['bill_year'])) {
        $where[] = 'YEAR(bills.bill_date) = ?';
        $qparam[] = $billInfo['bill_year'];
    }

    if (isset($billInfo['bill_paydate']) && phpaga_checkdate($billInfo['bill_paydate'], false)) {
        $where[] = 'bills.bill_paydate = ?';
        $qparam[] = $billInfo['bill_paydate'];
    }

    if (count($where)) {
        $h = sprintf('WHERE %s ', implode(' AND ', $where));
        $query .= $h;
        $countquery .= $h;
    }

    $query .= ' ORDER BY bill_date DESC, bill_id DESC ';

    $d = new PhPagaDbData;

    $count_bills = $d->queryOneVal($countquery, $qparam);

    if (!PhPagaError::isError($count_bills))
        $count = $count_bills;

    $bills = $d->queryAll($query, $qparam, $offset, $limit);

    if (PhPagaError::isError($bills))
        return $bills;

    foreach ($bills as $qrow) {

        phpaga_sanitizearray($qrow);

        if (isset($qrow['bill_date_due']) && strlen($qrow['bill_date_due']) && (strtotime($qrow['bill_date_due']) <= strtotime("now")))
            $qrow['bill_overdue'] = true;
        else
            $qrow['bill_overdue'] = false;

        $qrow['bill_person_recipient'] = phpaga_persons_formatname($qrow['pe_lastname'], $qrow['pe_firstname']);

        $rows[] = $qrow;
    }

    phpaga_sanitizearray($rows);
    return $rows;
}


/**
 * Search recurring bills matching search criteria.
 *
 * @param int    $count     Number of records matching the query
 * @param array  $billInfo  Invoice information
 * @param int    $offset    Offset
 * @param int    $limit     Limit
 *
 * @return array $rows      Array containing the results. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.4
 */

function phpaga_bills_recurrSearch(&$count, $billInfo, $offset = null, $limit = null)
{

    global $phpaga_rbill_types;

    $count = 0;
    $count_bills = 0;
    $rows = array();
    $where = array();
    $where_clause = null;
    $qparam = array();

    $countquery = 'SELECT COUNT(DISTINCT recurrbills.rbill_id) AS count_bill
                   FROM recurrbills
                   LEFT JOIN bills ON bills.rbill_id = recurrbills.rbill_id
                   LEFT JOIN companies ON companies.cpn_id = bills.cpn_id ';

    $query = 'SELECT DISTINCT recurrbills.rbill_id, rbill_type, rbill_start, rbill_end, rbill_status,
        bills.cpn_id, cpn_name
        FROM recurrbills
        LEFT JOIN bills ON bills.rbill_id = recurrbills.rbill_id
        LEFT JOIN companies ON companies.cpn_id = bills.cpn_id ';

    $where[] = 'bills.rbill_id IS NOT NULL';

    if (isset($billInfo['bill_id']) && is_numeric($billInfo['bill_id'])) {
        $where[] = 'bills.bill_id = ?';
        $qparam[] = $billInfo['bill_id'];
    }

    if (isset($billInfo['rbill_id']) && is_numeric($billInfo['rbill_id'])) {
        $where[] = 'recurrbills.rbill_id = ?';
        $qparam[] = $billInfo['rbill_id'];
    }

    if (isset($billInfo['rbill_type']) && is_numeric($billInfo['rbill_type'])) {
        $where[] = 'rbill_type = ?';
        $qparam[] = $billInfo['rbill_type'];
    }

    if (isset($billInfo['rbill_status']) && ((int)$billInfo['rbill_status'] >= 0)) {
        $where[] = 'rbill_status = ?';
        $qparam[] = $billInfo['rbill_status'];
    }

    if (isset($billInfo['cpn_id']) && is_numeric($billInfo['cpn_id'])) {
        $where[] = 'bills.cpn_id = ?';
        $qparam[] = $billInfo['cpn_id'];
    }

    if (count($where)) {
        $h = sprintf('WHERE %s ', implode(' AND ', $where));
        $query .= $h;
        $countquery .= $h;
    }

    $query .= ' ORDER BY rbill_start DESC, rbill_id DESC ';

    $d = new PhPagaDbData;

    $count_bills = $d->queryOneVal($countquery, $qparam);

    if (!PhPagaError::isError($count_bills))
        $count = $count_bills;

    $bills = $d->queryAll($query, $qparam, $offset, $limit);

    if (PhPagaError::isError($bills))
        return $bills;

    foreach ($bills as $qrow) {

        if (isset($phpaga_rbill_types[$qrow['rbill_type']]))
            $qrow['rbill_type_text'] = $phpaga_rbill_types[$qrow['rbill_type']];
        else
            $qrow['rbill_type_text'] = _('unknown');

        switch ($qrow['rbill_status']) {
            case PHPAGA_RBILL_STATUS_ENABLED:
                $qrow['rbill_status_text'] = _('enabled');
                break;

            case PHPAGA_RBILL_STATUS_DISABLED:
                $qrow['rbill_status_text'] = _('disabled');
                break;

            default:
                $qrow['rbill_status_text'] = _('unknown');
                break;
        }

        $rows[] = $qrow;
    }

    return $rows;
}


/**
 * Sets the payment date for an invoice.
 *
 * @param int       $rbill_id     Recurring bill ID
 * @param int       $rbill_status Status
 *
 * @return bool     $result       True. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.4
 */

function phpaga_bills_recurrSetStatus($rbill_id, $rbill_status)
{

    if (!isset($rbill_id) || !is_numeric($rbill_id) || !isset($rbill_status) || !is_numeric($rbill_status))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_INVALID_PARAMTYPE);

    $fields = array('rbill_status' => $rbill_status);

    $d = new PhPagaDbData;
    $r = $d->update('recurrbills', $fields, 'rbill_id = ?', $rbill_id);

   if (PhPagaError::isError($r))
        return $r;

    return true;
}


/**
 * Deletes a recurring invoice.
 *
 * @param int       $rbill_id     Recurring bill ID
 *
 * @return bool     $result       True. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.4
 */

function phpaga_bills_recurrDelete($rbill_id)
{

    if (!isset($rbill_id) || !is_numeric($rbill_id))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_INVALID_PARAMTYPE);

    $d = new PhPagaDbData;

    $r = $d->beginTransaction();

    if (PhPagaError::isError($r))
        return $r;

    $r = $d->update('bills', array('rbill_id' => null), 'rbill_id = ?', $rbill_id);

    if (PhPagaError::isError($r)) {
        $d->rollback();
        return $r;
    }

    $r = $d->execute('DELETE FROM recurrbills WHERE rbill_id = ?', $rbill_id);

    if (PhPagaError::isError($r)) {
        $d->rollback();
        return $r;
    }

    $r = $d->commit();

    return $r;
}


/**
 * Gets a list of due recurring invoices.
 *
 * @return array $info      Array containing information about the due invoices.
 *                          A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.4
 */

function phpaga_bills_recurrGetDueList() {

    global $phpaga_rbill_types;
    global $phpaga_rbill_intervals;

    $info = array();

    $d = new PhPagaDbData;

    $r = $d->queryAll('SELECT rbill_id, rbill_type, rbill_start, rbill_end, rbill_created
                       FROM recurrbills
                       WHERE rbill_status = ? AND (rbill_end IS NULL OR rbill_end >= CURRENT_TIMESTAMP)
                       ORDER BY rbill_start',
                      PHPAGA_RBILL_STATUS_ENABLED);

    if (PhPagaError::isError($r))
        return $r;

    foreach ($r as $b) {

        if (!isset($phpaga_rbill_intervals[$b['rbill_type']]))
            return PhPagaError::raiseError(_('Invalid recurrent billing type.'));

        if (PHPAGA_DB_SYSTEM == 'mysql') {
            $u = $d->queryRow('select bill_id, bill_number, bill_date,
                           bill_endsum, curr_name, b.cpn_id, cpn_name
                           from bills b
                           left join companies c on c.cpn_id = b.cpn_id
                           left join currencies u on u.curr_id = b.curr_id
                           where rbill_id = ?
                           and bill_date < SUBDATE(CURRENT_TIMESTAMP, INTERVAL '.$phpaga_rbill_intervals[$b['rbill_type']].')
                           and (select count(bill_id) from bills where rbill_id = ?
                                and bill_date >= CURRENT_TIMESTAMP - INTERVAL '.$phpaga_rbill_intervals[$b['rbill_type']].') = 0
                           order by bill_date desc limit 1',
                              array($b['rbill_id'], $b['rbill_id']));

        } else {
            $u = $d->queryRow('select bill_id, bill_number, bill_date,
                           bill_endsum, curr_name, b.cpn_id, cpn_name
                           from bills b
                           left join companies c on c.cpn_id = b.cpn_id
                           left join currencies u on u.curr_id = b.curr_id
                           where rbill_id = ?
                           and bill_date < CURRENT_TIMESTAMP - INTERVAL ?
                           and (select count(bill_id) from bills where rbill_id = ?
                                and bill_date >= CURRENT_TIMESTAMP - INTERVAL ?) = 0
                           order by bill_date desc limit 1',
                              array($b['rbill_id'], $phpaga_rbill_intervals[$b['rbill_type']],
                                    $b['rbill_id'], $phpaga_rbill_intervals[$b['rbill_type']]));
        }

        if (PhPagaError::isError($b))
            return $b;

        if (is_array($u) && count($u)) {

            $ttext = _('unknown');

            if (isset($phpaga_rbill_types[$b['rbill_type']]))
                $ttext = $phpaga_rbill_types[$b['rbill_type']];

            $info[] = array('rbill_id' => $b['rbill_id'],
                            'rbill_type' => $b['rbill_type'],
                            'rbill_type_text' => $ttext,
                            'rbill_start' => $b['rbill_start'],
                            'rbill_end' => $b['rbill_end'],
                            'bill_id' => $u['bill_id'],
                            'bill_number' => $u['bill_number'],
                            'bill_date' => $u['bill_date'],
                            'bill_endsum' => $u['bill_endsum'],
                            'curr_name' => $u['curr_name'],
                            'cpn_id' => $u['cpn_id'],
                            'cpn_name' => $u['cpn_name']);
        }
    }

    return $info;
}


/**
 * Return the date of the oldest invoice (bill_date).
 *
 * @return string   $result   Date of the oldest invoice. A PhPagaError object on failure
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_bills_getInvoiceMinDate()
{

    $d = new PhPagaDbData;

    $r = $d->queryOneVal('SELECT min(bill_date) AS min_date FROM bills');

    return $r;
}


/**
 * Return an array containing al the different currencies actually
 * used for the invoices.
 *
 * @return array   $result    Array containing used currencies. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_bills_getCurrenciesUsed()
{

    $currencies = array();

    $d = new PhPagaDbData;

    $r = $d->queryAll('SELECT DISTINCT bills.curr_id, curr_name
                       FROM bills LEFT JOIN currencies ON currencies.curr_id = bills.curr_id');

    if (PhPagaError::isError($r))
        return $r;

    foreach ($r as $t)
        $currencies[$t['curr_id']] = $t['curr_name'];

    return $currencies;
}


/**
 * Sets the payment date for an invoice.
 *
 * @param int       $bill_id      Bill ID
 * @param string    $paydate      Date the payment was done
 *
 * @return bool     $result       True. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_bills_setInvoicePaydate($bill_id, $paydate)
{

    if (!isset($bill_id) || !is_numeric($bill_id))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_INVALID_INVOICEID);

    if(!isset($paydate) || !phpaga_checkdate($paydate, false))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_INVALID_DATEFORMAT);

    $fields = array('bill_paydate' => $paydate);

    $d = new PhPagaDbData;
    $r = $d->update('bills', $fields, 'bill_id = ?', $bill_id);

   if (PhPagaError::isError($r))
        return $r;

    return true;
}


/**
 * Gets a list of billing method plugins from the database and returns
 * them as an array.
 *
 * @return array $rows  Array containing the results. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_bills_getPluginList()
{

    $matches = array();
    $author = _('unknown');
    $instfiles = array();
    $bmtlist = array('installed' => array(),
                     'notinstalled' => array());

    $query = 'SELECT bmt.bmt_id, bmt_description, bmt_filename, bmt.ctr_id,
        ctr_code, ctr_name, count(bill_id) as nr_used, bmt_status
        FROM billing_methods bmt
        LEFT JOIN countries ctr ON ctr.ctr_id = bmt.ctr_id
        LEFT JOIN bills ON bills.bmt_id = bmt.bmt_id
        GROUP BY bmt.bmt_id, bmt_description, bmt_filename,
        bmt.ctr_id, ctr_code, ctr_name, bmt_status
        ORDER BY ctr_name, bmt_filename';

    $d = new PhPagaDbData;

    $r = $d->queryAll($query);

    if (PhPagaError::isError($r))
        return $r;

    foreach ($r as $qrow) {
        phpaga_sanitizeArray($qrow);
        $bmtlist['installed'][] = $qrow;
        $instfiles[] = $qrow['bmt_filename'];
    }

    foreach (glob(PHPAGA_BMTPATH.'/*.php') as $bmtfile) {

        $filename = basename($bmtfile);
        if (!in_array($filename, $instfiles)) {

            $text = implode('', file($bmtfile));

            if (eregi("@author ([^\n]*)", $text, $matches))
                $author = trim($matches[1]);

            $bmtlist['notinstalled'][] = array('bmt_filename' => $filename, 'bmt_author' => $author);
        }
    }

    return $bmtlist;
}


/**
 * Creates and inserts a new billing method plugin.
 *
 * @param  array $bmt           Array containing plugin information
 *
 * @return int   $id            Billing plugin Id. A PhPagaError on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_bills_add_bmt_plugin($bmt)
{

    $bmt_id = null;

    if (!isset($bmt['bmt_filename']) || !strlen($bmt['bmt_filename']) || !isset($bmt['bmt_description'])
        || !strlen($bmt['bmt_description']) || !isset($bmt['ctr_id']) || !is_numeric($bmt['ctr_id']))

        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_INSERT_DATA_INVALIDFORMAT);

    $d = new PhPagaDbData;
    $bmt_id = $d->nextId('billing_methods_bmt_id');

    $r = $d->insert('billing_methods', array('bmt_id' => $bmt_id,
                                             'bmt_description' => $bmt['bmt_description'],
                                             'bmt_filename' => $bmt['bmt_filename'],
                                             'ctr_id' => $bmt['ctr_id']));

    if (PhPagaError::isError($r))
        return $r;

    return $id;
}


/**
 * Deletes a billing method plugin from the database (not from the
 * file system) if it is not used by any invoce.
 *
 * @param  int     $bmt_id      ID of the billing method plugin
 *
 * @return bool    $result      True. A PhPagaError object on failure.
 *
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_bills_delete_bmt_plugin($bmt_id)
{

    if (!isset($bmt_id) || !is_numeric($bmt_id))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_BILL_INVALID_BMTID);

    $d = new PhPagaDbData;

    $n = $d->queryOneVal('SELECT COUNT(bmt_id) FROM bills WHERE bmt_id = ?', $bmt_id);

    if (PhPagaError::isError($n))
        return $n;

    if ($n > 0)
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_BMT_DELETEUSED);

    $result = $d->execute('DELETE FROM billing_methods WHERE bmt_id = ?', $bmt_id);

    if (PhPagaError::isError($result))
        return $result;

    return true;
}


/**
 * Sets the status of a billing method plugin.
 *
 * @param  int     $bmt_id      ID of the billing method plugin
 * @param  int     $status      Status: 0 (or null) enabled, 10 disabled
 *
 * @return bool    $result      True, or PhPagaError
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_bills_setPluginStatus($bmt_id, $status)
{

    if (!isset($bmt_id) || !is_numeric($bmt_id))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_BILL_INVALID_BMTID);

    if (!is_numeric($status))
        $status = null;

    $fields = array('bmt_status' => $status);

    $d = new PhPagaDbData;
    $r = $d->update('billing_methods', $fields, 'bmt_id = ?', $bmt_id);

    if (PhPagaError::isError($r))
        return $r;

    return true;
}


/**
 * Gets a billing summary for a company.
 *
 * @param  int     $cpn_id      Company ID
 *
 * @return array   $summary     Billing summary. A PhPagaError object on failure.
 *
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_bills_getSummaryCpn($cpn_id)
{

    $summary = array('issued' => array(),
                     'outstanding' => array());

    if (!isset($cpn_id) || !is_numeric($cpn_id))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_COMPANY_INVALIDID);

    $d = new PhPagaDbData;

    $r = $d->queryAll('SELECT count(bill_id) as count, sum(bill_endsum) as amount, b.curr_id,
        curr_name, min(bill_date) as min_date, max(bill_date) as max_date
        from bills b left join currencies c on c.curr_id = b.curr_id
        where cpn_id = ?
        group by b.curr_id, curr_name', $cpn_id);

    if (PhPagaError::isError($r))
        return $r;

    $summary['issued'] = $r;

    $r2 = $d->queryAll('SELECT count(bill_id) as count, sum(bill_endsum) as amount, b.curr_id,
        curr_name, min(bill_date) as min_date, max(bill_date) as max_date
        from bills b left join currencies c on c.curr_id = b.curr_id
        where cpn_id = ?
        and bill_paydate is null
        group by b.curr_id, curr_name', $cpn_id);

    if (PhPagaError::isError($r2))
        return $r2;

    $summary['outstanding'] = $r2;

    return $summary;
}


/**
 * Returns a summary overview of all not (yet) billed hours over all
 * billable projects.
 *
 * @param  bool    $percustomer Group results per customer (optional).
 *                              Default is false (grouped per project)
 *
 * @return array   $summary     Summary information. A PhPagaError object on failure.
 *
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_bills_getSummaryNotBilledHours($percustomer = false)
{

    $summary = array('rows' => array(),
                     'sum_amount' => 0,
                     'sum_duration' => 0,
                     'sum_material' => 0);

    switch(PHPAGA_DB_SYSTEM) {

        case 'pgsql':

            if ($percustomer)
                $query = 'SELECT cpn_id, cpn_name, prj_duration, prj_amount,
                    op_min_start, op_max_end, material_price
                    FROM v_summary_notbilledhours_bycustomer
                    WHERE prj_amount IS NOT NULL';
            else
                $query = 'SELECT cpn_id, cpn_name, prj_id, prj_title, prj_duration, prj_amount,
                    op_min_start, op_max_end, material_price
                    FROM v_summary_notbilledhours
                    WHERE prj_amount IS NOT NULL';
            break;

        case 'mysql':
            if ($percustomer)

                $query = 'SELECT p.cpn_id, c.cpn_name, sum(o.op_duration) AS prj_duration, sum(
                           CASE
                           WHEN ph.phr_hourlyrate IS NOT NULL THEN ph.phr_hourlyrate
                           ELSE
                           CASE
                           WHEN pm.prjmem_hourlyrate IS NOT NULL THEN pm.prjmem_hourlyrate
                           ELSE phn.phr_hourlyrate
                           END
                           END * (o.op_duration / 60)) AS prj_amount, from_unixtime(min(o.op_timestamp_start)) AS op_min_start, from_unixtime(max(o.op_timestamp_end)) AS op_max_end,
                           (select SUM(mat_price) from material left join projects p2 on p2.prj_id = material.prj_id where (p2.prj_billabletype IS NULL OR p2.prj_billabletype = 0) and p2.cpn_id = p.cpn_id and (material.bill_id IS NULL OR material.bill_id = 0)) as material_price
                           FROM projects p
                           LEFT JOIN operations o ON o.prj_id = p.prj_id
                           LEFT JOIN project_members pm ON pm.prj_id = o.prj_id AND pm.pe_id = o.pe_id
                           LEFT JOIN companies c ON c.cpn_id = p.cpn_id
                           LEFT JOIN projects_hourlyrates ph ON ph.opcat_id = o.opcat_id AND ph.prj_id = o.prj_id AND ph.pe_id = o.pe_id
                           LEFT JOIN projects_hourlyrates phn ON phn.opcat_id = o.opcat_id AND phn.prj_id = o.prj_id AND phn.pe_id IS NULL
                           WHERE (p.prj_billabletype IS NULL OR p.prj_billabletype = 0) AND (o.bill_id IS NULL OR o.bill_id = 0) AND (o.op_billabletype IS NULL OR o.op_billabletype = 0) AND (o.op_duration IS NOT NULL)
                           GROUP BY p.cpn_id, c.cpn_name
                           ORDER BY c.cpn_name';

            else

              $query = 'SELECT p.cpn_id, c.cpn_name, p.prj_id, p.prj_title, sum(o.op_duration) AS prj_duration, sum(
                         CASE
                         WHEN ph.phr_hourlyrate IS NOT NULL THEN ph.phr_hourlyrate
                         ELSE
                         CASE
                         WHEN pm.prjmem_hourlyrate IS NOT NULL THEN pm.prjmem_hourlyrate
                         ELSE phn.phr_hourlyrate
                         END
                         END * (o.op_duration / 60)) AS prj_amount, from_unixtime(min(o.op_timestamp_start)) AS op_min_start, from_unixtime(max(o.op_timestamp_end)) AS op_max_end,
                         (select SUM(mat_price) from material where material.prj_id = p.prj_id and (material.bill_id IS NULL OR material.bill_id = 0)) as material_price
                         FROM projects p
                         LEFT JOIN operations o ON o.prj_id = p.prj_id
                         LEFT JOIN project_members pm ON pm.prj_id = o.prj_id AND pm.pe_id = o.pe_id
                         LEFT JOIN companies c ON c.cpn_id = p.cpn_id
                         LEFT JOIN projects_hourlyrates ph ON ph.opcat_id = o.opcat_id AND ph.prj_id = o.prj_id AND ph.pe_id = o.pe_id
                         LEFT JOIN projects_hourlyrates phn ON phn.opcat_id = o.opcat_id AND phn.prj_id = o.prj_id AND phn.pe_id IS NULL
                         WHERE (p.prj_billabletype IS NULL OR p.prj_billabletype = 0) AND (o.bill_id IS NULL OR o.bill_id = 0) AND (o.op_billabletype IS NULL OR o.op_billabletype = 0) AND (o.op_duration IS NOT NULL)
                         GROUP BY p.cpn_id, c.cpn_name, p.prj_id, p.prj_title
                         ORDER BY c.cpn_name, p.prj_title';

            break;

        default:
            return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_QUERYNOTYETSUPPORTED);
            break;
    }

    $d = new PhPagaDbData;

    $r = $d->queryAll($query);

    if (PhPagaError::isError($r))
        return $r;

    foreach ($r as $qrow) {
        $summary['rows'][] = $qrow;
        $summary['sum_amount'] += (float)$qrow['prj_amount'];
        $summary['sum_duration'] += (int)$qrow['prj_duration'];
        $summary['sum_material'] += (int)$qrow['material_price'];
    }

    return $summary;
}


/**
 * Gets an invoice's information
 *
 * @param int    $bill_id   Invoice ID
 *
 * @return array $peInfo Array containing the results. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_bills_getinfo($bill_id)
{

    $billInfo = array();

    $query = 'SELECT bill_id, bill_number, bill_date, bill_date_due,
        bills.cpn_id, cpn_name, bill_sent, bill_payterm,
        bill_paydate, bill_startsum, bill_endsum, bills.bmt_id, bills.rbill_id,
        bills.curr_id, curr_name, bmt_description, pe_id_recipient, pe_lastname, pe_firstname
        FROM bills
        LEFT JOIN companies ON companies.cpn_id = bills.cpn_id
        LEFT JOIN currencies ON currencies.curr_id = bills.curr_id
        LEFT JOIN billing_methods ON billing_methods.bmt_id = bills.bmt_id
        LEFT JOIN persons ON persons.pe_id = bills.pe_id_recipient
        WHERE bill_id = ?';

    $d = new PhPagaDbData;
    $billInfo = $d->queryRow($query, $bill_id);

    if (PhPagaError::isError($billInfo) || !count($billInfo))
        return $billInfo;

    if (count($billInfo)) {

        if (isset($billInfo['bill_date_due']) && strlen($billInfo['bill_date_due'])
            && (!isset($billInfo['bill_paydate']) || !strlen($billInfo['bill_paydate']))
            && (strtotime($billInfo['bill_date_due']) < strtotime(date('Y-m-d'))))

            $billInfo['bill_overdue'] = true;
        else
            $billInfo['bill_overdue'] = false;

        $billInfo['bill_person_recipient'] = phpaga_persons_formatname($billInfo['pe_lastname'], $billInfo['pe_firstname']);

        $opsearch = array('bill_id' => $billInfo['bill_id']);
        $operations = array();
        $count = 0;
        $duration = 0;
        $opIds = array();

        /* Line items */

        $query = 'SELECT lit_id, op_id, mat_id, lit_prodcode, lit_desc, lit_qty, lit_netprice
            FROM lineitems
            WHERE bill_id = ?
            ORDER BY lit_id';

        $l = $d->queryAll($query, $billInfo['bill_id']);

        if (PhPagaError::isError($l) || !count($l))
            return $l;

        $billInfo['lineitems'] = array();

        foreach ($l as $qrow) {
            $qrow['lit_netprice_format'] = phpaga_formatamount((float)$qrow['lit_netprice']);
            $billInfo['lineitems'][] = $qrow;
        }

        /* Expenses */

        $expInfo = array('bill_id' => $billInfo['bill_id']);
        $expenses = array();
        $expensesum = 0;

        $expenses = phpaga_expenses_getexpenses($expInfo);

        if (PhPagaError::isError($expenses))
            return $expenses;

        $billInfo['expenses'] = $expenses;
        if (count($expenses) > 0) {
            foreach ($expenses as $exp)
                $expensesum += $exp['exp_sum'];
        }

        /* Calculation detail */

        $billInfo['bill_details'] = array();

        $plres = phpaga_bills_includebmtPlugin($billInfo['bmt_id']);

        if (PhPagaError::isError($plres)) {
            phpaga_errlog(sprintf('%s: %s line %s: %s',
                                  __FILE__,
                                  __FUNCTION__,
                                  __LINE__,
                                  $plres->getMessage()));
            return $plres;
        }

        $endsum = 0;

        phpaga_bill_calculate($billInfo['bill_startsum'], $billInfo['curr_id'], $endsum, $details, $expensesum);

        $billInfo['bill_details'] = $details->details;

        /* associated tasks */

        $operations = phpaga_operations_search($count, $duration, $opIds, $opsearch);

        if (PhPagaError::isError($operations))
            return $operations;

        $billInfo['operations'] = array('oprows' => $operations,
                                        'count' => $count,
                                        'duration' => $duration,
                                        'opIds' => $opIds);
    }

    return $billInfo;
}

?>