<?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 726 2006-08-05 22:34:30Z 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
        {
            phpaga_currencies_getCurrencyInfo($currInfo, $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.
 *
 * @param  array $invoiceTypeList  Array containing all available billing methods
 *
 * @return int   $result           A result code.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_bills_getInvoiceTypeList (&$invoiceTypeList)
{

    $db = "";
    $result = "";

    $invoiceTypeList = array();

    $query = "SELECT bmt_id, bmt_description ".
        "FROM billing_methods ".
        "WHERE bmt_status IS NULL OR bmt_status = 0 ".
        "ORDER BY bmt_description";

    phpaga_db_connect($db);
    $result = $db->query($query);
    if (DB::isError($result))
        return PHPAGA_ERR_DB_EXEC_QUERY;

    while ($qrow = $result->fetchRow ())
    {
        phpaga_sanitizeArray ($qrow);
        $invoiceTypeList[$qrow["bmt_id"]] = $qrow["bmt_description"];
    }

    $result->free();
    return PHPAGA_ERR_NONE;
}


/**
 * Checks if the billing information is valid.
 *
 * @param  array   $billInfo    Array containing billoing information
 * @return array   $errors      Array containing error codes
 *
 * @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["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;
    }

    return $errors;

}


/**
 * Creates and inserts a new invoice.
 *
 * @param  array   $billInfo    Array containing billing information
 * @param  int     $billID      ID of the newly inserted invoice record
 * @return array   $errors      Array containing error codes
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_bills_add ($billInfo,
                           &$billID)
{

    $billID = "";

    $result = phpaga_bills_checkInfo($billInfo);

    if (count($result) > 0)
        return PHPAGA_ERR_INSERT_DATA_INVALIDFORMAT;

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

    phpaga_db_connect($db);
    $billID = $db->nextId("bills_bill_id");

    $fields = array("bill_id" => $billID,
                    "bill_number" => $billInfo["bill_number"],
                    "bill_date" => $billInfo["bill_date"],
                    "bill_payterm" => $billInfo["bill_payterm"],
                    "cpn_id" => $billInfo["cpn_id"],
                    "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"]);

    $result = $db->autoExecute("bills", $fields, DB_AUTOQUERY_INSERT);

    if (DB::isError($result))
        return PHPAGA_ERR_DB_EXEC_QUERY;

    /* insert line items */
    $fields = array("bill_id",
                    "lit_id",
                    "op_id",
                    "mat_id",
                    "lit_prodcode",
                    "lit_desc",
                    "lit_qty",
                    "lit_netprice");

    $sth = $db->autoPrepare("lineitems", $fields,
                            DB_AUTOQUERY_INSERT);

    if (PEAR::isError($sth))
        return PHPAGA_ERR_DB_EXEC_QUERY;

    $materials = 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,
                     $db->nextId("lineitems_lit_id"),
                     $lit["op_id"],
                     $lit["mat_id"],
                     $lit["lit_prodcode"],
                     $lit["lit_desc"],
                     $lit["lit_qty"],
                     $lit["lit_netprice"]);

        $res =& $db->execute($sth, $val);

        if (PEAR::isError($res))
            return PHPAGA_ERR_DB_EXEC_QUERY;
    }

    /* if there are any related operations then update them */
    if (isset($billInfo["operations_listin"])
        && strlen($billInfo["operations_listin"]))
    {
        $query =
            "UPDATE operations SET bill_id = ".
            $db->quoteSmart($billID).
            " WHERE op_id IN (".
            $billInfo["operations_listin"].")";

        $result = $db->query($query);

        if (DB::isError($result))
            return PHPAGA_ERR_DB_EXEC_QUERY;
    }

    /* if there are any related materials then update them */
    if (count($materials))
    {
        $query = "UPDATE material SET bill_id = ". $db->quoteSmart($billID).
            " WHERE mat_id IN (".implode(", ", $materials).")";

        $result = $db->query($query);

        if (DB::isError($result))
            return PHPAGA_ERR_DB_EXEC_QUERY;
    }

    return PHPAGA_ERR_NONE;

}

