diff --git a/modules/report/commerce_pos_report.module b/modules/report/commerce_pos_report.module index 4e73b29..a0bf19a 100644 --- a/modules/report/commerce_pos_report.module +++ b/modules/report/commerce_pos_report.module @@ -6,6 +6,7 @@ */ define('COMMERCE_POS_REPORT_JOURNAL_ROLE_DEFAULT_ITEMS_PER_PAGE', 25); +define('COMMERCE_POS_REPORT_SALES_REPORT_DEFAULT_ITEMS_PER_PAGE', 25); /** * Implements hook_menu(). @@ -17,7 +18,6 @@ function commerce_pos_report_menu() { 'file' => 'includes/commerce_pos_report.pages.inc', 'access arguments' => array('view commerce pos reports'), ); - $items['admin/commerce/pos/reports/end-of-day'] = array( 'title' => 'End of Day Report', 'page callback' => 'drupal_get_form', @@ -32,6 +32,13 @@ function commerce_pos_report_menu() { 'file' => 'includes/commerce_pos_report.pages.inc', 'access arguments' => array('view commerce pos reports'), ); + $items['admin/commerce/pos/reports/sales-report'] = array( + 'title' => 'Sales Report', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('commerce_pos_report_sales_report'), + 'file' => 'includes/commerce_pos_report.pages.inc', + 'access arguments' => array('view commerce pos reports'), + ); $items['admin/commerce/pos/end-of-day/print/%'] = array( 'title' => 'Print Transaction Receipt', 'page callback' => 'commerce_pos_report_receipt_print', diff --git a/modules/report/css/commerce_pos_report.css b/modules/report/css/commerce_pos_report.css index 3ec5fce..762cf1f 100644 --- a/modules/report/css/commerce_pos_report.css +++ b/modules/report/css/commerce_pos_report.css @@ -126,6 +126,22 @@ a:hover .commerce-pos-report-ico-journal-role { vertical-align: middle; } +.commerce-pos-report-ico-sales-report{ + position: relative; + bottom: 2px; + background: url(../../../images/sprite_icons.png) -56px 0; + width: 18px; + height: 20px; + display: inline-block; + vertical-align: middle; + margin-right: 10px; + +} +a.active .commerce-pos-report-ico-sales-report, +a:hover .commerce-pos-report-ico-sales-report { + background-position: -80px 0; +} + /* -- Report Filters -- */ #commerce-pos-report-journal-role-options-container { @@ -556,3 +572,62 @@ a:hover .commerce-pos-report-ico-journal-role { border-bottom: 2px solid #ddd; padding-top: 5px; } +.element-form-report{ + padding-bottom: 20px; +} + + /*Table theme for reports*/ +.report-table-theme table.sticky-enabled.tableheader-processed.sticky-table thead th { + border-bottom: 2px solid #ddd; + font-size: 16px; + text-transform: none; + text-align: center; + background: #fff; +} +.report-table-theme table.sticky-enabled thead th:first-of-type{ + text-align: left; +} +.report-table-theme table.sticky-enabled thead th.active{ + background: #fff; +} +.report-table-theme table.sticky-enabled thead th a { + color: #000; +} +.report-table-theme table.sticky-enabled thead th a:hover{ + text-decoration: none; + color: #337ab7; +} +.report-table-theme table.sticky-enabled thead th img { + position: relative; + bottom: 1px; + left: 6px; + top: 0; + right: 0; +} +.report-table-theme table.tableheader-processed.sticky-table tbody tr.even, .report-table-theme table.tableheader-processed.sticky-table tbody tr.odd{ + background-color: #f5f5f5; + color: #000; + border-top:1px solid #e2e2e2; + font-size: 14px; +} +.report-table-theme table.sticky-enabled.tableheader-processed.sticky-table tbody tr:last-of-type { + border-top: 2px solid #ddd; + font-weight: bold; +} +.report-table-theme table.tableheader-processed.sticky-table tbody tr:hover{ + background: #e5ecf2; +} +.report-table-theme table.sticky-enabled tbody td { + text-align: center; + border: 0; +} +.report-table-theme table.sticky-enabled tbody td:first-of-type{ + text-align: left; +} +.report-table-theme table.sticky-enabled tbody td.active { + background: transparent; + border-top: 1px solid #e2e2e2; +} +.report-table-theme table.sticky-enabled tbody td.empty{ + text-align: center; +} \ No newline at end of file diff --git a/modules/report/includes/commerce_pos_report.pages.inc b/modules/report/includes/commerce_pos_report.pages.inc index dcd405f..144270d 100644 --- a/modules/report/includes/commerce_pos_report.pages.inc +++ b/modules/report/includes/commerce_pos_report.pages.inc @@ -184,8 +184,8 @@ function commerce_pos_report_end_of_day($form, &$form_state) { if (isset($currency_totals['commerce_pos_change'])) { $change_amounts = &$currency_totals['commerce_pos_change']; - // The change amount reflects the change we GAVE BACK, so we have to add - // it to the expected amount of cash. + // The change amount reflects the change we GAVE BACK, so we have to + // add it to the expected amount of cash. $expected_amount += ($change_amounts[CommercePosService::TRANSACTION_TYPE_SALE] - $change_amounts[CommercePosService::TRANSACTION_TYPE_RETURN]); unset($change_amounts); } @@ -337,7 +337,8 @@ function commerce_pos_report_end_of_day($form, &$form_state) { * Get the reports for the specified day of a single register. * * @param string $date_filter - * The date range to filter by in compatible strtotime format. Will search this time +1day. + * The date range to filter by in compatible strtotime format. + * Will search this time +1day. * @param int $register_id * The id of the register to get totals for. * @@ -412,8 +413,11 @@ function commerce_pos_report_get_totals($date_filter, $register_id) { CommercePosService::TRANSACTION_TYPE_RETURN => 0, ); } - $method = &$totals[$row->commerce_order_total_currency_code][$row->payment_method]; - $method[$row->type] += $row->amount; + + if ($row->status == COMMERCE_PAYMENT_STATUS_SUCCESS) { + $method = &$totals[$row->commerce_order_total_currency_code][$row->payment_method]; + $method[$row->type] += $row->amount; + } if (in_array($row->status, $visible_statuses)) { if (!isset($transaction_counts[$row->payment_method])) { @@ -608,6 +612,7 @@ function commerce_pos_report_journal_role($form, &$form_state) { global $user; $form['#tree'] = TRUE; + $form['#attributes']['class'][] = 'element-form-report'; $form['header'] = array( '#markup' => theme('commerce_pos_header', array('account' => $user)), ); @@ -641,7 +646,7 @@ function commerce_pos_report_journal_role($form, &$form_state) { $form['#attached']['libraries_load'][] = array('jquery-print'); $form['#attached']['css'][] = drupal_get_path('module', 'commerce_pos') . '/css/commerce_pos_style.css'; $form['#attached']['css'][] = drupal_get_path('module', 'commerce_pos_report') . '/css/commerce_pos_report.css'; - $form['#attached']['js'][] = drupal_get_path('module', 'commerce_pos_report') . '/js/commerce_pos_report.journal_role.js'; + $form['#attached']['js'][] = drupal_get_path('module', 'commerce_pos_report') . '/js/commerce_pos_report.sorter.js'; $form['#attached']['js'][] = array( 'type' => 'setting', 'data' => $js_settings, @@ -742,6 +747,11 @@ function commerce_pos_report_journal_role($form, &$form_state) { $form['results'] = array( '#type' => 'container', '#id' => $form_state['results_container_id'], + '#attributes' => array( + 'class' => array( + 'report-table-theme', + ), + ), ); $form['results']['table'] = commerce_pos_report_build_journal_role_table($query_params); @@ -785,7 +795,6 @@ function commerce_pos_report_journal_role_submit($form, &$form_state) { * A render array for the result table. */ function commerce_pos_report_build_journal_role_table(array $filters = array()) { - $header = array( array('data' => t('Order No.'), 'field' => 't.order_id'), array('data' => t('Time'), 'field' => 't.completed'), @@ -942,7 +951,7 @@ function commerce_pos_report_build_journal_role_table(array $filters = array()) 'class' => array('commerce-pos-transaction-type-' . $row->type), ); - // Add another row right below that will serve as a placeholder for loading + // Add another row right below it will serve as a placeholder for loading // in order data. $rows[] = array( array( @@ -988,7 +997,7 @@ function commerce_pos_report_build_journal_role_table(array $filters = array()) '#header' => $header, '#attached' => array( 'js' => array( - drupal_get_path('module', 'commerce_pos_report') . '/js/commerce_pos_report.journal_role.js', + drupal_get_path('module', 'commerce_pos_report') . '/js/commerce_pos_report.sorter.js', ), 'library' => array( array('system', 'drupal.ajax'), @@ -1004,3 +1013,310 @@ function commerce_pos_report_build_journal_role_table(array $filters = array()) ); } } + +/** + * Builds a Sales Report result table. + * + * @param array $filters + * An array of filters for the query. Can include any of the following: + * 'cashier' - The cashier who performed the transactions. + * 'payment_type' - The type of payment the transactions had. + * 'results_per_page' - The number of results per page. + * + * @return array + * A render array for the result table. + */ +function commerce_pos_report_build_sales_report_table(array $filters = array()) { + $header = array( + array('data' => t('Cashier Id'), 'field' => 't.cashier'), + array('data' => t('Cashier'), 'field' => 'cashier.name'), + array('data' => t('Net Sales')), + array('data' => t('Transactions')), + array('data' => t('Items Sold')), + array('data' => t('Unit per Trans (UPT)')), + array('data' => t('$ Per Transaction (DPT)')), + ); + + $query = db_select('commerce_pos_transaction', 't') + ->extend('PagerDefault') + ->extend('TableSort'); + + $query->fields('t', array('order_id', 'completed', 'uid', 'type')); + $query->addField('t', 'cashier', 'cashier_id'); + + // Cashier data + // We left join because legacy transactions may not have a cashier. + $query->leftJoin('commerce_pos_cashier', 'cashier', 'cashier.cashier_id = t.cashier'); + $query->addField('cashier', 'name', 'cashier'); + + // Payment transaction data. + $query->join('commerce_payment_transaction', 'pt', 'pt.order_id = t.order_id'); + $query->fields('pt', array('transaction_id')); + + // Join order data. + $query->join('commerce_order', 'o', 'o.order_id = t.order_id'); + $query->leftJoin('users', 'customer', 'customer.uid = o.uid'); + $query->addField('customer', 'mail', 'customer_mail'); + + // Join order total data. + $query->join('field_data_commerce_order_total', 'ot', 't.order_id = ot.entity_id AND ot.entity_type = :commerce_order', array( + ':commerce_order' => 'commerce_order', + )); + $query->fields('ot', array( + 'commerce_order_total_amount', + 'commerce_order_total_currency_code', + 'commerce_order_total_data', + )); + + // Join line item data. + $query->join('commerce_line_item', 'l', 'l.order_id = t.order_id'); + $query->addField('l', 'quantity'); + + $line_item_types = commerce_product_line_item_types(); + $query->condition('l.type', $line_item_types, 'IN'); + + // Make sure line item count is aggregated. + $query->groupBy('l.order_id'); + + /* As mysql do not support limits in sub queries, create sub query + for pagination and to limit records use a separate query.*/ + $pagination_query = db_select('commerce_pos_cashier', 'cpc') + ->extend('PagerDefault') + ->extend('TableSort'); + + $pagination_query->fields('cpc', array('cashier_id')); + + if ($filters['results_per_page'] > 0) { + $pagination_query->limit($filters['results_per_page']); + } + + $limit_rows_result = $pagination_query->execute(); + if ($limit_rows_result->rowCount() > 0) { + foreach ($limit_rows_result as $fetchCashier) { + $cashiers_id[] = $fetchCashier->cashier_id; + } + } + $query->condition('cashier.cashier_id', $cashiers_id, 'IN'); + + // Set up our filters/conditions. + $query->orderByHeader($header); + $result = $query->execute(); + + $rows = array(); + $totals = array( + 'net_sales' => 0, + 'transactions' => 0, + 'item_sold' => 0, + 'unit_per_transaction' => 0, + 'dollar_per_transaction' => 0, + ); + + if ($result->rowCount() > 0) { + $report = sales_process_data($result); + foreach ($report as $cashier_transactions) { + $table_row = array(); + $transactions = $cashier_transactions['cashier']['transactions']; + $currency_code = $cashier_transactions['cashier']['currency_code']; + $net_sales = $cashier_transactions['transaction']['net_sales']; + $net_sold_items = ($cashier_transactions['transaction']['purchase_count'] - $cashier_transactions['transaction']['return_count']); + $unit_per_transaction = round(($net_sold_items / $transactions), 2); + $dollar_per_transaction = ($net_sales / $transactions); + $table_row[] = $cashier_transactions['cashier']['id']; + $table_row[] = $cashier_transactions['cashier']['name']; + $table_row[] = commerce_currency_format($net_sales, $currency_code); + $table_row[] = $transactions; + $table_row[] = $net_sold_items; + // Unit per Trans (UPT) + $table_row[] = $unit_per_transaction; + $table_row[] = commerce_currency_format($dollar_per_transaction, $currency_code); + // Table feet with total. + $totals['net_sales'] += $net_sales; + $totals['transactions'] += $transactions; + $totals['item_sold'] += $net_sold_items; + $totals['unit_per_transaction'] += $unit_per_transaction; + $totals['dollar_per_transaction'] += $dollar_per_transaction; + $rows[] = array( + 'data' => $table_row, + 'class' => array(), + ); + } + + $rows[] = array( + '', + '', + commerce_currency_format($totals['net_sales'], $currency_code), + $totals['transactions'], + $totals['item_sold'], + $totals['unit_per_transaction'], + commerce_currency_format($totals['dollar_per_transaction'], $currency_code), + ); + } + $table = array( + '#theme' => 'table', + '#rows' => $rows, + '#header' => $header, + '#empty' => t('No transactions found'), + '#attached' => array( + 'js' => array( + drupal_get_path('module', 'commerce_pos_report') . '/js/commerce_pos_report.sorter.js', + ), + 'library' => array( + array('system', 'drupal.ajax'), + ), + ), + ); + return $table; +} + +/** + * Callback for the Sales form/report. + */ +function commerce_pos_report_sales_report($form, &$form_state) { + global $user; + $form['#tree'] = TRUE; + $form['#attributes']['class'][] = 'element-form-report'; + $form['header'] = array( + '#markup' => theme('commerce_pos_header', array('account' => $user)), + ); + + $query_filter_defaults = array( + 'results_per_page' => COMMERCE_POS_REPORT_SALES_REPORT_DEFAULT_ITEMS_PER_PAGE, + ); + + $query_params = drupal_get_query_parameters() + $query_filter_defaults; + + $form['filters'] = array( + '#type' => 'container', + ); + + if (!isset($form_state['results_container_id'])) { + $form_state['results_container_id'] = 'commerce-pos-report-sale-container'; + } + + $js_settings = array( + 'commercePosReport' => array( + 'cssUrl' => url(drupal_get_path('module', 'commerce_pos_report') . '/css/commerce_pos_report_receipt.css', array( + 'absolute' => TRUE, + )), + ), + ); + + $form['#attached']['libraries_load'][] = array('jquery-print'); + $form['#attached']['css'][] = drupal_get_path('module', 'commerce_pos') . '/css/commerce_pos_style.css'; + $form['#attached']['css'][] = drupal_get_path('module', 'commerce_pos_report') . '/css/commerce_pos_report.css'; + $form['#attached']['js'][] = drupal_get_path('module', 'commerce_pos_report') . '/js/commerce_pos_report.sorter.js'; + $form['#attached']['js'][] = array( + 'type' => 'setting', + 'data' => $js_settings, + ); + $form['report-options'] = array( + '#theme' => 'commerce_pos_report_options', + ); + $form['options'] = array( + '#type' => 'container', + '#id' => 'commerce-pos-report-journal-role-options-container', + ); + $form['options']['results_per_page'] = array( + '#type' => 'select', + '#title' => t('Items per page'), + '#options' => array( + 25 => '25', + 50 => '50', + 100 => '100', + 500 => '500', + -1 => t('all'), + ), + '#default_value' => $query_params['results_per_page'], + '#attributes' => array( + 'class' => array('commerce-pos-report-journal-role-filter'), + ), + ); + $form['results'] = array( + '#type' => 'container', + '#id' => $form_state['results_container_id'], + '#attributes' => array( + 'class' => array( + 'report-table-theme', + ), + ), + ); + $form['results']['table'] = commerce_pos_report_build_sales_report_table($query_params); + // Pager has to come after we've build the journal role table. + $form['options']['pager'] = array('#theme' => 'pager'); + $form['results']['pager'] = array('#theme' => 'pager'); + + $form['submit'] = array( + '#value' => t('Submit'), + '#type' => 'submit', + '#weight' => -50, + '#attributes' => array( + 'class' => array('element-invisible', 'commerce-pos-report-journal-role-submit'), + ), + ); + + return $form; +} + +/** + * Submit handler for the journal role report form. + */ +function commerce_pos_report_sales_report_submit($form, &$form_state) { + $query_params = array_merge(drupal_get_query_parameters(), $form_state['values']['options']); + $form_state['redirect'] = array(current_path(), array('query' => $query_params)); +} + +/** + * Helper function for Sales table to process data. + */ +function sales_process_data($result) { + $table_row = array(); + foreach ($result as $row) { + $return_count = 0; + $purchase_count = 0; + $transactions = 1; + $cashier_id = $row->cashier_id; + + if (!isset($table_row[$cashier_id])) { + $table_row[$cashier_id] = array( + 'transaction' => array( + 'net_sales' => 0, + 'purchase_count' => 0, + 'return_count' => 0, + ), + 'cashier' => array( + 'transactions' => 0, + ), + ); + } + + switch ($row->type) { + case CommercePosService::TRANSACTION_TYPE_RETURN: + $return_count = $row->quantity; + // Order total. + $table_row[$cashier_id]['transaction']['net_sales'] -= $row->commerce_order_total_amount; + break; + + default: + $purchase_count = $row->quantity; + // Order total. + $table_row[$cashier_id]['transaction']['net_sales'] += $row->commerce_order_total_amount; + break; + } + + // Build order ID link. + $table_row[$cashier_id]['cashier']['id'] = $cashier_id; + // Cashier. + $table_row[$cashier_id]['cashier']['name'] = $row->cashier; + // Number of transactions. + $table_row[$cashier_id]['cashier']['transactions'] += $transactions; + // Purchased. + $table_row[$cashier_id]['transaction']['purchase_count'] += $purchase_count; + // Returned. + $table_row[$cashier_id]['transaction']['return_count'] += $return_count; + + // Discount flag. + $table_row[$cashier_id]['cashier']['currency_code'] = $row->commerce_order_total_currency_code; + + } + return $table_row; +} diff --git a/modules/report/js/commerce_pos_report.journal_role.js b/modules/report/js/commerce_pos_report.sorter.js similarity index 100% rename from modules/report/js/commerce_pos_report.journal_role.js rename to modules/report/js/commerce_pos_report.sorter.js diff --git a/modules/report/theme/commerce_pos_report.theme.inc b/modules/report/theme/commerce_pos_report.theme.inc index 5e71f5a..c6f342d 100644 --- a/modules/report/theme/commerce_pos_report.theme.inc +++ b/modules/report/theme/commerce_pos_report.theme.inc @@ -53,6 +53,10 @@ function theme_commerce_pos_report_options(&$variables) { 'attributes' => array('class' => array('commerce-pos-journal-role')), 'html' => TRUE, )), + l('' . t('Sales Report'), 'admin/commerce/pos/reports/sales-report', array( + 'attributes' => array('class' => array('commerce-pos-sales-report')), + 'html' => TRUE, + )), ), 'attributes' => array( 'class' => array('commerce-pos-report-options'), diff --git a/theme/commerce_pos.theme.inc b/theme/commerce_pos.theme.inc index 8206da7..6f7cd5d 100644 --- a/theme/commerce_pos.theme.inc +++ b/theme/commerce_pos.theme.inc @@ -139,19 +139,24 @@ function theme_commerce_pos_order_balance_summary(&$variables) { $rows = array(); $totals = array(); foreach ($transactions as $transaction) { + $payment_action = ""; $payment_method = commerce_payment_method_load($transaction->payment_method); $status = $transaction->status; $currency_code = $transaction->currency_code; $amount = $transaction->amount; + if ($status === COMMERCE_PAYMENT_STATUS_SUCCESS) { + $payment_action = commerce_currency_format($amount, $currency_code) . '
' . theme('commerce_pos_transaction_actions', array('transaction' => $transaction)); + $action_class = "amount"; + } + elseif ($status == 'void') { + $payment_action = t('VOID'); + $action_class = "transaction-voided"; + } + // If a payment transaction doesn't count toward the total paid, show its // status in the title. $title = $payment_method['title']; - $formatted_amount = commerce_currency_format($amount, $currency_code); - if (!empty($transaction_statuses[$status]) && !$transaction_statuses[$status]['total']) { - $title .= ' - ' . $transaction_statuses[$status]['title']; - $formatted_amount = '' . $formatted_amount . ''; - } $title = '
' . $title . '
'; // Omit buttons from this summary in reports. @@ -161,8 +166,9 @@ function theme_commerce_pos_order_balance_summary(&$variables) { } // Add a separator between remote ID and actions, if there are actions. - $remote_id = !empty($actions) ? ' - ' . $transaction->remote_id : $transaction->remote_id; - + if (isset($transaction->remote_id)) { + $remote_id = !empty($actions) ? ' - ' . $transaction->remote_id : $transaction->remote_id; + } // Don't add any markup if the transaction doesn't have a remote ID. $remote_id = $remote_id ? '' . $remote_id . '' : ''; @@ -174,15 +180,15 @@ function theme_commerce_pos_order_balance_summary(&$variables) { $row = array( 'data' => array( array( - 'data' => $title . $actions . $remote_id, + 'data' => $title . $remote_id, 'class' => $row_classes, ), ), ); $row['data'][] = array( - 'data' => $formatted_amount, - 'class' => array('amount'), + 'data' => $payment_action, + 'class' => array($action_class), ); $rows[] = $row; @@ -237,7 +243,6 @@ function theme_commerce_pos_transaction_actions($variables) { 'class' => array( 'commerce-pos-' . strtolower($action) . '-payment', 'button-link', - 'small', ), 'data-transaction-id' => $transaction->transaction_id, ),