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!

 
+ 'pro_plan_success' => 'Danke, dass Sie Invoice Ninja\'s Pro-Tarif gewählt haben!

 
Nächste SchritteEine bezahlbare Rechnung wurde an die Mailadresse, welche mit Ihrem Account verbunden ist, geschickt. Um alle der umfangreichen Pro Funktionen freizuschalten, folgen Sie bitte den Anweisungen in der Rechnung um ein Jahr die Pro Funktionen zu nutzen. Sie finden die Rechnung nicht? Sie benötigen weitere Hilfe? Wir helfen gerne - -- schicken Sie uns doch eine Email an contact@invoice-ninja.com', + -- schicken Sie uns doch eine E-Mail an contact@invoice-ninja.com', 'unsaved_changes' => 'Es liegen ungespeicherte Änderungen vor', 'custom_fields' => 'Benutzerdefinierte Felder', 'company_fields' => 'Firmenfelder', @@ -504,7 +504,7 @@ $LANG = array( 'notification_quote_approved_subject' => 'Angebot :invoice wurde von :client angenommen.', 'notification_quote_approved' => 'Der folgende Kunde :client nahm das Angebot :invoice über :amount an.', 'resend_confirmation' => 'Bestätigungsmail erneut senden', - 'confirmation_resent' => 'Bestätigungsemail wurde erneut versendet', + 'confirmation_resent' => 'Bestätigungs-E-Mail wurde erneut versendet', 'gateway_help_42' => ':link zum Registrieren auf BitPay.
Hinweis: benutze einen Legacy API Key, keinen API token.', 'payment_type_credit_card' => 'Kreditkarte', 'payment_type_paypal' => 'PayPal', @@ -602,7 +602,7 @@ $LANG = array( 'pro_plan_feature5' => 'Multi-Benutzer Zugriff & Aktivitätstracking', 'pro_plan_feature6' => 'Angebote & pro-forma Rechnungen erstellen', 'pro_plan_feature7' => 'Rechungstitelfelder und Nummerierung anpassen', - 'pro_plan_feature8' => 'PDFs an Kunden-Emails anhängen', + 'pro_plan_feature8' => 'PDFs an Kunden-E-Mails anhängen', 'resume' => 'Fortfahren', 'break_duration' => 'Pause', 'edit_details' => 'Details bearbeiten', @@ -645,8 +645,8 @@ $LANG = array( 'styles' => 'Stile', 'defaults' => 'Standards', 'margins' => 'Außenabstände', - 'header' => 'Kopf', - 'footer' => 'Fußzeile', + 'header' => 'Header-Code', + 'footer' => 'Footer-Code', 'custom' => 'Benutzerdefiniert', 'invoice_to' => 'Rechnung an', 'invoice_no' => 'Rechnung Nr.', @@ -689,9 +689,9 @@ $LANG = array( 'auto_bill' => 'Automatische Verrechnung', 'military_time' => '24-Stunden-Zeit', 'last_sent' => 'Zuletzt versendet', - 'reminder_emails' => 'Erinnerungs-Emails', - 'quote_reminder_emails' => 'Angebot Erinngerungs Emails', - 'templates_and_reminders' => 'Vorlagen & Erinnerungen', + 'reminder_emails' => 'Mahnungs-E-Mails', + 'quote_reminder_emails' => 'Angebots-Erinngerungs-E-Mails', + 'templates_and_reminders' => 'Vorlagen & Mahnungen', 'subject' => 'Betreff', 'body' => 'Inhalt', 'first_reminder' => 'Erste Erinnerung', @@ -889,9 +889,9 @@ $LANG = array( 'custom_invoice_charges_helps' => 'Füge ein Rechnungsgebührenfeld hinzu. Erfasse die Kosten, wenn eine neue Rechnung erstellt wird und addiere sie in den Zwischensummen der Rechnung.', 'token_expired' => 'Validierungstoken ist abgelaufen. Bitte probieren Sie es erneut.', 'invoice_link' => 'Link zur Rechnung', - 'button_confirmation_message' => 'Klicke um Deine Email zu bestätigen', + 'button_confirmation_message' => 'Bestätige deine E-Mail-Adresse.', 'confirm' => 'Bestätigen', - 'email_preferences' => 'Email Einstellungen', + 'email_preferences' => 'E-Mail-Einstellungen', 'created_invoices' => ':count Rechnung(en) erfolgreich erstellt', 'next_invoice_number' => 'Die nächste Rechnungsnummer ist :number.', 'next_quote_number' => 'Die nächste Angebotsnummer ist :number.', @@ -1048,13 +1048,13 @@ $LANG = array( 'invitation_status_sent' => 'Gesendet', 'invitation_status_opened' => 'Geöffnet', 'invitation_status_viewed' => 'Gesehen', - 'email_error_inactive_client' => 'Emails können nicht zu inaktiven Kunden gesendet werden', - 'email_error_inactive_contact' => 'Emails können nicht zu inaktiven Kontakten gesendet werden', - 'email_error_inactive_invoice' => 'Emails können nicht zu inaktiven Rechnungen gesendet werden', - 'email_error_inactive_proposal' => 'Emails können nicht für inaktive Vorschläge gesendet werden', - 'email_error_user_unregistered' => 'Bitte registrieren Sie sich um Emails zu versenden', - 'email_error_user_unconfirmed' => 'Bitte bestätigen Sie Ihr Konto um Emails zu senden', - 'email_error_invalid_contact_email' => 'Ungültige Kontakt Email Adresse', + 'email_error_inactive_client' => 'E-Mails können nicht zu inaktiven Kunden gesendet werden', + 'email_error_inactive_contact' => 'E-Mails können nicht zu inaktiven Kontakten gesendet werden', + 'email_error_inactive_invoice' => 'E-Mails können nicht zu inaktiven Rechnungen gesendet werden', + 'email_error_inactive_proposal' => 'E-Mails können nicht für inaktive Vorschläge gesendet werden', + 'email_error_user_unregistered' => 'Bitte registrieren Sie sich um E-Mails zu versenden', + 'email_error_user_unconfirmed' => 'Bitte bestätigen Sie Ihr Konto um E-Mails zu versenden', + 'email_error_invalid_contact_email' => 'Ungültige Kontakt-E-Mail-Adresse', 'navigation' => 'Navigation', 'list_invoices' => 'Rechnungen anzeigen', @@ -1316,7 +1316,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese 'enabled' => 'Aktiviert', 'paypal' => 'PayPal', 'braintree_enable_paypal' => 'PayPal Zahlungen mittels BrainTree aktivieren', - 'braintree_paypal_disabled_help' => 'Das PayPal Gateway bearbeitet PayPal-Zahlungen', + 'braintree_paypal_disabled_help' => 'Das PayPal-Gateway bearbeitet gerade PayPal-Zahlungen', 'braintree_paypal_help' => 'Sie müssen auch :link', 'braintree_paypal_help_link_text' => 'PayPal Konto mit BrainTree verknüpfen', 'token_billing_braintree_paypal' => 'Zahlungsdetails speichern', @@ -1339,7 +1339,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese 'wepay_description_help' => 'Zweck des Kontos.', 'wepay_tos_agree' => 'Ich stimme den :link zu', 'wepay_tos_link_text' => 'WePay Servicebedingungen', - 'resend_confirmation_email' => 'Bestätigungsemail nochmal senden', + 'resend_confirmation_email' => 'Bestätigungs-E-Mail nochmal senden', 'manage_account' => 'Account managen', 'action_required' => 'Handeln erforderlich', 'finish_setup' => 'Setup abschliessen', @@ -1846,7 +1846,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese 'buy_now_buttons_disabled' => 'Diese Funktion setzt voraus, dass ein Produkt erstellt und ein Zahlungs-Gateway konfiguriert wurde.', 'enable_buy_now_buttons_help' => 'Aktiviere Unterstützung für "Kaufe jetzt"-Buttons', 'changes_take_effect_immediately' => 'Anmerkung: Änderungen treten sofort in Kraft', - 'wepay_account_description' => 'Zahlungsanbieter für Invoice Ninja', + 'wepay_account_description' => 'Zahlungs-Gateway für Invoice Ninja', 'payment_error_code' => 'Bei der Bearbeitung Ihrer Zahlung [:code] gab es einen Fehler. Bitte versuchen Sie es später erneut.', 'standard_fees_apply' => 'Standardgebühren werden erhoben: 2,9% + 0,25€ pro erfolgreicher Belastung bei nicht-europäischen Kreditkarten und 1,4% + 0,25€ bei europäischen Kreditkarten.', 'limit_import_rows' => 'Daten müssen in Stapeln von :count Zeilen oder weniger importiert werden', @@ -1986,38 +1986,6 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese 'authorization' => 'Genehmigung', 'signed' => 'unterzeichnet', - // BlueVine - 'bluevine_promo' => 'Factoring und Bonitätsauskünfte von BlueVine bestellen.', - 'bluevine_modal_label' => 'Anmelden mit BlueVine', - 'bluevine_modal_text' => '

Schnelle Finanzierung ohne Papierkram.

-', - 'bluevine_create_account' => 'Konto erstellen', - 'quote_types' => 'Angebot erhalten für', - 'invoice_factoring' => 'Factoring', - 'line_of_credit' => 'Bonitätsprüfung', - 'fico_score' => 'Ihre FICO Bewertung', - 'business_inception' => 'Gründungsdatum', - 'average_bank_balance' => 'durchschnittlicher Kontostand', - 'annual_revenue' => 'Jahresertrag', - 'desired_credit_limit_factoring' => 'Gewünschtes Factoring Limit', - 'desired_credit_limit_loc' => 'gewünschter Kreditrahmen', - 'desired_credit_limit' => 'gewünschtes Kreditlimit', - 'bluevine_credit_line_type_required' => 'Sie müssen mindestens eine auswählen', - 'bluevine_field_required' => 'Dies ist ein Pflichtfeld', - 'bluevine_unexpected_error' => 'Ein unerwarteter Fehler ist aufgetreten.', - 'bluevine_no_conditional_offer' => 'Mehr Information ist vonnöten um ein Angebot erstellen zu können. Bitte klicken Sie unten auf Weiter.', - 'bluevine_invoice_factoring' => 'Factoring', - 'bluevine_conditional_offer' => 'Freibleibendes Angebot', - 'bluevine_credit_line_amount' => 'Kreditline', - 'bluevine_advance_rate' => 'Finanzierungsanteil', - 'bluevine_weekly_discount_rate' => 'Wöchentlicher Rabatt', - 'bluevine_minimum_fee_rate' => 'Minimale Gebühr', - 'bluevine_line_of_credit' => 'Kreditline', - 'bluevine_interest_rate' => 'Zinssatz', - 'bluevine_weekly_draw_rate' => 'Wöchtentliche Rückzahlungsquote', - 'bluevine_continue' => 'Weiter zu BlueVine', - 'bluevine_completed' => 'BlueVine Anmeldung abgeschlossen', - 'vendor_name' => 'Lieferant', 'entity_state' => 'Status', 'client_created_at' => 'Erstellungsdatum', @@ -2286,8 +2254,8 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese 'plan_price' => 'Tarifkosten', 'wrong_confirmation' => 'Falscher Bestätigungscode', 'oauth_taken' => 'Dieses Konto ist bereits registriert', - 'emailed_payment' => 'Zahlungs eMail erfolgreich gesendet', - 'email_payment' => 'Sende Zahlungs eMail', + 'emailed_payment' => 'Zahlungs-E-Mail erfolgreich gesendet', + 'email_payment' => 'Sende Zahlungs-E-Mail', 'invoiceplane_import' => 'Benutzer :link für die Datenmigration von InvoicePlane.', 'duplicate_expense_warning' => 'Achtung: :link evtl. schon vorhanden.', 'expense_link' => 'Ausgabe', @@ -2568,7 +2536,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese 'time_hr' => 'Stunde', 'time_hrs' => 'Stunden', 'clear' => 'Löschen', - 'warn_payment_gateway' => 'Hinweis: Die Annahme von Online-Zahlungen erfordert einen Zahlungsanbieter. Zum hinzufügen :link.', + 'warn_payment_gateway' => 'Hinweis: Die Annahme von Online-Zahlungen erfordert ein Zahlungs-Gateway. Zum hinzufügen :link.', 'task_rate' => 'Kosten für Tätigkeit', 'task_rate_help' => 'Legen Sie den Standardtarif für fakturierte Aufgaben fest.', 'past_due' => 'Überfällig', @@ -2687,10 +2655,10 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese 'module_task' => 'Aufgaben und Projekte', 'module_expense' => 'Ausgaben & Lieferanten', 'module_ticket' => 'Tickets', - 'reminders' => 'Erinnerungen', - 'send_client_reminders' => 'E-Mail Erinnerungen versenden', + 'reminders' => 'Mahnungen', + 'send_client_reminders' => 'Mahnung per E-Mail versenden', 'can_view_tasks' => 'Aufgaben sind im Portal sichtbar', - 'is_not_sent_reminders' => 'Erinnerungen werden nicht gesendet', + 'is_not_sent_reminders' => 'Mahnungen werden nicht versendet', 'promotion_footer' => 'Ihre Promotion läuft bald ab, :link, um jetzt ein Upgrade durchzuführen.', 'unable_to_delete_primary' => 'Hinweis: Um diese Firma zu löschen, löschen Sie zunächst alle verknüpften Unternehmen.', 'please_register' => 'Bitte erstellen Sie sich einen Account', @@ -2853,7 +2821,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese 'accept' => 'Akzeptieren', 'accepted_terms' => 'Die neuesten Nutzungsbedingungen wurden akzeptiert.', 'invalid_url' => 'Ungültige URL', - 'workflow_settings' => 'Workflow Einstellungen', + 'workflow_settings' => 'Workflow-Einstellungen', 'auto_email_invoice' => 'Automatische Email', 'auto_email_invoice_help' => 'Senden Sie wiederkehrende Rechnungen automatisch per E-Mail, wenn sie erstellt werden.', 'auto_archive_invoice' => 'Automatisches Archiv', @@ -2872,7 +2840,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese 'purge_client_warning' => 'Alle zugehörigen Datensätze (Rechnungen, Aufgaben, Ausgaben, Dokumente usw.) werden ebenfalls gelöscht.', 'clone_product' => 'Produkt duplizieren', 'item_details' => 'Artikeldetails', - 'send_item_details_help' => 'Senden Sie die Einzelpostendetails an das Zahlungsportal.', + 'send_item_details_help' => 'Senden Sie die Einzelpostendetails an das Zahlungs-Gateway.', 'view_proposal' => 'Vorschlag ansehen', 'view_in_portal' => 'Im Portal anzeigen', 'cookie_message' => 'Diese Website verwendet Cookies, um sicherzustellen, dass Sie das beste Ergebnis auf unserer Website erzielen.', @@ -2974,7 +2942,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese 'size' => 'Größe', 'net' => 'Netto', 'show_tasks' => 'Aufgaben anzeigen', - 'email_reminders' => 'E-Mail Erinnerungen', + 'email_reminders' => 'Mahnungs-E-Mail', 'reminder1' => 'Erste Erinnerung', 'reminder2' => 'Zweite Erinnerung', 'reminder3' => 'Dritte Erinnerung', @@ -3217,7 +3185,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese 'surcharge_field' => 'Zuschlagsfeld', 'company_value' => 'Firmenwert', 'credit_field' => 'Kredit-Feld', - 'payment_field' => 'Zahlungs-Feld', + 'payment_field' => 'Zahlungsfeld', 'group_field' => 'Gruppen-Feld', 'number_counter' => 'Nummernzähler', 'number_pattern' => 'Nummernschema', @@ -3256,7 +3224,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese 'ocde' => 'Code', 'date_format' => 'Datumsformat', 'datetime_format' => 'Datums-/Zeitformat', - 'send_reminders' => 'Erinnerungen senden', + 'send_reminders' => 'Mahnung senden', 'timezone' => 'Zeitzone', 'filtered_by_group' => 'Gefiltert nach Gruppe', 'filtered_by_invoice' => 'Gefiltert nach Rechnung', @@ -3274,7 +3242,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese 'upload_logo' => 'Logo hochladen', 'uploaded_logo' => 'Logo erfolgreich hochgeladen', 'saved_settings' => 'Einstellungen erfolgreich gespeichert', - 'device_settings' => 'Geräteeinstellungen', + 'device_settings' => 'Geräte-Einstellungen', 'credit_cards_and_banks' => 'Kreditkarten & Banken', 'price' => 'Preis', 'email_sign_up' => 'E-Mail-Registrierung', @@ -3454,9 +3422,9 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese 'default_tax_rate_2' => 'Standard-Steuersatz 2', 'default_tax_name_3' => 'Standard-Steuername 3', 'default_tax_rate_3' => 'Standard-Steuersatz 3', - 'email_subject_invoice' => 'EMail Rechnung Betreff', - 'email_subject_quote' => 'EMail Angebot Betreff', - 'email_subject_payment' => 'EMail Zahlung Betreff', + 'email_subject_invoice' => 'E-Mail Rechnung Betreff', + 'email_subject_quote' => 'E-Mail Angebot Betreff', + 'email_subject_payment' => 'E-Mail Zahlung Betreff', 'switch_list_table' => 'Listenansicht umschalten', 'client_city' => 'Kunden-Stadt', 'client_state' => 'Kunden-Bundesland/Kanton', @@ -3544,7 +3512,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese 'clone_to_credit' => 'Duplizieren in Gutschrift', 'emailed_credit' => 'Guthaben erfolgreich per E-Mail versendet', 'marked_credit_as_sent' => 'Guthaben erfolgreich als versendet markiert', - 'email_subject_payment_partial' => 'EMail Teilzahlung Betreff', + 'email_subject_payment_partial' => 'E-Mail Teilzahlung Betreff', 'is_approved' => 'Wurde angenommen', 'migration_went_wrong' => 'Upps, da ist etwas schiefgelaufen! Stellen Sie sicher, dass Sie InvoiceNinja v5 richtig eingerichtet haben, bevor Sie die Migration starten.', 'cross_migration_message' => 'Kontoübergreifende Migration ist nicht erlaubt. Mehr Informationen finden Sie hier: @@ -3591,7 +3559,7 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'search_users' => 'Suche Benutzer', 'search_tax_rates' => 'Suche Steuersatz', 'search_tasks' => 'Suche Aufgaben', - 'search_settings' => 'Suche Einstellungen', + 'search_settings' => 'Such-Einstellungen', 'search_projects' => 'Suche nach Projekten', 'search_expenses' => 'Suche Ausgaben', 'search_payments' => 'Suche Zahlungen', @@ -3652,7 +3620,7 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'gross' => 'Gesamtbetrag', 'net_amount' => 'Netto Betrag', 'net_balance' => 'Netto Betrag', - 'client_settings' => 'Kundeneinstellungen', + 'client_settings' => 'Kunden-Einstellungen', 'selected_invoices' => 'Ausgewählte Rechnungen', 'selected_payments' => 'Ausgewählte Zahlungen', 'selected_quotes' => 'Ausgewählte Angebote', @@ -3724,16 +3692,16 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'mark_invoiceable_help' => 'Ermögliche diese Ausgabe in Rechnung zu stellen', 'add_documents_to_invoice_help' => 'Dokumente sichtbar machen', 'convert_currency_help' => 'Wechselkurs setzen', - 'expense_settings' => 'Ausgaben Einstellungen', + 'expense_settings' => 'Ausgaben-Einstellungen', 'clone_to_recurring' => 'Duplizieren zu Widerkehrend', 'crypto' => 'Verschlüsselung', - 'user_field' => 'Benutzer Feld', + 'user_field' => 'Benutzerfeld', 'variables' => 'Variablen', 'show_password' => 'Zeige Passwort', 'hide_password' => 'Verstecke Passwort', 'copy_error' => 'Kopier Fehler', 'capture_card' => 'Zahlungsmittel für die weitere Verwendung speichern', - 'auto_bill_enabled' => 'Automatische Rechnungsstellung aktivieren', + 'auto_bill_enabled' => 'Automatische Bezahlung aktivieren', 'total_taxes' => 'Gesamt Steuern', 'line_taxes' => 'Belegposition Steuer', 'total_fields' => 'Gesamt Felder', @@ -3741,7 +3709,7 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'started_recurring_invoice' => 'Wiederkehrende Rechnung erfolgreich gestartet', 'resumed_recurring_invoice' => 'Wiederkehrende Rechnung erfolgreich fortgesetzt', 'gateway_refund' => 'Zahlungsanbieter Rückerstattung', - 'gateway_refund_help' => 'Bearbeite die Rückerstattung über den Zahlungsanbieter', + 'gateway_refund_help' => 'Rückerstattung über das Zahlungs-Gateway abwickeln', 'due_date_days' => 'Fälligkeitsdatum', 'paused' => 'Pausiert', 'day_count' => 'Tag :count', @@ -3785,7 +3753,7 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'invoice_task_timelog_help' => 'Zeitdetails in der Rechnungsposition ausweisen', 'auto_start_tasks_help' => 'Beginne Aufgabe vor dem Speichern', 'configure_statuses' => 'Stati bearbeiten', - 'task_settings' => 'Aufgaben Einstellungen', + 'task_settings' => 'Aufgaben-Einstellungen', 'configure_categories' => 'Kategorien bearbeiten', 'edit_expense_category' => 'Ausgaben Kategorie bearbeiten', 'removed_expense_category' => 'Ausgaben Kategorie erfolgreich entfernt', @@ -3924,7 +3892,7 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'before_taxes' => 'Vor Steuern', 'after_taxes' => 'Nach Steuern', 'color' => 'Farbe', - 'show' => 'anzeigen', + 'show' => 'Anzeigen', 'empty_columns' => 'Leere Spalten', 'project_name' => 'Projektname', 'counter_pattern_error' => 'Um :client_counter zu verwenden, fügen Sie bitte entweder :client_number oder :client_id_number hinzu, um Konflikte zu vermeiden', @@ -4180,7 +4148,7 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'count_days' => ':count Tage', 'web_session_timeout' => 'Web-Sitzungs-Timeout', 'security_settings' => 'Sicherheitseinstellungen', - 'resend_email' => 'Bestätigungsemail erneut versenden ', + 'resend_email' => 'Bestätigungs-E-Mail erneut versenden ', 'confirm_your_email_address' => 'Bitte bestätigen Sie Ihre E-Mail-Adresse', 'freshbooks' => 'FreshBooks', 'invoice2go' => 'Invoice2go', @@ -4195,7 +4163,7 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'billing_coupon_notice' => 'Ihr Rabatt wird an der Kasse abgezogen.', 'use_last_email' => 'Vorherige E-Mail benutzen', 'activate_company' => 'Unternehmen aktivieren', - 'activate_company_help' => 'Aktivieren sie Email, wiederkehrende Rechnungen und Benachrichtigungen', + 'activate_company_help' => 'E-Mails, wiederkehrende Rechnungen und Benachrichtigungen aktivieren', 'an_error_occurred_try_again' => 'Ein Fehler ist aufgetreten, bitte versuchen Sie es erneut.', 'please_first_set_a_password' => 'Bitte vergeben Sie zuerst ein Passwort.', 'changing_phone_disables_two_factor' => 'Achtung: Das Ändern deiner Telefonnummer wird die Zwei-Faktor-Authentifizierung deaktivieren', @@ -4269,7 +4237,7 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'user_duplicate_error' => 'Derselbe Benutzer kann nicht derselben Firma hinzugefügt werden', 'user_cross_linked_error' => 'Der Benutzer ist vorhanden, kann aber nicht mit mehreren Konten verknüpft werden', 'ach_verification_notification_label' => 'ACH-Verifizierung', - 'ach_verification_notification' => 'Für die Verbindung von Bankkonten ist eine Überprüfung erforderlich. Das Zahlungsgateway sendet zu diesem Zweck automatisch zwei kleine Einzahlungen. Es dauert 1-2 Werktage, bis diese Einzahlungen auf dem Online-Kontoauszug des Kunden erscheinen.', + 'ach_verification_notification' => 'Für die Verbindung von Bankkonten ist eine Überprüfung erforderlich. Das Zahlungs-Gateway sendet zu diesem Zweck automatisch zwei kleine Einzahlungen. Es dauert 1-2 Werktage, bis diese Einzahlungen auf dem Online-Kontoauszug des Kunden erscheinen.', 'login_link_requested_label' => 'Anmeldelink angefordert', 'login_link_requested' => 'Es gab eine Aufforderung, sich über einen Link anzumelden. Wenn Sie dies nicht angefordert haben, können Sie es ignorieren.', 'invoices_backup_subject' => 'Ihre Rechnungen stehen zum Download bereit', @@ -4281,7 +4249,7 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'company_import_failure_subject' => 'Fehler beim Importieren von :company', 'company_import_failure_body' => 'Beim Importieren der Unternehmensdaten ist ein Fehler aufgetreten, die Fehlermeldung lautete:', 'recurring_invoice_due_date' => 'Fälligkeitsdatum', - 'amount_cents' => 'Betrag in Pfennigen, Pence oder Cents', + 'amount_cents' => 'Betrag in Pennies, Pence oder Cent, d. h. für $0.10 bitte 10 eingeben', 'default_payment_method_label' => 'Standard Zahlungsart', 'default_payment_method' => 'Machen Sie dies zu Ihrer bevorzugten Zahlungsmethode', 'already_default_payment_method' => 'Dies ist die von Ihnen bevorzugte Art der Bezahlung.', @@ -4363,7 +4331,7 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'show_pdf_preview_help' => 'PDF-Vorschau bei der Bearbeitung von Rechnungen anzeigen', 'print_pdf' => 'PDF drucken', 'remind_me' => 'Erinnere mich', - 'instant_bank_pay' => 'Instant Bank Pay', + 'instant_bank_pay' => 'Sofortige Banküberweisung', 'click_selected' => 'Ausgewähltes anklicken', 'hide_preview' => 'Vorschau ausblenden', 'edit_record' => 'Datensatz bearbeiten', @@ -4380,8 +4348,8 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'persist_ui_help' => 'UI-Status lokal speichern, damit die Anwendung an der letzten Position startet (Deaktivierung kann die Leistung verbessern)', 'client_postal_code' => 'Postleitzahl des Kunden', 'client_vat_number' => 'Umsatzsteuer-Identifikationsnummer des Kunden', - 'has_tasks' => 'Has Tasks', - 'registration' => 'Registration', + 'has_tasks' => 'Hat Aufgaben', + 'registration' => 'Registrierung', 'unauthorized_stripe_warning' => 'Bitte autorisieren Sie Stripe zur Annahme von Online-Zahlungen.', 'fpx' => 'FPX', 'update_all_records' => 'Alle Datensätze aktualisieren', @@ -4389,14 +4357,14 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'updated_company' => 'Unternehmen wurde erfolgreich aktualisiert', 'kbc' => 'KBC', 'why_are_you_leaving' => 'Helfen Sie uns, uns zu verbessern, indem Sie uns sagen, warum (optional)', - 'webhook_success' => 'Webhook Success', + 'webhook_success' => 'Webhook erfolgreich', 'error_cross_client_tasks' => 'Die Aufgaben müssen alle zum selben Kunden gehören', 'error_cross_client_expenses' => 'Die Ausgaben müssen alle zu demselben Kunden gehören', 'app' => 'App', 'for_best_performance' => 'Für die beste Leistung laden Sie die App herunter :app', - 'bulk_email_invoice' => 'Email Invoice', - 'bulk_email_quote' => 'Email Quote', - 'bulk_email_credit' => 'Email Credit', + 'bulk_email_invoice' => 'Email Rechnung', + 'bulk_email_quote' => 'Angebot per E-Mail senden', + 'bulk_email_credit' => 'Guthaben per E-Mail senden', 'removed_recurring_expense' => 'Erfolgreich wiederkehrende Ausgaben entfernt', 'search_recurring_expense' => 'Wiederkehrende Ausgaben suchen', 'search_recurring_expenses' => 'Wiederkehrende Ausgaben suchen', @@ -4404,18 +4372,18 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'include_drafts' => 'Entwürfe einschließen', 'include_drafts_help' => 'Entwürfe von Aufzeichnungen in Berichte einbeziehen', 'is_invoiced' => 'Ist in Rechnung gestellt', - 'change_plan' => 'Change Plan', + 'change_plan' => 'Tarif ändern', 'persist_data' => 'Daten aufbewahren', 'customer_count' => 'Kundenzahl', 'verify_customers' => 'Kunden überprüfen', 'google_analytics_tracking_id' => 'Google Analytics Tracking ID', - 'decimal_comma' => 'Decimal Comma', + 'decimal_comma' => 'Dezimaltrennzeichen', 'use_comma_as_decimal_place' => 'Komma als Dezimalstelle in Formularen verwenden', - 'select_method' => 'Select Method', - 'select_platform' => 'Select Platform', + 'select_method' => 'Methode auswählen', + 'select_platform' => 'Plattform auswählen', 'use_web_app_to_connect_gmail' => 'Bitte verwenden Sie die Web-App, um sich mit Gmail zu verbinden', 'expense_tax_help' => 'Postensteuersätze sind deaktiviert', - 'enable_markdown' => 'Enable Markdown', + 'enable_markdown' => 'Markdown verwenden', 'enable_markdown_help' => 'Konvertierung von Markdown in HTML in der PDF-Datei', 'add_second_contact' => 'Zweiten Kontakt hinzufügen', 'previous_page' => 'Vorherige Seite', @@ -4462,36 +4430,36 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'include_deleted_clients_help' => 'Datensätze von gelöschten Kunden laden', 'step_1_sign_in' => 'Schritt 1: Registrieren', 'step_2_authorize' => 'Schritt 2: autorisieren', - 'account_id' => 'Account ID', + 'account_id' => 'Konto-ID', 'migration_not_yet_completed' => 'Die Migration ist noch nicht abgeschlossen', 'show_task_end_date' => 'Ende der Aufgabe anzeigen', 'show_task_end_date_help' => 'Aktivieren Sie die Angabe des Enddatums der Aufgabe', 'gateway_setup' => 'Gateway-Einstellungen', 'preview_sidebar' => 'Vorschau der Seitenleiste', - 'years_data_shown' => 'Years Data Shown', + 'years_data_shown' => 'Daten für wie viele Jahre anzeigen?', 'ended_all_sessions' => 'alle Sitzungen erfolgreich beendet', 'end_all_sessions' => 'Alle Sitzungen beenden', - 'count_session' => '1 Session', - 'count_sessions' => ':count Sessions', + 'count_session' => '1 Sitzung', + 'count_sessions' => ':count Sitzungen', 'invoice_created' => 'Rechnung erstellt', 'quote_created' => 'Angebot erstellt', 'credit_created' => 'Gutschrift erstellt', 'enterprise' => 'Enterprise', - 'invoice_item' => 'Invoice Item', - 'quote_item' => 'Quote Item', - 'order' => 'Order', - 'search_kanban' => 'Search Kanban', - 'search_kanbans' => 'Search Kanban', - 'move_top' => 'Nach oben bewegen', - 'move_up' => 'Nach unten bewegen', - 'move_down' => 'Move Down', - 'move_bottom' => 'Move Bottom', + 'invoice_item' => 'Rechnungsposition', + 'quote_item' => 'Angebotsposition', + 'order' => 'Bestellung', + 'search_kanban' => 'Kanban auswählen', + 'search_kanbans' => 'Kanban auswählen', + 'move_top' => 'Ganz nach oben verschieben', + 'move_up' => 'Nach oben verschieben', + 'move_down' => 'Nach unten verschieben', + 'move_bottom' => 'Ganz nach unten verschieben', 'body_variable_missing' => 'Fehler: das benutzerdefinierte E-Mail Template muss die :body Variable beinhalten', 'add_body_variable_message' => 'bitte stelle sicher das die :body Variable eingefügt ist', - 'view_date_formats' => 'View Date Formats', - 'is_viewed' => 'Is Viewed', + 'view_date_formats' => 'Zeige Datumsformate', + 'is_viewed' => 'Ist angesehen', 'letter' => 'Letter', - 'legal' => 'Legal', + 'legal' => 'Rechtliches', 'page_layout' => 'Seiten Layout', 'portrait' => 'Hochformat', 'landscape' => 'Querformat', @@ -4499,31 +4467,31 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'upgrade_to_paid_plan' => 'Führen Sie ein Upgrade auf einen kostenpflichtigen Plan durch, um die erweiterten Einstellungen zu aktivieren', 'invoice_payment_terms' => 'Zahlungsbedingungen für Rechnungen', 'quote_valid_until' => 'Angebot gültig bis', - 'no_headers' => 'No Headers', - 'add_header' => 'Add Header', - 'remove_header' => 'Remove Header', - 'return_url' => 'Return URL', - 'rest_method' => 'REST Method', - 'header_key' => 'Header Key', - 'header_value' => 'Header Value', - 'recurring_products' => 'Recurring Products', - 'promo_discount' => 'Promo Discount', - 'allow_cancellation' => 'Allow Cancellation', - 'per_seat_enabled' => 'Per Seat Enabled', - 'max_seats_limit' => 'Max Seats Limit', - 'trial_enabled' => 'Trial Enabled', - 'trial_duration' => 'Trial Duration', - 'allow_query_overrides' => 'Allow Query Overrides', - 'allow_plan_changes' => 'Allow Plan Changes', + 'no_headers' => 'Keine Header', + 'add_header' => 'Header hinzufügen', + 'remove_header' => 'Kopfzeile entfernen', + 'return_url' => 'Return-URL', + 'rest_method' => 'REST-Methode', + 'header_key' => 'Header-Key', + 'header_value' => 'Header-Wert', + 'recurring_products' => 'Wiederkehrende Produkte', + 'promo_discount' => 'Promo-Rabatt', + 'allow_cancellation' => 'Ermögliche Storno', + 'per_seat_enabled' => 'Pro Platz Aktiviert', + 'max_seats_limit' => 'Max. Plätze Limit', + 'trial_enabled' => 'Testversion aktiv', + 'trial_duration' => 'Testzeitraum', + 'allow_query_overrides' => 'Überschreiben von Abfragen zulassen', + 'allow_plan_changes' => 'Planänderungen zulassen', 'plan_map' => 'Plan Map', - 'refund_period' => 'Refund Period', - 'webhook_configuration' => 'Webhook Configuration', - 'purchase_page' => 'Purchase Page', + 'refund_period' => 'Erstattungszeitraum', + 'webhook_configuration' => 'Webhook-Konfiguration', + 'purchase_page' => 'Kauf-Seite', 'email_bounced' => 'E-Mail zurückgesendet', - 'email_spam_complaint' => 'Spam Complaint', + 'email_spam_complaint' => 'Spam-Beschwerde', 'email_delivery' => 'E-Mail-Zustellung', - 'webhook_response' => 'Webhook Response', - 'pdf_response' => 'PDF Response', + 'webhook_response' => 'Webhook-Antwort', + 'pdf_response' => 'PDF-Antwort', 'authentication_failure' => 'Authentifizierungsfehler', 'pdf_failed' => 'PDF fehgeschlagen', 'pdf_success' => 'PDF erfolgreich', @@ -4532,7 +4500,7 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'html_mode_help' => 'Vorschau von Aktualisierungen schneller, aber weniger genau', 'status_color_theme' => 'Status Farbschema', 'load_color_theme' => 'lade Farbschema', - 'lang_Estonian' => 'Estonian', + 'lang_Estonian' => 'estnisch', 'marked_credit_as_paid' => 'Guthaben erfolgreich als bezahlt markiert', 'marked_credits_as_paid' => 'Erfolgreich Kredite als bezahlt markiert', 'wait_for_loading' => 'Daten werden geladen - bitte warten Sie, bis der Vorgang abgeschlossen ist', @@ -4546,7 +4514,69 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'activity_123' => ':user löschte wiederkehrende Ausgabe :recurring_expense', 'activity_124' => ':user stellte wiederkehrende Ausgabe :recurring_expense wieder her', 'fpx' => "FPX", - + 'to_view_entity_set_password' => 'Um die :entity zu sehen, müssen Sie ein Passwort festlegen.', + 'unsubscribe' => 'Deabonnieren', + 'unsubscribed' => 'Deabonniert', + 'unsubscribed_text' => 'Du erhältst nun keine Benachrichtigungen für dieses Dokument mehr.', + 'client_shipping_state' => 'Liefer-Region Kunde', + 'client_shipping_city' => 'Lieferort Kunde', + 'client_shipping_postal_code' => 'Liefer-PLZ Kunde', + 'client_shipping_country' => 'Kunde Lieferung LAND', + 'load_pdf' => 'PDF laden', + 'start_free_trial' => 'Kostenlose Testversion starten', + 'start_free_trial_message' => 'Teste den Pro-Tarif GRATIS für 14 Tage', + 'due_on_receipt' => 'Fällig bei Erhalt', + 'is_paid' => 'Ist bezahlt', + 'age_group_paid' => 'Bezahlt', + 'id' => 'ID', + 'convert_to' => 'Umwandeln in', + 'client_currency' => 'Kundenwährung', + 'company_currency' => 'Firmenwährung', + 'custom_emails_disabled_help' => 'Um Spam zu verhindern braucht es ein Upgrade zu einem bezahlten Account um das E-Mail anzupassen.', + 'upgrade_to_add_company' => 'Upgrade deinen Tarif um weitere Firmen hinzuzufügen', + 'file_saved_in_downloads_folder' => 'Die Datei wurde im Downloads-Ordner gespeichert', + 'small' => 'Klein', + 'quotes_backup_subject' => 'Deine Angebote stehen zum Download bereit', + 'credits_backup_subject' => 'Deine Guthaben stehen zum Download bereit', + 'document_download_subject' => 'Deine Dokumente stehen zum Download bereit', + 'reminder_message' => 'Mahnung für Rechnung :number über :balance', + 'gmail_credentials_invalid_subject' => 'Senden mit ungültigen GMail-Anmeldedaten', + 'gmail_credentials_invalid_body' => 'Ihre GMail-Anmeldedaten sind nicht korrekt. Bitte melden Sie sich im Administratorportal an und navigieren Sie zu Einstellungen > Benutzerdetails und trennen Sie Ihr GMail-Konto und verbinden Sie es erneut. Wir werden Ihnen diese Benachrichtigung täglich senden, bis das Problem behoben ist', + 'notification_invoice_sent' => 'Rechnung :invoice über :amount wurde an den Kunden :client versendet.', + 'total_columns' => 'Felder insgesamt', + 'view_task' => 'Aufgabe anzeugen', + 'cancel_invoice' => 'Abbrechen', + 'changed_status' => 'Erfolgreich Aufgabenstatus geändert', + 'change_status' => 'Status ändern', + 'enable_touch_events' => 'Touchscreen-Modus aktivieren', + 'enable_touch_events_help' => 'Scrollen durch wischen', + 'after_saving' => 'Nach dem Speichern', + 'view_record' => 'Datensatz anzeigen', + 'enable_email_markdown' => 'Markdown in E-Mails verwenden', + 'enable_email_markdown_help' => 'Visuellen Markdown-Editor für E-Mails verwenden', + 'enable_pdf_markdown' => 'Markdown in PDFs verwenden', + 'json_help' => 'Achtung: JSON-Dateien, die mit v4 der App erstellt wurden, werden nicht unterstützt', + 'release_notes' => 'Versionshinweise', + 'upgrade_to_view_reports' => 'Upgrade deinen Tarif um Berichte anzusehen', + 'started_tasks' => ':value Aufgaben erfolgreich gestartet', + 'stopped_tasks' => ':value Aufgaben erfolgreich angehalten', + 'approved_quote' => 'Angebot erfolgreich angenommen', + 'approved_quotes' => ':value Angebote erfolgreich angenommen', + 'client_website' => 'Kunden-Website', + 'invalid_time' => 'Ungültige Zeit', + 'signed_in_as' => 'Angemeldet als', + 'total_results' => 'Ergebnisse insgesamt', + 'restore_company_gateway' => 'Zahlungs-Gateway wiederherstellen', + 'archive_company_gateway' => 'Zahlungs-Gateway aktivieren', + 'delete_company_gateway' => 'Zahlungs-Gateway löschen', + 'exchange_currency' => 'Währung wechseln', + 'tax_amount1' => 'Steuerhöhe 1', + 'tax_amount2' => 'Steuerhöhe 2', + 'tax_amount3' => 'Steuerhöhe 3', + 'update_project' => 'Projekt aktualisieren', + 'auto_archive_invoice_cancelled' => 'Auto-Archivieren Stornierte Rechnung', + 'auto_archive_invoice_cancelled_help' => 'Automatisch Rechnungen archivieren wenn sie storniert werden', + ); return $LANG; diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 11191744bf39..c4fbd6aeac82 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4574,6 +4574,14 @@ $LANG = array( 'update_project' => 'Update Project', 'auto_archive_invoice_cancelled' => 'Auto Archive Cancelled Invoice', 'auto_archive_invoice_cancelled_help' => 'Automatically archive invoices when they are cancelled', + 'no_invoices_found' => 'No invoices found', + 'created_record' => 'Successfully created record', + 'auto_archive_paid_invoices' => 'Auto Archive Paid', + 'auto_archive_paid_invoices_help' => 'Automatically archive invoices when they are paid.', + 'auto_archive_cancelled_invoices' => 'Auto Archive Cancelled', + 'auto_archive_cancelled_invoices_help' => 'Automatically archive invoices when they are cancelled.', + 'alternate_pdf_viewer' => 'Alternate PDF Viewer', + 'alternate_pdf_viewer_help' => 'Improve scrolling over the PDF preview [BETA]', ); diff --git a/resources/views/email/import/completed.blade.php b/resources/views/email/import/completed.blade.php index acefb2c17802..d4681962a1be 100644 --- a/resources/views/email/import/completed.blade.php +++ b/resources/views/email/import/completed.blade.php @@ -5,66 +5,66 @@

If your logo imported correctly it will display below. If it didn't import, you'll need to reupload your logo

-

+

- @if(isset($company) && $company->clients->count() >=1) -

{{ 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()); + + } + + +} + +