/**
 * Deletes an invoice and resets the operations and expenses
 * associated with it.
 *
 * @param  int     $billID      ID of the invoice
 *
 * @return int     $result      A result code.
 *
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_bills_delete ($billID)
{

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

    phpaga_db_connect($db);

    if (phpaga_db_transaction_begin($db) != PHPAGA_ERR_NONE)
        return PHPAGA_ERR_DB_TRANSACTBEGIN;

    $result = $db->query("UPDATE operations SET bill_id = NULL ".
                         "WHERE bill_id = ".$db->quoteSmart($billID));

    if (DB::isError($result))
    {
        if (phpaga_db_transaction_rollback($db) != PHPAGA_ERR_NONE)
            return PHPAGA_ERR_DB_TRANSACTROLLBACK;
        else
            return PHPAGA_ERR_DB_EXEC_QUERY;
    }

    $result = $db->query("UPDATE material SET bill_id = NULL ".
                         "WHERE bill_id = ".$db->quoteSmart($billID));

    if (DB::isError($result))
    {
        if (phpaga_db_transaction_rollback($db) != PHPAGA_ERR_NONE)
            return PHPAGA_ERR_DB_TRANSACTROLLBACK;
        else
            return PHPAGA_ERR_DB_EXEC_QUERY;
    }

    $result = $db->query("UPDATE expenses SET bill_id = NULL ".
                         "WHERE bill_id = ".$db->quoteSmart($billID));

    if (DB::isError($result))
    {
        if (phpaga_db_transaction_rollback($db) != PHPAGA_ERR_NONE)
            return PHPAGA_ERR_DB_TRANSACTROLLBACK;
        else
            return PHPAGA_ERR_DB_EXEC_QUERY;
    }

    $result = $db->query("DELETE FROM lineitems ".
                         "WHERE bill_id = ".$db->quoteSmart($billID));

    if (DB::isError($result))
    {
        if (phpaga_db_transaction_rollback($db) != PHPAGA_ERR_NONE)
            return PHPAGA_ERR_DB_TRANSACTROLLBACK;
        else
            return PHPAGA_ERR_DB_EXEC_QUERY;
    }

    $result = $db->query("DELETE FROM bills ".
                         "WHERE bill_id = ".$db->quoteSmart($billID));

    if (DB::isError($result))
    {
        if (phpaga_db_transaction_rollback($db) != PHPAGA_ERR_NONE)
            return PHPAGA_ERR_DB_TRANSACTROLLBACK;
        else
            return PHPAGA_ERR_DB_EXEC_QUERY;
    }

    if (phpaga_db_transaction_commit($db) != PHPAGA_ERR_NONE)
        return PHPAGA_ERR_DB_TRANSACTCOMMIT;

    return PHPAGA_ERR_NONE;
}


/**
 * Generates a new invoice number for the current year.
 * This "number" can be any alphanumeric combination.
 *
 * @param  mixed $invoiceNumber    An invoice number
 *
 * @return int   $result           A result code.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_bills_getNewInvoiceNumber (&$invoiceNumber)
{

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

    phpaga_db_connect($db);

    $query = "SELECT bill_id, bill_number ".
        "FROM bills WHERE YEAR(bill_date) = YEAR(NOW()) ";

    $result = $db->getAssoc($query);

    if (DB::isError($result))
        return PHPAGA_ERR_DB_EXEC_QUERY;

    if (count($result) == 0)
        $invoiceNumber = 1;
    else
    {

        foreach($result as $dummy => $num)
            $billnums[] = $num;

        natsort($billnums);
        $billnums = array_values($billnums);
        $invoiceNumber = $billnums[count($billnums) - 1];

        /* increase the existing invoice number by one; note that this
         * works also with alphanumeric strings */

        $invoiceNumber++;
    }

    return PHPAGA_ERR_NONE;
}

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

