diff --git a/app/Console/Commands/CheckData.php b/app/Console/Commands/CheckData.php index ac14c8a54574..11120817a88d 100644 --- a/app/Console/Commands/CheckData.php +++ b/app/Console/Commands/CheckData.php @@ -102,17 +102,9 @@ class CheckData extends Command config(['database.default' => $database]); } - $this->checkInvoiceBalances(); - $this->checkInvoiceBalancesNew(); - //$this->checkInvoicePayments(); - - //$this->checkPaidToDates(); - + $this->checkInvoiceBalances(); $this->checkPaidToDatesNew(); - // $this->checkPaidToCompanyDates(); - $this->checkClientBalances(); - $this->checkContacts(); $this->checkVendorContacts(); $this->checkEntityInvitations(); @@ -123,7 +115,6 @@ class CheckData extends Command if (! $this->option('client_id')) { $this->checkOAuth(); - //$this->checkFailedJobs(); } $this->logMessage('Done: '.strtoupper($this->isValid ? Account::RESULT_SUCCESS : Account::RESULT_FAILURE)); @@ -359,7 +350,6 @@ class CheckData extends Command } } - private function checkEntityInvitations() { @@ -420,35 +410,6 @@ class CheckData extends Command } - // private function checkPaidToCompanyDates() - // { - // Company::cursor()->each(function ($company){ - - // $payments = Payment::where('is_deleted', 0) - // ->where('company_id', $company->id) - // ->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED]) - // ->pluck('id'); - - // $unapplied = Payment::where('is_deleted', 0) - // ->where('company_id', $company->id) - // ->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED]) - // ->sum(\DB::Raw('amount - applied')); - - // $paymentables = Paymentable::whereIn('payment_id', $payments)->sum(\DB::Raw('amount - refunded')); - - // $client_paid_to_date = Client::where('company_id', $company->id)->where('is_deleted', 0)->withTrashed()->sum('paid_to_date'); - - // $total_payments = $paymentables + $unapplied; - - // if (round($total_payments, 2) != round($client_paid_to_date, 2)) { - // $this->wrong_paid_to_dates++; - - // $this->logMessage($company->present()->name.' id = # '.$company->id." - Paid to date does not match Client Paid To Date = {$client_paid_to_date} - Invoice Payments = {$total_payments}"); - // } - - // }); - - // } private function clientPaidToDateQuery() { $results = \DB::select( \DB::raw(" @@ -528,14 +489,11 @@ class CheckData extends Command } - - private function checkPaidToDates() { $this->wrong_paid_to_dates = 0; $credit_total_applied = 0; - $clients = DB::table('clients') ->leftJoin('payments', function($join) { $join->on('payments.client_id', '=', 'clients.id') @@ -605,29 +563,6 @@ class CheckData extends Command $this->logMessage("{$this->wrong_paid_to_dates} clients with incorrect paid to dates"); } -/* -SELECT -SUM(payments.applied) as payments_applied, -SUM(invoices.amount - invoices.balance) as invoices_paid_amount, -SUM(credits.amount - credits.balance) as credits_balance, -SUM(invoices.balance) as invoices_balance, -clients.id -FROM payments -JOIN clients -ON clients.id = payments.client_id -JOIN credits -ON credits.client_id = clients.id -JOIN invoices -ON invoices.client_id = payments.client_id -WHERE payments.is_deleted = 0 -AND payments.status_id IN (1,4,5,6) -AND invoices.is_deleted = 0 -AND invoices.status_id != 1 -GROUP BY clients.id -HAVING (payments_applied - credits_balance - invoices_balance) != invoices_paid_amount -ORDER BY clients.id; -*/ - private function checkInvoicePayments() { $this->wrong_balances = 0; @@ -660,33 +595,6 @@ ORDER BY clients.id; $this->logMessage("{$this->wrong_balances} clients with incorrect invoice balances"); } - - - // $clients = DB::table('clients') - // ->leftJoin('invoices', function($join){ - // $join->on('invoices.client_id', '=', 'clients.id') - // ->where('invoices.is_deleted',0) - // ->where('invoices.status_id', '>', 1); - // }) - // ->leftJoin('credits', function($join){ - // $join->on('credits.client_id', '=', 'clients.id') - // ->where('credits.is_deleted',0) - // ->where('credits.status_id', '>', 1); - // }) - // ->leftJoin('payments', function($join) { - // $join->on('payments.client_id', '=', 'clients.id') - // ->where('payments.is_deleted', 0) - // ->whereIn('payments.status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED]); - // }) - // ->where('clients.is_deleted',0) - // //->where('clients.updated_at', '>', now()->subDays(2)) - // ->groupBy('clients.id') - // ->havingRaw('sum(coalesce(invoices.amount - invoices.balance - credits.amount)) != sum(coalesce(payments.amount - payments.refunded, 0))') - // ->get(['clients.id', DB::raw('sum(coalesce(invoices.amount - invoices.balance - credits.amount)) as invoice_amount'), DB::raw('sum(coalesce(payments.amount - payments.refunded, 0)) as payment_amount')]); - - - - private function clientBalanceQuery() { $results = \DB::select( \DB::raw(" @@ -708,9 +616,6 @@ ORDER BY clients.id; return $results; } - - - private function checkClientBalances() { $this->wrong_balances = 0; @@ -722,66 +627,30 @@ ORDER BY clients.id; { $client = (array)$client; - $invoice_balance = $client['invoice_balance']; - - // $ledger = CompanyLedger::where('client_id', $client['client_id'])->orderBy('id', 'DESC')->first(); - - if ((string) $invoice_balance != (string) $client['client_balance']) { + if ((string) $client['invoice_balance'] != (string) $client['client_balance']) { $this->wrong_paid_to_dates++; $client_object = Client::withTrashed()->find($client['client_id']); - $this->logMessage($client_object->present()->name.' - '.$client_object->id." - calculated client balances do not match Invoice Balances = {$invoice_balance} - Client Balance = ".rtrim($client['client_balance'], '0')); + $this->logMessage($client_object->present()->name.' - '.$client_object->id." - calculated client balances do not match Invoice Balances = ". $client['invoice_balance'] ." - Client Balance = ".rtrim($client['client_balance'], '0')); - - if($this->option('ledger_balance')){ + if($this->option('client_balance')){ - $this->logMessage("# {$client_object->id} " . $client_object->present()->name.' - '.$client_object->number." Fixing {$client_object->balance} to {$invoice_balance}"); - $client_object->balance = $invoice_balance; + $this->logMessage("# {$client_object->id} " . $client_object->present()->name.' - '.$client_object->number." Fixing {$client_object->balance} to " . $client['invoice_balance']); + $client_object->balance = $client['invoice_balance']; $client_object->save(); - // $ledger->adjustment = $invoice_balance; - // $ledger->balance = $invoice_balance; - // $ledger->notes = 'Ledger Adjustment'; - // $ledger->save(); } - $this->isValid = false; } } - // foreach (Client::cursor()->where('is_deleted', 0)->where('clients.updated_at', '>', now()->subDays(2)) as $client) { - - // $invoice_balance = Invoice::where('client_id', $client->id)->where('is_deleted', false)->where('status_id', '>', 1)->withTrashed()->sum('balance'); - // $credit_balance = Credit::where('client_id', $client->id)->where('is_deleted', false)->withTrashed()->sum('balance'); - - // if($client->balance != $invoice_balance) - // $invoice_balance -= $credit_balance; - - // $ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first(); - - // if ($ledger && (string) $invoice_balance != (string) $client->balance) { - // $this->wrong_paid_to_dates++; - // $this->logMessage($client->present()->name.' - '.$client->id." - calculated client balances do not match Invoice Balances = {$invoice_balance} - Client Balance = ".rtrim($client->balance, '0'). " Ledger balance = {$ledger->balance}"); - - // $this->isValid = false; - - // } - // } - $this->logMessage("{$this->wrong_paid_to_dates} clients with incorrect client balances"); } - //fix for client balances = - //$adjustment = ($invoice_balance-$client->balance) - //$client->balance += $adjustment; - - //$ledger_adjustment = $ledger->balance - $client->balance; - //$ledger->balance += $ledger_adjustment - private function invoiceBalanceQuery() { $results = \DB::select( \DB::raw(" @@ -803,7 +672,7 @@ ORDER BY clients.id; return $results; } - private function checkInvoiceBalancesNew() + private function checkInvoiceBalances() { $this->wrong_balances = 0; $this->wrong_paid_to_dates = 0; @@ -818,25 +687,30 @@ ORDER BY clients.id; $ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first(); - if ($ledger && number_format($invoice_balance, 4) != number_format($client->balance, 4)) { + if (number_format($invoice_balance, 4) != number_format($client->balance, 4)) { $this->wrong_balances++; - $this->logMessage("# {$client->id} " . $client->present()->name.' - '.$client->number." - Balance Failure - Invoice Balances = {$invoice_balance} Client Balance = {$client->balance} Ledger Balance = {$ledger->balance}"); + $ledger_balance = $ledger ? $ledger->balance : 0; + + $this->logMessage("# {$client->id} " . $client->present()->name.' - '.$client->number." - Balance Failure - Invoice Balances = {$invoice_balance} Client Balance = {$client->balance} Ledger Balance = {$ledger_balance}"); $this->isValid = false; - if($this->option('client_balance')){ $this->logMessage("# {$client->id} " . $client->present()->name.' - '.$client->number." Fixing {$client->balance} to {$invoice_balance}"); $client->balance = $invoice_balance; $client->save(); + } + + if($ledger && (number_format($invoice_balance, 4) != number_format($ledger->balance, 4))) + { $ledger->adjustment = $invoice_balance; $ledger->balance = $invoice_balance; $ledger->notes = 'Ledger Adjustment'; $ledger->save(); } - + } } @@ -844,7 +718,7 @@ ORDER BY clients.id; } - private function checkInvoiceBalances() + private function checkLedgerBalances() { $this->wrong_balances = 0; $this->wrong_paid_to_dates = 0; @@ -880,26 +754,6 @@ ORDER BY clients.id; private function checkLogoFiles() { - // $accounts = DB::table('accounts') - // ->where('logo', '!=', '') - // ->orderBy('id') - // ->get(['logo']); - - // $countMissing = 0; - - // foreach ($accounts as $account) { - // $path = public_path('logo/' . $account->logo); - // if (! file_exists($path)) { - // $this->logMessage('Missing file: ' . $account->logo); - // $countMissing++; - // } - // } - - // if ($countMissing > 0) { - // $this->isValid = false; - // } - - // $this->logMessage($countMissing . ' missing logo files'); } /** @@ -985,60 +839,21 @@ ORDER BY clients.id; { Account::where('plan_expires', '<=', now()->subDays(2))->cursor()->each(function ($account){ - $client = Client::on('db-ninja-01')->where('company_id', config('ninja.ninja_default_company_id'))->where('custom_value2', $account->key)->first(); - - if($client){ - $payment = Payment::on('db-ninja-01') - ->where('company_id', config('ninja.ninja_default_company_id')) - ->where('client_id', $client->id) - ->where('date', '>=', now()->subDays(2)) - ->exists(); - - if($payment) - $this->logMessage("I found a payment for {$account->key}"); + $client = Client::on('db-ninja-01')->where('company_id', config('ninja.ninja_default_company_id'))->where('custom_value2', $account->key)->first(); + + if($client){ + $payment = Payment::on('db-ninja-01') + ->where('company_id', config('ninja.ninja_default_company_id')) + ->where('client_id', $client->id) + ->where('date', '>=', now()->subDays(2)) + ->exists(); + + if($payment) + $this->logMessage("I found a payment for {$account->key}"); - - } - + } }); } -} - - -/* //used to set a company owner on the company_users table - -$c = Company::whereDoesntHave('company_users', function ($query){ - $query->where('is_owner', true)->withTrashed(); -})->cursor()->each(function ($company){ - - if(!$company->company_users()->exists()){ - echo "No company users AT ALL {$company->id}\n"; - - } - else{ - - $cu = $company->company_users()->orderBy('id', 'ASC')->orderBy('is_admin', 'ASC')->first(); - echo "{$company->id} - {$cu->id} \n"; - $cu->is_owner=true; - $cu->save(); - - } -}); -*/ - -/* query if we want to company company ledger to client balance - $results = \DB::select( \DB::raw(" - SELECT - clients.id as client_id, - clients.balance as client_balance - from clients, - (select max(company_ledgers.id) as cid, company_ledgers.client_id as client_id, company_ledgers.balance as balance - FROM company_ledgers) ledger - where clients.id=ledger.client_id - AND clients.balance != ledger.balance - GROUP BY clients.id - ORDER BY clients.id; - ") ); - */ \ No newline at end of file +} \ No newline at end of file diff --git a/app/Console/Commands/DemoMode.php b/app/Console/Commands/DemoMode.php index 7652568d18b7..bbb24e2a89db 100644 --- a/app/Console/Commands/DemoMode.php +++ b/app/Console/Commands/DemoMode.php @@ -131,6 +131,7 @@ class DemoMode extends Command 'enabled_modules' => 32767, 'company_key' => 'KEY', 'enable_shop_api' => true, + 'markdown_email_enabled' => false, ]); $settings = $company->settings; diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index b82d43a32134..250bba51d10c 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -70,6 +70,7 @@ class BaseExport $header = []; foreach($this->input['report_keys'] as $value){ + $key = array_search ($value, $this->entity_keys); $key = str_replace("item.", "", $key); @@ -77,7 +78,6 @@ class BaseExport $key = str_replace("client.", "", $key); $key = str_replace("contact.", "", $key); - $header[] = ctrans("texts.{$key}"); } diff --git a/app/Export/CSV/ClientExport.php b/app/Export/CSV/ClientExport.php index 4193e2fa7b58..b4d4df64e67e 100644 --- a/app/Export/CSV/ClientExport.php +++ b/app/Export/CSV/ClientExport.php @@ -54,7 +54,7 @@ class ClientExport extends BaseExport 'name' => 'client.name', 'number' => 'client.number', 'paid_to_date' => 'client.paid_to_date', - 'phone' => 'client.phone', + 'client_phone' => 'client.phone', 'postal_code' => 'client.postal_code', 'private_notes' => 'client.private_notes', 'public_notes' => 'client.public_notes', @@ -70,7 +70,7 @@ class ClientExport extends BaseExport 'currency' => 'client.currency', 'first_name' => 'contact.first_name', 'last_name' => 'contact.last_name', - 'phone' => 'contact.phone', + 'contact_phone' => 'contact.phone', 'contact_custom_value1' => 'contact.custom_value1', 'contact_custom_value2' => 'contact.custom_value2', 'contact_custom_value3' => 'contact.custom_value3', @@ -78,46 +78,6 @@ class ClientExport extends BaseExport 'email' => 'contact.email', ]; - protected array $all_keys = [ - 'client.address1', - 'client.address2', - 'client.balance', - 'client.city', - 'client.country_id', - 'client.credit_balance', - 'client.custom_value1', - 'client.custom_value2', - 'client.custom_value3', - 'client.custom_value4', - 'client.id_number', - 'client.industry_id', - 'client.last_login', - 'client.name', - 'client.number', - 'client.paid_to_date', - 'client.phone', - 'client.postal_code', - 'client.private_notes', - 'client.public_notes', - 'client.shipping_address1', - 'client.shipping_address2', - 'client.shipping_city', - 'client.shipping_country_id', - 'client.shipping_postal_code', - 'client.shipping_state', - 'client.state', - 'client.vat_number', - 'client.website', - 'client.currency', - 'contact.first_name', - 'contact.last_name', - 'contact.phone', - 'contact.custom_value1', - 'contact.custom_value2', - 'contact.custom_value3', - 'contact.custom_value4', - 'contact.email', - ]; private array $decorate_keys = [ 'client.country_id', 'client.shipping_country_id', @@ -146,7 +106,7 @@ class ClientExport extends BaseExport $this->csv = Writer::createFromString(); if(count($this->input['report_keys']) == 0) - $this->input['report_keys'] = $this->all_keys; + $this->input['report_keys'] = array_values($this->entity_keys); //insert the header $this->csv->insertOne($this->buildHeader()); @@ -178,20 +138,22 @@ class ClientExport extends BaseExport if($contact = $client->contacts()->first()) $transformed_contact = $this->contact_transformer->transform($contact); - $entity = []; foreach(array_values($this->input['report_keys']) as $key){ $parts = explode(".",$key); - $entity[$parts[1]] = ""; + + $keyval = array_search($key, $this->entity_keys); if($parts[0] == 'client' && array_key_exists($parts[1], $transformed_client)) { - $entity[$parts[1]] = $transformed_client[$parts[1]]; + $entity[$keyval] = $transformed_client[$parts[1]]; } - elseif($parts[0] == 'contact' && array_key_exists($parts[1], $transformed_client)) { - $entity[$parts[1]] = $transformed_contact[$parts[1]]; + elseif($parts[0] == 'contact' && array_key_exists($parts[1], $transformed_contact)) { + $entity[$keyval] = $transformed_contact[$parts[1]]; } + else + $entity[$keyval] = ""; } @@ -202,16 +164,16 @@ class ClientExport extends BaseExport private function decorateAdvancedFields(Client $client, array $entity) :array { - if(in_array('country_id', $this->input['report_keys'])) - $entity['country_id'] = $client->country ? ctrans("texts.country_{$client->country->name}") : ""; + if(in_array('client.country_id', $this->input['report_keys'])) + $entity['country'] = $client->country ? ctrans("texts.country_{$client->country->name}") : ""; - if(in_array('shipping_country_id', $this->input['report_keys'])) - $entity['shipping_country_id'] = $client->shipping_country ? ctrans("texts.country_{$client->shipping_country->name}") : ""; + if(in_array('client.shipping_country_id', $this->input['report_keys'])) + $entity['shipping_country'] = $client->shipping_country ? ctrans("texts.country_{$client->shipping_country->name}") : ""; - if(in_array('currency', $this->input['report_keys'])) - $entity['currency_id'] = $client->currency() ? $client->currency()->code : $client->company->currency()->code; + if(in_array('client.currency', $this->input['report_keys'])) + $entity['currency'] = $client->currency() ? $client->currency()->code : $client->company->currency()->code; - if(in_array('industry_id', $this->input['report_keys'])) + if(in_array('client.industry_id', $this->input['report_keys'])) $entity['industry_id'] = $client->industry ? ctrans("texts.industry_{$client->industry->name}") : ""; return $entity; diff --git a/app/Export/CSV/ContactExport.php b/app/Export/CSV/ContactExport.php index 8e5030b4b551..1c938b187f46 100644 --- a/app/Export/CSV/ContactExport.php +++ b/app/Export/CSV/ContactExport.php @@ -50,7 +50,7 @@ class ContactExport extends BaseExport 'name' => 'client.name', 'number' => 'client.number', 'paid_to_date' => 'client.paid_to_date', - 'phone' => 'client.phone', + 'client_phone' => 'client.phone', 'postal_code' => 'client.postal_code', 'private_notes' => 'client.private_notes', 'public_notes' => 'client.public_notes', @@ -66,7 +66,7 @@ class ContactExport extends BaseExport 'currency' => 'client.currency', 'first_name' => 'contact.first_name', 'last_name' => 'contact.last_name', - 'phone' => 'contact.phone', + 'contact_phone' => 'contact.phone', 'contact_custom_value1' => 'contact.custom_value1', 'contact_custom_value2' => 'contact.custom_value2', 'contact_custom_value3' => 'contact.custom_value3', @@ -74,49 +74,6 @@ class ContactExport extends BaseExport 'email' => 'contact.email', ]; - - protected array $all_keys = [ - 'client.address1', - 'client.address2', - 'client.balance', - 'client.city', - 'client.country_id', - 'client.credit_balance', - 'client.custom_value1', - 'client.custom_value2', - 'client.custom_value3', - 'client.custom_value4', - 'client.id_number', - 'client.industry_id', - 'client.last_login', - 'client.name', - 'client.number', - 'client.paid_to_date', - 'client.phone', - 'client.postal_code', - 'client.private_notes', - 'client.public_notes', - 'client.shipping_address1', - 'client.shipping_address2', - 'client.shipping_city', - 'client.shipping_country_id', - 'client.shipping_postal_code', - 'client.shipping_state', - 'client.state', - 'client.vat_number', - 'client.website', - 'client.currency', - 'contact.first_name', - 'contact.last_name', - 'contact.phone', - 'contact.custom_value1', - 'contact.custom_value2', - 'contact.custom_value3', - 'contact.custom_value4', - 'contact.email', - ]; - - private array $decorate_keys = [ 'client.country_id', 'client.shipping_country_id', @@ -145,7 +102,7 @@ class ContactExport extends BaseExport $this->csv = Writer::createFromString(); if(count($this->input['report_keys']) == 0) - $this->input['report_keys'] = $this->all_keys; + $this->input['report_keys'] = array_values($this->entity_keys); //insert the header $this->csv->insertOne($this->buildHeader()); @@ -178,15 +135,16 @@ class ContactExport extends BaseExport foreach(array_values($this->input['report_keys']) as $key){ $parts = explode(".",$key); - $entity[$parts[1]] = ""; + $keyval = array_search($key, $this->entity_keys); - if($parts[0] == 'client') { - $entity[$parts[1]] = $transformed_client[$parts[1]]; + if($parts[0] == 'client' && array_key_exists($parts[1], $transformed_client)) { + $entity[$keyval] = $transformed_client[$parts[1]]; } - elseif($parts[0] == 'contact') { - $entity[$parts[1]] = $transformed_contact[$parts[1]]; + elseif($parts[0] == 'contact' && array_key_exists($parts[1], $transformed_contact)) { + $entity[$keyval] = $transformed_contact[$parts[1]]; } - + else + $entity[$keyval] = ""; } return $this->decorateAdvancedFields($contact->client, $entity); @@ -196,16 +154,16 @@ class ContactExport extends BaseExport private function decorateAdvancedFields(Client $client, array $entity) :array { - if(array_key_exists('country_id', $entity)) - $entity['country_id'] = $client->country ? ctrans("texts.country_{$client->country->name}") : ""; + if(in_array('client.country_id', $this->input['report_keys'])) + $entity['country'] = $client->country ? ctrans("texts.country_{$client->country->name}") : ""; - if(array_key_exists('shipping_country_id', $entity)) - $entity['shipping_country_id'] = $client->shipping_country ? ctrans("texts.country_{$client->shipping_country->name}") : ""; + if(in_array('client.shipping_country_id', $this->input['report_keys'])) + $entity['shipping_country'] = $client->shipping_country ? ctrans("texts.country_{$client->shipping_country->name}") : ""; - if(array_key_exists('currency', $entity)) - $entity['currency'] = $client->currency()->code; + if(in_array('client.currency', $this->input['report_keys'])) + $entity['currency'] = $client->currency() ? $client->currency()->code : $client->company->currency()->code; - if(array_key_exists('industry_id', $entity)) + if(in_array('client.industry_id', $this->input['report_keys'])) $entity['industry_id'] = $client->industry ? ctrans("texts.industry_{$client->industry->name}") : ""; return $entity; diff --git a/app/Export/CSV/CreditExport.php b/app/Export/CSV/CreditExport.php index 72dbebc4f470..e28711bfe111 100644 --- a/app/Export/CSV/CreditExport.php +++ b/app/Export/CSV/CreditExport.php @@ -68,45 +68,6 @@ class CreditExport extends BaseExport 'currency' => 'currency' ]; - protected array $all_keys = [ - 'amount', - 'balance', - 'client_id', - 'custom_surcharge1', - 'custom_surcharge2', - 'custom_surcharge3', - 'custom_surcharge4', - 'country_id', - 'custom_value1', - 'custom_value2', - 'custom_value3', - 'custom_value4', - 'date', - 'discount', - 'due_date', - 'exchange_rate', - 'footer', - 'invoice_id', - 'number', - 'paid_to_date', - 'partial', - 'partial_due_date', - 'po_number', - 'private_notes', - 'public_notes', - 'status_id', - 'tax_name1', - 'tax_name2', - 'tax_name3', - 'tax_rate1', - 'tax_rate2', - 'tax_rate3', - 'terms', - 'total_taxes', - 'currency' - ]; - - private array $decorate_keys = [ 'country', 'client', @@ -133,12 +94,12 @@ class CreditExport extends BaseExport //load the CSV document from a string $this->csv = Writer::createFromString(); + if(count($this->input['report_keys']) == 0) + $this->input['report_keys'] = array_values($this->entity_keys); + //insert the header $this->csv->insertOne($this->buildHeader()); - if(count($this->input['report_keys']) == 0) - $this->input['report_keys'] = $this->all_keys; - $query = Credit::query() ->withTrashed() ->with('client')->where('company_id', $this->company->id) @@ -166,7 +127,13 @@ class CreditExport extends BaseExport foreach(array_values($this->input['report_keys']) as $key){ - $entity[$key] = $transformed_credit[$key]; + $keyval = array_search($key, $this->entity_keys); + + if(array_key_exists($key, $transformed_credit)) + $entity[$keyval] = $transformed_credit[$key]; + else + $entity[$keyval] = ''; + } return $this->decorateAdvancedFields($credit, $entity); @@ -176,17 +143,20 @@ class CreditExport extends BaseExport private function decorateAdvancedFields(Credit $credit, array $entity) :array { - if(array_key_exists('country_id', $entity)) - $entity['country_id'] = $credit->client->country ? ctrans("texts.country_{$credit->client->country->name}") : ""; + if(in_array('country_id', $this->input['report_keys'])) + $entity['country'] = $credit->client->country ? ctrans("texts.country_{$credit->client->country->name}") : ""; - if(array_key_exists('currency', $entity)) - $entity['currency'] = $credit->client->currency()->code; + if(in_array('currency_id', $this->input['report_keys'])) + $entity['currency_id'] = $credit->client->currency() ? $credit->client->currency()->code : $invoice->company->currency()->code;; - if(array_key_exists('invoice_id', $entity)) - $entity['invoice_id'] = $credit->invoice ? $credit->invoice->number : ""; + if(in_array('invoice_id', $this->input['report_keys'])) + $entity['invoice'] = $credit->invoice ? $credit->invoice->number : ""; - if(array_key_exists('client_id', $entity)) - $entity['client_id'] = $credit->client->present()->name(); + if(in_array('client_id', $this->input['report_keys'])) + $entity['client'] = $credit->client->present()->name(); + + if(in_array('status_id',$this->input['report_keys'])) + $entity['status'] = $credit->stringStatus($credit->status_id); return $entity; } diff --git a/app/Export/CSV/DocumentExport.php b/app/Export/CSV/DocumentExport.php index a782bc1151b1..b70a854af7bd 100644 --- a/app/Export/CSV/DocumentExport.php +++ b/app/Export/CSV/DocumentExport.php @@ -33,20 +33,12 @@ class DocumentExport extends BaseExport protected array $entity_keys = [ 'record_type' => 'record_type', - 'record_name' => 'record_name', + // 'record_name' => 'record_name', 'name' => 'name', 'type' => 'type', 'created_at' => 'created_at', ]; - protected array $all_keys = [ - 'record_type', - 'record_name', - 'name', - 'type', - 'created_at', - ]; - private array $decorate_keys = [ ]; @@ -71,7 +63,7 @@ class DocumentExport extends BaseExport $this->csv = Writer::createFromString(); if(count($this->input['report_keys']) == 0) - $this->input['report_keys'] = $this->all_keys; + $this->input['report_keys'] = array_values($this->entity_keys); //insert the header $this->csv->insertOne($this->buildHeader()); @@ -100,8 +92,12 @@ class DocumentExport extends BaseExport foreach(array_values($this->input['report_keys']) as $key){ - $entity[$key] = $transformed_entity[$key]; - + $keyval = array_search($key, $this->entity_keys); + + if(array_key_exists($key, $transformed_entity)) + $entity[$keyval] = $transformed_entity[$key]; + else + $entity[$keyval] = ''; } return $this->decorateAdvancedFields($document, $entity); @@ -111,11 +107,11 @@ class DocumentExport extends BaseExport private function decorateAdvancedFields(Document $document, array $entity) :array { - if(array_key_exists('record_type', $entity)) + if(in_array('record_type', $this->input['report_keys'])) $entity['record_type'] = class_basename($document->documentable); - if(array_key_exists('record_name', $entity)) - $entity['record_name'] = $document->hashed_id; + // if(in_array('record_name', $this->input['report_keys'])) + // $entity['record_name'] = $document->hashed_id; return $entity; } diff --git a/app/Export/CSV/ExpenseExport.php b/app/Export/CSV/ExpenseExport.php index 6d202556ade9..d4ccaaa23674 100644 --- a/app/Export/CSV/ExpenseExport.php +++ b/app/Export/CSV/ExpenseExport.php @@ -63,40 +63,6 @@ class ExpenseExport extends BaseExport 'invoice' => 'invoice_id', ]; - protected array $all_keys = [ - 'amount', - 'category_id', - 'client_id', - 'custom_value1', - 'custom_value2', - 'custom_value3', - 'custom_value4', - 'currency_id', - 'date', - 'exchange_rate', - 'foreign_amount', - 'invoice_currency_id', - 'payment_date', - 'number', - 'payment_type_id', - 'private_notes', - 'project_id', - 'public_notes', - 'tax_amount1', - 'tax_amount2', - 'tax_amount3', - 'tax_name1', - 'tax_name2', - 'tax_name3', - 'tax_rate1', - 'tax_rate2', - 'tax_rate3', - 'transaction_reference', - 'vendor_id', - 'invoice_id', - ]; - - private array $decorate_keys = [ 'client', 'currency', @@ -127,7 +93,7 @@ class ExpenseExport extends BaseExport $this->csv = Writer::createFromString(); if(count($this->input['report_keys']) == 0) - $this->input['report_keys'] = $this->all_keys; + $this->input['report_keys'] = array_values($this->entity_keys); //insert the header $this->csv->insertOne($this->buildHeader()); @@ -161,7 +127,13 @@ class ExpenseExport extends BaseExport foreach(array_values($this->input['report_keys']) as $key){ - $entity[$key] = $transformed_expense[$key]; + $keyval = array_search($key, $this->entity_keys); + + if(array_key_exists($key, $transformed_expense)) + $entity[$keyval] = $transformed_expense[$key]; + else + $entity[$keyval] = ''; + } return $this->decorateAdvancedFields($expense, $entity); @@ -170,26 +142,26 @@ class ExpenseExport extends BaseExport private function decorateAdvancedFields(Expense $expense, array $entity) :array { - if(array_key_exists('currency_id', $entity)) - $entity['currency_id'] = $expense->currency ? $expense->currency->code : ""; + if(in_array('currency_id', $this->input['report_keys'])) + $entity['currency'] = $expense->currency ? $expense->currency->code : ""; - if(array_key_exists('client_id', $entity)) - $entity['client_id'] = $expense->client ? $expense->client->present()->name() : ""; + if(in_array('client_id', $this->input['report_keys'])) + $entity['client'] = $expense->client ? $expense->client->present()->name() : ""; - if(array_key_exists('invoice_id', $entity)) - $entity['invoice_id'] = $expense->invoice ? $expense->invoice->number : ""; + if(in_array('invoice_id', $this->input['report_keys'])) + $entity['invoice'] = $expense->invoice ? $expense->invoice->number : ""; - if(array_key_exists('category_id', $entity)) - $entity['category_id'] = $expense->category ? $expense->category->name : ""; + if(in_array('category_id', $this->input['report_keys'])) + $entity['category'] = $expense->category ? $expense->category->name : ""; - if(array_key_exists('vendor_id', $entity)) - $entity['vendor_id'] = $expense->vendor ? $expense->vendor->name : ""; + if(in_array('vendor_id', $this->input['report_keys'])) + $entity['vendor'] = $expense->vendor ? $expense->vendor->name : ""; - if(array_key_exists('payment_type_id', $entity)) - $entity['payment_type_id'] = $expense->payment_type ? $expense->payment_type->name : ""; + if(in_array('payment_type_id', $this->input['report_keys'])) + $entity['payment_type'] = $expense->payment_type ? $expense->payment_type->name : ""; - if(array_key_exists('project_id', $entity)) - $entity['project_id'] = $expense->project ? $expense->project->name : ""; + if(in_array('project_id', $this->input['report_keys'])) + $entity['project'] = $expense->project ? $expense->project->name : ""; return $entity; diff --git a/app/Export/CSV/InvoiceExport.php b/app/Export/CSV/InvoiceExport.php index 79af4a7cf1e9..76ecf16a18c4 100644 --- a/app/Export/CSV/InvoiceExport.php +++ b/app/Export/CSV/InvoiceExport.php @@ -66,48 +66,13 @@ class InvoiceExport extends BaseExport 'currency_id' => 'currency_id' ]; - - protected array $all_keys = [ - 'amount', - 'balance', - 'client_id', - 'custom_surcharge1', - 'custom_surcharge2', - 'custom_surcharge3', - 'custom_surcharge4', - 'custom_value1', - 'custom_value2', - 'custom_value3', - 'custom_value4', - 'date', - 'discount', - 'due_date', - 'exchange_rate', - 'footer', - 'number', - 'paid_to_date', - 'partial', - 'partial_due_date', - 'po_number', - 'private_notes', - 'public_notes', - 'status_id', - 'tax_name1', - 'tax_name2', - 'tax_name3', - 'tax_rate1', - 'tax_rate2', - 'tax_rate3', - 'terms', - 'total_taxes', - 'currency_id', - ]; - private array $decorate_keys = [ 'country', 'client', 'currency_id', 'status', + 'vendor', + 'project', ]; public function __construct(Company $company, array $input) @@ -130,14 +95,15 @@ class InvoiceExport extends BaseExport $this->csv = Writer::createFromString(); if(count($this->input['report_keys']) == 0) - $this->input['report_keys'] = $this->all_keys; + $this->input['report_keys'] = array_values($this->entity_keys); //insert the header $this->csv->insertOne($this->buildHeader()); $query = Invoice::query() ->withTrashed() - ->with('client')->where('company_id', $this->company->id) + ->with('client') + ->where('company_id', $this->company->id) ->where('is_deleted',0); $query = $this->addDateRange($query); @@ -162,8 +128,12 @@ class InvoiceExport extends BaseExport foreach(array_values($this->input['report_keys']) as $key){ - if(array_key_exists($key, $transformed_invoice)) - $entity[$key] = $transformed_invoice[$key]; + $keyval = array_search($key, $this->entity_keys); + + if(array_key_exists($key, $transformed_invoice)) + $entity[$keyval] = $transformed_invoice[$key]; + else + $entity[$keyval] = ''; } return $this->decorateAdvancedFields($invoice, $entity); @@ -172,14 +142,17 @@ class InvoiceExport extends BaseExport private function decorateAdvancedFields(Invoice $invoice, array $entity) :array { - if(in_array('currency_id',$this->input['report_keys'])) - $entity['currency_id'] = $invoice->client->currency()->code ?: $invoice->company->currency()->code; + if(in_array('country_id', $this->input['report_keys'])) + $entity['country'] = $invoice->client->country ? ctrans("texts.country_{$invoice->client->country->name}") : ""; - if(in_array('client_id',$this->input['report_keys'])) - $entity['client_id'] = $invoice->client->present()->name(); + if(in_array('currency_id', $this->input['report_keys'])) + $entity['currency_id'] = $invoice->client->currency() ? $invoice->client->currency()->code : $invoice->company->currency()->code; + + if(in_array('client_id', $this->input['report_keys'])) + $entity['client'] = $invoice->client->present()->name(); if(in_array('status_id',$this->input['report_keys'])) - $entity['status_id'] = $invoice->stringStatus($invoice->status_id); + $entity['status'] = $invoice->stringStatus($invoice->status_id); return $entity; } diff --git a/app/Export/CSV/InvoiceItemExport.php b/app/Export/CSV/InvoiceItemExport.php index a70cf3328065..fdaceae88cc2 100644 --- a/app/Export/CSV/InvoiceItemExport.php +++ b/app/Export/CSV/InvoiceItemExport.php @@ -64,7 +64,7 @@ class InvoiceItemExport extends BaseExport 'terms' => 'terms', 'total_taxes' => 'total_taxes', 'currency' => 'currency_id', - 'qty' => 'item.quantity', + 'quantity' => 'item.quantity', 'unit_cost' => 'item.cost', 'product_key' => 'item.product_key', 'cost' => 'item.product_cost', @@ -85,64 +85,9 @@ class InvoiceItemExport extends BaseExport 'invoice4' => 'item.custom_value4', ]; - protected array $all_keys = [ - 'amount', - 'balance', - 'client_id', - 'custom_surcharge1', - 'custom_surcharge2', - 'custom_surcharge3', - 'custom_surcharge4', - 'custom_value1', - 'custom_value2', - 'custom_value3', - 'custom_value4', - 'date', - 'discount', - 'due_date', - 'exchange_rate', - 'footer', - 'number', - 'paid_to_date', - 'partial', - 'partial_due_date', - 'po_number', - 'private_notes', - 'public_notes', - 'status_id', - 'tax_name1', - 'tax_name2', - 'tax_name3', - 'tax_rate1', - 'tax_rate2', - 'tax_rate3', - 'terms', - 'total_taxes', - // 'currency_id', - 'item.quantity', - 'item.cost', - 'item.product_key', - 'item.product_cost', - 'item.notes', - 'item.discount', - 'item.is_amount_discount', - 'item.tax_rate1', - 'item.tax_rate2', - 'item.tax_rate3', - 'item.tax_name1', - 'item.tax_name2', - 'item.tax_name3', - 'item.line_total', - 'item.gross_line_total', - 'item.custom_value1', - 'item.custom_value2', - 'item.custom_value3', - 'item.custom_value4', - ]; - private array $decorate_keys = [ 'client', - 'currency', + 'currency_id', ]; public function __construct(Company $company, array $input) @@ -165,7 +110,7 @@ class InvoiceItemExport extends BaseExport $this->csv = Writer::createFromString(); if(count($this->input['report_keys']) == 0) - $this->input['report_keys'] = ksort($this->all_keys); + $this->input['report_keys'] = array_values($this->entity_keys); //insert the header $this->csv->insertOne($this->buildHeader()); @@ -209,16 +154,20 @@ class InvoiceItemExport extends BaseExport $entity = []; - $transformed_items = array_merge($transformed_invoice, $item_array); - - $transformed_items = $this->decorateAdvancedFields($invoice, $transformed_items); - foreach(array_values($this->input['report_keys']) as $key) { - $key = str_replace("item.", "", $key); - $entity[$key] = $transformed_items[$key]; + $keyval = array_search($key, $this->entity_keys); + + if(array_key_exists($key, $transformed_items)) + $entity[$keyval] = $transformed_items[$key]; + else + $entity[$keyval] = ""; + } + $transformed_items = array_merge($transformed_invoice, $item_array); + $entity = $this->decorateAdvancedFields($invoice, $transformed_items); + $this->csv->insertOne($entity); } @@ -234,8 +183,12 @@ class InvoiceItemExport extends BaseExport foreach(array_values($this->input['report_keys']) as $key){ - if(!str_contains($key, "item.")) - $entity[$key] = $transformed_invoice[$key]; + $keyval = array_search($key, $this->entity_keys); + + if(array_key_exists($key, $transformed_invoice)) + $entity[$keyval] = $transformed_invoice[$key]; + else + $entity[$keyval] = ""; } @@ -245,14 +198,14 @@ class InvoiceItemExport extends BaseExport private function decorateAdvancedFields(Invoice $invoice, array $entity) :array { - if(array_key_exists('currency_id', $entity)) - $entity['currency_id'] = $invoice->client->currency()->code; + if(in_array('currency_id', $this->input['report_keys'])) + $entity['currency'] = $invoice->client->currency() ? $invoice->client->currency()->code : $invoice->company->currency()->code; - if(array_key_exists('client_id', $entity)) - $entity['client_id'] = $invoice->client->present()->name(); + if(in_array('client_id', $this->input['report_keys'])) + $entity['client'] = $invoice->client->present()->name(); - if(array_key_exists('status_id', $entity)) - $entity['status_id'] = $invoice->stringStatus($invoice->status_id); + if(in_array('status_id', $this->input['report_keys'])) + $entity['status'] = $invoice->stringStatus($invoice->status_id); return $entity; } diff --git a/app/Export/CSV/PaymentExport.php b/app/Export/CSV/PaymentExport.php index 86a8d1be9c67..0b5b4f309772 100644 --- a/app/Export/CSV/PaymentExport.php +++ b/app/Export/CSV/PaymentExport.php @@ -43,7 +43,7 @@ class PaymentExport extends BaseExport 'custom_value4' => 'custom_value4', 'date' => 'date', 'exchange_currency' => 'exchange_currency_id', - 'gateway_type' => 'gateway_type_id', + 'gateway' => 'gateway_type_id', 'number' => 'number', 'private_notes' => 'private_notes', 'project' => 'project_id', @@ -54,28 +54,6 @@ class PaymentExport extends BaseExport 'vendor' => 'vendor_id', ]; - protected array $all_keys = [ - 'amount', - 'applied', - 'client_id', - 'currency_id', - 'custom_value1', - 'custom_value2', - 'custom_value3', - 'custom_value4', - 'date', - 'exchange_currency_id', - 'gateway_type_id', - 'number', - 'private_notes', - 'project_id', - 'refunded', - 'status_id', - 'transaction_reference', - 'type_id', - 'vendor_id', - ]; - private array $decorate_keys = [ 'vendor', 'status', @@ -106,7 +84,7 @@ class PaymentExport extends BaseExport $this->csv = Writer::createFromString(); if(count($this->input['report_keys']) == 0) - $this->input['report_keys'] = $this->all_keys; + $this->input['report_keys'] = array_values($this->entity_keys); //insert the header $this->csv->insertOne($this->buildHeader()); @@ -135,7 +113,12 @@ class PaymentExport extends BaseExport foreach(array_values($this->input['report_keys']) as $key){ - $entity[$key] = $transformed_entity[$key]; + $keyval = array_search($key, $this->entity_keys); + + if(array_key_exists($key, $transformed_entity)) + $entity[$keyval] = $transformed_entity[$key]; + else + $entity[$keyval] = ''; } @@ -146,26 +129,29 @@ class PaymentExport extends BaseExport private function decorateAdvancedFields(Payment $payment, array $entity) :array { - if(array_key_exists('status_id', $entity)) - $entity['status_id'] = $payment->stringStatus($payment->status_id); + if(in_array('status_id', $this->input['report_keys'])) + $entity['status'] = $payment->stringStatus($payment->status_id); - if(array_key_exists('vendor_id', $entity)) - $entity['vendor_id'] = $payment->vendor()->exists() ? $payment->vendor->name : ''; + if(in_array('vendor_id', $this->input['report_keys'])) + $entity['vendor'] = $payment->vendor()->exists() ? $payment->vendor->name : ''; - if(array_key_exists('project_id', $entity)) - $entity['project_id'] = $payment->project()->exists() ? $payment->project->name : ''; + if(in_array('project_id', $this->input['report_keys'])) + $entity['project'] = $payment->project()->exists() ? $payment->project->name : ''; - if(array_key_exists('currency_id', $entity)) - $entity['currency_id'] = $payment->currency()->exists() ? $payment->currency->code : ''; + if(in_array('currency_id', $this->input['report_keys'])) + $entity['currency'] = $payment->currency()->exists() ? $payment->currency->code : ''; - if(array_key_exists('exchange_currency_id', $entity)) - $entity['exchange_currency_id'] = $payment->exchange_currency()->exists() ? $payment->exchange_currency->code : ''; + if(in_array('exchange_currency_id', $this->input['report_keys'])) + $entity['exchange_currency'] = $payment->exchange_currency()->exists() ? $payment->exchange_currency->code : ''; - if(array_key_exists('client_id', $entity)) - $entity['client_id'] = $payment->client->present()->name(); + if(in_array('client_id', $this->input['report_keys'])) + $entity['client'] = $payment->client->present()->name(); - if(array_key_exists('type_id', $entity)) - $entity['type_id'] = $payment->translatedType(); + if(in_array('type_id', $this->input['report_keys'])) + $entity['type'] = $payment->translatedType(); + + if(in_array('gateway_type_id', $this->input['report_keys'])) + $entity['gateway'] = $payment->gateway_type ? $payment->gateway_type->name : "Unknown Type"; return $entity; } diff --git a/app/Export/CSV/ProductExport.php b/app/Export/CSV/ProductExport.php index bf02fd1acd63..9523f8ad050a 100644 --- a/app/Export/CSV/ProductExport.php +++ b/app/Export/CSV/ProductExport.php @@ -52,26 +52,6 @@ class ProductExport extends BaseExport 'tax_name3' => 'tax_name3', ]; - protected array $all_keys = [ - 'project_id', - 'vendor_id', - 'custom_value1', - 'custom_value2', - 'custom_value3', - 'custom_value4', - 'product_key', - 'notes', - 'cost', - 'price', - 'quantity', - 'tax_rate1', - 'tax_rate2', - 'tax_rate3', - 'tax_name1', - 'tax_name2', - 'tax_name3', - ]; - private array $decorate_keys = [ 'vendor', 'project', @@ -97,7 +77,7 @@ class ProductExport extends BaseExport $this->csv = Writer::createFromString(); if(count($this->input['report_keys']) == 0) - $this->input['report_keys'] = $this->all_keys; + $this->input['report_keys'] = array_values($this->entity_keys); //insert the header $this->csv->insertOne($this->buildHeader()); @@ -126,7 +106,13 @@ class ProductExport extends BaseExport foreach(array_values($this->input['report_keys']) as $key){ - $entity[$key] = $transformed_entity[$key]; + $keyval = array_search($key, $this->entity_keys); + + if(array_key_exists($key, $transformed_entity)) + $entity[$keyval] = $transformed_entity[$key]; + else + $entity[$keyval] = ''; + } @@ -137,11 +123,11 @@ class ProductExport extends BaseExport private function decorateAdvancedFields(Product $product, array $entity) :array { - if(array_key_exists('vendor_id', $entity)) - $entity['vendor_id'] = $product->vendor()->exists() ? $product->vendor->name : ''; + if(in_array('vendor_id', $this->input['report_keys'])) + $entity['vendor'] = $product->vendor()->exists() ? $product->vendor->name : ''; - if(array_key_exists('project_id', $entity)) - $entity['project_id'] = $product->project()->exists() ? $product->project->name : ''; + if(array_key_exists('project_id', $this->input['report_keys'])) + $entity['project'] = $product->project()->exists() ? $product->project->name : ''; return $entity; } diff --git a/app/Export/CSV/QuoteExport.php b/app/Export/CSV/QuoteExport.php index f4589e1b15e5..fe1dff206eb6 100644 --- a/app/Export/CSV/QuoteExport.php +++ b/app/Export/CSV/QuoteExport.php @@ -63,48 +63,10 @@ class QuoteExport extends BaseExport 'tax_rate3' => 'tax_rate3', 'terms' => 'terms', 'total_taxes' => 'total_taxes', - 'currency' => 'client_id', + 'currency' => 'currency_id', 'invoice' => 'invoice_id', ]; - protected array $all_keys = [ - 'amount', - 'balance', - 'client_id', - 'custom_surcharge1', - 'custom_surcharge2', - 'custom_surcharge3', - 'custom_surcharge4', - 'custom_value1', - 'custom_value2', - 'custom_value3', - 'custom_value4', - 'date', - 'discount', - 'due_date', - 'exchange_rate', - 'footer', - 'number', - 'paid_to_date', - 'partial', - 'partial_due_date', - 'po_number', - 'private_notes', - 'public_notes', - 'status_id', - 'tax_name1', - 'tax_name2', - 'tax_name3', - 'tax_rate1', - 'tax_rate2', - 'tax_rate3', - 'terms', - 'total_taxes', - 'client_id', - 'invoice_id', - ]; - - private array $decorate_keys = [ 'client', 'currency', @@ -131,13 +93,14 @@ class QuoteExport extends BaseExport $this->csv = Writer::createFromString(); if(count($this->input['report_keys']) == 0) - $this->input['report_keys'] = $this->all_keys; + $this->input['report_keys'] = array_values($this->entity_keys); //insert the header $this->csv->insertOne($this->buildHeader()); $query = Quote::query() - ->with('client')->where('company_id', $this->company->id) + ->with('client') + ->where('company_id', $this->company->id) ->where('is_deleted',0); $query = $this->addDateRange($query); @@ -163,7 +126,12 @@ class QuoteExport extends BaseExport foreach(array_values($this->input['report_keys']) as $key){ - $entity[$key] = $transformed_quote[$key]; + $keyval = array_search($key, $this->entity_keys); + + if(array_key_exists($key, $transformed_quote)) + $entity[$keyval] = $transformed_quote[$key]; + else + $entity[$keyval] = ''; } return $this->decorateAdvancedFields($quote, $entity); @@ -172,13 +140,16 @@ class QuoteExport extends BaseExport private function decorateAdvancedFields(Quote $quote, array $entity) :array { - if(array_key_exists('currency', $entity)) + if(in_array('currency_id', $this->input['report_keys'])) $entity['currency'] = $quote->client->currency()->code; - if(array_key_exists('client_id', $entity)) - $entity['client_id'] = $quote->client->present()->name(); + if(in_array('client_id', $this->input['report_keys'])) + $entity['client'] = $quote->client->present()->name(); - if(array_key_exists('invoice', $entity)) + if(in_array('status_id',$this->input['report_keys'])) + $entity['status'] = $quote->stringStatus($quote->status_id); + + if(in_array('invoice_id', $this->input['report_keys'])) $entity['invoice'] = $quote->invoice ? $quote->invoice->number : ""; return $entity; diff --git a/app/Export/CSV/QuoteItemExport.php b/app/Export/CSV/QuoteItemExport.php index bb6e499394f0..fb00ab918877 100644 --- a/app/Export/CSV/QuoteItemExport.php +++ b/app/Export/CSV/QuoteItemExport.php @@ -79,66 +79,12 @@ class QuoteItemExport extends BaseExport 'tax_name3' => 'item.tax_name3', 'line_total' => 'item.line_total', 'gross_line_total' => 'item.gross_line_total', - 'invoice1' => 'item.custom_value1', - 'invoice2' => 'item.custom_value2', - 'invoice3' => 'item.custom_value3', - 'invoice4' => 'item.custom_value4', + 'custom_value1' => 'item.custom_value1', + 'custom_value2' => 'item.custom_value2', + 'custom_value3' => 'item.custom_value3', + 'custom_value4' => 'item.custom_value4', ]; - protected array $all_keys = [ - 'amount', - 'balance', - 'client_id', - 'custom_surcharge1', - 'custom_surcharge2', - 'custom_surcharge3', - 'custom_surcharge4', - 'custom_value1', - 'custom_value2', - 'custom_value3', - 'custom_value4', - 'date', - 'discount', - 'due_date', - 'exchange_rate', - 'footer', - 'number', - 'paid_to_date', - 'partial', - 'partial_due_date', - 'po_number', - 'private_notes', - 'public_notes', - 'status_id', - 'tax_name1', - 'tax_name2', - 'tax_name3', - 'tax_rate1', - 'tax_rate2', - 'tax_rate3', - 'terms', - 'total_taxes', - 'currency_id', - 'item.quantity', - 'item.cost', - 'item.product_key', - 'item.product_cost', - 'item.notes', - 'item.discount', - 'item.is_amount_discount', - 'item.tax_rate1', - 'item.tax_rate2', - 'item.tax_rate3', - 'item.tax_name1', - 'item.tax_name2', - 'item.tax_name3', - 'item.line_total', - 'item.gross_line_total', - 'item.custom_value1', - 'item.custom_value2', - 'item.custom_value3', - 'item.custom_value4', - ]; private array $decorate_keys = [ 'client', @@ -165,7 +111,8 @@ class QuoteItemExport extends BaseExport $this->csv = Writer::createFromString(); if(count($this->input['report_keys']) == 0) - $this->input['report_keys'] = $this->all_keys; + $this->input['report_keys'] = array_values($this->entity_keys); + //insert the header $this->csv->insertOne($this->buildHeader()); @@ -209,16 +156,20 @@ class QuoteItemExport extends BaseExport $entity = []; - $transformed_items = array_merge($transformed_quote, $item_array); - - $transformed_items = $this->decorateAdvancedFields($quote, $transformed_items); - foreach(array_values($this->input['report_keys']) as $key) { - $key = str_replace("item.", "", $key); - $entity[$key] = $transformed_items[$key]; + $keyval = array_search($key, $this->entity_keys); + + if(array_key_exists($key, $transformed_items)) + $entity[$keyval] = $transformed_items[$key]; + else + $entity[$keyval] = ""; } + + $transformed_items = array_merge($transformed_quote, $item_array); + $entity = $this->decorateAdvancedFields($quote, $transformed_items); + $this->csv->insertOne($entity); } @@ -234,8 +185,12 @@ class QuoteItemExport extends BaseExport foreach(array_values($this->input['report_keys']) as $key){ - if(!str_contains($key, "item.")) - $entity[$key] = $transformed_quote[$key]; + $keyval = array_search($key, $this->entity_keys); + + if(array_key_exists($key, $transformed_quote)) + $entity[$keyval] = $transformed_quote[$key]; + else + $entity[$keyval] = ""; } @@ -245,14 +200,14 @@ class QuoteItemExport extends BaseExport private function decorateAdvancedFields(Quote $quote, array $entity) :array { - if(array_key_exists('currency_id', $entity)) - $entity['currency_id'] = $quote->client->currency()->code; + if(in_array('currency_id', $this->input['report_keys'])) + $entity['currency'] = $quote->client->currency() ? $quote->client->currency()->code : $quote->company->currency()->code; - if(array_key_exists('client_id', $entity)) - $entity['client_id'] = $quote->client->present()->name(); + if(in_array('client_id', $this->input['report_keys'])) + $entity['client'] = $quote->client->present()->name(); - if(array_key_exists('status_id', $entity)) - $entity['status_id'] = $quote->stringStatus($quote->status_id); + if(in_array('status_id', $this->input['report_keys'])) + $entity['status'] = $quote->stringStatus($quote->status_id); return $entity; } diff --git a/app/Export/CSV/RecurringInvoiceExport.php b/app/Export/CSV/RecurringInvoiceExport.php index 81738e80d5d5..47232ebcfa12 100644 --- a/app/Export/CSV/RecurringInvoiceExport.php +++ b/app/Export/CSV/RecurringInvoiceExport.php @@ -63,49 +63,11 @@ class RecurringInvoiceExport extends BaseExport 'tax_rate3' => 'tax_rate3', 'terms' => 'terms', 'total_taxes' => 'total_taxes', - 'currency' => 'client_id', + 'currency' => 'currency_id', 'vendor' => 'vendor_id', 'project' => 'project_id', ]; - protected array $all_keys = [ - 'amount', - 'balance', - 'client_id', - 'custom_surcharge1', - 'custom_surcharge2', - 'custom_surcharge3', - 'custom_surcharge4', - 'custom_value1', - 'custom_value2', - 'custom_value3', - 'custom_value4', - 'date', - 'discount', - 'due_date', - 'exchange_rate', - 'footer', - 'number', - 'paid_to_date', - 'partial', - 'partial_due_date', - 'po_number', - 'private_notes', - 'public_notes', - 'status_id', - 'tax_name1', - 'tax_name2', - 'tax_name3', - 'tax_rate1', - 'tax_rate2', - 'tax_rate3', - 'terms', - 'total_taxes', - 'client_id', - 'vendor_id', - 'project_id', - ]; - private array $decorate_keys = [ 'country', 'client', @@ -135,7 +97,7 @@ class RecurringInvoiceExport extends BaseExport $this->csv = Writer::createFromString(); if(count($this->input['report_keys']) == 0) - $this->input['report_keys'] = $this->all_keys; + $this->input['report_keys'] = array_values($this->entity_keys); //insert the header $this->csv->insertOne($this->buildHeader()); @@ -167,7 +129,13 @@ class RecurringInvoiceExport extends BaseExport foreach(array_values($this->input['report_keys']) as $key){ - $entity[$key] = $transformed_invoice[$key]; + $keyval = array_search($key, $this->entity_keys); + + if(array_key_exists($key, $transformed_invoice)) + $entity[$keyval] = $transformed_invoice[$key]; + else + $entity[$keyval] = ''; + } return $this->decorateAdvancedFields($invoice, $entity); @@ -176,20 +144,23 @@ class RecurringInvoiceExport extends BaseExport private function decorateAdvancedFields(RecurringInvoice $invoice, array $entity) :array { - if(array_key_exists('currency', $entity)) - $entity['currency'] = $invoice->client->currency()->code; + if(in_array('country_id', $this->input['report_keys'])) + $entity['country'] = $invoice->client->country ? ctrans("texts.country_{$invoice->client->country->name}") : ""; - if(array_key_exists('client_id', $entity)) - $entity['client_id'] = $invoice->client->present()->name(); + if(in_array('currency_id', $this->input['report_keys'])) + $entity['currency'] = $invoice->client->currency() ? $invoice->client->currency()->code : $invoice->company->currency()->code; - if(array_key_exists('status_id', $entity)) - $entity['status_id'] = $invoice->stringStatus($invoice->status_id); + if(in_array('client_id', $this->input['report_keys'])) + $entity['client'] = $invoice->client->present()->name(); - if(array_key_exists('vendor_id', $entity)) - $entity['vendor_id'] = $invoice->vendor()->exists() ? $invoice->vendor->name : ''; + if(in_array('status_id',$this->input['report_keys'])) + $entity['status'] = $invoice->stringStatus($invoice->status_id); - if(array_key_exists('project_id', $entity)) - $entity['project'] = $invoice->project()->exists() ? $invoice->project->name : ''; + if(in_array('project_id',$this->input['report_keys'])) + $entity['project'] = $invoice->project ? $invoice->project->name : ""; + + if(in_array('vendor_id',$this->input['report_keys'])) + $entity['vendor'] = $invoice->vendor ? $invoice->vendor->name : ""; return $entity; } diff --git a/app/Export/CSV/TaskExport.php b/app/Export/CSV/TaskExport.php index 5ccdfa5d54d0..f700c8c83063 100644 --- a/app/Export/CSV/TaskExport.php +++ b/app/Export/CSV/TaskExport.php @@ -52,23 +52,6 @@ class TaskExport extends BaseExport 'client' => 'client_id', ]; - protected array $all_keys = [ - 'start_date', - 'end_date', - 'duration', - 'rate', - 'number', - 'description', - 'custom_value1', - 'custom_value2', - 'custom_value3', - 'custom_value4', - 'status_id', - 'project_id', - 'invoice_id', - 'client_id', - ]; - private array $decorate_keys = [ 'status', 'project', @@ -99,10 +82,12 @@ class TaskExport extends BaseExport //load the CSV document from a string $this->csv = Writer::createFromString(); - + + ksort($this->entity_keys); if(count($this->input['report_keys']) == 0) - $this->input['report_keys'] = $this->all_keys; + $this->input['report_keys'] = array_values($this->entity_keys); + //insert the header $this->csv->insertOne($this->buildHeader()); @@ -132,27 +117,35 @@ class TaskExport extends BaseExport foreach(array_values($this->input['report_keys']) as $key){ + $keyval = array_search($key, $this->entity_keys); + if(array_key_exists($key, $transformed_entity)) - $entity[$key] = $transformed_entity[$key]; + $entity[$keyval] = $transformed_entity[$key]; else - $entity[$key] = ''; + $entity[$keyval] = ''; } + $entity['start_date'] = ""; + $entity['end_date'] = ""; + $entity['duration'] = ""; + $entity = $this->decorateAdvancedFields($task, $entity); + ksort($entity); $this->csv->insertOne($entity); + } elseif(is_array(json_decode($task->time_log,1)) && count(json_decode($task->time_log,1)) > 0) { foreach(array_values($this->input['report_keys']) as $key){ - if(array_key_exists($key, $transformed_entity)) - $entity[$key] = $transformed_entity[$key]; - else - $entity[$key] = ''; - } + $keyval = array_search($key, $this->entity_keys); - $entity = $this->decorateAdvancedFields($task, $entity); + if(array_key_exists($key, $transformed_entity)) + $entity[$keyval] = $transformed_entity[$key]; + else + $entity[$keyval] = ''; + } $this->iterateLogs($task, $entity); } @@ -168,30 +161,45 @@ class TaskExport extends BaseExport $timezone_name = $timezone->name; $logs = json_decode($task->time_log,1); + + $date_format_default = "Y-m-d"; + + $date_format = DateFormat::find($task->company->settings->date_format_id); + + if($date_format) + $date_format_default = $date_format->format; foreach($logs as $key => $item) { - if(in_array("start_date",$this->input['report_keys'])){ - $entity['start_date'] = Carbon::createFromTimeStamp($item[0])->setTimezone($timezone_name); - nlog("start date" . $entity['start_date']); + if(in_array("start_date", $this->input['report_keys'])){ + $entity['start_date'] = Carbon::createFromTimeStamp($item[0])->setTimezone($timezone_name)->format($date_format_default); } - if(in_array("end_date",$this->input['report_keys']) && $item[1] > 0){ - $entity['end_date'] = Carbon::createFromTimeStamp($item[1])->setTimezone($timezone_name); - nlog("start date" . $entity['end_date']); + if(in_array("end_date", $this->input['report_keys']) && $item[1] > 0){ + $entity['end_date'] = Carbon::createFromTimeStamp($item[1])->setTimezone($timezone_name)->format($date_format_default); } - if(in_array("end_date",$this->input['report_keys']) && $item[1] == 0){ + if(in_array("end_date", $this->input['report_keys']) && $item[1] == 0){ $entity['end_date'] = ctrans('texts.is_running'); - nlog("start date" . $entity['end_date']); } - if(in_array("duration",$this->input['report_keys'])){ + if(in_array("duration", $this->input['report_keys'])){ $entity['duration'] = $task->calcDuration(); - nlog("duration" . $entity['duration']); } + if(!array_key_exists('duration', $entity)) + $entity['duration'] = ""; + + if(!array_key_exists('start_date', $entity)) + $entity['start_date'] = ""; + + if(!array_key_exists('end_date', $entity)) + $entity['end_date'] = ""; + + $entity = $this->decorateAdvancedFields($task, $entity); + + ksort($entity); $this->csv->insertOne($entity); unset($entity['start_date']); @@ -204,16 +212,17 @@ class TaskExport extends BaseExport private function decorateAdvancedFields(Task $task, array $entity) :array { - if(array_key_exists('status_id', $entity)) - $entity['status_id'] = $task->status()->exists() ? $task->status->name : ''; + if(in_array('status_id', $this->input['report_keys'])) + $entity['status'] = $task->status()->exists() ? $task->status->name : ''; - if(array_key_exists('project_id', $entity)) - $entity['project_id'] = $task->project()->exists() ? $task->project->name : ''; - - if(array_key_exists('client_id', $entity)) - $entity['client_id'] = $task->client->present()->name(); + if(in_array('project_id', $this->input['report_keys'])) + $entity['project'] = $task->project()->exists() ? $task->project->name : ''; + if(in_array('client_id', $this->input['report_keys'])) + $entity['client'] = $task->client ? $task->client->present()->name() : ""; + if(in_array('invoice_id', $this->input['report_keys'])) + $entity['invoice'] = $task->invoice ? $task->invoice->number : ""; return $entity; } diff --git a/app/Factory/ExpenseFactory.php b/app/Factory/ExpenseFactory.php index e41b282415c8..997412ef1b5f 100644 --- a/app/Factory/ExpenseFactory.php +++ b/app/Factory/ExpenseFactory.php @@ -39,6 +39,9 @@ class ExpenseFactory $expense->custom_value2 = ''; $expense->custom_value3 = ''; $expense->custom_value4 = ''; + $expense->tax_amount1 = 0; + $expense->tax_amount2 = 0; + $expense->tax_amount3 = 0; return $expense; } diff --git a/app/Factory/PaymentFactory.php b/app/Factory/PaymentFactory.php index 3af36ee1b21d..e746cfc0834a 100644 --- a/app/Factory/PaymentFactory.php +++ b/app/Factory/PaymentFactory.php @@ -33,6 +33,7 @@ class PaymentFactory $payment->transaction_reference = null; $payment->payer_id = null; $payment->status_id = Payment::STATUS_PENDING; + $payment->exchange_rate = 1; return $payment; } diff --git a/app/Http/Controllers/ClientPortal/PaymentMethodController.php b/app/Http/Controllers/ClientPortal/PaymentMethodController.php index 0c4407f280a5..778723809956 100644 --- a/app/Http/Controllers/ClientPortal/PaymentMethodController.php +++ b/app/Http/Controllers/ClientPortal/PaymentMethodController.php @@ -30,6 +30,11 @@ class PaymentMethodController extends Controller { use MakesDates; + public function __construct() + { + $this->middleware('throttle:10,1')->only('store'); + } + /** * Display a listing of the resource. * @@ -92,7 +97,6 @@ class PaymentMethodController extends Controller public function verify(ClientGatewayToken $payment_method) { -// $gateway = $this->getClientGateway(); return $payment_method->gateway ->driver(auth()->user()->client) diff --git a/app/Http/Controllers/Reports/ProfitAndLossController.php b/app/Http/Controllers/Reports/ProfitAndLossController.php new file mode 100644 index 000000000000..7c2ea81ef56b --- /dev/null +++ b/app/Http/Controllers/Reports/ProfitAndLossController.php @@ -0,0 +1,87 @@ +user()->company(), $request->all()); + $pnl->build(); + + $csv = $pnl->getCsv(); + + $headers = array( + 'Content-Disposition' => 'attachment', + 'Content-Type' => 'text/csv', + ); + + return response()->streamDownload(function () use ($csv) { + echo $csv; + }, $this->filename, $headers); + + } + + + +} diff --git a/app/Http/Requests/Report/GenericReportRequest.php b/app/Http/Requests/Report/GenericReportRequest.php index 9f80bd87c719..e49bfb872d9c 100644 --- a/app/Http/Requests/Report/GenericReportRequest.php +++ b/app/Http/Requests/Report/GenericReportRequest.php @@ -32,7 +32,7 @@ class GenericReportRequest extends Request 'end_date' => 'string|date', 'date_key' => 'string', 'date_range' => 'string', - 'report_keys' => 'sometimes|array' + 'report_keys' => 'present|array' ]; } } diff --git a/app/Http/Requests/Report/ProfitLossRequest.php b/app/Http/Requests/Report/ProfitLossRequest.php new file mode 100644 index 000000000000..44fc0cae3937 --- /dev/null +++ b/app/Http/Requests/Report/ProfitLossRequest.php @@ -0,0 +1,39 @@ +user()->isAdmin(); + } + + public function rules() + { + return [ + 'start_date' => 'string|date', + 'end_date' => 'string|date', + 'is_income_billed' => 'required|bail|bool', + 'is_expense_billed' => 'required|bail|bool', + 'include_tax' => 'required|bail|bool', + 'date_range' => 'required|bail|string' + ]; + } +} diff --git a/app/Http/ValidationRules/Account/BlackListRule.php b/app/Http/ValidationRules/Account/BlackListRule.php index 47f8e9748a6c..8dee26c6ad2b 100644 --- a/app/Http/ValidationRules/Account/BlackListRule.php +++ b/app/Http/ValidationRules/Account/BlackListRule.php @@ -21,6 +21,8 @@ class BlackListRule implements Rule { private array $blacklist = [ 'candassociates.com', + 'vusra.com', + 'fourthgenet.com', ]; /** diff --git a/app/Jobs/Company/CompanyImport.php b/app/Jobs/Company/CompanyImport.php index 8eb40800f5e7..3a8529262d43 100644 --- a/app/Jobs/Company/CompanyImport.php +++ b/app/Jobs/Company/CompanyImport.php @@ -277,11 +277,13 @@ class CompanyImport implements ShouldQueue 'errors' => [] ]; + $_company = Company::find($this->company->id); + $nmo = new NinjaMailerObject; - $nmo->mailable = new ImportCompleted($this->company, $data); - $nmo->company = $this->company; - $nmo->settings = $this->company->settings; - $nmo->to_user = $this->company->owner(); + $nmo->mailable = new ImportCompleted($_company, $data); + $nmo->company = $_company; + $nmo->settings = $_company->settings; + $nmo->to_user = $_company->owner(); NinjaMailerJob::dispatchNow($nmo); } @@ -1450,6 +1452,20 @@ class CompanyImport implements ShouldQueue $new_obj->save(['timestamps' => false]); $new_obj->number = $this->getNextRecurringExpenseNumber($new_obj); } + elseif($class == 'App\Models\Project' && is_null($obj->{$match_key})){ + $new_obj = new Project(); + $new_obj->company_id = $this->company->id; + $new_obj->fill($obj_array); + $new_obj->save(['timestamps' => false]); + $new_obj->number = $this->getNextProjectNumber($new_obj); + } + elseif($class == 'App\Models\Task' && is_null($obj->{$match_key})){ + $new_obj = new Task(); + $new_obj->company_id = $this->company->id; + $new_obj->fill($obj_array); + $new_obj->save(['timestamps' => false]); + $new_obj->number = $this->getNextTaskNumber($new_obj); + } elseif($class == 'App\Models\CompanyLedger'){ $new_obj = $class::firstOrNew( [$match_key => $obj->{$match_key}, 'company_id' => $this->company->id], @@ -1514,10 +1530,9 @@ class CompanyImport implements ShouldQueue } if (! array_key_exists($resource, $this->ids)) { - nlog($resource); $this->sendImportMail("The Import failed due to missing data in the import file. Resource {$resource} not available."); - nlog($this->ids); + throw new \Exception("Resource {$resource} not available."); } @@ -1547,8 +1562,10 @@ class CompanyImport implements ShouldQueue $t = app('translator'); $t->replace(Ninja::transformTranslations($this->company->settings)); + $_company = Company::find($this->company->id); + $nmo = new NinjaMailerObject; - $nmo->mailable = new CompanyImportFailure($this->company, $message); + $nmo->mailable = new CompanyImportFailure($_company, $message); $nmo->company = $this->company; $nmo->settings = $this->company->settings; $nmo->to_user = $this->company->owner(); diff --git a/app/Jobs/Mail/NinjaMailerJob.php b/app/Jobs/Mail/NinjaMailerJob.php index c128088de30d..80487f24912d 100644 --- a/app/Jobs/Mail/NinjaMailerJob.php +++ b/app/Jobs/Mail/NinjaMailerJob.php @@ -131,7 +131,7 @@ class NinjaMailerJob implements ShouldQueue $response = $e->getResponse(); $message_body = json_decode($response->getBody()->getContents()); - if(property_exists($message_body, 'Message')){ + if($message_body && property_exists($message_body, 'Message')){ $message = $message_body->Message; nlog($message); } @@ -268,9 +268,10 @@ class NinjaMailerJob implements ShouldQueue return false; /* On the hosted platform, if the user is over the email quotas, we do not send the email. */ - if(Ninja::isHosted() && $this->company->account->emailQuotaExceeded()) + if(Ninja::isHosted() && $this->company->account && $this->company->account->emailQuotaExceeded()) return true; + /* Ensure the user has a valid email address */ if(!str_contains($this->nmo->to_user->email, "@")) return true; diff --git a/app/Jobs/Report/ProfitAndLoss.php b/app/Jobs/Report/ProfitAndLoss.php new file mode 100644 index 000000000000..da26197b4148 --- /dev/null +++ b/app/Jobs/Report/ProfitAndLoss.php @@ -0,0 +1,89 @@ +company = $company; + + $this->payload = $payload; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() : void + { + + MultiDB::setDb($this->company->db); + + /* + payload variables. + + start_date - Y-m-d + end_date - Y-m-d + date_range - + all + last7 + last30 + this_month + last_month + this_quarter + last_quarter + this_year + custom + income_billed - true = Invoiced || false = Payments + expense_billed - true = Expensed || false = Expenses marked as paid + include_tax - true tax_included || false - tax_excluded + + */ + + $pl = new ProfitLoss($this->company, $this->payload); + + $pl->build(); + + } + + + + + + public function failed($exception = null) + { + + } +} diff --git a/app/Mail/Company/CompanyDeleted.php b/app/Mail/Company/CompanyDeleted.php index ffb4d1d6bed8..5a536f6e0cbb 100644 --- a/app/Mail/Company/CompanyDeleted.php +++ b/app/Mail/Company/CompanyDeleted.php @@ -50,10 +50,6 @@ class CompanyDeleted extends Mailable public function build() { App::forgetInstance('translator'); - - if($this->company) - App::setLocale($this->company->getLocale()); - $t = app('translator'); $t->replace(Ninja::transformTranslations($this->settings)); diff --git a/app/Mail/Engine/InvoiceEmailEngine.php b/app/Mail/Engine/InvoiceEmailEngine.php index 300ddedebd2b..99bf3041515f 100644 --- a/app/Mail/Engine/InvoiceEmailEngine.php +++ b/app/Mail/Engine/InvoiceEmailEngine.php @@ -14,14 +14,19 @@ namespace App\Mail\Engine; use App\DataMapper\EmailTemplateDefaults; use App\Jobs\Entity\CreateEntityPdf; use App\Models\Account; +use App\Models\Expense; +use App\Models\Task; use App\Utils\HtmlEngine; use App\Utils\Ninja; use App\Utils\Number; +use App\Utils\Traits\MakesHash; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Lang; class InvoiceEmailEngine extends BaseEmailEngine { + use MakesHash; + public $invitation; public $client; @@ -146,6 +151,53 @@ class InvoiceEmailEngine extends BaseEmailEngine $this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => $document->type]]); } + $line_items = $this->invoice->line_items; + + $expense_ids = []; + + foreach($line_items as $item) + { + if(property_exists($item, 'expense_id')) + { + $expense_ids[] = $item->expense_id; + } + + if(count($expense_ids) > 0){ + + $expenses = Expense::whereIn('id', $this->transformKeys($expense_ids)) + ->where('invoice_documents', 1) + ->cursor() + ->each(function ($expense){ + + foreach($expense->documents as $document) + { + $this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => $document->type]]); + } + + }); + } + + if(property_exists($item, 'task_id')) + { + $task_ids[] = $item->task_id; + } + + if(count($task_ids) > 0 && $this->invoice->company->invoice_task_documents){ + + $tasks = Task::whereIn('id', $this->transformKeys($task_ids)) + ->cursor() + ->each(function ($task){ + + foreach($task->documents as $document) + { + $this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => $document->type]]); + } + + }); + } + + } + } diff --git a/app/Mail/Import/ImportCompleted.php b/app/Mail/Import/ImportCompleted.php index 20da4e2c24c8..fbf7234bbbbe 100644 --- a/app/Mail/Import/ImportCompleted.php +++ b/app/Mail/Import/ImportCompleted.php @@ -58,6 +58,22 @@ class ImportCompleted extends Mailable 'logo' => $this->company->present()->logo(), 'settings' => $this->company->settings, 'company' => $this->company, + 'client_count' => $this->company->clients()->count(), + 'product_count' => $this->company->products()->count(), + 'invoice_count' => $this->company->invoices()->count(), + 'quote_count' => $this->company->quotes()->count(), + 'credit_count' => $this->company->credits()->count(), + 'project_count' => $this->company->projects()->count(), + 'task_count' => $this->company->tasks()->count(), + 'vendor_count' => $this->company->vendors()->count(), + 'payment_count' => $this->company->payments()->count(), + 'recurring_invoice_count' => $this->company->recurring_invoices()->count(), + 'expense_count' => $this->company->expenses()->count(), + 'company_gateway_count' => $this->company->company_gateways()->count(), + 'client_gateway_token_count' => $this->company->client_gateway_tokens()->count(), + 'tax_rate_count' => $this->company->tax_rates()->count(), + 'document_count' => $this->company->documents()->count(), + ]); return $this diff --git a/app/Models/Client.php b/app/Models/Client.php index fd28df5e9c5c..199cf9fbd472 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -253,7 +253,7 @@ class Client extends BaseModel implements HasLocalePreference public function system_logs() { - return $this->hasMany(SystemLog::class)->orderBy('id', 'desc'); + return $this->hasMany(SystemLog::class)->take(50)->orderBy('id', 'desc'); } public function timezone() diff --git a/app/Models/Company.php b/app/Models/Company.php index b659e8980ce7..7ff29a5b3c9a 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -118,9 +118,7 @@ class Company extends BaseModel 'client_registration_fields' => 'array', ]; - protected $with = [ - // 'tokens' - ]; + protected $with = []; public static $modules = [ self::ENTITY_RECURRING_INVOICE => 1, @@ -553,4 +551,17 @@ class Company extends BaseModel { return ctrans('texts.company'); } + + public function date_format() + { + $date_formats = Cache::get('date_formats'); + + if(!$date_formats) + $this->buildCache(true); + + return $date_formats->filter(function ($item) { + return $item->id == $this->getSetting('date_format_id'); + })->first()->format; + } + } diff --git a/app/Models/Credit.php b/app/Models/Credit.php index 8bc7a8756b95..3999f538c7e4 100644 --- a/app/Models/Credit.php +++ b/app/Models/Credit.php @@ -319,4 +319,25 @@ class Credit extends BaseModel { return ctrans('texts.credit'); } + + public static function stringStatus(int $status) + { + switch ($status) { + case self::STATUS_DRAFT: + return ctrans('texts.draft'); + break; + case self::STATUS_SENT: + return ctrans('texts.sent'); + break; + case self::STATUS_PARTIAL: + return ctrans('texts.partial'); + break; + case self::STATUS_APPLIED: + return ctrans('texts.applied'); + break; + default: + return ""; + break; + } + } } diff --git a/app/Models/Expense.php b/app/Models/Expense.php index f0fdeafca3e6..4258a70dd8a5 100644 --- a/app/Models/Expense.php +++ b/app/Models/Expense.php @@ -114,7 +114,7 @@ class Expense extends BaseModel public function category() { - return $this->belongsTo(ExpenseCategory::class); + return $this->belongsTo(ExpenseCategory::class)->withTrashed(); } public function payment_type() diff --git a/app/Models/Product.php b/app/Models/Product.php index 59f0090afe0b..44ba4c668ef2 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -55,6 +55,11 @@ class Product extends BaseModel return $this->belongsTo(User::class)->withTrashed(); } + public function vendor() + { + return $this->belongsTo(Vendor::class)->withTrashed(); + } + public function assigned_user() { return $this->belongsTo(User::class, 'assigned_user_id', 'id')->withTrashed(); diff --git a/app/Notifications/Ninja/WePayFailureNotification.php b/app/Notifications/Ninja/WePayFailureNotification.php index 08e96cb856c0..f08b2e8ca02a 100644 --- a/app/Notifications/Ninja/WePayFailureNotification.php +++ b/app/Notifications/Ninja/WePayFailureNotification.php @@ -72,11 +72,15 @@ class WePayFailureNotification extends Notification public function toSlack($notifiable) { + $ip = ""; + + if(request()) + $ip = request()->getClientIp(); - (new SlackMessage) + return (new SlackMessage) ->success() ->from(ctrans('texts.notification_bot')) ->image('https://app.invoiceninja.com/favicon.png') - ->content("New WePay ACH Failure from Company ID: ". $this->company_id); + ->content("New WePay ACH Failure from Company ID: {$this->company_id} IP: {$ip}" ); } } diff --git a/app/PaymentDrivers/GoCardlessPaymentDriver.php b/app/PaymentDrivers/GoCardlessPaymentDriver.php index 9aa9a3f5447b..6a774fe1999d 100644 --- a/app/PaymentDrivers/GoCardlessPaymentDriver.php +++ b/app/PaymentDrivers/GoCardlessPaymentDriver.php @@ -230,14 +230,11 @@ class GoCardlessPaymentDriver extends BaseDriver public function processWebhookRequest(PaymentWebhookRequest $request) { // Allow app to catch up with webhook request. - sleep(2); - $this->init(); nlog("GoCardless Event"); nlog($request->all()); - if(!is_array($request->events) || !is_object($request->events)){ nlog("No GoCardless events to process in response?"); @@ -245,19 +242,24 @@ class GoCardlessPaymentDriver extends BaseDriver } + sleep(1); + foreach ($request->events as $event) { if ($event['action'] === 'confirmed') { + + nlog("Searching for transaction reference"); + $payment = Payment::query() ->where('transaction_reference', $event['links']['payment']) - ->where('company_id', $request->getCompany()->id) + // ->where('company_id', $request->getCompany()->id) ->first(); if ($payment) { $payment->status_id = Payment::STATUS_COMPLETED; $payment->save(); } - - + else + nlog("I was unable to find the payment for this reference"); //finalize payments on invoices here. } @@ -266,7 +268,7 @@ class GoCardlessPaymentDriver extends BaseDriver $payment = Payment::query() ->where('transaction_reference', $event['links']['payment']) - ->where('company_id', $request->getCompany()->id) + // ->where('company_id', $request->getCompany()->id) ->first(); if ($payment) { diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index 731f254d6c2a..7f0537fe3503 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -601,9 +601,14 @@ class StripePaymentDriver extends BaseDriver } } elseif ($request->type === 'source.chargeable') { + $this->init(); foreach ($request->data as $transaction) { + + if(!$request->data['object']['amount'] || empty($request->data['object']['amount'])) + continue; + $charge = \Stripe\Charge::create([ 'amount' => $request->data['object']['amount'], 'currency' => $request->data['object']['currency'], @@ -619,6 +624,7 @@ class StripePaymentDriver extends BaseDriver ->orWhere('transaction_reference', $transaction['id']); }) ->first(); + if ($payment) { $payment->status_id = Payment::STATUS_COMPLETED; $payment->save(); diff --git a/app/Repositories/ExpenseRepository.php b/app/Repositories/ExpenseRepository.php index 9f8577683e69..4cba59bab284 100644 --- a/app/Repositories/ExpenseRepository.php +++ b/app/Repositories/ExpenseRepository.php @@ -12,8 +12,10 @@ namespace App\Repositories; use App\Factory\ExpenseFactory; +use App\Libraries\Currency\Conversion\CurrencyApi; use App\Models\Expense; use App\Utils\Traits\GeneratesCounter; +use Illuminate\Support\Carbon; /** * ExpenseRepository. @@ -34,6 +36,10 @@ class ExpenseRepository extends BaseRepository public function save(array $data, Expense $expense) : ?Expense { $expense->fill($data); + + if(!$expense->id) + $expense = $this->processExchangeRates($data, $expense); + $expense->number = empty($expense->number) ? $this->getNextExpenseNumber($expense) : $expense->number; $expense->save(); @@ -57,4 +63,27 @@ class ExpenseRepository extends BaseRepository ExpenseFactory::create(auth()->user()->company()->id, auth()->user()->id) ); } + + public function processExchangeRates($data, $expense) + { + + if(array_key_exists('exchange_rate', $data) && isset($data['exchange_rate']) && $data['exchange_rate'] != 1){ + return $expense; + } + + $expense_currency = $data['currency_id']; + $company_currency = $expense->company->settings->currency_id; + + if ($company_currency != $expense_currency) { + + $exchange_rate = new CurrencyApi(); + + $expense->exchange_rate = $exchange_rate->exchangeRate($expense_currency, $company_currency, Carbon::parse($expense->date)); + + return $expense; + } + + return $expense; + } + } diff --git a/app/Repositories/PaymentRepository.php b/app/Repositories/PaymentRepository.php index 470b92d66aa4..53170d76e718 100644 --- a/app/Repositories/PaymentRepository.php +++ b/app/Repositories/PaymentRepository.php @@ -66,7 +66,11 @@ class PaymentRepository extends BaseRepository { //check currencies here and fill the exchange rate data if necessary if (! $payment->id) { - $this->processExchangeRates($data, $payment); + $payment = $this->processExchangeRates($data, $payment); + + /* This is needed here otherwise the ->fill() overwrites anything that exists*/ + if($payment->exchange_rate != 1) + unset($data['exchange_rate']); $is_existing_payment = false; $client = Client::where('id', $data['client_id'])->withTrashed()->first(); @@ -100,7 +104,12 @@ class PaymentRepository extends BaseRepository { $payment->status_id = Payment::STATUS_COMPLETED; if (! $payment->currency_id && $client) { - $payment->currency_id = $client->company->settings->currency_id; + + if(property_exists($client->settings, 'currency_id')) + $payment->currency_id = $client->settings->currency_id; + else + $payment->currency_id = $client->company->settings->currency_id; + } $payment->save(); @@ -199,8 +208,9 @@ class PaymentRepository extends BaseRepository { public function processExchangeRates($data, $payment) { - if(array_key_exists('exchange_rate', $data) && isset($data['exchange_rate'])) + if(array_key_exists('exchange_rate', $data) && isset($data['exchange_rate']) && $data['exchange_rate'] != 1){ return $payment; + } $client = Client::withTrashed()->find($data['client_id']); @@ -212,7 +222,6 @@ class PaymentRepository extends BaseRepository { $exchange_rate = new CurrencyApi(); $payment->exchange_rate = $exchange_rate->exchangeRate($client_currency, $company_currency, Carbon::parse($payment->date)); - // $payment->exchange_currency_id = $client_currency; $payment->exchange_currency_id = $company_currency; $payment->currency_id = $client_currency; @@ -221,7 +230,6 @@ class PaymentRepository extends BaseRepository { $payment->currency_id = $company_currency; - return $payment; } diff --git a/app/Services/Client/Statement.php b/app/Services/Client/Statement.php index 3169d83b60f4..b96e4b51ff12 100644 --- a/app/Services/Client/Statement.php +++ b/app/Services/Client/Statement.php @@ -226,6 +226,7 @@ class Statement ->whereIn('status_id', $this->invoiceStatuses()) ->whereBetween('date', [Carbon::parse($this->options['start_date']), Carbon::parse($this->options['end_date'])]) ->orderBy('due_date', 'ASC') + ->orderBy('date', 'ASC') ->cursor(); } @@ -241,10 +242,10 @@ class Statement return [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL, Invoice::STATUS_PAID]; break; case 'paid': - return [Invoice::STATUS_PARTIAL, Invoice::STATUS_PAID]; + return [Invoice::STATUS_PAID]; break; case 'unpaid': - return [Invoice::STATUS_SENT]; + return [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]; break; default: diff --git a/app/Services/Invoice/MarkPaid.php b/app/Services/Invoice/MarkPaid.php index 5c4a282d5ff3..87e321625fd9 100644 --- a/app/Services/Invoice/MarkPaid.php +++ b/app/Services/Invoice/MarkPaid.php @@ -61,7 +61,7 @@ class MarkPaid extends AbstractService $payment->transaction_reference = ctrans('texts.manual_entry'); $payment->currency_id = $this->invoice->client->getSetting('currency_id'); $payment->is_manual = true; - + if($this->invoice->company->timezone()) $payment->date = now()->addSeconds($this->invoice->company->timezone()->utc_offset)->format('Y-m-d'); @@ -149,7 +149,7 @@ class MarkPaid extends AbstractService //$payment->exchange_currency_id = $client_currency; // 23/06/2021 $payment->exchange_currency_id = $company_currency; - $payment->save(); + $payment->saveQuietly(); } diff --git a/app/Services/PdfMaker/Design.php b/app/Services/PdfMaker/Design.php index ff8332bf1991..8522ba9133de 100644 --- a/app/Services/PdfMaker/Design.php +++ b/app/Services/PdfMaker/Design.php @@ -226,7 +226,7 @@ class Design extends BaseDesign { if ($this->type === 'statement') { - $s_date = $this->translateDate($this->options['end_date'], $this->client->date_format(), $this->client->locale()); + $s_date = $this->translateDate(now()->format($client->company->date_format()), $this->client->date_format(), $this->client->locale()); return [ ['element' => 'tr', 'properties' => ['data-ref' => 'statement-label'], 'elements' => [ diff --git a/app/Services/Quote/ApplyNumber.php b/app/Services/Quote/ApplyNumber.php index 9366ee5f4f38..3ceca85242a6 100644 --- a/app/Services/Quote/ApplyNumber.php +++ b/app/Services/Quote/ApplyNumber.php @@ -37,7 +37,7 @@ class ApplyNumber switch ($this->client->getSetting('counter_number_applied')) { case 'when_saved': $quote = $this->trySaving($quote); - // $quote->number = $this->getNextQuoteNumber($this->client, $quote); + // $quote->number = $this->getNextQuoteNumber($this->client, $quote); break; case 'when_sent': if ($quote->status_id == Quote::STATUS_SENT) { diff --git a/app/Services/Report/ProfitLoss.php b/app/Services/Report/ProfitLoss.php new file mode 100644 index 000000000000..7905301cd713 --- /dev/null +++ b/app/Services/Report/ProfitLoss.php @@ -0,0 +1,679 @@ +currency_api = new CurrencyApi(); + + $this->company = $company; + + $this->payload = $payload; + + $this->setBillingReportType(); + } + + public function build() + { + MultiDB::setDb($this->company->db); + + if($this->is_income_billed){ //get invoiced amounts + + $this->filterIncome(); + + }else { + + //$this->filterPaymentIncome(); + $this->filterInvoicePaymentIncome(); + } + + $this->expenseData()->buildExpenseBreakDown(); + + return $this; + } + + public function getIncome() :float + { + return round($this->income,2); + } + + public function getIncomeMap() :array + { + return $this->income_map; + } + + public function getIncomeTaxes() :float + { + return round($this->income_taxes,2); + } + + public function getExpenses() :array + { + return $this->expenses; + } + + public function getExpenseBreakDown() :array + { + ksort($this->expense_break_down); + + return $this->expense_break_down; + } + + private function filterIncome() + { + $invoices = $this->invoiceIncome(); + + $this->foreign_income = []; + + $this->income = 0; + $this->income_taxes = 0; + $this->income_map = $invoices; + + foreach($invoices as $invoice){ + $this->income += $invoice->net_converted_amount; + $this->income_taxes += $invoice->net_converted_taxes; + + + $currency = Currency::find(intval(str_replace('"','',$invoice->currency_id))); + $currency->name = ctrans('texts.currency_'.Str::slug($currency->name, '_')); + + $this->foreign_income[] = ['currency' => $currency->name, 'amount' => $invoice->amount, 'total_taxes' => $invoice->total_taxes]; + } + + return $this; + + } + + private function filterInvoicePaymentIncome() + { + + $this->paymentEloquentIncome(); + + foreach($this->invoice_payment_map as $map) { + $this->income += $map->amount_payment_paid_converted - $map->tax_amount_converted; + $this->income_taxes += $map->tax_amount_converted; + + $this->credit += $map->amount_credit_paid_converted - $map->tax_amount_credit_converted; + $this->credit_taxes += $map->tax_amount_credit_converted; + } + + // $invoices = $this->invoicePaymentIncome(); + + // $this->income = 0; + // $this->income_taxes = 0; + // $this->income_map = $invoices; + + // foreach($invoices as $invoice){ + // $this->income += $invoice->net_converted_amount; + // $this->income_taxes += $invoice->net_converted_taxes; + // } + + return $this; + + } + + private function getForeignIncome() :array + { + return $this->foreign_income; + } + + private function filterPaymentIncome() + { + $payments = $this->paymentIncome(); + + return $this; + } + + /* + //returns an array of objects + => [ + {#2047 + +"amount": "706.480000", + +"total_taxes": "35.950000", + +"currency_id": ""1"", + +"net_converted_amount": "670.5300000000", + +"net_converted_taxes": "10" + }, + {#2444 + +"amount": "200.000000", + +"total_taxes": "0.000000", + +"currency_id": ""23"", + +"net_converted_amount": "1.7129479802", + +"net_converted_taxes": "10" + }, + {#2654 + +"amount": "140.000000", + +"total_taxes": "40.000000", + +"currency_id": ""12"", + +"net_converted_amount": "69.3275024282", + +"net_converted_taxes": "10" + }, + ] + */ + private function invoiceIncome() + { + return \DB::select( \DB::raw(" + SELECT + sum(invoices.amount) as amount, + sum(invoices.total_taxes) as total_taxes, + (sum(invoices.total_taxes) / IFNULL(invoices.exchange_rate, 1)) AS net_converted_taxes, + sum(invoices.amount - invoices.total_taxes) as net_amount, + IFNULL(JSON_EXTRACT( settings, '$.currency_id' ), :company_currency) AS currency_id, + (sum(invoices.amount - invoices.total_taxes) / IFNULL(invoices.exchange_rate, 1)) AS net_converted_amount + FROM clients + JOIN invoices + on invoices.client_id = clients.id + WHERE invoices.status_id IN (2,3,4) + AND invoices.company_id = :company_id + AND invoices.amount > 0 + AND clients.is_deleted = 0 + AND invoices.is_deleted = 0 + AND (invoices.date BETWEEN :start_date AND :end_date) + GROUP BY currency_id + "), ['company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $this->start_date, 'end_date' => $this->end_date] ); + + } + + /** + * The income calculation is based on the total payments received during + * the selected time period. + * + * Once we have the payments we iterate through the attached invoices and + * we also determine the total taxes paid as our + * Profit and loss statement should be net of all taxes + * + * This calculation also considers partial payments and pro rata's any taxes. + * + * This calculation also considers exchange rates and we convert (based on the payment exchange rate) + * to the native company currency. + */ + private function paymentEloquentIncome() + { + + $this->invoice_payment_map = []; + + Payment::where('company_id', $this->company->id) + ->whereIn('status_id', [1,4,5]) + ->where('is_deleted', 0) + ->whereBetween('date', [$this->start_date, $this->end_date]) + ->whereHas('client', function ($query) { + $query->where('is_deleted',0); + }) + ->with(['company','client']) + ->cursor() + ->each(function ($payment){ + + $company = $payment->company; + $client = $payment->client; + + $map = new \stdClass; + $amount_payment_paid = 0; + $amount_credit_paid = 0; + $amount_payment_paid_converted = 0; + $amount_credit_paid_converted = 0; + $tax_amount = 0; + $tax_amount_converted = 0; + $tax_amount_credit = 0; + $tax_amount_credit_converted = $tax_amount_credit_converted = 0; + + foreach($payment->paymentables as $pivot) + { + + if($pivot->paymentable instanceOf \App\Models\Invoice){ + + $invoice = $pivot->paymentable; + + $amount_payment_paid += $pivot->amount - $pivot->refunded; + $amount_payment_paid_converted += $amount_payment_paid / ($payment->exchange_rate ?: 1); + + $tax_amount += ($amount_payment_paid / $invoice->amount) * $invoice->total_taxes; + $tax_amount_converted += (($amount_payment_paid / $invoice->amount) * $invoice->total_taxes) / $payment->exchange_rate; + + } + + + if($pivot->paymentable instanceOf \App\Models\Credit){ + + $amount_credit_paid += $pivot->amount - $pivot->refunded; + $amount_credit_paid_converted += $amount_payment_paid / ($payment->exchange_rate ?: 1); + + $tax_amount_credit += ($amount_payment_paid / $invoice->amount) * $invoice->total_taxes; + $tax_amount_credit_converted += (($amount_payment_paid / $invoice->amount) * $invoice->total_taxes) / $payment->exchange_rate; + } + + } + + $map->amount_payment_paid = $amount_payment_paid; + $map->amount_payment_paid_converted = $amount_payment_paid_converted; + $map->tax_amount = $tax_amount; + $map->tax_amount_converted = $tax_amount_converted; + $map->amount_credit_paid = $amount_credit_paid; + $map->amount_credit_paid_converted = $amount_credit_paid_converted; + $map->tax_amount_credit = $tax_amount_credit; + $map->tax_amount_credit_converted = $tax_amount_credit_converted; + $map->currency_id = $payment->currency_id; + + $this->invoice_payment_map[] = $map; + + }); + + return $this; + + } + + /** + => [ + {#2047 + +"amount": "110.000000", + +"total_taxes": "10.0000000000000000", + +"net_converted_amount": "110.0000000000", + +"net_converted_taxes": "10.00000000000000000000", + +"currency_id": ""1"", + }, + {#2444 + +"amount": "50.000000", + +"total_taxes": "4.5454545454545455", + +"net_converted_amount": "61.1682150381", + +"net_converted_taxes": "5.56074682164393914741", + +"currency_id": ""2"", + }, + ] + */ + + public function getCsv() + { + + MultiDB::setDb($this->company->db); + App::forgetInstance('translator'); + App::setLocale($this->company->locale()); + $t = app('translator'); + $t->replace(Ninja::transformTranslations($this->company->settings)); + + $csv = Writer::createFromString(); + + $csv->insertOne([ctrans('texts.profit_and_loss')]); + $csv->insertOne([ctrans('texts.company_name'), $this->company->present()->name()]); + $csv->insertOne([ctrans('texts.date_range'), Carbon::parse($this->start_date)->format($this->company->date_format()), Carbon::parse($this->end_date)->format($this->company->date_format())]); + + //gross sales ex tax + + $csv->insertOne(['--------------------']); + + $csv->insertOne([ctrans('texts.total_revenue'), Number::formatMoney($this->income, $this->company)]); + + //total taxes + + $csv->insertOne([ctrans('texts.total_taxes'), Number::formatMoney($this->income_taxes, $this->company)]); + + //expense + + $csv->insertOne(['--------------------']); + foreach($this->expense_break_down as $expense_breakdown) + { + $csv->insertOne([$expense_breakdown['category_name'], Number::formatMoney($expense_breakdown['total'], $this->company)]); + } + //total expense taxes + + $csv->insertOne(['--------------------']); + $csv->insertOne([ctrans('texts.total_expenses'), Number::formatMoney(array_sum(array_column($this->expense_break_down, 'total')), $this->company)]); + + $csv->insertOne([ctrans('texts.total_taxes'), Number::formatMoney(array_sum(array_column($this->expense_break_down, 'tax')), $this->company)]); + + + $csv->insertOne(['--------------------']); + $csv->insertOne([ctrans('texts.total_profit'), Number::formatMoney($this->income - array_sum(array_column($this->expense_break_down, 'total')), $this->company)]); + + //net profit + + $csv->insertOne(['--------------------']); + $csv->insertOne(['']); + $csv->insertOne(['']); + + $csv->insertOne([ctrans('texts.currency'), ctrans('texts.amount'), ctrans('texts.total_taxes')]); + foreach($this->foreign_income as $foreign_income) + { + $csv->insertOne([$foreign_income['currency'], ($foreign_income['amount'] - $foreign_income['total_taxes']), $foreign_income['total_taxes']]); + } + + return $csv->toString(); + + } + + private function invoicePaymentIncome() + { + return \DB::select( \DB::raw(" + SELECT + sum(invoices.amount - invoices.balance) as amount, + sum(invoices.total_taxes) * ((sum(invoices.amount - invoices.balance)/invoices.amount)) as total_taxes, + (sum(invoices.amount - invoices.balance) / IFNULL(invoices.exchange_rate, 1)) AS net_converted_amount, + (sum(invoices.total_taxes) * ((sum(invoices.amount - invoices.balance)/invoices.amount)) / IFNULL(invoices.exchange_rate, 1)) AS net_converted_taxes, + IFNULL(JSON_EXTRACT( settings, '$.currency_id' ), :company_currency) AS currency_id + FROM clients + JOIN invoices + on invoices.client_id = clients.id + WHERE invoices.status_id IN (3,4) + AND invoices.company_id = :company_id + AND invoices.amount > 0 + AND clients.is_deleted = 0 + AND invoices.is_deleted = 0 + AND (invoices.date BETWEEN :start_date AND :end_date) + GROUP BY currency_id + "), ['company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $this->start_date, 'end_date' => $this->end_date] ); + } + + /** + +"payments": "12260.870000", + +"payments_converted": "12260.870000000000", + +"currency_id": 1, + */ + private function paymentIncome() + { + return \DB::select( \DB::raw(" + SELECT + SUM(coalesce(payments.amount - payments.refunded,0)) as payments, + SUM(coalesce(payments.amount - payments.refunded,0)) * IFNULL(payments.exchange_rate ,1) as payments_converted, + payments.currency_id as currency_id + FROM clients + INNER JOIN + payments ON + clients.id=payments.client_id + WHERE payments.status_id IN (1,4,5,6) + AND clients.is_deleted = false + AND payments.is_deleted = false + AND payments.company_id = :company_id + AND (payments.date BETWEEN :start_date AND :end_date) + GROUP BY currency_id + ORDER BY currency_id; + "), ['company_id' => $this->company->id, 'start_date' => $this->start_date, 'end_date' => $this->end_date]); + + + } + + private function expenseData() + { + + $expenses = Expense::where('company_id', $this->company->id) + ->where('is_deleted', 0) + ->withTrashed() + ->whereBetween('date', [$this->start_date, $this->end_date]) + ->cursor(); + + + $this->expenses = []; + + foreach($expenses as $expense) + { + $map = new \stdClass; + + $amount = $expense->amount; + + $map->total = $expense->amount; + $map->converted_total = $converted_total = $this->getConvertedTotal($expense->amount, $expense->exchange_rate); + $map->tax = $tax = $this->getTax($expense); + $map->net_converted_total = $expense->uses_inclusive_taxes ? ( $converted_total - $tax ) : $converted_total; + $map->category_id = $expense->category_id; + $map->category_name = $expense->category ? $expense->category->name : "No Category Defined"; + $map->currency_id = $expense->currency_id ?: $expense->company->settings->currency_id; + + $this->expenses[] = $map; + + } + + + return $this; + } + + private function buildExpenseBreakDown() + { + $data = []; + + foreach($this->expenses as $expense) + { + if(!array_key_exists($expense->category_id, $data)) + $data[$expense->category_id] = []; + + if(!array_key_exists('total', $data[$expense->category_id])) + $data[$expense->category_id]['total'] = 0; + + if(!array_key_exists('tax', $data[$expense->category_id])) + $data[$expense->category_id]['tax'] = 0; + + $data[$expense->category_id]['total'] += $expense->net_converted_total; + $data[$expense->category_id]['category_name'] = $expense->category_name; + $data[$expense->category_id]['tax'] += $expense->tax; + + } + + $this->expense_break_down = $data; + + return $this; + + } + + private function getTax($expense) + { + $amount = $expense->amount; + //is amount tax + + if($expense->calculate_tax_by_amount) + { + nlog($expense->tax_amount1); + nlog($expense->tax_amount2); + nlog($expense->tax_amount3); + + return $expense->tax_amount1 + $expense->tax_amount2 + $expense->tax_amount3; + } + + + if($expense->uses_inclusive_taxes){ + + $inclusive = 0; + + $inclusive += ($amount - ($amount / (1 + ($expense->tax_rate1 / 100)))); + $inclusive += ($amount - ($amount / (1 + ($expense->tax_rate2 / 100)))); + $inclusive += ($amount - ($amount / (1 + ($expense->tax_rate3 / 100)))); + + return round($inclusive,2); + + } + + $exclusive = 0; + + $exclusive += $amount * ($expense->tax_rate1 / 100); + $exclusive += $amount * ($expense->tax_rate2 / 100); + $exclusive += $amount * ($expense->tax_rate3 / 100); + + + return $exclusive; + + } + + private function getConvertedTotal($amount, $exchange_rate = 1) + { + return round(($amount * $exchange_rate) ,2); + } + + private function expenseCalcWithTax() + { + + return \DB::select( \DB::raw(" + SELECT sum(expenses.amount) as amount, + IFNULL(expenses.currency_id, :company_currency) as currency_id + FROM expenses + WHERE expenses.is_deleted = 0 + AND expenses.company_id = :company_id + AND (expenses.date BETWEEN :start_date AND :end_date) + GROUP BY currency_id + "), ['company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $this->start_date, 'end_date' => $this->end_date] ); + + } + + private function setBillingReportType() + { + + if(array_key_exists('is_income_billed', $this->payload)) + $this->is_income_billed = boolval($this->payload['is_income_billed']); + + if(array_key_exists('is_expense_billed', $this->payload)) + $this->is_expense_billed = boolval($this->payload['is_expense_billed']); + + if(array_key_exists('include_tax', $this->payload)) + $this->is_tax_included = boolval($this->payload['include_tax']); + + $this->addDateRange(); + + return $this; + + } + + private function addDateRange() + { + $date_range = 'this_year'; + + if(array_key_exists('date_range', $this->payload)) + $date_range = $this->payload['date_range']; + + try{ + + $custom_start_date = Carbon::parse($this->payload['start_date']); + $custom_end_date = Carbon::parse($this->payload['end_date']); + + } + catch(\Exception $e){ + + $custom_start_date = now()->startOfYear(); + $custom_end_date = now(); + + } + + switch ($date_range) { + + case 'all': + $this->start_date = now()->subYears(50); + $this->end_date = now(); + // return $query; + case 'last7': + $this->start_date = now()->subDays(7); + $this->end_date = now(); + // return $query->whereBetween($this->date_key, [now()->subDays(7), now()])->orderBy($this->date_key, 'ASC'); + case 'last30': + $this->start_date = now()->subDays(30); + $this->end_date = now(); + // return $query->whereBetween($this->date_key, [now()->subDays(30), now()])->orderBy($this->date_key, 'ASC'); + case 'this_month': + $this->start_date = now()->startOfMonth(); + $this->end_date = now(); + //return $query->whereBetween($this->date_key, [now()->startOfMonth(), now()])->orderBy($this->date_key, 'ASC'); + case 'last_month': + $this->start_date = now()->startOfMonth()->subMonth(); + $this->end_date = now()->startOfMonth()->subMonth()->endOfMonth(); + //return $query->whereBetween($this->date_key, [now()->startOfMonth()->subMonth(), now()->startOfMonth()->subMonth()->endOfMonth()])->orderBy($this->date_key, 'ASC'); + case 'this_quarter': + $this->start_date = (new \Carbon\Carbon('-3 months'))->firstOfQuarter(); + $this->end_date = (new \Carbon\Carbon('-3 months'))->lastOfQuarter(); + //return $query->whereBetween($this->date_key, [(new \Carbon\Carbon('-3 months'))->firstOfQuarter(), (new \Carbon\Carbon('-3 months'))->lastOfQuarter()])->orderBy($this->date_key, 'ASC'); + case 'last_quarter': + $this->start_date = (new \Carbon\Carbon('-6 months'))->firstOfQuarter(); + $this->end_date = (new \Carbon\Carbon('-6 months'))->lastOfQuarter(); + //return $query->whereBetween($this->date_key, [(new \Carbon\Carbon('-6 months'))->firstOfQuarter(), (new \Carbon\Carbon('-6 months'))->lastOfQuarter()])->orderBy($this->date_key, 'ASC'); + case 'this_year': + $this->start_date = now()->startOfYear(); + $this->end_date = now(); + //return $query->whereBetween($this->date_key, [now()->startOfYear(), now()])->orderBy($this->date_key, 'ASC'); + case 'custom': + $this->start_date = $custom_start_date; + $this->end_date = $custom_end_date; + //return $query->whereBetween($this->date_key, [$custom_start_date, $custom_end_date])->orderBy($this->date_key, 'ASC'); + default: + $this->start_date = now()->startOfYear(); + $this->end_date = now(); + // return $query->whereBetween($this->date_key, [now()->startOfYear(), now()])->orderBy($this->date_key, 'ASC'); + + } + + return $this; + + } + +} diff --git a/database/factories/CompanyFactory.php b/database/factories/CompanyFactory.php index 2a4a9850c680..6fb2d23ef793 100644 --- a/database/factories/CompanyFactory.php +++ b/database/factories/CompanyFactory.php @@ -40,18 +40,6 @@ class CompanyFactory extends Factory 'default_password_timeout' => 30*60000, 'enabled_modules' => config('ninja.enabled_modules'), 'custom_fields' => (object) [ - //'invoice1' => 'Custom Date|date', - // 'invoice2' => '2|switch', - // 'invoice3' => '3|', - // 'invoice4' => '4', - // 'client1'=>'1', - // 'client2'=>'2', - // 'client3'=>'3|date', - // 'client4'=>'4|switch', - // 'company1'=>'1|date', - // 'company2'=>'2|switch', - // 'company3'=>'3', - // 'company4'=>'4', ], ]; } diff --git a/resources/lang/de/texts.php b/resources/lang/de/texts.php index 0414e0f90bdc..cc25bc786eaf 100644 --- a/resources/lang/de/texts.php +++ b/resources/lang/de/texts.php @@ -22,7 +22,7 @@ $LANG = array( 'currency_id' => 'Währung', 'size_id' => 'Firmengröße', 'industry_id' => 'Branche', - 'private_notes' => 'Private Notizen', + 'private_notes' => 'Interne Notizen', 'invoice' => 'Rechnung', 'client' => 'Kunde', 'invoice_date' => 'Rechnungsdatum', @@ -115,7 +115,7 @@ $LANG = array( 'upcoming_invoices' => 'Ausstehende Rechnungen', 'average_invoice' => 'Durchschnittlicher Rechnungsbetrag', 'archive' => 'Archivieren', - 'delete' => 'löschen', + 'delete' => 'Löschen', 'archive_client' => 'Kunde archivieren', 'delete_client' => 'Kunde löschen', 'archive_payment' => 'Zahlung archivieren', @@ -172,7 +172,7 @@ $LANG = array( 'localization' => 'Lokalisierung', 'remove_logo' => 'Logo entfernen', 'logo_help' => 'Unterstützt: JPEG, GIF und PNG', - 'payment_gateway' => 'Zahlungseingang', + 'payment_gateway' => 'Zahlungs-Gateway', 'gateway_id' => 'Zahlungsanbieter', 'email_notifications' => 'E-Mail Benachrichtigungen', 'email_sent' => 'Benachrichtigen, wenn eine Rechnung versendet wurde', @@ -246,7 +246,7 @@ $LANG = array( 'payment_subject' => 'Zahlungseingang', 'payment_message' => 'Vielen Dank für Ihre Zahlung von :amount.', 'email_salutation' => 'Sehr geehrte/r :name,', - 'email_signature' => 'Mit freundlichen Grüßen,', + 'email_signature' => 'Mit freundlichen Grüßen', 'email_from' => 'Das InvoiceNinja Team', 'invoice_link_message' => 'Um deine Kundenrechnung anzuschauen, klicke auf den folgenden Link:', 'notification_invoice_paid_subject' => 'Die Rechnung :invoice wurde von :client bezahlt.', @@ -272,13 +272,13 @@ $LANG = array( 'erase_data' => 'Ihr Konto ist nicht registriert, diese Aktion wird Ihre Daten unwiderruflich löschen.', 'password' => 'Passwort', 'pro_plan_product' => 'Pro Plan', - 'pro_plan_success' => 'Danke, dass Sie Invoice Ninja\'s Pro gewählt haben!
If your logo imported correctly it will display below. If it didn't import, you'll need to reupload your logo
-{{ ctrans('texts.clients') }}: {{ $company->clients->count() }}
+ @if(isset($company)) +{{ ctrans('texts.clients') }}: {{ $client_count }}
@endif - @if(isset($company) && count($company->products) >=1) -{{ ctrans('texts.products') }}: {{ count($company->products) }}
+ @if(isset($company)) +{{ ctrans('texts.products') }}: {{ $product_count }}
@endif - @if(isset($company) && count($company->invoices) >=1) -{{ ctrans('texts.invoices') }}: {{ count($company->invoices) }}
+ @if(isset($company)) +{{ ctrans('texts.invoices') }}: {{ $invoice_count }}
@endif - @if(isset($company) && count($company->payments) >=1) -{{ ctrans('texts.payments') }}: {{ count($company->payments) }}
+ @if(isset($company)) +{{ ctrans('texts.payments') }}: {{ $payment_count }}
@endif - @if(isset($company) && count($company->recurring_invoices) >=1) -{{ ctrans('texts.recurring_invoices') }}: {{ count($company->recurring_invoices) }}
+ @if(isset($company)) +{{ ctrans('texts.recurring_invoices') }}: {{ $recurring_invoice_count }}
@endif - @if(isset($company) && count($company->quotes) >=1) -{{ ctrans('texts.quotes') }}: {{ count($company->quotes) }}
+ @if(isset($company)) +{{ ctrans('texts.quotes') }}: {{ $quote_count }}
@endif - @if(isset($company) && count($company->credits) >=1) -{{ ctrans('texts.credits') }}: {{ count($company->credits) }}
+ @if(isset($company)) +{{ ctrans('texts.credits') }}: {{ $credit_count }}
@endif - @if(isset($company) && count($company->projects) >=1) -{{ ctrans('texts.projects') }}: {{ count($company->projects) }}
+ @if(isset($company)) +{{ ctrans('texts.projects') }}: {{ $project_count }}
@endif - @if(isset($company) && count($company->tasks) >=1) -{{ ctrans('texts.tasks') }}: {{ count($company->tasks) }}
+ @if(isset($company)) +{{ ctrans('texts.tasks') }}: {{ $task_count }}
@endif - @if(isset($company) && count($company->vendors) >=1) -{{ ctrans('texts.vendors') }}: {{ count($company->vendors) }}
+ @if(isset($company)) +{{ ctrans('texts.vendors') }}: {{ $vendor_count }}
@endif - @if(isset($company) && count($company->expenses) >=1) -{{ ctrans('texts.expenses') }}: {{ count($company->expenses) }}
+ @if(isset($company)) +{{ ctrans('texts.expenses') }}: {{ $expense_count }}
@endif - @if(isset($company) && count($company->company_gateways) >=1) -{{ ctrans('texts.gateways') }}: {{ count($company->company_gateways) }}
+ @if(isset($company)) +{{ ctrans('texts.gateways') }}: {{ $company_gateway_count }}
@endif - @if(isset($company) && count($company->client_gateway_tokens) >=1) -{{ ctrans('texts.tokens') }}: {{ count($company->client_gateway_tokens) }}
+ @if(isset($company)) +{{ ctrans('texts.tokens') }}: {{ $client_gateway_token_count }}
@endif - @if(isset($company) && count($company->tax_rates) >=1) -{{ ctrans('texts.tax_rates') }}: {{ count($company->tax_rates) }}
+ @if(isset($company)) +{{ ctrans('texts.tax_rates') }}: {{ $tax_rate_count }}
@endif - @if(isset($company) && count($company->documents) >=1) -{{ ctrans('texts.documents') }}: {{ count($company->documents) }}
+ @if(isset($company)) +{{ ctrans('texts.documents') }}: {{ $document_count }}
@endif @if(isset($check_data)) diff --git a/routes/api.php b/routes/api.php index bff9838c65cc..baa3f56494d0 100644 --- a/routes/api.php +++ b/routes/api.php @@ -19,11 +19,11 @@ Route::group(['middleware' => ['throttle:300,1', 'api_secret_check']], function }); Route::group(['middleware' => ['throttle:10,1','api_secret_check','email_db']], function () { - Route::post('api/v1/login', 'Auth\LoginController@apiLogin')->name('login.submit'); + Route::post('api/v1/login', 'Auth\LoginController@apiLogin')->name('login.submit')->middleware('throttle:20,1');; Route::post('api/v1/reset_password', 'Auth\ForgotPasswordController@sendResetLinkEmail'); }); -Route::group(['middleware' => ['throttle:300,1', 'api_db', 'token_auth', 'locale'], 'prefix' => 'api/v1', 'as' => 'api.'], function () { +Route::group(['middleware' => ['throttle:100,1', 'api_db', 'token_auth', 'locale'], 'prefix' => 'api/v1', 'as' => 'api.'], function () { Route::post('check_subdomain', 'SubdomainController@index')->name('check_subdomain'); Route::get('ping', 'PingController@index')->name('ping'); Route::get('health_check', 'PingController@health')->name('health_check'); @@ -152,7 +152,7 @@ Route::group(['middleware' => ['throttle:300,1', 'api_db', 'token_auth', 'locale Route::post('recurring_quotes/bulk', 'RecurringQuoteController@bulk')->name('recurring_quotes.bulk'); Route::put('recurring_quotes/{recurring_quote}/upload', 'RecurringQuoteController@upload'); - Route::post('refresh', 'Auth\LoginController@refresh'); + Route::post('refresh', 'Auth\LoginController@refresh')->middleware('throttle:30,1'); Route::post('reports/clients', 'Reports\ClientReportController'); Route::post('reports/contacts', 'Reports\ClientContactReportController'); @@ -167,6 +167,7 @@ Route::group(['middleware' => ['throttle:300,1', 'api_db', 'token_auth', 'locale Route::post('reports/payments', 'Reports\PaymentReportController'); Route::post('reports/products', 'Reports\ProductReportController'); Route::post('reports/tasks', 'Reports\TaskReportController'); + Route::post('reports/profitloss', 'Reports\ProfitAndLossController'); Route::get('scheduler', 'SchedulerController@index'); Route::post('support/messages/send', 'Support\Messages\SendingController'); diff --git a/routes/client.php b/routes/client.php index 295957d4db35..b6750d0ca76a 100644 --- a/routes/client.php +++ b/routes/client.php @@ -8,7 +8,7 @@ Route::get('client/login', 'Auth\ContactLoginController@showLoginForm')->name('c Route::post('client/login', 'Auth\ContactLoginController@login')->name('client.login.submit'); Route::get('client/register/{company_key?}', 'Auth\ContactRegisterController@showRegisterForm')->name('client.register')->middleware(['domain_db', 'contact_account', 'contact_register','locale']); -Route::post('client/register/{company_key?}', 'Auth\ContactRegisterController@register')->middleware(['domain_db', 'contact_account', 'contact_register', 'locale','throttle:10,1']); +Route::post('client/register/{company_key?}', 'Auth\ContactRegisterController@register')->middleware(['domain_db', 'contact_account', 'contact_register', 'locale', 'throttle:10,1']); Route::get('client/password/reset', 'Auth\ContactForgotPasswordController@showLinkRequestForm')->name('client.password.request')->middleware(['domain_db', 'contact_account','locale']); Route::post('client/password/email', 'Auth\ContactForgotPasswordController@sendResetLinkEmail')->name('client.password.email')->middleware('locale'); @@ -62,7 +62,7 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'domain_db','check_clie Route::put('profile/{client_contact}/localization', 'ClientPortal\ProfileController@updateClientLocalization')->name('profile.edit_localization'); Route::get('payment_methods/{payment_method}/verification', 'ClientPortal\PaymentMethodController@verify')->name('payment_methods.verification'); - Route::post('payment_methods/{payment_method}/verification', 'ClientPortal\PaymentMethodController@processVerification'); + Route::post('payment_methods/{payment_method}/verification', 'ClientPortal\PaymentMethodController@processVerification')->middleware(['throttle:10,1']); Route::get('payment_methods/confirm', 'ClientPortal\PaymentMethodController@store')->name('payment_methods.confirm'); diff --git a/tests/Feature/Export/ClientCsvTest.php b/tests/Feature/Export/ClientCsvTest.php new file mode 100644 index 000000000000..0708848505c1 --- /dev/null +++ b/tests/Feature/Export/ClientCsvTest.php @@ -0,0 +1,76 @@ +withoutMiddleware( + ThrottleRequests::class + ); + + $this->makeTestData(); + + $this->withoutExceptionHandling(); + } + + public function testClientExportCsv() + { + + $data = [ + "date_range" => "this_year", + "report_keys" => [], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/clients' , $data); + + $response->assertStatus(200); + + } + + public function testContactExportCsv() + { + + $data = [ + "date_range" => "this_year", + "report_keys" => [], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/contacts' , $data); + + $response->assertStatus(200); + + } + +} diff --git a/tests/Feature/Export/ProfitAndLossReportTest.php b/tests/Feature/Export/ProfitAndLossReportTest.php new file mode 100644 index 000000000000..1e2bb450c592 --- /dev/null +++ b/tests/Feature/Export/ProfitAndLossReportTest.php @@ -0,0 +1,566 @@ +faker = \Faker\Factory::create(); + + $this->withoutMiddleware( + ThrottleRequests::class + ); + + $this->withoutExceptionHandling(); + + } + + public $company; + + public $user; + + public $payload; + + public $account; +/** + * + * start_date - Y-m-d + end_date - Y-m-d + date_range - + all + last7 + last30 + this_month + last_month + this_quarter + last_quarter + this_year + custom + is_income_billed - true = Invoiced || false = Payments + expense_billed - true = Expensed || false = Expenses marked as paid + include_tax - true tax_included || false - tax_excluded + +*/ + + private function buildData() + { + + $this->account = Account::factory()->create([ + 'hosted_client_count' => 1000, + 'hosted_company_count' => 1000 + ]); + + $this->account->num_users = 3; + $this->account->save(); + + $this->user = User::factory()->create([ + 'account_id' => $this->account->id, + 'confirmation_code' => 'xyz123', + 'email' => $this->faker->unique()->safeEmail, + ]); + + $settings = CompanySettings::defaults(); + $settings->client_online_payment_notification = false; + $settings->client_manual_payment_notification = false; + + $this->company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings + ]); + + $this->payload = [ + 'start_date' => '2000-01-01', + 'end_date' => '2030-01-11', + 'date_range' => 'custom', + 'is_income_billed' => true, + 'include_tax' => false + ]; + + } + + public function testProfitLossInstance() + { + $this->buildData(); + + $pl = new ProfitLoss($this->company, $this->payload); + + $this->assertInstanceOf(ProfitLoss::class, $pl); + + $this->account->delete(); + } + + public function testSimpleInvoiceIncome() + { + $this->buildData(); + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'is_deleted' => 0, + ]); + + Invoice::factory()->count(2)->create([ + 'client_id' => $client->id, + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'amount' => 11, + 'balance' => 11, + 'status_id' => 2, + 'total_taxes' => 1, + 'date' => '2022-01-01', + 'terms' => 'nada', + 'discount' => 0, + 'tax_rate1' => 0, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name1' => '', + 'tax_name2' => '', + 'tax_name3' => '', + 'uses_inclusive_taxes' => false, + ]); + + $pl = new ProfitLoss($this->company, $this->payload); + $pl->build(); + + + $this->assertEquals(20.0, $pl->getIncome()); + $this->assertEquals(2, $pl->getIncomeTaxes()); + + $this->account->delete(); + } + + public function testSimpleInvoiceIncomeWithInclusivesTaxes() + { + $this->buildData(); + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'is_deleted' => 0, + ]); + + Invoice::factory()->count(2)->create([ + 'client_id' => $client->id, + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'amount' => 10, + 'balance' => 10, + 'status_id' => 2, + 'total_taxes' => 1, + 'date' => '2022-01-01', + 'terms' => 'nada', + 'discount' => 0, + 'tax_rate1' => 10, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name1' => "GST", + 'tax_name2' => '', + 'tax_name3' => '', + 'uses_inclusive_taxes' => true, + ]); + + $pl = new ProfitLoss($this->company, $this->payload); + $pl->build(); + + + $this->assertEquals(18.0, $pl->getIncome()); + $this->assertEquals(2, $pl->getIncomeTaxes()); + + $this->account->delete(); + } + + + public function testSimpleInvoiceIncomeWithForeignExchange() + { + $this->buildData(); + + $settings = ClientSettings::defaults(); + $settings->currency_id = "2"; + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'is_deleted' => 0, + 'settings' => $settings, + ]); + + Invoice::factory()->count(2)->create([ + 'client_id' => $client->id, + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'amount' => 10, + 'balance' => 10, + 'status_id' => 2, + 'total_taxes' => 1, + 'date' => '2022-01-01', + 'terms' => 'nada', + 'discount' => 0, + 'tax_rate1' => 10, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name1' => "GST", + 'tax_name2' => '', + 'tax_name3' => '', + 'uses_inclusive_taxes' => true, + 'exchange_rate' => 0.5 + ]); + + $pl = new ProfitLoss($this->company, $this->payload); + $pl->build(); + + $this->assertEquals(36.0, $pl->getIncome()); + $this->assertEquals(4, $pl->getIncomeTaxes()); + + $this->account->delete(); + } + + + public function testSimpleInvoicePaymentIncome() + { + $this->buildData(); + + $this->payload = [ + 'start_date' => '2000-01-01', + 'end_date' => '2030-01-11', + 'date_range' => 'custom', + 'is_income_billed' => false, + 'include_tax' => false + ]; + + + $settings = ClientSettings::defaults(); + $settings->currency_id = "1"; + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'is_deleted' => 0, + 'settings' => $settings, + ]); + + $contact = ClientContact::factory()->create([ + 'client_id' => $client->id + ]); + + $i = Invoice::factory()->create([ + 'client_id' => $client->id, + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'amount' => 10, + 'balance' => 10, + 'status_id' => 2, + 'total_taxes' => 0, + 'date' => '2022-01-01', + 'terms' => 'nada', + 'discount' => 0, + 'tax_rate1' => 0, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name1' => "", + 'tax_name2' => '', + 'tax_name3' => '', + 'uses_inclusive_taxes' => true, + 'exchange_rate' => 1 + ]); + + $i->service()->markPaid()->save(); + + $pl = new ProfitLoss($this->company, $this->payload); + $pl->build(); + + $this->assertEquals(10.0, $pl->getIncome()); + + $this->account->delete(); + } + + + public function testSimpleExpense() + { + $this->buildData(); + + $e = Expense::factory()->create([ + 'amount' => 10, + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'date' => '2022-01-01', + ]); + + $pl = new ProfitLoss($this->company, $this->payload); + $pl->build(); + + $expenses = $pl->getExpenses(); + + $expense = $expenses[0]; + + $this->assertEquals(10, $expense->total); + + $this->account->delete(); + + } + + public function testSimpleExpenseAmountTax() + { + $this->buildData(); + + $e = ExpenseFactory::create($this->company->id, $this->user->id); + $e->amount = 10; + $e->date = '2022-01-01'; + $e->calculate_tax_by_amount = true; + $e->tax_amount1 = 10; + $e->save(); + + $pl = new ProfitLoss($this->company, $this->payload); + $pl->build(); + + $expenses = $pl->getExpenses(); + + $expense = $expenses[0]; + + $this->assertEquals(10, $expense->total); + $this->assertEquals(10, $expense->tax); + + $this->account->delete(); + + } + + public function testSimpleExpenseTaxRateExclusive() + { + $this->buildData(); + + $e = ExpenseFactory::create($this->company->id, $this->user->id); + $e->amount = 10; + $e->date = '2022-01-01'; + $e->tax_rate1 = 10; + $e->tax_name1 = 'GST'; + $e->uses_inclusive_taxes = false; + $e->save(); + + $pl = new ProfitLoss($this->company, $this->payload); + $pl->build(); + + $expenses = $pl->getExpenses(); + + $expense = $expenses[0]; + + $this->assertEquals(10, $expense->total); + $this->assertEquals(1, $expense->tax); + + $this->account->delete(); + + } + + public function testSimpleExpenseTaxRateInclusive() + { + $this->buildData(); + + $e = ExpenseFactory::create($this->company->id, $this->user->id); + $e->amount = 10; + $e->date = '2022-01-01'; + $e->tax_rate1 = 10; + $e->tax_name1 = 'GST'; + $e->uses_inclusive_taxes = false; + $e->save(); + + $pl = new ProfitLoss($this->company, $this->payload); + $pl->build(); + + $expenses = $pl->getExpenses(); + + $expense = $expenses[0]; + + $this->assertEquals(10, $expense->total); + $this->assertEquals(1, $expense->tax); + + $this->account->delete(); + + } + + + public function testSimpleExpenseBreakdown() + { + $this->buildData(); + + $e = Expense::factory()->create([ + 'amount' => 10, + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'date' => '2022-01-01', + 'exchange_rate' => 1, + 'currency_id' => $this->company->settings->currency_id + ]); + + $pl = new ProfitLoss($this->company, $this->payload); + $pl->build(); + + $expenses = $pl->getExpenses(); + + $bd = $pl->getExpenseBreakDown(); + + $this->assertEquals(array_sum(array_column($bd,'total')), 10); + + $this->account->delete(); + + } + + + public function testSimpleExpenseCategoriesBreakdown() + { + $this->buildData(); + + $ec = ExpenseCategoryFactory::create($this->company->id, $this->user->id); + $ec->name = 'Accounting'; + $ec->save(); + + $e = Expense::factory()->create([ + 'category_id' => $ec->id, + 'amount' => 10, + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'date' => '2022-01-01', + 'exchange_rate' => 1, + 'currency_id' => $this->company->settings->currency_id + ]); + + + $ec = ExpenseCategoryFactory::create($this->company->id, $this->user->id); + $ec->name = 'Fuel'; + $ec->save(); + + $e = Expense::factory(2)->create([ + 'category_id' => $ec->id, + 'amount' => 10, + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'date' => '2022-01-01', + 'exchange_rate' => 1, + 'currency_id' => $this->company->settings->currency_id + ]); + + + $pl = new ProfitLoss($this->company, $this->payload); + $pl->build(); + + $expenses = $pl->getExpenses(); + + $bd = $pl->getExpenseBreakDown(); + + $this->assertEquals(array_sum(array_column($bd,'total')), 30); + + $this->account->delete(); + + } + + + public function testCsvGeneration() + { + $this->buildData(); + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'is_deleted' => 0, + ]); + + Invoice::factory()->count(1)->create([ + 'client_id' => $client->id, + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'amount' => 10, + 'balance' => 10, + 'status_id' => 2, + 'total_taxes' => 1, + 'date' => '2022-01-01', + 'terms' => 'nada', + 'discount' => 0, + 'tax_rate1' => 10, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name1' => "GST", + 'tax_name2' => '', + 'tax_name3' => '', + 'uses_inclusive_taxes' => true, + ]); + + $ec = ExpenseCategoryFactory::create($this->company->id, $this->user->id); + $ec->name = 'Accounting'; + $ec->save(); + + $e = Expense::factory()->create([ + 'category_id' => $ec->id, + 'amount' => 10, + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'date' => '2022-01-01', + 'exchange_rate' => 1, + 'currency_id' => $this->company->settings->currency_id + ]); + + + $ec = ExpenseCategoryFactory::create($this->company->id, $this->user->id); + $ec->name = 'Fuel'; + $ec->save(); + + $e = Expense::factory(2)->create([ + 'category_id' => $ec->id, + 'amount' => 10, + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'date' => '2022-01-01', + 'exchange_rate' => 1, + 'currency_id' => $this->company->settings->currency_id + ]); + + + $pl = new ProfitLoss($this->company, $this->payload); + $pl->build(); + +echo($pl->getCsv()); + + $this->assertNotNull($pl->getCsv()); + + $this->account->delete(); + + } + +} diff --git a/tests/Unit/ValidationRules/UniqueInvoiceNumberValidationTest.php b/tests/Unit/ValidationRules/UniqueInvoiceNumberValidationTest.php new file mode 100644 index 000000000000..c443fd6604cd --- /dev/null +++ b/tests/Unit/ValidationRules/UniqueInvoiceNumberValidationTest.php @@ -0,0 +1,81 @@ +withoutMiddleware( + ThrottleRequests::class + ); + + $this->makeTestData(); + + $this->withoutExceptionHandling(); + + } + + public function testValidEmailRule() + { + auth()->login($this->user); + auth()->user()->setCompany($this->company); + + Invoice::factory()->create([ + 'client_id' => $this->client->id, + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'paid_to_date' => 100, + 'status_id' => 4, + 'date' => now(), + 'due_date'=> now(), + 'number' => 'db_record' + ]); + + $data = [ + 'client_id' => $this->client->hashed_id, + 'paid_to_date' => 100, + 'status_id' => 4, + 'date' => now(), + 'due_date'=> now(), + 'number' => 'db_record' + ]; + + $rules = (new StoreInvoiceRequest())->rules(); + + $validator = Validator::make($data, $rules); + + $this->assertFalse($validator->passes()); + + } + + +} + +