From 0d812f97a0a8e4fcb81d9b871afaeceb6d3e7d21 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 10 May 2022 13:39:08 +1000 Subject: [PATCH 01/42] Fixes for Client CSV Export --- app/Export/CSV/BaseExport.php | 2 +- app/Export/CSV/ClientExport.php | 75 +++++++++------------------------ 2 files changed, 21 insertions(+), 56 deletions(-) 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..986e5d69704e 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,23 +138,28 @@ 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] = ""; } + nlog($this->decorateAdvancedFields($client, $entity)); + return $this->decorateAdvancedFields($client, $entity); } @@ -202,16 +167,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; From 9c359bc3b47e6c3c3ab85fb96e9c38be75c904b5 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 10 May 2022 13:59:39 +1000 Subject: [PATCH 02/42] Fixes for contact exports --- app/Export/CSV/ClientExport.php | 5 +-- app/Export/CSV/ContactExport.php | 76 +++++++------------------------- 2 files changed, 18 insertions(+), 63 deletions(-) diff --git a/app/Export/CSV/ClientExport.php b/app/Export/CSV/ClientExport.php index 986e5d69704e..b4d4df64e67e 100644 --- a/app/Export/CSV/ClientExport.php +++ b/app/Export/CSV/ClientExport.php @@ -144,8 +144,7 @@ class ClientExport extends BaseExport $parts = explode(".",$key); - $keyval = array_search ($key, $this->entity_keys); - + $keyval = array_search($key, $this->entity_keys); if($parts[0] == 'client' && array_key_exists($parts[1], $transformed_client)) { $entity[$keyval] = $transformed_client[$parts[1]]; @@ -158,8 +157,6 @@ class ClientExport extends BaseExport } - nlog($this->decorateAdvancedFields($client, $entity)); - return $this->decorateAdvancedFields($client, $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; From e0e53af87f3496042d302bfea90a4a2f17f3dca2 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 10 May 2022 14:25:16 +1000 Subject: [PATCH 03/42] Fixes for credits --- app/Export/CSV/BaseExport.php | 2 + app/Export/CSV/CreditExport.php | 70 +++++++----------------- app/Models/Credit.php | 21 +++++++ tests/Feature/Export/ClientCsvTest.php | 76 ++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 50 deletions(-) create mode 100644 tests/Feature/Export/ClientCsvTest.php diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index 250bba51d10c..6e06d0f89752 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -81,6 +81,8 @@ class BaseExport $header[] = ctrans("texts.{$key}"); } +nlog($header); + return $header; } } \ No newline at end of file diff --git a/app/Export/CSV/CreditExport.php b/app/Export/CSV/CreditExport.php index 72dbebc4f470..2526b169f5fe 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)) + if(in_array('currency', $this->input['report_keys'])) $entity['currency'] = $credit->client->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/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/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); + + } + +} From 434b7d77e764755783edb734e60260e07864bd25 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 10 May 2022 14:51:39 +1000 Subject: [PATCH 04/42] Fixes for invoice exports --- app/Export/CSV/CreditExport.php | 4 +-- app/Export/CSV/InvoiceExport.php | 60 ++++++++------------------------ 2 files changed, 17 insertions(+), 47 deletions(-) diff --git a/app/Export/CSV/CreditExport.php b/app/Export/CSV/CreditExport.php index 2526b169f5fe..e28711bfe111 100644 --- a/app/Export/CSV/CreditExport.php +++ b/app/Export/CSV/CreditExport.php @@ -146,8 +146,8 @@ class CreditExport extends BaseExport if(in_array('country_id', $this->input['report_keys'])) $entity['country'] = $credit->client->country ? ctrans("texts.country_{$credit->client->country->name}") : ""; - if(in_array('currency', $this->input['report_keys'])) - $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(in_array('invoice_id', $this->input['report_keys'])) $entity['invoice'] = $credit->invoice ? $credit->invoice->number : ""; diff --git a/app/Export/CSV/InvoiceExport.php b/app/Export/CSV/InvoiceExport.php index 79af4a7cf1e9..30159642f300 100644 --- a/app/Export/CSV/InvoiceExport.php +++ b/app/Export/CSV/InvoiceExport.php @@ -66,43 +66,6 @@ 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', @@ -130,7 +93,7 @@ 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()); @@ -162,8 +125,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 +139,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; } From bab52faa56551d6f2fd33fffb885cf6c877033e0 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 10 May 2022 15:03:03 +1000 Subject: [PATCH 05/42] Documents Export --- app/Export/CSV/DocumentExport.php | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) 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; } From 27c7ff9f66ccb0b04a7dd5c6f5909d9f69e2b689 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 10 May 2022 15:06:27 +1000 Subject: [PATCH 06/42] Expense Export --- app/Export/CSV/ExpenseExport.php | 72 ++++++++++---------------------- 1 file changed, 22 insertions(+), 50 deletions(-) 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; From 660a5b7aaa8fa0903e9aa4c09b3f059412dc7107 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 10 May 2022 16:13:06 +1000 Subject: [PATCH 07/42] invoice item export --- app/Export/CSV/InvoiceItemExport.php | 97 +++++++--------------------- 1 file changed, 25 insertions(+), 72 deletions(-) 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; } From b7de59beb4df21cb53c4857ce6837d06e9e5a5a1 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 10 May 2022 16:14:57 +1000 Subject: [PATCH 08/42] Requirements for reports --- app/Http/Requests/Report/GenericReportRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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' ]; } } From 7feab5dc9a778e759ee74ca30cbc7f7b3abe2e5f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 10 May 2022 16:27:04 +1000 Subject: [PATCH 09/42] Payment CSV exports --- app/Export/CSV/PaymentExport.php | 64 +++++++++++++------------------- 1 file changed, 25 insertions(+), 39 deletions(-) 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; } From 12918da9c6a7c1845277637885ff5e0684ca2101 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 10 May 2022 16:39:05 +1000 Subject: [PATCH 10/42] Products CSV export --- app/Export/CSV/ProductExport.php | 38 ++++++++++---------------------- app/Models/Product.php | 5 +++++ 2 files changed, 17 insertions(+), 26 deletions(-) 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/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(); From 99c55ec2178a4c496aecdcd21a6aa2a4db7e7ed2 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 10 May 2022 16:50:30 +1000 Subject: [PATCH 11/42] Quotes CSV Exports --- app/Export/CSV/InvoiceExport.php | 3 +- app/Export/CSV/QuoteExport.php | 63 +++++++++----------------------- 2 files changed, 19 insertions(+), 47 deletions(-) diff --git a/app/Export/CSV/InvoiceExport.php b/app/Export/CSV/InvoiceExport.php index 30159642f300..f6c4559b9d14 100644 --- a/app/Export/CSV/InvoiceExport.php +++ b/app/Export/CSV/InvoiceExport.php @@ -100,7 +100,8 @@ class InvoiceExport extends BaseExport $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); 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; From f3ee7355c4b6ea05e71b0ef39dff51ce93ba2ccc Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 10 May 2022 16:59:41 +1000 Subject: [PATCH 12/42] Quote Items --- app/Export/CSV/QuoteItemExport.php | 101 ++++++++--------------------- 1 file changed, 28 insertions(+), 73 deletions(-) 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; } From 252d9a37455799f1b28e21a841ae3c844da8a981 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 10 May 2022 17:04:24 +1000 Subject: [PATCH 13/42] Recurring invoice export CSV --- app/Export/CSV/InvoiceExport.php | 2 + app/Export/CSV/RecurringInvoiceExport.php | 73 +++++++---------------- 2 files changed, 24 insertions(+), 51 deletions(-) diff --git a/app/Export/CSV/InvoiceExport.php b/app/Export/CSV/InvoiceExport.php index f6c4559b9d14..76ecf16a18c4 100644 --- a/app/Export/CSV/InvoiceExport.php +++ b/app/Export/CSV/InvoiceExport.php @@ -71,6 +71,8 @@ class InvoiceExport extends BaseExport 'client', 'currency_id', 'status', + 'vendor', + 'project', ]; public function __construct(Company $company, array $input) 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; } From 2852a1a5e39852f62ba38f4d3b1a1081ec25dd2d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 10 May 2022 18:05:15 +1000 Subject: [PATCH 14/42] Fixes for task columns in export --- app/Export/CSV/BaseExport.php | 2 - app/Export/CSV/TaskExport.php | 102 +++++++++++++++++++--------------- 2 files changed, 58 insertions(+), 46 deletions(-) diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index 6e06d0f89752..250bba51d10c 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -81,8 +81,6 @@ class BaseExport $header[] = ctrans("texts.{$key}"); } -nlog($header); - return $header; } } \ No newline at end of file diff --git a/app/Export/CSV/TaskExport.php b/app/Export/CSV/TaskExport.php index 5ccdfa5d54d0..31851e075b0d 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,37 @@ 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); + nlog("no time logs"); + nlog($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 +163,48 @@ 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); + + nlog("with time logs"); + nlog($entity); + + ksort($entity); $this->csv->insertOne($entity); unset($entity['start_date']); @@ -204,16 +217,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; } From c9a9c285cc0f7dc17ff7bc3a512fc0f97e94a193 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 10 May 2022 18:23:13 +1000 Subject: [PATCH 15/42] Clean up for task export --- app/Export/CSV/TaskExport.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/Export/CSV/TaskExport.php b/app/Export/CSV/TaskExport.php index 31851e075b0d..f700c8c83063 100644 --- a/app/Export/CSV/TaskExport.php +++ b/app/Export/CSV/TaskExport.php @@ -131,8 +131,6 @@ class TaskExport extends BaseExport $entity = $this->decorateAdvancedFields($task, $entity); - nlog("no time logs"); - nlog($entity); ksort($entity); $this->csv->insertOne($entity); @@ -200,9 +198,6 @@ class TaskExport extends BaseExport $entity['end_date'] = ""; $entity = $this->decorateAdvancedFields($task, $entity); - - nlog("with time logs"); - nlog($entity); ksort($entity); $this->csv->insertOne($entity); From a72de5efb42f58e12decc39030d04df7aa6fb506 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 10 May 2022 20:06:40 +1000 Subject: [PATCH 16/42] Profit and loss: --- .../Export/ProfitAndLossReportTest.php | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/Feature/Export/ProfitAndLossReportTest.php diff --git a/tests/Feature/Export/ProfitAndLossReportTest.php b/tests/Feature/Export/ProfitAndLossReportTest.php new file mode 100644 index 000000000000..2cd0c7755046 --- /dev/null +++ b/tests/Feature/Export/ProfitAndLossReportTest.php @@ -0,0 +1,54 @@ +withoutMiddleware( + ThrottleRequests::class + ); + + $this->makeTestData(); + + $this->withoutExceptionHandling(); + } + + private function buildReportData() + { + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + ]); + + } + + public function testExportCsv() + { + + } +} From 62f518e25b3d60cc813a4ae384175d3ae851319d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 11 May 2022 10:01:24 +1000 Subject: [PATCH 17/42] Minor fixes for statement dates --- app/Services/PdfMaker/Design.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Services/PdfMaker/Design.php b/app/Services/PdfMaker/Design.php index ff8332bf1991..12f8587a0b47 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('Y-m-d'), $this->client->date_format(), $this->client->locale()); return [ ['element' => 'tr', 'properties' => ['data-ref' => 'statement-label'], 'elements' => [ From ffbfc1140755d18059c44e4ce0401e7d1522c841 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 11 May 2022 12:23:40 +1000 Subject: [PATCH 18/42] Add domains to blacklist --- app/Http/ValidationRules/Account/BlackListRule.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Http/ValidationRules/Account/BlackListRule.php b/app/Http/ValidationRules/Account/BlackListRule.php index 47f8e9748a6c..8f175708ad6a 100644 --- a/app/Http/ValidationRules/Account/BlackListRule.php +++ b/app/Http/ValidationRules/Account/BlackListRule.php @@ -21,6 +21,7 @@ class BlackListRule implements Rule { private array $blacklist = [ 'candassociates.com', + 'vusra.com', ]; /** From f604e463c2be8d4f86f2641c7df331bcb43234ef Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 11 May 2022 15:25:33 +1000 Subject: [PATCH 19/42] Throttle payment methods to prevent spam: --- app/Factory/PaymentFactory.php | 1 + .../ClientPortal/PaymentMethodController.php | 6 +- .../ValidationRules/Account/BlackListRule.php | 1 + app/Jobs/Company/CompanyImport.php | 14 + app/Jobs/Report/ProfitAndLoss.php | 89 +++++++ app/Repositories/PaymentRepository.php | 18 +- app/Services/Invoice/MarkPaid.php | 4 +- app/Services/Report/ProfitLoss.php | 246 ++++++++++++++++++ routes/client.php | 4 +- 9 files changed, 373 insertions(+), 10 deletions(-) create mode 100644 app/Jobs/Report/ProfitAndLoss.php create mode 100644 app/Services/Report/ProfitLoss.php 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/ValidationRules/Account/BlackListRule.php b/app/Http/ValidationRules/Account/BlackListRule.php index 8f175708ad6a..8dee26c6ad2b 100644 --- a/app/Http/ValidationRules/Account/BlackListRule.php +++ b/app/Http/ValidationRules/Account/BlackListRule.php @@ -22,6 +22,7 @@ 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..56697694b435 100644 --- a/app/Jobs/Company/CompanyImport.php +++ b/app/Jobs/Company/CompanyImport.php @@ -1450,6 +1450,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], 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/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/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/Report/ProfitLoss.php b/app/Services/Report/ProfitLoss.php new file mode 100644 index 000000000000..de6c2ab10e45 --- /dev/null +++ b/app/Services/Report/ProfitLoss.php @@ -0,0 +1,246 @@ +company = $company; + + $this->payload = $payload; + + $this->setBillingReportType(); + } + + public function build() + { + //get income + + //sift foreign currencies - calculate both converted foreign amounts to native currency and also also group amounts by currency. + + //get expenses + + + } + + + /* + //returns an array of objects + => [ + {#2047 + +"amount": "706.480000", + +"total_taxes": "35.950000", + +"currency_id": ""1"", + +"net_converted_amount": "670.5300000000", + }, + {#2444 + +"amount": "200.000000", + +"total_taxes": "0.000000", + +"currency_id": ""23"", + +"net_converted_amount": "1.7129479802", + }, + {#2654 + +"amount": "140.000000", + +"total_taxes": "40.000000", + +"currency_id": ""12"", + +"net_converted_amount": "69.3275024282", + }, + ] + */ + private function invoiceIncome() + { + return \DB::select( \DB::raw(" + SELECT + sum(invoices.amount) as amount, + sum(invoices.total_taxes) as total_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] ); + + + // + // $total = array_reduce( commissionsArray, function ($sum, $entry) { + // $sum += $entry->commission; + // return $sum; + // }, 0); + } + + 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 + 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 payments.currency_id + ORDER BY payments.currency_id; + "), ['company_id' => $this->company->id, 'start_date' => $this->start_date, 'end_date' => $this->end_date]); + + + } + + private function expenseCalc() + { + + 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('income_billed', $this->payload)) + $this->is_income_billed = boolval($this->payload['income_billed']); + + if(array_key_exists('expense_billed', $this->payload)) + $this->is_expense_billed = boolval($this->payload['expense_billed']); + + if(array_key_exists('include_tax', $this->payload)) + $this->is_tax_included = boolval($this->payload['is_tax_included']); + + return $this; + + } + + private function addDateRange($query) + { + + $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'); + + } + + } + +} 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'); From eaa6ba1d3971fc042f3cd63dc16487b340967688 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 11 May 2022 16:29:56 +1000 Subject: [PATCH 20/42] Fixes for ACH notification with WePay --- app/Console/Commands/DemoMode.php | 1 + .../Ninja/WePayFailureNotification.php | 2 +- app/Repositories/ExpenseRepository.php | 29 ++++++++++ app/Services/Report/ProfitLoss.php | 56 ++++++++++++++++++- 4 files changed, 86 insertions(+), 2 deletions(-) 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/Notifications/Ninja/WePayFailureNotification.php b/app/Notifications/Ninja/WePayFailureNotification.php index 08e96cb856c0..011020492d6e 100644 --- a/app/Notifications/Ninja/WePayFailureNotification.php +++ b/app/Notifications/Ninja/WePayFailureNotification.php @@ -73,7 +73,7 @@ class WePayFailureNotification extends Notification public function toSlack($notifiable) { - (new SlackMessage) + return (new SlackMessage) ->success() ->from(ctrans('texts.notification_bot')) ->image('https://app.invoiceninja.com/favicon.png') 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/Services/Report/ProfitLoss.php b/app/Services/Report/ProfitLoss.php index de6c2ab10e45..983ba68b8c71 100644 --- a/app/Services/Report/ProfitLoss.php +++ b/app/Services/Report/ProfitLoss.php @@ -11,7 +11,9 @@ namespace App\Services\Report; +use App\Libraries\Currency\Conversion\CurrencyApi; use App\Models\Company; +use App\Models\Expense; use Illuminate\Support\Carbon; class ProfitLoss @@ -26,6 +28,8 @@ class ProfitLoss private $end_date; + protected CurrencyApi $currency_api; + /* payload variables. @@ -51,8 +55,9 @@ class ProfitLoss protected Company $company; - public function __construct(Company $company, array $payload) + public function __construct(Company $company, array $payload, CurrencyApi $currency_api) { + $this->currency_api = $currency_api; $this->company = $company; @@ -148,6 +153,55 @@ class ProfitLoss } private function expenseCalc() + { + + $expenses = Expense::where('company_id', $this->company->id) + ->where('is_deleted', 0) + ->withTrashed() + ->whereBetween('date', [$this->start_date, $this->end_date]) + ->cursor(); + + + if($this->is_tax_included) + return $this->calculateExpensesWithTaxes($expenses); + + return $this->calculateExpensesWithoutTaxes($expenses); + + } + + private function calculateExpensesWithTaxes($expenses) + { + + foreach($expenses as $expense) + { + + if(!$expense->calculate_tax_by_amount && !$expense->uses_inclusive_taxes) + { + + } + + } + + } + + private function calculateExpensesWithoutTaxes($expenses) + { + $total = 0; + $converted_total = 0; + + foreach($expenses as $expense) + { + $total += $expense->amount; + $total += $this->getConvertedTotal($expense); + } + } + + private function getConvertedTotal($expense) + { + + } + + private function expenseCalcWithTax() { return \DB::select( \DB::raw(" From 2c765d5187e49f48a447771e3157767373d5dee9 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 11 May 2022 18:24:15 +1000 Subject: [PATCH 21/42] Updated translations --- app/Services/Report/ProfitLoss.php | 4 +- resources/lang/de/texts.php | 274 ++++++++++++++++------------- resources/lang/en/texts.php | 8 + 3 files changed, 162 insertions(+), 124 deletions(-) diff --git a/app/Services/Report/ProfitLoss.php b/app/Services/Report/ProfitLoss.php index 983ba68b8c71..5866e1f4085e 100644 --- a/app/Services/Report/ProfitLoss.php +++ b/app/Services/Report/ProfitLoss.php @@ -192,13 +192,13 @@ class ProfitLoss foreach($expenses as $expense) { $total += $expense->amount; - $total += $this->getConvertedTotal($expense); + $converted_total += $this->getConvertedTotal($expense); } } private function getConvertedTotal($expense) { - + return round($expense->amount * $expense->exchange_rate,2); } private function expenseCalcWithTax() diff --git a/resources/lang/de/texts.php b/resources/lang/de/texts.php index 0414e0f90bdc..d7d11e55661b 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' => 'Confirm your email.', '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.

-
  • Flexible Bonitätsprüfung und Factoring.
', - '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.', @@ -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,7 +4357,7 @@ 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', @@ -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', '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', + 'invoice_item' => 'Rechnungsposition', + 'quote_item' => 'Angebotsposition', + 'order' => 'Bestellung', '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', + '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', + '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' => 'Allow Cancellation', 'per_seat_enabled' => 'Per Seat Enabled', 'max_seats_limit' => 'Max Seats Limit', - 'trial_enabled' => 'Trial Enabled', - 'trial_duration' => 'Trial Duration', + 'trial_enabled' => 'Testversion aktiv', + 'trial_duration' => 'Testzeitraum', 'allow_query_overrides' => 'Allow Query Overrides', 'allow_plan_changes' => 'Allow Plan Changes', 'plan_map' => 'Plan Map', - 'refund_period' => 'Refund Period', - 'webhook_configuration' => 'Webhook Configuration', + 'refund_period' => 'Erstattungszeitraum', + 'webhook_configuration' => 'Webhook-Konfiguration', 'purchase_page' => 'Purchase Page', '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', @@ -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' => 'To view the :entity you need to set password.', + 'unsubscribe' => 'Deabonnieren', + 'unsubscribed' => 'Deabonniert', + 'unsubscribed_text' => 'Du erhältst nun keine Benachrichtigungen für dieses Dokument mehr.', + 'client_shipping_state' => 'Client Shipping State', + 'client_shipping_city' => 'Client Shipping City', + 'client_shipping_postal_code' => 'Client Shipping Postal Code', + 'client_shipping_country' => 'Client Shipping Country', + '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' => 'Due on Receipt', + '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' => 'To prevent spam we require upgrading to a paid account to customize the email', + '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' => 'Send with GMail invalid credentials', + 'gmail_credentials_invalid_body' => 'Your GMail credentials are not correct, please log into the administrator portal and navigate to Settings > User Details and disconnect and reconnect your GMail account. We will send you this notification daily until this issue is resolved', + 'notification_invoice_sent' => 'Rechnung :invoice über :amount wurde an den Kunden :client versendet.', + 'total_columns' => 'Total Fields', + 'view_task' => 'Aufgabe anzeugen', + 'cancel_invoice' => 'Cancel', + 'changed_status' => 'Successfully changed task status', + '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' => 'Exchange currency', + 'tax_amount1' => 'Tax Amount 1', + 'tax_amount2' => 'Tax Amount 2', + 'tax_amount3' => 'Tax Amount 3', + 'update_project' => 'Update Project', + 'auto_archive_invoice_cancelled' => 'Auto Archive Cancelled Invoice', + 'auto_archive_invoice_cancelled_help' => 'Automatically archive invoices when they are cancelled', + ); 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]', ); From 755b366c817c2af52547e0b17d6b868c3c623847 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 11 May 2022 22:38:19 +1000 Subject: [PATCH 22/42] Add throttling --- .../Ninja/WePayFailureNotification.php | 6 ++- app/Services/Report/ProfitLoss.php | 53 ++++++++++--------- routes/api.php | 6 +-- 3 files changed, 37 insertions(+), 28 deletions(-) diff --git a/app/Notifications/Ninja/WePayFailureNotification.php b/app/Notifications/Ninja/WePayFailureNotification.php index 011020492d6e..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(); 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/Services/Report/ProfitLoss.php b/app/Services/Report/ProfitLoss.php index 5866e1f4085e..a36765b1613d 100644 --- a/app/Services/Report/ProfitLoss.php +++ b/app/Services/Report/ProfitLoss.php @@ -162,43 +162,48 @@ class ProfitLoss ->cursor(); - if($this->is_tax_included) - return $this->calculateExpensesWithTaxes($expenses); + return $this->calculateExpenses($expenses); - return $this->calculateExpensesWithoutTaxes($expenses); - } - private function calculateExpensesWithTaxes($expenses) - { - - foreach($expenses as $expense) - { - - if(!$expense->calculate_tax_by_amount && !$expense->uses_inclusive_taxes) - { - - } - - } - - } private function calculateExpensesWithoutTaxes($expenses) { - $total = 0; - $converted_total = 0; + + $data = []; + foreach($expenses as $expense) { - $total += $expense->amount; - $converted_total += $this->getConvertedTotal($expense); + $data[] = [ + 'total' => $expense->amount, + 'converted_total' => $this->getConvertedTotal($expense->amount, $expense->exchange_rate), + 'tax' => $this->getTax($expense), + ]; + } + } - private function getConvertedTotal($expense) + private function getTax($expense) { - return round($expense->amount * $expense->exchange_rate,2); + $amount = $expense->amount; + + //is amount tax + + if($expense->calculate_tax_by_amount) + { + $total_tax = $expense->tax_amount1 + $expense->tax_amount2 + $expense->tax_amount3; + } + + + return ($amount - ($amount / (1 + ($tax_rate / 100)))); + + } + + private function getConvertedTotal($amount, $exchange_rate) + { + return round($amount * $exchange_rate,2); } private function expenseCalcWithTax() diff --git a/routes/api.php b/routes/api.php index bff9838c65cc..e7992191791f 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:20,1'); Route::post('reports/clients', 'Reports\ClientReportController'); Route::post('reports/contacts', 'Reports\ClientContactReportController'); From 0f8d9bd4ad58f0d57fe71f4bb7d482f2bb64c490 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 12 May 2022 09:23:24 +1000 Subject: [PATCH 23/42] Updated translations --- resources/lang/de/texts.php | 70 ++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/resources/lang/de/texts.php b/resources/lang/de/texts.php index d7d11e55661b..cc25bc786eaf 100644 --- a/resources/lang/de/texts.php +++ b/resources/lang/de/texts.php @@ -889,7 +889,7 @@ $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' => 'Confirm your email.', + 'button_confirmation_message' => 'Bestätige deine E-Mail-Adresse.', 'confirm' => 'Bestätigen', 'email_preferences' => 'E-Mail-Einstellungen', 'created_invoices' => ':count Rechnung(en) erfolgreich erstellt', @@ -4331,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', @@ -4362,9 +4362,9 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', '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', @@ -4379,8 +4379,8 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'google_analytics_tracking_id' => 'Google Analytics Tracking ID', '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' => 'Markdown verwenden', @@ -4448,8 +4448,8 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'invoice_item' => 'Rechnungsposition', 'quote_item' => 'Angebotsposition', 'order' => 'Bestellung', - 'search_kanban' => 'Search Kanban', - 'search_kanbans' => 'Search Kanban', + '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', @@ -4476,17 +4476,17 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'header_value' => 'Header-Wert', 'recurring_products' => 'Wiederkehrende Produkte', 'promo_discount' => 'Promo-Rabatt', - 'allow_cancellation' => 'Allow Cancellation', - 'per_seat_enabled' => 'Per Seat Enabled', - 'max_seats_limit' => 'Max Seats Limit', + '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' => 'Allow Query Overrides', - 'allow_plan_changes' => 'Allow Plan Changes', + 'allow_query_overrides' => 'Überschreiben von Abfragen zulassen', + 'allow_plan_changes' => 'Planänderungen zulassen', 'plan_map' => 'Plan Map', 'refund_period' => 'Erstattungszeitraum', 'webhook_configuration' => 'Webhook-Konfiguration', - 'purchase_page' => 'Purchase Page', + 'purchase_page' => 'Kauf-Seite', 'email_bounced' => 'E-Mail zurückgesendet', 'email_spam_complaint' => 'Spam-Beschwerde', 'email_delivery' => 'E-Mail-Zustellung', @@ -4500,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', @@ -4514,25 +4514,25 @@ 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' => 'To view the :entity you need to set password.', + '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' => 'Client Shipping State', - 'client_shipping_city' => 'Client Shipping City', - 'client_shipping_postal_code' => 'Client Shipping Postal Code', - 'client_shipping_country' => 'Client Shipping Country', + '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' => 'Due on Receipt', + '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' => 'To prevent spam we require upgrading to a paid account to customize the email', + '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', @@ -4540,13 +4540,13 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', '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' => 'Send with GMail invalid credentials', - 'gmail_credentials_invalid_body' => 'Your GMail credentials are not correct, please log into the administrator portal and navigate to Settings > User Details and disconnect and reconnect your GMail account. We will send you this notification daily until this issue is resolved', + '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' => 'Total Fields', + 'total_columns' => 'Felder insgesamt', 'view_task' => 'Aufgabe anzeugen', - 'cancel_invoice' => 'Cancel', - 'changed_status' => 'Successfully changed task status', + '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', @@ -4569,13 +4569,13 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting', 'restore_company_gateway' => 'Zahlungs-Gateway wiederherstellen', 'archive_company_gateway' => 'Zahlungs-Gateway aktivieren', 'delete_company_gateway' => 'Zahlungs-Gateway löschen', - 'exchange_currency' => 'Exchange currency', - 'tax_amount1' => 'Tax Amount 1', - 'tax_amount2' => 'Tax Amount 2', - 'tax_amount3' => 'Tax Amount 3', - 'update_project' => 'Update Project', - 'auto_archive_invoice_cancelled' => 'Auto Archive Cancelled Invoice', - 'auto_archive_invoice_cancelled_help' => 'Automatically archive invoices when they are cancelled', + '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', ); From dd5800eac730eae8ac65e9555a460bd1a891ec20 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 12 May 2022 10:57:58 +1000 Subject: [PATCH 24/42] TDD for profit and loss --- app/Models/Expense.php | 2 +- app/Services/Report/ProfitLoss.php | 43 ++++++++++++++----- .../Export/ProfitAndLossReportTest.php | 37 ++++++++++++++-- 3 files changed, 67 insertions(+), 15 deletions(-) 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/Services/Report/ProfitLoss.php b/app/Services/Report/ProfitLoss.php index a36765b1613d..74270403d106 100644 --- a/app/Services/Report/ProfitLoss.php +++ b/app/Services/Report/ProfitLoss.php @@ -55,9 +55,9 @@ class ProfitLoss protected Company $company; - public function __construct(Company $company, array $payload, CurrencyApi $currency_api) + public function __construct(Company $company, array $payload) { - $this->currency_api = $currency_api; + $this->currency_api = new CurrencyApi(); $this->company = $company; @@ -167,7 +167,7 @@ class ProfitLoss } - private function calculateExpensesWithoutTaxes($expenses) + private function calculateExpenses($expenses) { $data = []; @@ -177,8 +177,11 @@ class ProfitLoss { $data[] = [ 'total' => $expense->amount, - 'converted_total' => $this->getConvertedTotal($expense->amount, $expense->exchange_rate), - 'tax' => $this->getTax($expense), + 'converted_total' => $converted_total = $this->getConvertedTotal($expense->amount, $expense->exchange_rate), + 'tax' => $tax = $this->getTax($expense), + 'net_converted_total' => $expense->uses_inclusive_taxes ? ( $converted_total - $tax ) : $converted_total, + 'category_id' => $expense->category_id, + 'category_name' => $expense->category ? $expense->category->name : "No Category Defined", ]; } @@ -193,17 +196,37 @@ class ProfitLoss if($expense->calculate_tax_by_amount) { - $total_tax = $expense->tax_amount1 + $expense->tax_amount2 + $expense->tax_amount3; + return $expense->tax_amount1 + $expense->tax_amount2 + $expense->tax_amount3; } - return ($amount - ($amount / (1 + ($tax_rate / 100)))); + 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) + private function getConvertedTotal($amount, $exchange_rate = 1) { - return round($amount * $exchange_rate,2); + return round(($amount * $exchange_rate) ,2); } private function expenseCalcWithTax() @@ -231,7 +254,7 @@ class ProfitLoss $this->is_expense_billed = boolval($this->payload['expense_billed']); if(array_key_exists('include_tax', $this->payload)) - $this->is_tax_included = boolval($this->payload['is_tax_included']); + $this->is_tax_included = boolval($this->payload['include_tax']); return $this; diff --git a/tests/Feature/Export/ProfitAndLossReportTest.php b/tests/Feature/Export/ProfitAndLossReportTest.php index 2cd0c7755046..7f8f2351b192 100644 --- a/tests/Feature/Export/ProfitAndLossReportTest.php +++ b/tests/Feature/Export/ProfitAndLossReportTest.php @@ -10,7 +10,9 @@ */ namespace Tests\Feature\Export; +use App\Models\Company; use App\Models\Invoice; +use App\Services\Report\ProfitLoss; use App\Utils\Traits\MakesHash; use Illuminate\Routing\Middleware\ThrottleRequests; use Illuminate\Support\Facades\Storage; @@ -20,6 +22,7 @@ use Tests\TestCase; /** * @test + * @covers App\Services\Report\ProfitLoss */ class ProfitAndLossReportTest extends TestCase { @@ -39,16 +42,42 @@ class ProfitAndLossReportTest extends TestCase $this->withoutExceptionHandling(); } - private function buildReportData() +/** + * + * 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 + +*/ + public function testProfitLossInstance() { $company = Company::factory()->create([ 'account_id' => $this->account->id, ]); - } + $payload = [ + 'start_date' => '2000-01-01', + 'end_date' => '2030-01-11', + 'date_range' => 'custom', + 'income_billed' => true, + 'include_tax' => false + ]; - public function testExportCsv() - { + $pl = new ProfitLoss($company, $payload); + + $this->assertInstanceOf(ProfitLoss::class, $pl); } } From fb5b2882d3aed4d062c27d3416f2a830e749eaf0 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 12 May 2022 11:10:47 +1000 Subject: [PATCH 25/42] Fixes for tests --- .../Export/ProfitAndLossReportTest.php | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/Feature/Export/ProfitAndLossReportTest.php b/tests/Feature/Export/ProfitAndLossReportTest.php index 7f8f2351b192..bab9925f9fa2 100644 --- a/tests/Feature/Export/ProfitAndLossReportTest.php +++ b/tests/Feature/Export/ProfitAndLossReportTest.php @@ -40,8 +40,14 @@ class ProfitAndLossReportTest extends TestCase $this->makeTestData(); $this->withoutExceptionHandling(); + + $this->buildData(); } + public $company; + + public $payload; + /** * * start_date - Y-m-d @@ -61,13 +67,14 @@ class ProfitAndLossReportTest extends TestCase include_tax - true tax_included || false - tax_excluded */ - public function testProfitLossInstance() + + private function buildData() { - $company = Company::factory()->create([ + $this->company = Company::factory()->create([ 'account_id' => $this->account->id, ]); - $payload = [ + $this->payload = [ 'start_date' => '2000-01-01', 'end_date' => '2030-01-11', 'date_range' => 'custom', @@ -75,7 +82,12 @@ class ProfitAndLossReportTest extends TestCase 'include_tax' => false ]; - $pl = new ProfitLoss($company, $payload); + } + + public function testProfitLossInstance() + { + + $pl = new ProfitLoss($this->company, $this->payload); $this->assertInstanceOf(ProfitLoss::class, $pl); From d7084785fe9fee050430bcd4468b9ea4e1e44b33 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 12 May 2022 13:57:41 +1000 Subject: [PATCH 26/42] fixes for jobs breaking with massive payloads --- app/Jobs/Company/CompanyImport.php | 17 +-- app/Mail/Import/ImportCompleted.php | 16 +++ app/Models/Company.php | 4 +- app/Services/Quote/ApplyNumber.php | 2 +- app/Services/Report/ProfitLoss.php | 111 +++++++++++++++--- database/factories/CompanyFactory.php | 12 -- .../views/email/import/completed.blade.php | 62 +++++----- .../Export/ProfitAndLossReportTest.php | 84 ++++++++++++- .../UniqueInvoiceNumberValidationTest.php | 81 +++++++++++++ 9 files changed, 313 insertions(+), 76 deletions(-) create mode 100644 tests/Unit/ValidationRules/UniqueInvoiceNumberValidationTest.php diff --git a/app/Jobs/Company/CompanyImport.php b/app/Jobs/Company/CompanyImport.php index 56697694b435..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); } @@ -1528,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."); } @@ -1561,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/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/Company.php b/app/Models/Company.php index b659e8980ce7..9b2a11cd5be4 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, 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 index 74270403d106..5e027a43e3e8 100644 --- a/app/Services/Report/ProfitLoss.php +++ b/app/Services/Report/ProfitLoss.php @@ -12,6 +12,7 @@ namespace App\Services\Report; use App\Libraries\Currency\Conversion\CurrencyApi; +use App\Libraries\MultiDB; use App\Models\Company; use App\Models\Expense; use Illuminate\Support\Carbon; @@ -28,6 +29,14 @@ class ProfitLoss private $end_date; + private float $income = 0; + + private float $income_taxes = 0; + + private array $expenses; + + private array $income_map; + protected CurrencyApi $currency_api; /* @@ -45,8 +54,8 @@ class ProfitLoss last_quarter this_year custom - income_billed - true = Invoiced || false = Payments - expense_billed - true = Expensed || false = Expenses marked as paid + is_income_billed - true = Invoiced || false = Payments + is_expense_billed - true = Expensed || false = Expenses marked as paid include_tax - true tax_included || false - tax_excluded */ @@ -68,15 +77,66 @@ class ProfitLoss public function build() { - //get income + MultiDB::setDb($this->company->db); - //sift foreign currencies - calculate both converted foreign amounts to native currency and also also group amounts by currency. + if($this->is_income_billed){ //get invoiced amounts + + $this->filterIncome(); - //get expenses + }else { + + $this->filterPaymentIncome(); + } + + $this->expenseData(); + + 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; + } + + private function filterIncome() + { + $invoices = $this->invoiceIncome(); + + $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 filterPaymentIncome() + { + $payments = $this->paymentIncome(); + + return $this; + } /* //returns an array of objects @@ -86,27 +146,31 @@ class ProfitLoss +"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() - { + { nlog(['company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $this->start_date, 'end_date' => $this->end_date] ); 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 @@ -130,12 +194,19 @@ class ProfitLoss // }, 0); } + + /** + +"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 + 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 @@ -145,14 +216,14 @@ class ProfitLoss AND payments.is_deleted = false AND payments.company_id = :company_id AND (payments.date BETWEEN :start_date AND :end_date) - GROUP BY payments.currency_id - ORDER BY payments.currency_id; + 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 expenseCalc() + private function expenseData() { $expenses = Expense::where('company_id', $this->company->id) @@ -186,6 +257,8 @@ class ProfitLoss } + $this->expenses = $data; + } private function getTax($expense) @@ -247,23 +320,27 @@ class ProfitLoss private function setBillingReportType() { - if(array_key_exists('income_billed', $this->payload)) - $this->is_income_billed = boolval($this->payload['income_billed']); + if(array_key_exists('is_income_billed', $this->payload)) + $this->is_income_billed = boolval($this->payload['is_income_billed']); - if(array_key_exists('expense_billed', $this->payload)) - $this->is_expense_billed = boolval($this->payload['expense_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($query) + private function addDateRange() { + $date_range = 'this_year'; - $date_range = $this->payload['date_range']; + if(array_key_exists('date_range', $this->payload)) + $date_range = $this->payload['date_range']; try{ @@ -323,6 +400,8 @@ class ProfitLoss } + 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/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/tests/Feature/Export/ProfitAndLossReportTest.php b/tests/Feature/Export/ProfitAndLossReportTest.php index bab9925f9fa2..1d31bf04eb48 100644 --- a/tests/Feature/Export/ProfitAndLossReportTest.php +++ b/tests/Feature/Export/ProfitAndLossReportTest.php @@ -10,8 +10,12 @@ */ namespace Tests\Feature\Export; +use App\Factory\InvoiceFactory; +use App\Models\Account; +use App\Models\Client; use App\Models\Company; use App\Models\Invoice; +use App\Models\User; use App\Services\Report\ProfitLoss; use App\Utils\Traits\MakesHash; use Illuminate\Routing\Middleware\ThrottleRequests; @@ -27,18 +31,19 @@ use Tests\TestCase; class ProfitAndLossReportTest extends TestCase { use MakesHash; - use MockAccountData; + + public $faker; public function setUp() :void { parent::setUp(); + $this->faker = \Faker\Factory::create(); + $this->withoutMiddleware( ThrottleRequests::class ); - $this->makeTestData(); - $this->withoutExceptionHandling(); $this->buildData(); @@ -46,6 +51,8 @@ class ProfitAndLossReportTest extends TestCase public $company; + public $user; + public $payload; /** @@ -62,7 +69,7 @@ class ProfitAndLossReportTest extends TestCase last_quarter this_year custom - income_billed - true = Invoiced || false = Payments + is_income_billed - true = Invoiced || false = Payments expense_billed - true = Expensed || false = Expenses marked as paid include_tax - true tax_included || false - tax_excluded @@ -70,15 +77,31 @@ class ProfitAndLossReportTest extends TestCase private function buildData() { + + + $account = Account::factory()->create([ + 'hosted_client_count' => 1000, + 'hosted_company_count' => 1000 + ]); + + $account->num_users = 3; + $account->save(); + + $this->user = User::factory()->create([ + 'account_id' => $account->id, + 'confirmation_code' => 'xyz123', + 'email' => $this->faker->unique()->safeEmail, + ]); + $this->company = Company::factory()->create([ - 'account_id' => $this->account->id, + 'account_id' => $account->id, ]); $this->payload = [ 'start_date' => '2000-01-01', 'end_date' => '2030-01-11', 'date_range' => 'custom', - 'income_billed' => true, + 'is_income_billed' => true, 'include_tax' => false ]; @@ -92,4 +115,53 @@ class ProfitAndLossReportTest extends TestCase $this->assertInstanceOf(ProfitLoss::class, $pl); } + + public function testInvoiceIncome() + { + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'is_deleted' => 0, + ]); + + // 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' => 1, + // 'date' => '2022-01-01', + // 'terms' => 'nada', + // 'discount' => 0, + // 'tax_rate1' => 0, + // 'tax_rate2' => 0, + // 'tax_rate3' => 0, + // 'tax_name1' => '', + // 'tax_name2' => '', + // 'tax_name3' => '', + // ]); + + $i = InvoiceFactory::create($this->company->id, $this->user->id); + $i->client_id = $client->id; + $i->amount = 10; + $i->balance = 10; + $i->status_id = 2; + $i->terms = "nada"; + $i->total_taxes = 1; + $i->save(); + + nlog(Invoice::where('company_id', $this->company->id)->get()->toArray()); + + $pl = new ProfitLoss($this->company, $this->payload); + $pl->build(); + + + $this->assertEquals(9.0, $pl->getIncome()); + $this->assertEquals(1, $pl->getIncomeTaxes()); + + + } } 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()); + + } + + +} + + From 618d2234d11e35c8ae1328c49f49942935003584 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 12 May 2022 14:40:44 +1000 Subject: [PATCH 27/42] TDD Profit and loss --- app/Services/Report/ProfitLoss.php | 2 +- .../Export/ProfitAndLossReportTest.php | 162 +++++++++++++----- 2 files changed, 122 insertions(+), 42 deletions(-) diff --git a/app/Services/Report/ProfitLoss.php b/app/Services/Report/ProfitLoss.php index 5e027a43e3e8..7fda7c236247 100644 --- a/app/Services/Report/ProfitLoss.php +++ b/app/Services/Report/ProfitLoss.php @@ -165,7 +165,7 @@ class ProfitLoss ] */ private function invoiceIncome() - { nlog(['company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $this->start_date, 'end_date' => $this->end_date] ); + { return \DB::select( \DB::raw(" SELECT sum(invoices.amount) as amount, diff --git a/tests/Feature/Export/ProfitAndLossReportTest.php b/tests/Feature/Export/ProfitAndLossReportTest.php index 1d31bf04eb48..07ce57232de0 100644 --- a/tests/Feature/Export/ProfitAndLossReportTest.php +++ b/tests/Feature/Export/ProfitAndLossReportTest.php @@ -10,6 +10,7 @@ */ namespace Tests\Feature\Export; +use App\DataMapper\ClientSettings; use App\Factory\InvoiceFactory; use App\Models\Account; use App\Models\Client; @@ -46,7 +47,6 @@ class ProfitAndLossReportTest extends TestCase $this->withoutExceptionHandling(); - $this->buildData(); } public $company; @@ -55,6 +55,7 @@ class ProfitAndLossReportTest extends TestCase public $payload; + public $account; /** * * start_date - Y-m-d @@ -78,23 +79,22 @@ class ProfitAndLossReportTest extends TestCase private function buildData() { - - $account = Account::factory()->create([ + $this->account = Account::factory()->create([ 'hosted_client_count' => 1000, 'hosted_company_count' => 1000 ]); - $account->num_users = 3; - $account->save(); + $this->account->num_users = 3; + $this->account->save(); $this->user = User::factory()->create([ - 'account_id' => $account->id, + 'account_id' => $this->account->id, 'confirmation_code' => 'xyz123', 'email' => $this->faker->unique()->safeEmail, ]); $this->company = Company::factory()->create([ - 'account_id' => $account->id, + 'account_id' => $this->account->id, ]); $this->payload = [ @@ -109,15 +109,18 @@ class ProfitAndLossReportTest extends TestCase public function testProfitLossInstance() { - + $this->buildData(); + $pl = new ProfitLoss($this->company, $this->payload); $this->assertInstanceOf(ProfitLoss::class, $pl); + $this->account->delete(); } - public function testInvoiceIncome() + public function testSimpleInvoiceIncome() { + $this->buildData(); $client = Client::factory()->create([ 'user_id' => $this->user->id, @@ -125,43 +128,120 @@ class ProfitAndLossReportTest extends TestCase 'is_deleted' => 0, ]); - // 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' => 1, - // 'date' => '2022-01-01', - // 'terms' => 'nada', - // 'discount' => 0, - // 'tax_rate1' => 0, - // 'tax_rate2' => 0, - // 'tax_rate3' => 0, - // 'tax_name1' => '', - // 'tax_name2' => '', - // 'tax_name3' => '', - // ]); - - $i = InvoiceFactory::create($this->company->id, $this->user->id); - $i->client_id = $client->id; - $i->amount = 10; - $i->balance = 10; - $i->status_id = 2; - $i->terms = "nada"; - $i->total_taxes = 1; - $i->save(); - - nlog(Invoice::where('company_id', $this->company->id)->get()->toArray()); + 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(9.0, $pl->getIncome()); - $this->assertEquals(1, $pl->getIncomeTaxes()); - + $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(); + } + + } From 7ff3397616befbec0e11e6a7aca75f941a5205da Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 May 2022 07:52:02 +1000 Subject: [PATCH 28/42] limit system logs in client response --- app/Models/Client.php | 2 +- app/Services/Report/ProfitLoss.php | 65 +++++++++++++++++++++++++++--- routes/api.php | 2 +- 3 files changed, 61 insertions(+), 8 deletions(-) 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/Services/Report/ProfitLoss.php b/app/Services/Report/ProfitLoss.php index 7fda7c236247..a97ff59269df 100644 --- a/app/Services/Report/ProfitLoss.php +++ b/app/Services/Report/ProfitLoss.php @@ -131,6 +131,24 @@ class ProfitLoss } + private function filterInvoicePaymentIncome() + { + + $invoices = $this->invoicePaymentIncome(); + + $this->income = 0; + $this->income_taxes = 0; + $this->income_map = $invoices; + + foreach($invoices as $invoice){ + $this->income += $invoice->net_amount; + $this->income_taxes += $invoice->net_converted_taxes; + } + + return $this; + + } + private function filterPaymentIncome() { $payments = $this->paymentIncome(); @@ -186,15 +204,50 @@ class ProfitLoss 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] ); - - // - // $total = array_reduce( commissionsArray, function ($sum, $entry) { - // $sum += $entry->commission; - // return $sum; - // }, 0); } + /** + => [ + {#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"", + }, + ] + */ + + 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", diff --git a/routes/api.php b/routes/api.php index e7992191791f..0c5b72d88754 100644 --- a/routes/api.php +++ b/routes/api.php @@ -152,7 +152,7 @@ Route::group(['middleware' => ['throttle:100,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')->middleware('throttle:20,1'); + Route::post('refresh', 'Auth\LoginController@refresh')->middleware('throttle:30,1'); Route::post('reports/clients', 'Reports\ClientReportController'); Route::post('reports/contacts', 'Reports\ClientContactReportController'); From 00a99698ac4ac89bb7d9568b0acb35a2a8dec0a4 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 May 2022 09:11:40 +1000 Subject: [PATCH 29/42] Profit and loss income by cash --- app/Services/Report/ProfitLoss.php | 6 +-- .../Export/ProfitAndLossReportTest.php | 54 +++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/app/Services/Report/ProfitLoss.php b/app/Services/Report/ProfitLoss.php index a97ff59269df..20220fa76aea 100644 --- a/app/Services/Report/ProfitLoss.php +++ b/app/Services/Report/ProfitLoss.php @@ -85,8 +85,8 @@ class ProfitLoss }else { - $this->filterPaymentIncome(); - + //$this->filterPaymentIncome(); + $this->filterInvoicePaymentIncome(); } $this->expenseData(); @@ -141,7 +141,7 @@ class ProfitLoss $this->income_map = $invoices; foreach($invoices as $invoice){ - $this->income += $invoice->net_amount; + $this->income += $invoice->net_converted_amount; $this->income_taxes += $invoice->net_converted_taxes; } diff --git a/tests/Feature/Export/ProfitAndLossReportTest.php b/tests/Feature/Export/ProfitAndLossReportTest.php index 07ce57232de0..0d7a6e5f2935 100644 --- a/tests/Feature/Export/ProfitAndLossReportTest.php +++ b/tests/Feature/Export/ProfitAndLossReportTest.php @@ -244,4 +244,58 @@ class ProfitAndLossReportTest extends TestCase } + 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, + ]); + + $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(); + } + } From fd67d8202ebcac0e7507f236b84982c51a0e353b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 May 2022 11:13:25 +1000 Subject: [PATCH 30/42] Fixes for tests --- app/Services/Report/ProfitLoss.php | 46 +++++++++++++++++++ .../Export/ProfitAndLossReportTest.php | 6 +++ 2 files changed, 52 insertions(+) diff --git a/app/Services/Report/ProfitLoss.php b/app/Services/Report/ProfitLoss.php index 20220fa76aea..2c506a1613f6 100644 --- a/app/Services/Report/ProfitLoss.php +++ b/app/Services/Report/ProfitLoss.php @@ -15,6 +15,7 @@ use App\Libraries\Currency\Conversion\CurrencyApi; use App\Libraries\MultiDB; use App\Models\Company; use App\Models\Expense; +use App\Models\Payment; use Illuminate\Support\Carbon; class ProfitLoss @@ -33,6 +34,10 @@ class ProfitLoss private float $income_taxes = 0; + private float $credit = 0; + + private float $credit_taxes = 0; + private array $expenses; private array $income_map; @@ -206,6 +211,47 @@ class ProfitLoss } + private function paymentEloquentIncome() + { + + $amount_payment_paid = 0; + $amount_credit_paid = 0; + + 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) use($amount_payment_paid, $amount_credit_paid){ + + $company = $payment->company; + $client = $payment->client; + + foreach($payment->paymentables as $pivot) + { + + if($pivot->paymentable instanceOf \App\Models\Invoice){ + + $amount_payment_paid += $pivot->amount - $pivot->refunded; + //calc tax amount - pro rata if necessary + } + + + if($pivot->paymentable instanceOf \App\Models\Credit){ + + $amount_credit_paid += $pivot->amount - $pivot->refunded; + + } + + } + + }); + } + /** => [ diff --git a/tests/Feature/Export/ProfitAndLossReportTest.php b/tests/Feature/Export/ProfitAndLossReportTest.php index 0d7a6e5f2935..5904c4227e31 100644 --- a/tests/Feature/Export/ProfitAndLossReportTest.php +++ b/tests/Feature/Export/ProfitAndLossReportTest.php @@ -11,6 +11,7 @@ namespace Tests\Feature\Export; use App\DataMapper\ClientSettings; +use App\DataMapper\CompanySettings; use App\Factory\InvoiceFactory; use App\Models\Account; use App\Models\Client; @@ -93,8 +94,13 @@ class ProfitAndLossReportTest extends TestCase '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 = [ From 7df6b8f940bdec1454d5c0a9f5c6247ef5391ad6 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 May 2022 13:07:42 +1000 Subject: [PATCH 31/42] PnL Expense tests --- app/Services/Report/ProfitLoss.php | 145 +++++++++++++----- .../Export/ProfitAndLossReportTest.php | 60 ++++++++ 2 files changed, 170 insertions(+), 35 deletions(-) diff --git a/app/Services/Report/ProfitLoss.php b/app/Services/Report/ProfitLoss.php index 2c506a1613f6..60b9a1485b1b 100644 --- a/app/Services/Report/ProfitLoss.php +++ b/app/Services/Report/ProfitLoss.php @@ -36,9 +36,15 @@ class ProfitLoss private float $credit = 0; + private float $credit_invoice = 0; + private float $credit_taxes = 0; - private array $expenses; + private array $invoice_payment_map = []; + + private array $expenses = []; + + private array $expense_break_down = []; private array $income_map; @@ -94,7 +100,7 @@ class ProfitLoss $this->filterInvoicePaymentIncome(); } - $this->expenseData(); + $this->expenseData()->buildExpenseBreakDown(); return $this; } @@ -119,6 +125,11 @@ class ProfitLoss return $this->expenses; } + public function getExpenseBreakDown() :array + { + return $this->expense_break_down; + } + private function filterIncome() { $invoices = $this->invoiceIncome(); @@ -139,17 +150,27 @@ class ProfitLoss private function filterInvoicePaymentIncome() { - $invoices = $this->invoicePaymentIncome(); + $this->paymentEloquentIncome(); - $this->income = 0; - $this->income_taxes = 0; - $this->income_map = $invoices; + 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; - foreach($invoices as $invoice){ - $this->income += $invoice->net_converted_amount; - $this->income_taxes += $invoice->net_converted_taxes; + $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; } @@ -214,8 +235,7 @@ class ProfitLoss private function paymentEloquentIncome() { - $amount_payment_paid = 0; - $amount_credit_paid = 0; + $this->invoice_payment_map = []; Payment::where('company_id', $this->company->id) ->whereIn('status_id', [1,4,5]) @@ -226,32 +246,65 @@ class ProfitLoss }) ->with(['company','client']) ->cursor() - ->each(function ($payment) use($amount_payment_paid, $amount_credit_paid){ + ->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; - //calc tax amount - pro rata if necessary + $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; + + } /** => [ @@ -272,6 +325,7 @@ class ProfitLoss ] */ + private function invoicePaymentIncome() { return \DB::select( \DB::raw(" @@ -332,31 +386,52 @@ class ProfitLoss ->cursor(); - return $this->calculateExpenses($expenses); - - } - - - private function calculateExpenses($expenses) - { - - $data = []; - + $this->expenses = []; foreach($expenses as $expense) { - $data[] = [ - 'total' => $expense->amount, - 'converted_total' => $converted_total = $this->getConvertedTotal($expense->amount, $expense->exchange_rate), - 'tax' => $tax = $this->getTax($expense), - 'net_converted_total' => $expense->uses_inclusive_taxes ? ( $converted_total - $tax ) : $converted_total, - 'category_id' => $expense->category_id, - 'category_name' => $expense->category ? $expense->category->name : "No Category Defined", - ]; + $map = new \stdClass; + + $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; } - $this->expenses = $data; + + 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'] = $data[$expense->category_id]['total'] + $expense->net_converted_total; + $data[$expense->category_id]['category_name'] = $expense->category_name; + $data[$expense->category_id]['tax'] = $data[$expense->category_id]['tax'] + $expense->tax; + + } + + $this->expense_break_down = $data; + + return $this; } diff --git a/tests/Feature/Export/ProfitAndLossReportTest.php b/tests/Feature/Export/ProfitAndLossReportTest.php index 5904c4227e31..05badb9b5309 100644 --- a/tests/Feature/Export/ProfitAndLossReportTest.php +++ b/tests/Feature/Export/ProfitAndLossReportTest.php @@ -15,11 +15,14 @@ use App\DataMapper\CompanySettings; use App\Factory\InvoiceFactory; use App\Models\Account; use App\Models\Client; +use App\Models\ClientContact; use App\Models\Company; +use App\Models\Expense; use App\Models\Invoice; use App\Models\User; use App\Services\Report\ProfitLoss; use App\Utils\Traits\MakesHash; +use Database\Factories\ClientContactFactory; use Illuminate\Routing\Middleware\ThrottleRequests; use Illuminate\Support\Facades\Storage; use League\Csv\Writer; @@ -273,6 +276,10 @@ class ProfitAndLossReportTest extends TestCase 'settings' => $settings, ]); + $contact = ClientContact::factory()->create([ + 'client_id' => $client->id + ]); + $i = Invoice::factory()->create([ 'client_id' => $client->id, 'user_id' => $this->user->id, @@ -304,4 +311,57 @@ class ProfitAndLossReportTest extends TestCase $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 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(); + + } + + } From 4e8389f72e2379724a396f915f28483367ad84e2 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 May 2022 14:06:21 +1000 Subject: [PATCH 32/42] Minor fixes for check data --- app/Console/Commands/CheckData.php | 245 +++--------------- app/Services/Report/ProfitLoss.php | 4 +- .../Export/ProfitAndLossReportTest.php | 52 ++++ 3 files changed, 84 insertions(+), 217 deletions(-) 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/Services/Report/ProfitLoss.php b/app/Services/Report/ProfitLoss.php index 60b9a1485b1b..4da0e7794977 100644 --- a/app/Services/Report/ProfitLoss.php +++ b/app/Services/Report/ProfitLoss.php @@ -423,9 +423,9 @@ class ProfitLoss if(!array_key_exists('tax', $data[$expense->category_id])) $data[$expense->category_id]['tax'] = 0; - $data[$expense->category_id]['total'] = $data[$expense->category_id]['total'] + $expense->net_converted_total; + $data[$expense->category_id]['total'] += $expense->net_converted_total; $data[$expense->category_id]['category_name'] = $expense->category_name; - $data[$expense->category_id]['tax'] = $data[$expense->category_id]['tax'] + $expense->tax; + $data[$expense->category_id]['tax'] += $expense->tax; } diff --git a/tests/Feature/Export/ProfitAndLossReportTest.php b/tests/Feature/Export/ProfitAndLossReportTest.php index 05badb9b5309..b60de2283036 100644 --- a/tests/Feature/Export/ProfitAndLossReportTest.php +++ b/tests/Feature/Export/ProfitAndLossReportTest.php @@ -12,12 +12,14 @@ namespace Tests\Feature\Export; use App\DataMapper\ClientSettings; use App\DataMapper\CompanySettings; +use App\Factory\ExpenseCategoryFactory; use App\Factory\InvoiceFactory; use App\Models\Account; use App\Models\Client; use App\Models\ClientContact; use App\Models\Company; use App\Models\Expense; +use App\Models\ExpenseCategory; use App\Models\Invoice; use App\Models\User; use App\Services\Report\ProfitLoss; @@ -364,4 +366,54 @@ class ProfitAndLossReportTest extends TestCase } + 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(); + + +nlog($bd); + + $this->assertEquals(array_sum(array_column($bd,'total')), 30); + + $this->account->delete(); + + } + } From e0373006d8fc9e868b57ca497c1703846193a03e Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 May 2022 16:42:04 +1000 Subject: [PATCH 33/42] PnL Expense tests --- app/Factory/ExpenseFactory.php | 3 + app/Models/Company.php | 13 ++ app/Services/PdfMaker/Design.php | 2 +- app/Services/Report/ProfitLoss.php | 64 +++++++- .../Export/ProfitAndLossReportTest.php | 151 +++++++++++++++++- 5 files changed, 226 insertions(+), 7 deletions(-) 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/Models/Company.php b/app/Models/Company.php index 9b2a11cd5be4..7ff29a5b3c9a 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -551,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/Services/PdfMaker/Design.php b/app/Services/PdfMaker/Design.php index 12f8587a0b47..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(now()->format('Y-m-d'), $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/Report/ProfitLoss.php b/app/Services/Report/ProfitLoss.php index 4da0e7794977..0fe847df6821 100644 --- a/app/Services/Report/ProfitLoss.php +++ b/app/Services/Report/ProfitLoss.php @@ -16,7 +16,11 @@ use App\Libraries\MultiDB; use App\Models\Company; use App\Models\Expense; use App\Models\Payment; +use App\Utils\Ninja; +use App\Utils\Number; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\App; +use League\Csv\Writer; class ProfitLoss { @@ -126,7 +130,9 @@ class ProfitLoss } public function getExpenseBreakDown() :array - { + { + ksort($this->expense_break_down); + return $this->expense_break_down; } @@ -325,6 +331,54 @@ class ProfitLoss ] */ + 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 + + return $csv->toString(); + + } private function invoicePaymentIncome() { @@ -392,6 +446,8 @@ class ProfitLoss { $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); @@ -438,11 +494,14 @@ class ProfitLoss 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; } @@ -459,7 +518,6 @@ class ProfitLoss } - $exclusive = 0; $exclusive += $amount * ($expense->tax_rate1 / 100); diff --git a/tests/Feature/Export/ProfitAndLossReportTest.php b/tests/Feature/Export/ProfitAndLossReportTest.php index b60de2283036..c7b9636cb910 100644 --- a/tests/Feature/Export/ProfitAndLossReportTest.php +++ b/tests/Feature/Export/ProfitAndLossReportTest.php @@ -13,6 +13,7 @@ namespace Tests\Feature\Export; use App\DataMapper\ClientSettings; use App\DataMapper\CompanySettings; use App\Factory\ExpenseCategoryFactory; +use App\Factory\ExpenseFactory; use App\Factory\InvoiceFactory; use App\Models\Account; use App\Models\Client; @@ -336,9 +337,86 @@ class ProfitAndLossReportTest extends TestCase $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(); @@ -407,13 +485,80 @@ class ProfitAndLossReportTest extends TestCase $bd = $pl->getExpenseBreakDown(); - -nlog($bd); - $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(); + + $this->assertNotNull($pl->getCsv()); + + $this->account->delete(); + + } + } From 6c13512c6abd3bd0628f7aab40e8080b5477400d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 May 2022 17:15:05 +1000 Subject: [PATCH 34/42] Profit and loss --- app/Services/Report/ProfitLoss.php | 40 +++++++++++++++++++ .../Export/ProfitAndLossReportTest.php | 2 + 2 files changed, 42 insertions(+) diff --git a/app/Services/Report/ProfitLoss.php b/app/Services/Report/ProfitLoss.php index 0fe847df6821..7905301cd713 100644 --- a/app/Services/Report/ProfitLoss.php +++ b/app/Services/Report/ProfitLoss.php @@ -14,6 +14,7 @@ namespace App\Services\Report; use App\Libraries\Currency\Conversion\CurrencyApi; use App\Libraries\MultiDB; use App\Models\Company; +use App\Models\Currency; use App\Models\Expense; use App\Models\Payment; use App\Utils\Ninja; @@ -21,6 +22,7 @@ use App\Utils\Number; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\App; use League\Csv\Writer; +use Illuminate\Support\Str; class ProfitLoss { @@ -52,6 +54,8 @@ class ProfitLoss private array $income_map; + private array $foreign_income = []; + protected CurrencyApi $currency_api; /* @@ -140,6 +144,8 @@ class ProfitLoss { $invoices = $this->invoiceIncome(); + $this->foreign_income = []; + $this->income = 0; $this->income_taxes = 0; $this->income_map = $invoices; @@ -147,6 +153,12 @@ class ProfitLoss 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; @@ -181,6 +193,11 @@ class ProfitLoss } + private function getForeignIncome() :array + { + return $this->foreign_income; + } + private function filterPaymentIncome() { $payments = $this->paymentIncome(); @@ -238,6 +255,19 @@ class ProfitLoss } + /** + * 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() { @@ -376,6 +406,16 @@ class ProfitLoss //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(); } diff --git a/tests/Feature/Export/ProfitAndLossReportTest.php b/tests/Feature/Export/ProfitAndLossReportTest.php index c7b9636cb910..1e2bb450c592 100644 --- a/tests/Feature/Export/ProfitAndLossReportTest.php +++ b/tests/Feature/Export/ProfitAndLossReportTest.php @@ -555,6 +555,8 @@ class ProfitAndLossReportTest extends TestCase $pl = new ProfitLoss($this->company, $this->payload); $pl->build(); +echo($pl->getCsv()); + $this->assertNotNull($pl->getCsv()); $this->account->delete(); From dffd48b723585b1e26a7e4677eb97eae3d7809e0 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 May 2022 17:20:16 +1000 Subject: [PATCH 35/42] Update for statements --- app/Services/Client/Statement.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Services/Client/Statement.php b/app/Services/Client/Statement.php index 3169d83b60f4..f968e32bb740 100644 --- a/app/Services/Client/Statement.php +++ b/app/Services/Client/Statement.php @@ -241,10 +241,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: From f59a7653ff7ae1c4742e3b66ea5cd7361ff1182c Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 May 2022 18:53:38 +1000 Subject: [PATCH 36/42] Profit and loss controller end point --- .../Reports/ProfitAndLossController.php | 87 +++++++++++++++++++ .../Requests/Report/ProfitLossRequest.php | 39 +++++++++ routes/api.php | 1 + 3 files changed, 127 insertions(+) create mode 100644 app/Http/Controllers/Reports/ProfitAndLossController.php create mode 100644 app/Http/Requests/Report/ProfitLossRequest.php 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/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/routes/api.php b/routes/api.php index 0c5b72d88754..baa3f56494d0 100644 --- a/routes/api.php +++ b/routes/api.php @@ -167,6 +167,7 @@ Route::group(['middleware' => ['throttle:100,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'); From 8ef12f2ce98134d1228d54e2dd532a74c7c7bff4 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 May 2022 19:42:49 +1000 Subject: [PATCH 37/42] Attach expense documents to invoices --- app/Mail/Engine/InvoiceEmailEngine.php | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/app/Mail/Engine/InvoiceEmailEngine.php b/app/Mail/Engine/InvoiceEmailEngine.php index 300ddedebd2b..63b14110a80e 100644 --- a/app/Mail/Engine/InvoiceEmailEngine.php +++ b/app/Mail/Engine/InvoiceEmailEngine.php @@ -14,14 +14,18 @@ namespace App\Mail\Engine; use App\DataMapper\EmailTemplateDefaults; use App\Jobs\Entity\CreateEntityPdf; use App\Models\Account; +use App\Models\Expense; 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 +150,32 @@ 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]]); + } + + }); + } + } + } From d843b4303419f077ef23e1d1fd87c15932b6a78d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 May 2022 19:47:18 +1000 Subject: [PATCH 38/42] Attach task documents to invoice emails --- app/Mail/Engine/InvoiceEmailEngine.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/Mail/Engine/InvoiceEmailEngine.php b/app/Mail/Engine/InvoiceEmailEngine.php index 63b14110a80e..a3dae5f5aa9a 100644 --- a/app/Mail/Engine/InvoiceEmailEngine.php +++ b/app/Mail/Engine/InvoiceEmailEngine.php @@ -15,6 +15,7 @@ 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; @@ -162,6 +163,7 @@ class InvoiceEmailEngine extends BaseEmailEngine } if(count($expense_ids) > 0){ + $expenses = Expense::whereIn('id', $this->transformKeys($expense_ids)) ->where('invoice_documents', 1) ->cursor() @@ -174,6 +176,27 @@ class InvoiceEmailEngine extends BaseEmailEngine }); } + + if(property_exists($item, 'task_id')) + { + $task_ids[] = $item->task_id; + } + + if(count($task_ids) > 0){ + + $tasks = Task::whereIn('id', $this->transformKeys($task_ids)) + ->where('invoice_documents', 1) + ->cursor() + ->each(function ($task){ + + foreach($task->documents as $document) + { + $this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => $document->type]]); + } + + }); + } + } From 202ab0357c3aac0a90905e34bc9e32c4c7302436 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 14 May 2022 06:34:53 +1000 Subject: [PATCH 39/42] Attach task documents to invoice emails --- app/Mail/Engine/InvoiceEmailEngine.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Mail/Engine/InvoiceEmailEngine.php b/app/Mail/Engine/InvoiceEmailEngine.php index a3dae5f5aa9a..99bf3041515f 100644 --- a/app/Mail/Engine/InvoiceEmailEngine.php +++ b/app/Mail/Engine/InvoiceEmailEngine.php @@ -182,10 +182,9 @@ class InvoiceEmailEngine extends BaseEmailEngine $task_ids[] = $item->task_id; } - if(count($task_ids) > 0){ + if(count($task_ids) > 0 && $this->invoice->company->invoice_task_documents){ $tasks = Task::whereIn('id', $this->transformKeys($task_ids)) - ->where('invoice_documents', 1) ->cursor() ->each(function ($task){ From 641fd20ef2a648beeacf7ef56191796cd9ca880b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 14 May 2022 06:40:59 +1000 Subject: [PATCH 40/42] Minor fixes for deleted company mail --- app/Mail/Company/CompanyDeleted.php | 4 ---- 1 file changed, 4 deletions(-) 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)); From 0c55a9968dd937f09f98a52ff3b44d859518db1f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 14 May 2022 07:30:40 +1000 Subject: [PATCH 41/42] Minor fixes for gocardless --- app/PaymentDrivers/GoCardlessPaymentDriver.php | 16 +++++++++------- app/Services/Client/Statement.php | 1 + 2 files changed, 10 insertions(+), 7 deletions(-) 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/Services/Client/Statement.php b/app/Services/Client/Statement.php index f968e32bb740..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(); } From 3b6a4b75385a44074085f7bd26a61077ef85acb2 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 14 May 2022 07:54:19 +1000 Subject: [PATCH 42/42] Minor fixes for Stripe connect webhooks --- app/Jobs/Mail/NinjaMailerJob.php | 5 +++-- app/PaymentDrivers/StripePaymentDriver.php | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) 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/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();