function phpaga_bills_includebmtPlugin ($bmt_id = 0)
{

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

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

    if (function_exists("phpaga_bill_calculate"))
        return PHPAGA_ERR_BMTPLUGIN_ALREADYINCLUDED;

    $db = "";
    $result = "";
    $filename = "";

    phpaga_db_connect($db);

    $query = "SELECT bmt_filename ".
        "FROM billing_methods ".
        "WHERE bmt_id = ".$db->quoteSmart($bmt_id);

    $result = $db->query($query);

    if (DB::isError($result))
        return PHPAGA_ERR_DB_EXEC_QUERY;

    if ($result->numRows() < 1)
        return PHPAGA_ERR_DB_NO_ROWS;

    if ($qrow = $result->fetchRow())
        $filename = $qrow["bmt_filename"];

    $result->free();

    if (!is_file(PHPAGA_BMTPATH.$filename)
        || !is_readable(PHPAGA_BMTPATH.$filename))
    {
        return PHPAGA_ERR_FILE_READ;
    }
    else
    {
        define("PHPAGA_BILL_PLUGININCLUDED", $bmt_id);
        include_once(PHPAGA_BMTPATH.$filename);
        return PHPAGA_ERR_NONE;
    }
}


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

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

    phpaga_db_connect($db);

    /* how many total matching records are there */

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

    /* select limited number of matching records from bills */

    $query = "SELECT bill_id, bill_number, bill_date, ".
        "bills.cpn_id, cpn_name, bill_sent, bill_payterm, ".
        "bill_paydate, bill_startsum, bill_endsum, bills.bmt_id, ".
        "bills.curr_id, curr_name, bmt_description ".
        "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 ";

    /* collect elements of the where clause */

    if (isset($billInfo["bill_id"]) && is_numeric($billInfo["bill_id"]))
        $where[] = "bills.bill_id = ".$db->quoteSmart($billInfo["bill_id"]);

    if (isset($billInfo["curr_id"]) && is_numeric($billInfo["curr_id"]))
        $where[] = "bills.curr_id = ".$db->quoteSmart($billInfo["curr_id"]);

    if (isset($billInfo["cpn_id"]) && is_numeric($billInfo["cpn_id"]))
        $where[] = "bills.cpn_id = ".$db->quoteSmart($billInfo["cpn_id"]);

    if (isset($billInfo["bmt_id"]) && is_numeric($billInfo["bmt_id"]))
        $where[] = "bills.bmt_id = ".$db->quoteSmart($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 = ".$db->quoteSmart($billInfo["bill_number"]);

    if (isset($billInfo["bill_datefrom"])
        && phpaga_checkdate($billInfo["bill_datefrom"], false))
    {
        $where[] = "bills.bill_date >= ".$db->quoteSmart($billInfo["bill_datefrom"]);
    }
    if (isset($billInfo["bill_dateto"])
        && phpaga_checkdate($billInfo["bill_dateto"], false))
    {
        $where[] = "bills.bill_date <= ".$db->quoteSmart($billInfo["bill_dateto"]);
    }

    if (isset($billInfo["bill_year"]) && is_numeric($billInfo["bill_year"]))
        $where[] = "YEAR(bills.bill_date) = ".$db->quoteSmart($billInfo["bill_year"]);

    if (isset($billInfo["bill_paydate"]) &&
        phpaga_checkdate($billInfo["bill_paydate"], false))
    {
        $where[] = "bills.bill_paydate = ".$db->quoteSmart($billInfo["bill_paydate"]);
    }

    /* build the where clause */

    if (count ($where) > 0)
    {
        $where_clause .= "WHERE ".implode (" AND ", $where)." ";
        $query .= $where_clause;
        $countquery .= $where_clause;
    }

    /* sort by creation order, which is not necessarily the
     * same as sorted by bill number
     */

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

    /* first do the count stuff */

    $result = $db->query($countquery);

    if ((!DB::isError($result)) && ($result->numRows() > 0)
        && ($countrow = $result->fetchRow()))
    {
        $count = $countrow["count_bill"];
    }

    /* limit the number of results starting from $offset if both $offset
     * and $limit are set and valid */

    if (isset($offset) && is_numeric($offset) && isset($limit) &&
        is_numeric($limit) && ($limit > 0))
    {
        $result = $db->limitQuery($query, $offset, $limit);
    }
    else
        $result = $db->query($query);

    if (DB::isError($result))
        return PHPAGA_ERR_DB_EXEC_QUERY;

    if ($result->numRows() > 0)
        while ($qrow = $result->fetchRow ())
            $rows[] = $qrow;

    $result->free();
    return PHPAGA_ERR_NONE;
}


