diff --git a/application/helpers/pdf_helper.php b/application/helpers/pdf_helper.php index 327d44a02..3e2601820 100644 --- a/application/helpers/pdf_helper.php +++ b/application/helpers/pdf_helper.php @@ -258,3 +258,41 @@ function generate_quote_pdf($quote_id, $stream = true, $quote_template = null) return pdf_create($html, trans('quote') . '_' . str_replace(array('\\', '/'), '_', $quote->quote_number), $stream, $quote->quote_password); } + +/** + * Generate the PDF for the statement + * + * @param Mdl_Clients $client + * @param Mdl_Statement $statement + * @param $notes + * + * @return string + * @throws \Mpdf\MpdfException + */ +function generate_statement_pdf($client, $statement, $notes) +{ + $CI = &get_instance(); + + // Override language with system language + set_language($client->client_language); + + $statement_template = "InvoicePlane"; + if (!$statement_template) { + $statement_template = $CI->mdl_settings->setting('pdf_statement_template'); + } + + $data = array( + 'client' => $client, + 'statement' => $statement, + 'notes' => $notes, + ); + + $html = $CI->load->view('statement_templates/pdf/' . $statement_template, $data, true); + + $CI->load->helper('mpdf'); + + $pdf_password = null; + $stream = true; + + return pdf_create($html, trans('statement') . '_' . str_replace(array('\\', '/'), '_', $statement->GetStatement_number()), $stream, $pdf_password); +} diff --git a/application/language/english/ip_lang.php b/application/language/english/ip_lang.php index 705638552..6f70461aa 100644 --- a/application/language/english/ip_lang.php +++ b/application/language/english/ip_lang.php @@ -260,6 +260,7 @@ 'invoice_items' => 'Invoice Items', 'invoice_logo' => 'Invoice Logo', 'invoice_not_found' => 'Invoice Not Found', + 'invoice_number' => 'Invoice Number', 'invoice_overview' => 'Invoice Overview', 'invoice_overview_period' => 'Invoice Overview Period', 'invoice_password' => 'PDF password (optional)', @@ -272,6 +273,7 @@ 'invoice_tax_rate' => 'Invoice Tax Rate', 'invoice_template' => 'Invoice Template', 'invoice_terms' => 'Invoice Terms', + 'invoice_total' => 'Invoice Total', 'invoiced' => 'Invoiced', 'invoiceplane_news' => 'InvoicePlane News', 'invoices' => 'Invoices', @@ -436,6 +438,7 @@ 'record_successfully_updated' => 'Record successfully updated', 'recurring' => 'Recurring', 'recurring_invoices' => 'Recurring Invoices', + 'reference' => 'Reference', 'reject' => 'Reject', 'reject_this_quote' => 'Reject This Quote', 'rejected' => 'Rejected', @@ -512,6 +515,11 @@ 'sql_file' => 'SQL File', 'start_date' => 'Start Date', 'state' => 'State', + 'statement' => 'Statement', + 'statement_date' => 'Statement Date', + 'statement_end_date' => 'Statement End Date', + 'statement_number' => 'Statement Number', + 'statement_start_date' => 'Statement Start Date', 'status' => 'Status', 'stop' => 'Stop', 'street_address' => 'Street Address', @@ -550,6 +558,7 @@ 'total_balance' => 'Total Balance', 'total_billed' => 'Total Billed', 'total_paid' => 'Total Paid', + 'transaction_type' => 'Transaction Type', 'try_again' => 'Try Again', 'type' => 'Type', 'unknown' => 'Unknown', diff --git a/application/modules/clients/views/partial_client_table.php b/application/modules/clients/views/partial_client_table.php index a82aec30c..d278a206a 100644 --- a/application/modules/clients/views/partial_client_table.php +++ b/application/modules/clients/views/partial_client_table.php @@ -56,6 +56,11 @@ +
  • + + + +
  • diff --git a/application/modules/invoices/models/Mdl_invoices.php b/application/modules/invoices/models/Mdl_invoices.php index 57d7d360a..739c3bc36 100644 --- a/application/modules/invoices/models/Mdl_invoices.php +++ b/application/modules/invoices/models/Mdl_invoices.php @@ -526,6 +526,27 @@ public function by_client($client_id) return $this; } + /** + * Filter query in a date range. + * The filter can be open ended on one end by not supplied a value + * Dates must be in unixtime format + * + * @param time $start_date + * @param time $end_date + * @return Mdl_Invoices + */ + public function by_date_range($start_date = null, $end_date = null) + { + if (!empty($start_date)) { + $this->filter_where("invoice_date_created >= '" . $start_date . "' "); + } + if (!empty($end_date)) { + $this->filter_where("invoice_date_created <= '" . $end_date . "' "); + } + + return $this; + } + /** * @param $invoice_id */ diff --git a/application/modules/payments/models/Mdl_payments.php b/application/modules/payments/models/Mdl_payments.php index 4c8323d45..76f5aff46 100755 --- a/application/modules/payments/models/Mdl_payments.php +++ b/application/modules/payments/models/Mdl_payments.php @@ -223,4 +223,25 @@ public function by_client($client_id) return $this; } + /** + * Filter query in a date range. + * The filter can be open ended on one end by not supplied a value + * Dates must be in unixtime format + * + * @param time $start_date + * @param time $end_date + * @return Mdl_Payments + */ + public function by_date_range($start_date = null, $end_date = null) + { + if (!empty($start_date)) { + $this->filter_where("payment_date >= '" . $start_date . "' "); + } + if (!empty($end_date)) { + $this->filter_where("payment_date <= '" . $end_date . "' "); + } + + return $this; + } + } diff --git a/application/modules/statements/controllers/Statements.php b/application/modules/statements/controllers/Statements.php new file mode 100644 index 000000000..aace9bf55 --- /dev/null +++ b/application/modules/statements/controllers/Statements.php @@ -0,0 +1,376 @@ +load->model('clients/mdl_clients'); + } + + public function index($client_id) + { + $this->view($client_id); + } + + /** + * @param $client_id + * + */ + public function view($client_id) + { + $this->load->model('custom_fields/mdl_client_custom'); + + /* + * Load the client + */ + $client = $this->mdl_clients + ->where('ip_clients.client_id', $client_id) + ->get()->row(); + + if (!$client) { + show_404(); + } + + $custom_fields = $this->mdl_client_custom->get_by_client($client_id)->result(); + + $this->mdl_client_custom->prep_form($client_id); + + if($this->input->method() === 'post') + { + + if (!empty($this->input->post('statement_start_date'))) { + // BUG : strtotime is not recognising the date format d M,Y" and changing the date + // $statement_start_date = strtotime($this->input->post('statement_start_date')); + $date_time = date_create_from_format("d M,Y", $this->input->post('statement_start_date')); + $statement_start_date = $date_time->getTimestamp(); + + } else { + $statement_start_date = strtotime($this->input->post('sdate')); + } + $statement_end_date = $this->input->post('edate'); + + // BUG : strtotime is not recognising the date format d M,Y" and changing the date + // $statement_date = strtotime($this->input->post('statement_date_created')); + $date_time = date_create_from_format("d M,Y", $this->input->post('statement_date_created')); + $statement_date = $date_time->getTimestamp(); + + $statement_number = $this->input->post('statement_number'); + $notes = $this->input->post('notes'); + + } else { + + $statement_start_date = null; + $statement_end_date = null; + + $statement_number = null; + $statement_date = null; + $notes = null; + + } + + $statement = $this->build_statement($client_id, $statement_start_date, $statement_end_date, $statement_date, $statement_number); + + $this->layout->set( + array( + + 'client' => $client, + 'statement_start_date' => $statement->getStatement_start_date(), + 'statement_end_date' => $statement->getStatement_end_date(), + 'statement_date' => $statement->getStatement_date(), + 'custom_fields' => $custom_fields, + 'statement_transactions' => $statement->getStatement_transactions(), + 'opening_balance' => $statement->getOpening_balance(), + 'client_total_balance' => $statement->getStatement_balance(), + 'statement_number' => $statement->getStatement_number(), + + ) + ); + + $this->layout->buffer( + array( + array('content', 'statements/view') + ) + ); + + $this->layout->render(); + + } + + /** + * Populate the Statement model + * + * + * @param Mdl_Clients $client + * @param $statement_start_date + * @param $statement_end_date + * @param $statement_date, + * @param $statement_number + * + */ + private function build_statement($client_id, $statement_start_date = null, $statement_end_date = null, $statement_date = null, $statement_number = null) + { + $this->load->model('mdl_statement'); + + $this->load->model('invoices/mdl_invoices'); + $this->load->model('payments/mdl_payments'); + + + /* + * Use the user supplied start date, or set the start date to a month ago + */ + if (empty($statement_start_date)) { + $statement_start_date = strtotime("-1 month"); + } + + /* + * Use the user supplied end date, or draw the statement up to now + */ + if (empty($statement_end_date)) { + $statement_end_date = time(); + } + + /* + * Use the user supplied statament date, or use the current date + */ + if (empty($statement_date)) { + $statement_date = time(); + } + + /* + * Create the statement number based on the client id and date, or overwrite it with the user value. + */ + if (!empty($statement_number)) { + $this->mdl_statement->setStatement_number($statement_number); + } else { + $this->mdl_statement->setStatement_number( 'STM-' . $client_id . '-' . date('ymd')); + } + + /* + * Set the statement date to now, or overwrite it with the user value. + */ + $this->mdl_statement->setStatement_date($statement_date); + + + /* + * Calculate the opening statement as from the start of the user account to the start of the statement period + */ + $opening_balance_start_date = null; + $opening_balance_end_date = $statement_start_date; + + + $client_invoices = $this->mdl_invoices + ->by_client($client_id) + ->by_date_range(date('Y-m-d', $opening_balance_start_date), date('Y-m-d', $opening_balance_end_date)) + ->get() + ->result(); + $client_payments = $this->mdl_payments + ->by_client($client_id) + ->by_date_range(date('Y-m-d', $opening_balance_start_date), date('Y-m-d', $opening_balance_end_date)) + ->get() + ->result(); + + + $client_invoice_total = 0; + foreach ($client_invoices as $invoice_entry) { + $client_invoice_total += $invoice_entry->invoice_total; + } + + $client_payment_total = 0; + foreach ($client_payments as $payment_entry) { + $client_payment_total += $payment_entry->payment_amount; + } + + $client_opening_balance = $client_invoice_total - $client_payment_total; + + $this->mdl_statement->setOpening_balance($client_opening_balance); + + + $this->mdl_statement->setStatement_start_date($statement_start_date); + $this->mdl_statement->setStatement_end_date($statement_end_date); + + /* + * NOTE: These two calls brings back all invoices and payments over the + * ...date range, and we manually sum up the totals + */ + + $client_invoices = $this->mdl_invoices + ->by_client($client_id) + ->by_date_range(date('Y-m-d', $statement_start_date), date('Y-m-d', $statement_end_date)) + ->get() + ->result(); + $client_payments = $this->mdl_payments + ->by_client($client_id) + ->by_date_range(date('Y-m-d', $statement_start_date), date('Y-m-d', $statement_end_date)) + ->get() + ->result(); + + + $statement_transactions = array(); + $client_total_balance = $client_opening_balance; + foreach ($client_invoices as $invoice_entry) { + + $transaction = [ + 'transaction_type' => self::TRANSACTION_TYPE_INVOICE, + 'transaction_date' => $invoice_entry->invoice_date_created, + 'transaction_amount' => $invoice_entry->invoice_total, + + 'invoice_id' => $invoice_entry->invoice_id, + 'client_id' => $invoice_entry->client_id, + 'user_company' => $invoice_entry->user_company, + 'invoice_amount_id' => $invoice_entry->invoice_amount_id, + 'invoice_item_subtotal' => $invoice_entry->invoice_item_subtotal, + 'invoice_item_tax_total' => $invoice_entry->invoice_item_tax_total, + 'invoice_total' => $invoice_entry->invoice_total, + 'invoice_sign' => $invoice_entry->invoice_sign, + 'invoice_status_id' => $invoice_entry->invoice_status_id, + 'invoice_date_created' => $invoice_entry->invoice_date_created, + 'invoice_time_created' => $invoice_entry->invoice_time_created, + 'invoice_number' => $invoice_entry->invoice_number, + ]; + + $client_total_balance += $invoice_entry->invoice_total; + + $statement_transactions[] = $transaction; + + } + + foreach ($client_payments as $payment_entry) { + + $transaction = [ + 'transaction_type' => self::TRANSACTION_TYPE_PAYMENT, + 'transaction_date' => $payment_entry->payment_date, + 'transaction_amount' => $payment_entry->payment_amount, + + + 'invoice_id' => $payment_entry->invoice_id, + 'client_id' => $payment_entry->client_id, + 'invoice_date_created' => $payment_entry->invoice_date_created, + 'invoice_item_subtotal' => $payment_entry->invoice_item_subtotal, + 'invoice_item_tax_total' => $payment_entry->invoice_item_tax_total, + 'invoice_total' => $payment_entry->invoice_total, + 'invoice_sign' => $payment_entry->invoice_sign, + 'invoice_number' => $payment_entry->invoice_number, + 'payment_id' => $payment_entry->payment_id, + 'payment_method_id' => $payment_entry->payment_method_id, + 'payment_method_name' => $payment_entry->payment_method_name, + 'payment_date' => $payment_entry->payment_date, + 'payment_amount' => $payment_entry->payment_amount, + + ]; + + $statement_transactions[] = $transaction; + + $client_total_balance -= $payment_entry->payment_amount; + + } + + usort($statement_transactions, array($this, "compare_statement_dates")); + + $this->mdl_statement->setStatement_transactions($statement_transactions); + + $this->mdl_statement->setStatement_balance($client_total_balance); + + return $this->mdl_statement; + + } + + + /** + * Controller action to print pdf. From POST action. + * + */ + public function generate_pdf() + { + $this->load->model('clients/mdl_clients'); + $this->load->helper('country'); + + $client_id = $this->input->post('cid'); + $statement_number = $this->input->post('statement_number'); + + if (!empty($this->input->post('statement_start_date'))) { + //$statement_start_date = strtotime($this->input->post('statement_start_date')); + $date_time = date_create_from_format("d M,Y", $this->input->post('statement_start_date')); + $statement_start_date = $date_time->getTimestamp(); + + } else { + $statement_start_date = strtotime($this->input->post('sdate')); + } + $statement_end_date = strtotime($this->input->post('edate')); + + // BUG : strtotime is not recognising the date format d M,Y" and changing the date + // $statement_date = strtotime($this->input->post('statement_date_created')); + $date_time = date_create_from_format("d M,Y", $this->input->post('statement_date_created')); + $statement_date = $date_time->getTimestamp(); + + + $notes = $this->input->post('notes'); + + /* + * Load the client + */ + $client = $this->mdl_clients + ->where('ip_clients.client_id', $client_id) + ->get()->row(); + + if (!$client) { + show_404(); + } + + $statement = $this->build_statement($client->client_id, $statement_start_date, $statement_end_date); + + $this->load->helper('pdf'); + + generate_statement_pdf($client, $statement, $notes); + } + + + + /** + * Compare 2 dates + * + * NOTE: I am not sure if $this is the correct scope for this function. + * + * @param string $a The first date in string format + * @param string $b The second date in string format + * @return number + * 0 is the dates are the same + * 1 if date A > date B + * -1 if date A < date B + */ + private function compare_statement_dates($a, $b) + { + $timeA = strtotime($a['transaction_date']); + $timeB = strtotime($b['transaction_date']); + + if($timeA == $timeB) { + return 0; + } + + return $timeA < $timeB ? -1 : 1; + } + +} diff --git a/application/modules/statements/models/Mdl_statement.php b/application/modules/statements/models/Mdl_statement.php new file mode 100644 index 000000000..ba9a7b958 --- /dev/null +++ b/application/modules/statements/models/Mdl_statement.php @@ -0,0 +1,150 @@ +statement_number; + } + + /** + * @return mixed + */ + public function getStatement_transactions() + { + return $this->statement_transactions; + } + + /** + * @return mixed + */ + public function getStatement_start_date() + { + return $this->statement_start_date; + } + + /** + * @return mixed + */ + public function getStatement_end_date() + { + return $this->statement_end_date; + } + + /** + * @return mixed + */ + public function getStatement_date() + { + return $this->statement_date; + } + + /** + * @return mixed + */ + public function getOpening_balance() + { + return $this->opening_balance; + } + + /** + * @return mixed + */ + public function getStatement_balance() + { + return $this->statement_balance; + } + + /** + * @param mixed $statement_number + */ + public function setStatement_number($statement_number) + { + $this->statement_number = $statement_number; + } + + /** + * @param mixed $statement_transactions + */ + public function setStatement_transactions($statement_transactions) + { + $this->statement_transactions = $statement_transactions; + } + + /** + * @param mixed $statement_start_date + */ + public function setStatement_start_date($statement_start_date) + { + $this->statement_start_date = $statement_start_date; + } + + /** + * @param mixed $statement_end_date + */ + public function setStatement_end_date($statement_end_date) + { + $this->statement_end_date = $statement_end_date; + } + + /** + * @param mixed $statement_date + */ + public function setStatement_date($statement_date) + { + $this->statement_date = $statement_date; + } + + /** + * @param mixed $opening_balance + */ + public function setOpening_balance($opening_balance) + { + $this->opening_balance = $opening_balance; + } + + /** + * @param mixed $statement_balance + */ + public function setStatement_balance($statement_balance) + { + $this->statement_balance = $statement_balance; + } +} diff --git a/application/modules/statements/views/index.php b/application/modules/statements/views/index.php new file mode 100644 index 000000000..d71c1d11e --- /dev/null +++ b/application/modules/statements/views/index.php @@ -0,0 +1,103 @@ +
    + +

    + +
    + + + + +
    + +
    + uri->segment(3)), 'mdl_quotes'); ?> +
    + +
    + +
    + +
    + + + +
    + +
    + layout->load_view('quotes/partial_quote_table', array('quotes' => $quotes)); ?> +
    + +
    diff --git a/application/modules/statements/views/partial_item_table.php b/application/modules/statements/views/partial_item_table.php new file mode 100644 index 000000000..5ee19def8 --- /dev/null +++ b/application/modules/statements/views/partial_item_table.php @@ -0,0 +1,106 @@ + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + +
    + + + + + + + + + +
    +
    +
    + +
    + + +
    + + + +
    + + + + + +
    +
    + +
    diff --git a/application/modules/statements/views/statement_templates/pdf/.gitignore b/application/modules/statements/views/statement_templates/pdf/.gitignore new file mode 100644 index 000000000..193ab4cca --- /dev/null +++ b/application/modules/statements/views/statement_templates/pdf/.gitignore @@ -0,0 +1,3 @@ +* +!InvoicePlane.php +!.gitignore diff --git a/application/modules/statements/views/statement_templates/pdf/InvoicePlane.php b/application/modules/statements/views/statement_templates/pdf/InvoicePlane.php new file mode 100644 index 000000000..1a52ca7f2 --- /dev/null +++ b/application/modules/statements/views/statement_templates/pdf/InvoicePlane.php @@ -0,0 +1,191 @@ + + + + + + <?php _trans('statement') . $statement->getStatement_number(); ?> + + + + +
    + + + +
    +
    + client_name); ?> +
    + client_vat_id) { + echo '
    ' . trans('vat_id_short') . ': ' . $client->client_vat_id . '
    '; + } + if ($client->client_tax_code) { + echo '
    ' . trans('tax_code_short') . ': ' . $client->client_tax_code . '
    '; + } + if ($client->client_address_1) { + echo '
    ' . htmlsc($client->client_address_1) . '
    '; + } + if ($client->client_address_2) { + echo '
    ' . htmlsc($client->client_address_2) . '
    '; + } + if ($client->client_city || $client->client_state || $client->client_zip) { + echo '
    '; + if ($client->client_city) { + echo htmlsc($client->client_city) . ' '; + } + if ($client->client_state) { + echo htmlsc($client->client_state) . ' '; + } + if ($client->client_zip) { + echo htmlsc($client->client_zip); + } + echo '
    '; + } + if ($client->client_state) { + echo '
    ' . htmlsc($client->client_state) . '
    '; + } + if ($client->client_country) { + echo '
    ' . get_country_name(trans('cldr'), $client->client_country) . '
    '; + } + + echo '
    '; + + if ($client->client_phone) { + echo '
    ' . trans('phone_abbr') . ': ' . htmlsc($client->client_phone) . '
    '; + } ?> + +
    + +
    + +
    + +
    + + + + + + + + + + + + + + +
    getStatement_date()); ?>
    getStatement_start_date()); ?>
    getStatement_end_date()); ?>
    +
    + +

    getStatement_number(); ?>

    + + + + + + + + + + + + + + + + getOpening_balance())) { + ?> + + + + + + + + + + + + + GetStatement_transactions() as $transaction) { + + + if ($transaction['transaction_type'] == Statements::TRANSACTION_TYPE_INVOICE) { + $balance += $transaction['transaction_amount']; + } else { + $balance -= $transaction['transaction_amount']; + } + ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + GetStatement_start_date(); ?> + + + + + + + + getOpening_balance()); ?> +
    + + + + + + + + + +
     
    getStatement_balance()); ?>
     
    + +
    + + + + + diff --git a/application/modules/statements/views/statement_templates/public/.gitignore b/application/modules/statements/views/statement_templates/public/.gitignore new file mode 100644 index 000000000..8a8e758a3 --- /dev/null +++ b/application/modules/statements/views/statement_templates/public/.gitignore @@ -0,0 +1,3 @@ +* +!InvoicePlane_Web.php +!.gitignore diff --git a/application/modules/statements/views/view.php b/application/modules/statements/views/view.php new file mode 100644 index 000000000..17bfcc2a1 --- /dev/null +++ b/application/modules/statements/views/view.php @@ -0,0 +1,172 @@ + +
    +

    + +

    + +
    + +
    + + layout->load_view('layout/alerts'); ?> + +
    + + + + + +
    +
    + +
    +
    + +

    + + + +

    +
    +
    + layout->load_view('clients/partial_client_address', ['client' => $client]); ?> +
    + client_phone || $client->client_email) : ?> +
    + + client_phone): ?> +
    + :  + client_phone); ?> +
    + + client_email): ?> +
    + :  + client_email); ?> +
    + + +
    + +
    +
    +
    + +
    +
    +
    + +
    + +
    + +
    + +
    + +
    + + +
    +
    + +
    + +
    +
    + +
    + +
    + + + +
    +
    + +
    + +
    + + +
    +
    + + + +
    +
    + +
    + +
    + +
    +
    +
    +
    + +
    + + layout->load_view('statements/partial_item_table'); ?> + +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +