diff --git a/README.md b/README.md index df83d0e97094..1fb6bfd09232 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ ## [Hosted](https://www.invoiceninja.com) | [Self-hosted](https://www.invoiceninja.org) +**We're starting work on custom modules, join the discussion [here](https://github.com/invoiceninja/invoiceninja/issues/1131).** + All Pro and Enterprise features from our hosted app are included in both the [self-host zip](https://www.invoiceninja.com/self-host/) and the GitHub repository. Our [iPhone app](https://itunes.apple.com/WebObjects/MZStore.woa/wa/viewSoftware?id=1072566815&mt=8) is now available, our Android app is up next... diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index eaa3de652960..dc08c1af9d8d 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -429,6 +429,7 @@ class AccountController extends BaseController 'currencies' => Cache::get('currencies'), 'title' => trans('texts.localization'), 'weekdays' => Utils::getTranslatedWeekdayNames(), + 'months' => Utils::getMonthOptions(), ]; return View::make('accounts.localization', $data); @@ -674,11 +675,9 @@ class AccountController extends BaseController * @param $section * @return \Illuminate\Http\RedirectResponse */ - public function doSection($section = ACCOUNT_COMPANY_DETAILS) + public function doSection($section) { - if ($section === ACCOUNT_COMPANY_DETAILS) { - return AccountController::saveDetails(); - } elseif ($section === ACCOUNT_LOCALIZATION) { + if ($section === ACCOUNT_LOCALIZATION) { return AccountController::saveLocalization(); } elseif ($section == ACCOUNT_PAYMENTS) { return self::saveOnlinePayments(); @@ -1225,6 +1224,7 @@ class AccountController extends BaseController $account->military_time = Input::get('military_time') ? true : false; $account->show_currency_code = Input::get('show_currency_code') ? true : false; $account->start_of_week = Input::get('start_of_week') ? Input::get('start_of_week') : 0; + $account->financial_year_start = Input::get('financial_year_start') ? Input::get('financial_year_start') : null; $account->save(); event(new UserSettingsChanged()); diff --git a/app/Http/Controllers/ClientPortalController.php b/app/Http/Controllers/ClientPortalController.php index ca7f94414f45..f7f6798d0cbc 100644 --- a/app/Http/Controllers/ClientPortalController.php +++ b/app/Http/Controllers/ClientPortalController.php @@ -211,6 +211,8 @@ class ClientPortalController extends BaseController $client = $contact->client; $account = $client->account; + $account->loadLocalizationSettings($client); + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $customer = false; @@ -277,6 +279,7 @@ class ClientPortalController extends BaseController } $account = $contact->account; + $account->loadLocalizationSettings($contact->client); if (!$account->enable_client_portal) { return $this->returnError(); @@ -304,6 +307,7 @@ class ClientPortalController extends BaseController } $account = $contact->account; + $account->loadLocalizationSettings($contact->client); if (!$account->enable_client_portal) { return $this->returnError(); @@ -350,12 +354,14 @@ class ClientPortalController extends BaseController } $account = $contact->account; + $account->loadLocalizationSettings($contact->client); if (!$account->enable_client_portal) { return $this->returnError(); } $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + $data = [ 'color' => $color, 'account' => $account, @@ -420,12 +426,14 @@ class ClientPortalController extends BaseController } $account = $contact->account; + $account->loadLocalizationSettings($contact->client); if (!$account->enable_client_portal) { return $this->returnError(); } $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + $data = [ 'color' => $color, 'account' => $account, @@ -455,12 +463,14 @@ class ClientPortalController extends BaseController } $account = $contact->account; + $account->loadLocalizationSettings($contact->client); if (!$account->enable_client_portal) { return $this->returnError(); } $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + $data = [ 'color' => $color, 'account' => $account, @@ -489,12 +499,14 @@ class ClientPortalController extends BaseController } $account = $contact->account; + $account->loadLocalizationSettings($contact->client); if (!$account->enable_client_portal) { return $this->returnError(); } $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + $data = [ 'color' => $color, 'account' => $account, diff --git a/app/Http/Controllers/DashboardApiController.php b/app/Http/Controllers/DashboardApiController.php index d5ff416e7e13..c6cf454666d8 100644 --- a/app/Http/Controllers/DashboardApiController.php +++ b/app/Http/Controllers/DashboardApiController.php @@ -23,8 +23,8 @@ class DashboardApiController extends BaseAPIController $dashboardRepo = $this->dashboardRepo; $metrics = $dashboardRepo->totals($accountId, $userId, $viewAll); - $paidToDate = $dashboardRepo->paidToDate($accountId, $userId, $viewAll); - $averageInvoice = $dashboardRepo->averages($accountId, $userId, $viewAll); + $paidToDate = $dashboardRepo->paidToDate($account, $userId, $viewAll); + $averageInvoice = $dashboardRepo->averages($account, $userId, $viewAll); $balances = $dashboardRepo->balances($accountId, $userId, $viewAll); $activities = $dashboardRepo->activities($accountId, $userId, $viewAll); $pastDue = $dashboardRepo->pastDue($accountId, $userId, $viewAll); diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 50c5d3d394d8..14cb4c638bf1 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -33,9 +33,9 @@ class DashboardController extends BaseController $dashboardRepo = $this->dashboardRepo; $metrics = $dashboardRepo->totals($accountId, $userId, $viewAll); - $paidToDate = $dashboardRepo->paidToDate($accountId, $userId, $viewAll); - $averageInvoice = $dashboardRepo->averages($accountId, $userId, $viewAll); - $balances = $dashboardRepo->balances($accountId, $userId, $viewAll); + $paidToDate = $dashboardRepo->paidToDate($account, $userId, $viewAll); + $averageInvoice = $dashboardRepo->averages($account, $userId, $viewAll); + $balances = $dashboardRepo->balances($accountId, $userId, $viewAll); $activities = $dashboardRepo->activities($accountId, $userId, $viewAll); $pastDue = $dashboardRepo->pastDue($accountId, $userId, $viewAll); $upcoming = $dashboardRepo->upcoming($accountId, $userId, $viewAll); diff --git a/app/Http/Controllers/OnlinePaymentController.php b/app/Http/Controllers/OnlinePaymentController.php index 07013f2fe7b5..e7d709096e37 100644 --- a/app/Http/Controllers/OnlinePaymentController.php +++ b/app/Http/Controllers/OnlinePaymentController.php @@ -70,11 +70,13 @@ class OnlinePaymentController extends BaseController ]); } - if ( ! floatval($invitation->invoice->balance)) { + if ( ! $invitation->invoice->canBePaid()) { return redirect()->to('view/' . $invitation->invitation_key); } $invitation = $invitation->load('invoice.client.account.account_gateways.gateway'); + $account = $invitation->account; + $account->loadLocalizationSettings($invitation->invoice->client); if ( ! $gatewayTypeAlias) { $gatewayTypeId = Session::get($invitation->id . 'gateway_type'); @@ -84,7 +86,7 @@ class OnlinePaymentController extends BaseController $gatewayTypeId = $gatewayTypeAlias; } - $paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayTypeId); + $paymentDriver = $account->paymentDriver($invitation, $gatewayTypeId); try { return $paymentDriver->startPurchase(Input::all(), $sourceId); @@ -103,6 +105,10 @@ class OnlinePaymentController extends BaseController $gatewayTypeId = Session::get($invitation->id . 'gateway_type'); $paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayTypeId); + if ( ! $invitation->invoice->canBePaid()) { + return redirect()->to('view/' . $invitation->invitation_key); + } + try { $paymentDriver->completeOnsitePurchase($request->all()); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 23ed4f62cab4..b20e0b754465 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -18,7 +18,6 @@ class Kernel extends HttpKernel { 'App\Http\Middleware\VerifyCsrfToken', 'App\Http\Middleware\DuplicateSubmissionCheck', 'App\Http\Middleware\QueryLogging', - 'App\Http\Middleware\SessionDataCheckMiddleware', 'App\Http\Middleware\StartupCheck', ]; diff --git a/app/Http/Middleware/SessionDataCheckMiddleware.php b/app/Http/Middleware/SessionDataCheckMiddleware.php deleted file mode 100644 index c626fa6ee88f..000000000000 --- a/app/Http/Middleware/SessionDataCheckMiddleware.php +++ /dev/null @@ -1,31 +0,0 @@ -getLastUsed(); - - if ( ! $bag || $elapsed > $max) { - $request->session()->flush(); - Auth::logout(); - $request->session()->flash('warning', trans('texts.inactive_logout')); - } - - return $next($request); - } -} diff --git a/app/Http/Middleware/StartupCheck.php b/app/Http/Middleware/StartupCheck.php index 54d81033e1fd..776974a8946c 100644 --- a/app/Http/Middleware/StartupCheck.php +++ b/app/Http/Middleware/StartupCheck.php @@ -13,7 +13,7 @@ use Event; use Schema; use App\Models\Language; use App\Models\InvoiceDesign; -use App\Events\UserSettingsChanged; +use App\Events\UserLoggedIn; use App\Libraries\CurlUtils; /** @@ -118,7 +118,7 @@ class StartupCheck // Make sure the account/user localization settings are in the session if (Auth::check() && !Session::has(SESSION_TIMEZONE)) { - Event::fire(new UserSettingsChanged()); + Event::fire(new UserLoggedIn()); } // Check if the user is claiming a license (ie, additional invoices, white label, etc.) diff --git a/app/Includes/parsecsv.lib.php b/app/Includes/parsecsv.lib.php index 7fe04984db97..797beef30340 100644 --- a/app/Includes/parsecsv.lib.php +++ b/app/Includes/parsecsv.lib.php @@ -1,21 +1,21 @@ output (true, 'movies.csv', $array); ---------------- - + */ @@ -83,87 +83,87 @@ class parseCSV { * Configuration * - set these options with $object->var_name = 'value'; */ - + # use first line/entry as field names var $heading = true; - + # override field names var $fields = []; - + # sort entries by this field var $sort_by = null; var $sort_reverse = false; - + # delimiter (comma) and enclosure (double quote) var $delimiter = ','; var $enclosure = '"'; - + # basic SQL-like conditions for row matching var $conditions = null; - + # number of rows to ignore from beginning of data var $offset = null; - + # limits the number of returned rows to specified amount var $limit = null; - + # number of rows to analyze when attempting to auto-detect delimiter var $auto_depth = 15; - + # characters to ignore when attempting to auto-detect delimiter var $auto_non_chars = "a-zA-Z0-9\n\r"; - + # preferred delimiter characters, only used when all filtering method # returns multiple possible delimiters (happens very rarely) var $auto_preferred = ",;\t.:|"; - + # character encoding options var $convert_encoding = false; var $input_encoding = 'ISO-8859-1'; var $output_encoding = 'ISO-8859-1'; - + # used by unparse(), save(), and output() functions var $linefeed = "\r\n"; - + # only used by output() function var $output_delimiter = ','; var $output_filename = 'data.csv'; - - + + /** * Internal variables */ - + # current file var $file; - + # loaded file contents var $file_data; - + # array of field values in data parsed var $titles = []; - + # two dimentional array of CSV data var $data = []; - - + + /** * Constructor * @param input CSV file or string * @return nothing */ - function parseCSV ($input = null, $offset = null, $limit = null, $conditions = null) { + function __construct ($input = null, $offset = null, $limit = null, $conditions = null) { if ( $offset !== null ) $this->offset = $offset; if ( $limit !== null ) $this->limit = $limit; if ( count($conditions) > 0 ) $this->conditions = $conditions; if ( !empty($input) ) $this->parse($input); } - - + + // ============================================== // ----- [ Main Functions ] --------------------- // ============================================== - + /** * Parse CSV file or string * @param input CSV file or string @@ -184,7 +184,7 @@ class parseCSV { } return true; } - + /** * Save changes, or new file and/or data * @param file file to save to @@ -199,7 +199,7 @@ class parseCSV { $is_php = ( preg_match('/\.php$/i', $file) ) ? true : false ; return $this->_wfile($file, $this->unparse($data, $fields, $append, $is_php), $mode); } - + /** * Generate CSV based string for output * @param output if true, prints headers and strings to browser @@ -220,7 +220,7 @@ class parseCSV { } return $data; } - + /** * Convert character encoding * @param input input character encoding, uses default if left blank @@ -232,7 +232,7 @@ class parseCSV { if ( $input !== null ) $this->input_encoding = $input; if ( $output !== null ) $this->output_encoding = $output; } - + /** * Auto-Detect Delimiter: Find delimiter by analyzing a specific number of * rows to determine most probable delimiter character @@ -244,13 +244,13 @@ class parseCSV { * @return delimiter character */ function auto ($file = null, $parse = true, $search_depth = null, $preferred = null, $enclosure = null) { - + if ( $file === null ) $file = $this->file; if ( empty($search_depth) ) $search_depth = $this->auto_depth; if ( $enclosure === null ) $enclosure = $this->enclosure; - + if ( $preferred === null ) $preferred = $this->auto_preferred; - + if ( empty($this->file_data) ) { if ( $this->_check_data($file) ) { $data = &$this->file_data; @@ -258,24 +258,24 @@ class parseCSV { } else { $data = &$this->file_data; } - + $chars = []; $strlen = strlen($data); $enclosed = false; $n = 1; $to_end = true; - + // walk specific depth finding posssible delimiter characters for ( $i=0; $i < $strlen; $i++ ) { $ch = $data{$i}; $nch = ( isset($data{$i+1}) ) ? $data{$i+1} : false ; $pch = ( isset($data{$i-1}) ) ? $data{$i-1} : false ; - + // open and closing quotes if ( $ch == $enclosure && (!$enclosed || $nch != $enclosure) ) { $enclosed = ( $enclosed ) ? false : true ; - - // inline quotes + + // inline quotes } elseif ( $ch == $enclosure && $enclosed ) { $i++; @@ -287,7 +287,7 @@ class parseCSV { } else { $n++; } - + // count character } elseif (!$enclosed) { if ( !preg_match('/['.preg_quote($this->auto_non_chars, '/').']/i', $ch) ) { @@ -299,7 +299,7 @@ class parseCSV { } } } - + // filtering $depth = ( $to_end ) ? $n-1 : $n ; $filtered = []; @@ -308,24 +308,24 @@ class parseCSV { $filtered[$match] = $char; } } - + // capture most probable delimiter ksort($filtered); $delimiter = reset($filtered); $this->delimiter = $delimiter; - + // parse data if ( $parse ) $this->data = $this->parse_string(); - + return $delimiter; - + } - - + + // ============================================== // ----- [ Core Functions ] --------------------- // ============================================== - + /** * Read file to string and call parse_string() * @param file local CSV file @@ -336,7 +336,7 @@ class parseCSV { if ( empty($this->file_data) ) $this->load_data($file); return ( !empty($this->file_data) ) ? $this->parse_string() : false ; } - + /** * Parse CSV strings to arrays * @param data CSV string @@ -348,7 +348,7 @@ class parseCSV { $data = &$this->file_data; } else return false; } - + $rows = []; $row = []; $row_count = 0; @@ -358,19 +358,19 @@ class parseCSV { $enclosed = false; $was_enclosed = false; $strlen = strlen($data); - + // walk through each character for ( $i=0; $i < $strlen; $i++ ) { $ch = $data{$i}; $nch = ( isset($data{$i+1}) ) ? $data{$i+1} : false ; $pch = ( isset($data{$i-1}) ) ? $data{$i-1} : false ; - + // open and closing quotes if ( $ch == $this->enclosure && (!$enclosed || $nch != $this->enclosure) ) { $enclosed = ( $enclosed ) ? false : true ; if ( $enclosed ) $was_enclosed = true; - - // inline quotes + + // inline quotes } elseif ( $ch == $this->enclosure && $enclosed ) { $current .= $ch; $i++; @@ -382,7 +382,7 @@ class parseCSV { $row[$key] = $current; $current = ''; $col++; - + // end of row if ( $ch == "\n" || $ch == "\r" ) { if ( $this->_validate_offset($row_count) && $this->_validate_row_conditions($row, $this->conditions) ) { @@ -406,7 +406,7 @@ class parseCSV { $i = $strlen; } } - + // append character to current field } else { $current .= $ch; @@ -421,7 +421,7 @@ class parseCSV { } return $rows; } - + /** * Create CSV data from array * @param data 2D array with data @@ -436,10 +436,10 @@ class parseCSV { if ( !is_array($data) || empty($data) ) $data = &$this->data; if ( !is_array($fields) || empty($fields) ) $fields = &$this->titles; if ( $delimiter === null ) $delimiter = $this->delimiter; - + $string = ( $is_php ) ? "".$this->linefeed : '' ; $entry = []; - + // create heading if ( $this->heading && !$append ) { foreach( $fields as $key => $value ) { @@ -448,7 +448,7 @@ class parseCSV { $string .= implode($delimiter, $entry).$this->linefeed; $entry = []; } - + // create data foreach( $data as $key => $row ) { foreach( $row as $field => $value ) { @@ -457,10 +457,10 @@ class parseCSV { $string .= implode($delimiter, $entry).$this->linefeed; $entry = []; } - + return $string; } - + /** * Load local file or string * @param input local CSV file @@ -488,16 +488,16 @@ class parseCSV { } return false; } - - + + // ============================================== // ----- [ Internal Functions ] ----------------- // ============================================== - + /** * Validate a row against specified conditions * @param row array with values from a row - * @param conditions specified conditions that the row must match + * @param conditions specified conditions that the row must match * @return true of false */ function _validate_row_conditions ($row = [], $conditions = null) { @@ -523,11 +523,11 @@ class parseCSV { } return false; } - + /** * Validate a row against a single condition * @param row array with values from a row - * @param condition specified condition that the row must match + * @param condition specified condition that the row must match * @return true of false */ function _validate_row_condition ($row, $condition) { @@ -583,7 +583,7 @@ class parseCSV { } return '1'; } - + /** * Validates if the row is within the offset or not if sorting is disabled * @param current_row the current row number being processed @@ -593,7 +593,7 @@ class parseCSV { if ( $this->sort_by === null && $this->offset !== null && $current_row < $this->offset ) return false; return true; } - + /** * Enclose values if needed * - only used by unparse() @@ -611,7 +611,7 @@ class parseCSV { } return $value; } - + /** * Check file data * @param file local filename @@ -624,8 +624,8 @@ class parseCSV { } return true; } - - + + /** * Check if passed info might be delimiter * - only used by find_delimiter() @@ -656,7 +656,7 @@ class parseCSV { } else return false; } } - + /** * Read local file * @param file local filename @@ -689,7 +689,7 @@ class parseCSV { } return false; } - + } -?> \ No newline at end of file +?> diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index 88567ad27741..c7e722a0cd20 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -22,6 +22,9 @@ class Utils "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", ]; + public static $months = [ + 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december', + ]; public static function isRegistered() { @@ -92,6 +95,15 @@ class Utils return Utils::getResllerType() ? true : false; } + public static function isWhiteLabel() + { + if (Utils::isNinjaProd()) { + return false; + } + + return \App\Models\Account::first()->hasFeature(FEATURE_WHITE_LABEL); + } + public static function getResllerType() { return isset($_ENV['RESELLER_TYPE']) ? $_ENV['RESELLER_TYPE'] : false; @@ -151,6 +163,11 @@ class Utils return Auth::check() && Auth::user()->isTrial(); } + public static function isPaidPro() + { + return static::isPro() && ! static::isTrial(); + } + public static function isEnglish() { return App::getLocale() == 'en'; @@ -641,11 +658,22 @@ class Utils } } + public static function getMonthOptions() + { + $months = []; + + for ($i=1; $i<=count(static::$months); $i++) { + $month = static::$months[$i-1]; + $number = $i < 10 ? '0' . $i : $i; + $months["2000-{$number}-01"] = trans("texts.{$month}"); + } + + return $months; + } + private static function getMonth($offset) { - $months = ['january', 'february', 'march', 'april', 'may', 'june', - 'july', 'august', 'september', 'october', 'november', 'december', ]; - + $months = static::$months; $month = intval(date('n')) - 1; $month += $offset; diff --git a/app/Models/Account.php b/app/Models/Account.php index 4db6511136bc..b1b9a1ad7476 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -69,6 +69,7 @@ class Account extends Eloquent 'enable_second_tax_rate', 'include_item_taxes_inline', 'start_of_week', + 'financial_year_start', ]; /** diff --git a/app/Models/Client.php b/app/Models/Client.php index 7ab4ecac0a4c..d6230c709a6c 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -345,7 +345,7 @@ class Client extends EntityModel $contact = $this->contacts[0]; - return $contact->getDisplayName() ?: trans('texts.unnamed_client'); + return $contact->getDisplayName(); } /** diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 14b3d68a90c6..ad20fc1830e2 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -514,6 +514,11 @@ class Invoice extends EntityModel implements BalanceAffecting return storage_path() . '/pdfcache/cache-' . $this->id . '.pdf'; } + public function canBePaid() + { + return floatval($this->balance) > 0 && ! $this->is_deleted; + } + /** * @param $invoice * @return string diff --git a/app/Ninja/Repositories/DashboardRepository.php b/app/Ninja/Repositories/DashboardRepository.php index e115d963fcf7..f7cc88b6ca6a 100644 --- a/app/Ninja/Repositories/DashboardRepository.php +++ b/app/Ninja/Repositories/DashboardRepository.php @@ -175,29 +175,39 @@ class DashboardRepository return $metrics->groupBy('accounts.id')->first(); } - public function paidToDate($accountId, $userId, $viewAll) + public function paidToDate($account, $userId, $viewAll) { + $accountId = $account->id; $select = DB::raw( - 'SUM('.DB::getQueryGrammar()->wrap('clients.paid_to_date', true).') as value,' + 'SUM('.DB::getQueryGrammar()->wrap('payments.amount', true).' - '.DB::getQueryGrammar()->wrap('payments.refunded', true).') as value,' .DB::getQueryGrammar()->wrap('clients.currency_id', true).' as currency_id' ); - $paidToDate = DB::table('accounts') + $paidToDate = DB::table('payments') ->select($select) - ->leftJoin('clients', 'accounts.id', '=', 'clients.account_id') - ->where('accounts.id', '=', $accountId) - ->where('clients.is_deleted', '=', false); + ->leftJoin('invoices', 'invoices.id', '=', 'payments.invoice_id') + ->leftJoin('clients', 'clients.id', '=', 'invoices.client_id') + ->where('payments.account_id', '=', $accountId) + ->where('clients.is_deleted', '=', false) + ->where('invoices.is_deleted', '=', false) + ->whereNotIn('payments.payment_status_id', [PAYMENT_STATUS_VOIDED, PAYMENT_STATUS_FAILED]); if (!$viewAll){ - $paidToDate = $paidToDate->where('clients.user_id', '=', $userId); + $paidToDate->where('invoices.user_id', '=', $userId); } - return $paidToDate->groupBy('accounts.id') - ->groupBy(DB::raw('CASE WHEN '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' IS NULL THEN CASE WHEN '.DB::getQueryGrammar()->wrap('accounts.currency_id', true).' IS NULL THEN 1 ELSE '.DB::getQueryGrammar()->wrap('accounts.currency_id', true).' END ELSE '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' END')) + if ($account->financial_year_start) { + $yearStart = str_replace('2000', date('Y'), $account->financial_year_start); + $paidToDate->where('payments.payment_date', '>=', $yearStart); + } + + return $paidToDate->groupBy('payments.account_id') + ->groupBy(DB::raw('CASE WHEN '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' IS NULL THEN '.($account->currency_id ?: DEFAULT_CURRENCY).' ELSE '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' END')) ->get(); } - public function averages($accountId, $userId, $viewAll) + public function averages($account, $userId, $viewAll) { + $accountId = $account->id; $select = DB::raw( 'AVG('.DB::getQueryGrammar()->wrap('invoices.amount', true).') as invoice_avg, ' .DB::getQueryGrammar()->wrap('clients.currency_id', true).' as currency_id' @@ -213,7 +223,12 @@ class DashboardRepository ->where('invoices.is_recurring', '=', false); if (!$viewAll){ - $averageInvoice = $averageInvoice->where('invoices.user_id', '=', $userId); + $averageInvoice->where('invoices.user_id', '=', $userId); + } + + if ($account->financial_year_start) { + $yearStart = str_replace('2000', date('Y'), $account->financial_year_start); + $averageInvoice->where('invoices.invoice_date', '>=', $yearStart); } return $averageInvoice->groupBy('accounts.id') @@ -252,7 +267,7 @@ class DashboardRepository } return $activities->orderBy('activities.created_at', 'desc') - ->with('client.contacts', 'user', 'invoice', 'payment', 'credit', 'account', 'task', 'expense') + ->with('client.contacts', 'user', 'invoice', 'payment', 'credit', 'account', 'task', 'expense', 'contact') ->take(50) ->get(); } diff --git a/app/Services/InvoiceService.php b/app/Services/InvoiceService.php index 0d6bb9822c43..82bbfe35d662 100644 --- a/app/Services/InvoiceService.php +++ b/app/Services/InvoiceService.php @@ -86,11 +86,16 @@ class InvoiceService extends BaseService $sendInvoiceIds = []; foreach ($client->contacts as $contact) { - if ($contact->send_invoice || count($client->contacts) == 1) { + if ($contact->send_invoice) { $sendInvoiceIds[] = $contact->id; } } + // if no contacts are selected auto-select the first to enusre there's an invitation + if ( ! count($sendInvoiceIds)) { + $sendInvoiceIds[] = $client->contacts[0]->id; + } + foreach ($client->contacts as $contact) { $invitation = Invitation::scope()->whereContactId($contact->id)->whereInvoiceId($invoice->id)->first(); diff --git a/composer.json b/composer.json index b9118982172f..15ff96fa34a5 100644 --- a/composer.json +++ b/composer.json @@ -77,7 +77,7 @@ "gatepay/FedACHdir": "dev-master@dev", "websight/l5-google-cloud-storage": "^1.0", "wepay/php-sdk": "^0.2", - "collizo4sky/omnipay-wepay": "dev-additional-calls", + "collizo4sky/omnipay-wepay": "dev-address-fix", "barryvdh/laravel-ide-helper": "~2.2", "barryvdh/laravel-debugbar": "~2.2", "fzaninotto/faker": "^1.5", diff --git a/composer.lock b/composer.lock index 657987636bd8..2acfb5dadeee 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "f61ec0361b0757ca0cade78837862d39", - "content-hash": "37549f849d74405b68eb7e2df64dfb43", + "hash": "cf642e3384eec7504bcdace749d2bb88", + "content-hash": "c0a5b571bc2305c4b0d9eae18bf5011b", "packages": [ { "name": "agmscode/omnipay-agms", @@ -1081,16 +1081,16 @@ }, { "name": "collizo4sky/omnipay-wepay", - "version": "dev-additional-calls", + "version": "dev-address-fix", "source": { "type": "git", "url": "https://github.com/hillelcoren/omnipay-wepay.git", - "reference": "a341b9997d71803d0f774d86908cb49a8bc4c405" + "reference": "916785146c5433e9216f295d09d1cbcec2fdf33a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hillelcoren/omnipay-wepay/zipball/a341b9997d71803d0f774d86908cb49a8bc4c405", - "reference": "a341b9997d71803d0f774d86908cb49a8bc4c405", + "url": "https://api.github.com/repos/hillelcoren/omnipay-wepay/zipball/916785146c5433e9216f295d09d1cbcec2fdf33a", + "reference": "916785146c5433e9216f295d09d1cbcec2fdf33a", "shasum": "" }, "require": { @@ -1120,9 +1120,9 @@ "wepay" ], "support": { - "source": "https://github.com/sometechie/omnipay-wepay/tree/additional-calls" + "source": "https://github.com/hillelcoren/omnipay-wepay/tree/address-fix" }, - "time": "2016-05-25 19:18:42" + "time": "2016-11-01 10:54:54" }, { "name": "container-interop/container-interop", @@ -2184,7 +2184,7 @@ "shasum": null }, "type": "library", - "time": "2016-06-03 12:00:26" + "time": "2016-10-12 12:00:38" }, { "name": "google/apiclient", @@ -3091,6 +3091,7 @@ "purchase", "wechat" ], + "abandoned": "lokielse/omnipay-wechatpay", "time": "2016-05-10 08:43:41" }, { @@ -7211,16 +7212,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v2.8.9", + "version": "v2.8.13", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "889983a79a043dfda68f38c38b6dba092dd49cd8" + "reference": "25c576abd4e0f212e678fe8b2bd9a9a98c7ea934" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/889983a79a043dfda68f38c38b6dba092dd49cd8", - "reference": "889983a79a043dfda68f38c38b6dba092dd49cd8", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/25c576abd4e0f212e678fe8b2bd9a9a98c7ea934", + "reference": "25c576abd4e0f212e678fe8b2bd9a9a98c7ea934", "shasum": "" }, "require": { @@ -7267,7 +7268,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2016-07-28 16:56:28" + "time": "2016-10-13 01:43:15" }, { "name": "symfony/finder", @@ -7320,16 +7321,16 @@ }, { "name": "symfony/http-foundation", - "version": "v2.8.9", + "version": "v2.8.13", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "f20bea598906c990eebe3c70a63ca5ed18cdbc11" + "reference": "a6e6c34d337f3c74c39b29c5f54d33023de8897c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f20bea598906c990eebe3c70a63ca5ed18cdbc11", - "reference": "f20bea598906c990eebe3c70a63ca5ed18cdbc11", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a6e6c34d337f3c74c39b29c5f54d33023de8897c", + "reference": "a6e6c34d337f3c74c39b29c5f54d33023de8897c", "shasum": "" }, "require": { @@ -7371,7 +7372,7 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2016-07-30 07:20:35" + "time": "2016-10-24 15:52:36" }, { "name": "symfony/http-kernel", diff --git a/public/built.js b/public/built.js index 9fd5dbb520c9..615988d0b3f8 100644 --- a/public/built.js +++ b/public/built.js @@ -27,5 +27,5 @@ void(t._d=new Date(NaN));for(o=0;owindow.innerHeight&&(b.previousBodyPadding=document.body.style.paddingRight,document.body.style.paddingRight=H()+"px")}function e(){null!==b.previousBodyPadding&&(document.body.style.paddingRight=b.previousBodyPadding,b.previousBodyPadding=null)}function n(){var t=/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream;if(t&&!O(document.body,l.iosfix)){var e=document.body.scrollTop;document.body.style.top=e*-1+"px",L(document.body,l.iosfix)}}function i(){if(O(document.body,l.iosfix)){var t=parseInt(document.body.style.top,10);k(document.body,l.iosfix),document.body.scrollTop=t*-1}}function o(){if(void 0===arguments[0])return!1;var t=m({},U);switch(typeof arguments[0]){case"string":t.title=arguments[0],t.text=arguments[1]||"",t.type=arguments[2]||"";break;case"object":m(t,arguments[0]),t.extraParams=arguments[0].extraParams,"email"===t.input&&null===t.inputValidator&&(t.inputValidator=function(t){return new Promise(function(e,n){var i=/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;i.test(t)?e():n("Invalid email address")})});break;default:return!1}V(t);var e=y();return new Promise(function(n,i){function o(e,n){for(var i=S(t.focusCancel),o=0;o