/**
 * Return the date of the oldest invoice (bill_date).
 *
 * @param int    &$min_date     Date of the oldest invoice
 *
 * @return int   $result        A result code.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_bills_getInvoiceMinDate(&$min_date)
{

    phpaga_db_connect($db);

    $min_date = $db->getOne("SELECT min(bill_date) AS min_date ".
                            "FROM bills");

    if (DB::isError($min_date))
        return PHPAGA_ERR_DB_EXEC_QUERY;

    return PHPAGA_ERR_NONE;
}


/**
 * Return an array containing al the different currencies actually used
 * for the invoices.
 *
 * @param array    &$currencies      Array containing used currencies
 *
 * @return int   $result           A result code.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_bills_getCurrenciesUsed (&$currencies)
{

    $currencies = array();

    $query = "SELECT DISTINCT bills.curr_id, curr_name ".
        "FROM bills ".
        "LEFT JOIN currencies ON currencies.curr_id = bills.curr_id ";

    phpaga_db_connect($db);
    $result = $db->query($query);
    if (DB::isError($result))
        return PHPAGA_ERR_DB_EXEC_QUERY;

    if ($result->numRows() > 0)
    {
        while ($qrow = $result->fetchRow())
            $currencies[$qrow["curr_id"]] = $qrow["curr_name"];

        $result->free();
    }

    return PHPAGA_ERR_NONE;
}


/**
 * Sets the payment date for an invoice.
 *
 * @param int       $bill_id      Bill ID
 * @param string    $paydate      Date the payment was done
 *
 * @return int   $result           A result code.
 *
 * @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 PHPAGA_ERR_INVALID_INVOICEID;

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

    phpaga_db_connect($db);

    $query = "UPDATE bills ".
        "SET bill_paydate = ".$db->quoteSmart($paydate).
        " WHERE bill_id = ".$db->quoteSmart($bill_id);

    $result = $db->query($query);

    if (DB::isError($result))
        return PHPAGA_ERR_DB_EXEC_QUERY;
    else
        return PHPAGA_ERR_NONE;

}


/**
 * Gets a list of billing method plugins from the database and returns them as an
 * array.
 *
 * @param  array &$bmtlist         Array containing a list of available
 *                                 billing method plugins
 *
 * @return int   $result           A result code.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_bills_getPluginList (&$bmtlist)
{

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

    phpaga_db_connect($db);

    $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; ";

    $result = $db->query($query);
    if (DB::isError($result))
        return PHPAGA_ERR_DB_EXEC_QUERY;

    while ($qrow = $result->fetchRow ())
    {
        phpaga_sanitizeArray($qrow);
        $bmtlist["installed"][] = $qrow;
        $instfiles[] = $qrow["bmt_filename"];
    }

    $result->free();

    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 PHPAGA_ERR_NONE;
}


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

function phpaga_bills_add_bmt_plugin ($bmt)
{

    $bmt_id = "";

    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 PHPAGA_ERR_INSERT_DATA_INVALIDFORMAT;

    phpaga_db_connect($db);

    $bmt_id = $db->nextId("billing_methods_bmt_id");

    $query =
        "INSERT INTO billing_methods (bmt_id, bmt_description, bmt_filename, ".
        "ctr_id) VALUES ($bmt_id, ".$db->quoteSmart($bmt["bmt_description"]).", ".
        $db->quoteSmart($bmt["bmt_filename"]).", ".$db->quoteSmart($bmt["ctr_id"]).")";

    $result = $db->query($query);

    if (DB::isError($result))
        return PHPAGA_ERR_DB_EXEC_QUERY;
    else
        return PHPAGA_ERR_NONE;

}


/**
 * 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 int     $result      A result code.
 *
 *
 * @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 PHPAGA_ERR_BILL_INVALID_BMTID;

    phpaga_db_connect($db);

    $query = "select count(bmt_id) as num from bills ".
        "where bmt_id = ".$db->quoteSmart($bmt_id);

    $result = $db->query($query);

    if (DB::isError($result))
        return PHPAGA_ERR_DB_EXEC_QUERY;

    if (($qrow = $result->fetchRow()) && ($qrow["num"] > 0))
    {
        $result->free();
        return PHPAGA_ERR_BMT_DELETEUSED;
    }

    $result = $db->query("DELETE FROM billing_methods ".
                         "WHERE bmt_id = ".$db->quoteSmart($bmt_id));

    if (DB::isError($result))
        return PHPAGA_ERR_DB_EXEC_QUERY;

    return PHPAGA_ERR_NONE;
}


/**
 * 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 int     $result      A result code.
 *
 *
 * @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 PHPAGA_ERR_BILL_INVALID_BMTID;

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

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

    phpaga_db_connect($db);
    $result = $db->autoExecute("billing_methods", $fields, DB_AUTOQUERY_UPDATE,
                               "bmt_id = ".$db->quoteSmart($bmt_id));
    if (DB::isError($result))
        return PHPAGA_ERR_DB_EXEC_QUERY;

    return PHPAGA_ERR_NONE;

}


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

function phpaga_bills_getSummaryCpn ($cpn_id, &$summary)
{

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

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

    phpaga_db_connect($db);

    $query = "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 = ".$db->quoteSmart($cpn_id).
        " group by b.curr_id, curr_name";

    $result = $db->query($query);

    if (DB::isError($result))
        return PHPAGA_ERR_DB_EXEC_QUERY;

    while ($qrow = $result->fetchRow())
        $summary["issued"][] = $qrow;

    $result->free();

    $query = "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 = ".$db->quoteSmart($cpn_id).
        " and bill_paydate is null ".
        "group by b.curr_id, curr_name";

    $result = $db->query($query);

    if (DB::isError($result))
        return PHPAGA_ERR_DB_EXEC_QUERY;

    while ($qrow = $result->fetchRow())
        $summary["outstanding"][] = $qrow;

    $result->free();

    return PHPAGA_ERR_NONE;

}


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

function phpaga_bills_getSummaryNotBilledHours (&$summary, $percustomer = false)
{

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

    phpaga_db_connect($db);

    switch(PHPAGA_DB_SYSTEM)
    {

        case "pgsql":

            if ($percustomer)
                $query = "SELECT cpn_id, cpn_name, prj_duration, prj_amount, ".
                    "op_min_start, op_max_end ".
                    "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 ".
                    "FROM v_summary_notbilledhours ".
                    "WHERE prj_amount IS NOT NULL";
            break;

        case "mysql":
            if ($percustomer)
                /*
                $query = " SELECT p.cpn_id, cpn_name, sum(op_duration) AS prj_duration, ".
                    "sum(prjmem_hourlyrate * (op_duration / 60)) AS prj_amount, ".
                    "from_unixtime(min(op_timestamp_start)) AS op_min_start, ".
                    "from_unixtime(max(op_timestamp_end)) AS op_max_end ".
                    "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 ".
                    "WHERE (prj_billabletype IS NULL OR prj_billabletype = 0) ".
                    "AND (o.bill_id IS NULL OR o.bill_id = 0) ".
                    "AND (op_billabletype IS NULL OR op_billabletype = 0) ".
                    "GROUP BY p.cpn_id, cpn_name ".
                    "ORDER BY cpn_name";
                */

                $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
   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)
  GROUP BY p.cpn_id, c.cpn_name
  ORDER BY c.cpn_name;";

            else

                /*
                $query = "SELECT p.cpn_id, cpn_name, p.prj_id, prj_title, ".
                    "sum(op_duration) AS prj_duration, ".
                    "sum(prjmem_hourlyrate * (op_duration / 60)) AS prj_amount, ".
                    "from_unixtime(min(op_timestamp_start)) AS op_min_start, ".
                    "from_unixtime(max(op_timestamp_end)) AS op_max_end ".
                    "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 ".
                    "WHERE (prj_billabletype IS NULL OR prj_billabletype = 0) ".
                    "AND (o.bill_id IS NULL OR o.bill_id = 0) ".
                    "AND (op_billabletype IS NULL OR op_billabletype = 0) ".
                    "GROUP BY p.cpn_id, cpn_name, p.prj_id, prj_title ".
                    "ORDER BY cpn_name, prj_title";
                */
$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
   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)
  GROUP BY p.cpn_id, c.cpn_name, p.prj_id, p.prj_title
  ORDER BY c.cpn_name, p.prj_title;";


            break;

        default:
            /* this query is not yet supported by the underlying
             * database system */
            return PHPAGA_ERR_DB_QUERYNOTYETSUPPORTED;
            break;
    }

    $result = $db->query($query);

    if (DB::isError($result))
        return PHPAGA_ERR_DB_EXEC_QUERY;

    while ($qrow = $result->fetchRow())
    {
        $summary["rows"][] = $qrow;
        $summary["sum_amount"] += (float)$qrow["prj_amount"];
        $summary["sum_duration"] += (int)$qrow["prj_duration"];
    }

    $result->free();

    return PHPAGA_ERR_NONE;

}


/**
 * Gets the project that relates to a bill; Selects the project to
 * which the most operations are linked. If multiple projects had same
 * count would return first. This is a workaround to link project to a
 * bill.
 *
 * @param  int   $bill_id    Bill ID
 * @param  int   &$prj_id    Project ID
 *
 * @return int   $result     A result code.
 *
 *
 * @author Mark Parssey <markpa@users.sourceforge.net>
 * @since phpaga 0.3
 */

function phpaga_bills_getProject ($bill_id, &$prj_id)
{

    phpaga_db_connect($db);

    $query = "SELECT prj_id, count(*) count FROM operations ".
        "where bill_id = $bill_id ".
        "group by prj_id ".
        "order by 2 desc, 1";

    $result = $db->query($query);

    if (DB::isError($result))
        return PHPAGA_ERR_DB_EXEC_QUERY;

    if ($result->numRows() < 1)
        return PHPAGA_ERR_DB_NO_ROWS;

    if ($row = $result->fetchRow())
        $prj_id = $row["prj_id"];

    $result->free();

    return PHPAGA_ERR_NONE;

}


/**
 * Gets an invoice's information
 *
 * @param int    $bill_id   Invoice ID
 * @param array  $billInfo  Invoice information
 *
 * @return int   $result  A result code.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_bills_getinfo ($bill_id, &$billInfo)
{

    $billInfo = array();

    phpaga_db_connect($db);

    $query = "SELECT bill_id, bill_number, bill_date, ".
        "bills.cpn_id, cpn_name, bill_sent, bill_payterm, ".
        "bill_paydate, bill_startsum, bill_endsum, bills.bmt_id, ".
        "bills.curr_id, curr_name, bmt_description ".
        "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 ".
        "WHERE bill_id = ".$db->quoteSmart($bill_id);

    $result = $db->query($query);

    if (DB::isError($result))
        return PHPAGA_ERR_DB_EXEC_QUERY;

    if (($result->numRows() > 0) && ($qrow = $result->fetchRow ()))
    {
        $billInfo = $qrow;

        $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 = ".$db->quoteSmart($billInfo["bill_id"]).
            "ORDER BY lit_id";

        $litres = $db->query($query);

        if (DB::isError($litres))
            return PHPAGA_ERR_DB_EXEC_QUERY;

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

        while ($qrow = $litres->fetchRow ()) {
            $qrow["lit_netprice_format"] = phpaga_formatamount((float)$qrow["lit_netprice"]);
            $billInfo["lineitems"][] = $qrow;
        }

        /* calculation detail */

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

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

        if ($plres != PHPAGA_ERR_NONE) {
            phpaga_errlog(sprintf("%s: %s line %s: %s",
                                  __FILE__,
                                  __FUNCTION__,
                                  __LINE__,
                                  phpaga_errortext($plres)));
            return $plres;
        }

        $endsum = 0;
        $bcres = phpaga_bill_calculate($billInfo["bill_startsum"],
                                       $billInfo["curr_id"], $endsum, $details);

        if ($bcres != PHPAGA_ERR_NONE) {
            phpaga_errlog(sprintf("%s: %s line %s: %s",
                                  __FILE__,
                                  __FUNCTION__,
                                  __LINE__,
                                  phpaga_errortext($bcres)));
            return $bcres;
        }

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

        /* associated tasks */

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

        if (($opres != PHPAGA_ERR_NONE) && ($opres != PHPAGA_ERR_DB_NO_ROWS)) {
            phpaga_errlog(sprintf("%s: %s line %s: %s",
                                  __FILE__,
                                  __FUNCTION__,
                                  __LINE__,
                                  phpaga_errortext($opres)));
            return $opres;
        }

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

    $result->free();
    return PHPAGA_ERR_NONE;
}

?>