From 252effabef1bf0b0849e0d6aebdd5264b4176040 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 24 Apr 2023 09:31:28 +1000 Subject: [PATCH 01/14] Add manual payment notifications --- app/Listeners/Payment/PaymentNotification.php | 33 ++++++++++++++++++- lang/en/texts.php | 1 + 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/app/Listeners/Payment/PaymentNotification.php b/app/Listeners/Payment/PaymentNotification.php index b1b7c82eef12..8b1a64d2977e 100644 --- a/app/Listeners/Payment/PaymentNotification.php +++ b/app/Listeners/Payment/PaymentNotification.php @@ -56,8 +56,39 @@ class PaymentNotification implements ShouldQueue $this->trackRevenue($event); } - if($payment->is_manual) + /* Manual Payment Notifications */ + if($payment->is_manual){ + + foreach ($payment->company->company_users as $company_user) { + $user = $company_user->user; + + $methods = $this->findUserEntityNotificationType( + $payment, + $company_user, + [ + 'payment_manual', + 'payment_manual_all', + 'payment_manual_user', + 'all_notifications', ] + ); + + if (($key = array_search('mail', $methods)) !== false) { + unset($methods[$key]); + + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer((new EntityPaidObject($payment))->build()); + $nmo->company = $event->company; + $nmo->settings = $event->company->settings; + $nmo->to_user = $user; + + (new NinjaMailerJob($nmo))->handle(); + + $nmo = null; + } + } + return; + } /*User notifications*/ foreach ($payment->company->company_users as $company_user) { diff --git a/lang/en/texts.php b/lang/en/texts.php index f8519d1424f3..1b9cc11b114b 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5062,6 +5062,7 @@ $LANG = array( 'here' => 'here', 'industry_Restaurant & Catering' => 'Restaurant & Catering', 'show_credits_table' => 'Show Credits Table', + 'manual_payment' => 'Payment Manual', ); From f356ddd845a955ac0ae61a4a41ecb6faf19debe8 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 24 Apr 2023 11:47:48 +1000 Subject: [PATCH 02/14] Refactor for taxes --- app/DataMapper/Tax/BaseRule.php | 46 ++++++++++++++++------- app/Helpers/Invoice/InvoiceItemSum.php | 2 +- tests/Feature/Scheduler/SchedulerTest.php | 4 +- tests/Unit/Tax/EuTaxTest.php | 18 ++++----- tests/Unit/Tax/SumTaxTest.php | 30 +++++++++------ tests/Unit/Tax/UsTaxTest.php | 16 ++++---- 6 files changed, 71 insertions(+), 45 deletions(-) diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index 1c154e6035d1..c212c870fd8b 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -15,6 +15,7 @@ use App\Models\Client; use App\Models\Invoice; use App\Models\Product; use App\DataMapper\Tax\ZipTax\Response; +use App\DataProviders\USStates; class BaseRule implements RuleInterface { @@ -116,7 +117,7 @@ class BaseRule implements RuleInterface protected ?Client $client; - protected ?Response $tax_data; + public ?Response $tax_data; public mixed $invoice; @@ -129,29 +130,41 @@ class BaseRule implements RuleInterface return $this; } - public function setInvoice(mixed $invoice): self + public function setEntity(mixed $invoice): self { $this->invoice = $invoice; - - $this->configTaxData(); $this->client = $invoice->client; + $this->configTaxData() + ->resolveRegions(); + $this->tax_data = new Response($this->invoice->tax_data); - $this->resolveRegions(); - - return $this; } private function configTaxData(): self { + + if(!array_key_exists($this->client->country->iso_3166_2, $this->region_codes)) { + throw new \Exception('Automatic tax calculations not supported for this country'); + } + + $this->client_region = $this->region_codes[$this->client->country->iso_3166_2]; + if($this->invoice->tax_data && $this->invoice->status_id > 1) return $this; //determine if we are taxing locally or if we are taxing globally - // $this->invoice->tax_data = $this->invoice->client->tax_data; + $this->invoice->tax_data = $this->invoice->client->tax_data ?: new Response([]); + + if(strlen($this->invoice->tax_data?->originDestination) == 0 && $this->client->company->tax_data->seller_subregion != $this->client_subregion) { + $tax_data = $this->invoice->tax_data; + $tax_data->originDestination = "D"; + $this->invoice->tax_data = $tax_data; + $this->invoice->saveQuietly(); + } return $this; } @@ -160,20 +173,25 @@ class BaseRule implements RuleInterface private function resolveRegions(): self { - if(!array_key_exists($this->client->country->iso_3166_2, $this->region_codes)) - throw new \Exception('Automatic tax calculations not supported for this country'); - - $this->client_region = $this->region_codes[$this->client->country->iso_3166_2]; - match($this->client_region){ - 'US' => $this->client_subregion = $this->tax_data->geoState, + 'US' => $this->client_subregion = strlen($this->invoice?->tax_data?->geoState) > 1 ? $this->invoice?->tax_data?->geoState : $this->getUSState(), 'EU' => $this->client_subregion = $this->client->country->iso_3166_2, + 'AU' => $this->client_subregion = 'AU', default => $this->client_subregion = $this->client->country->iso_3166_2, }; return $this; } + private function getUSState(): string + { + try { + return USStates::getState(strlen($this->client->postal_code) > 1 ? $this->client->postal_code : $this->client->shipping_postal_code); + } catch (\Exception $e) { + return 'CA'; + } + } + public function isTaxableRegion(): bool { return $this->client->company->tax_data->regions->{$this->client_region}->tax_all_subregions || $this->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->apply_tax; diff --git a/app/Helpers/Invoice/InvoiceItemSum.php b/app/Helpers/Invoice/InvoiceItemSum.php index afef994e2ba0..6c6846470378 100644 --- a/app/Helpers/Invoice/InvoiceItemSum.php +++ b/app/Helpers/Invoice/InvoiceItemSum.php @@ -148,7 +148,7 @@ class InvoiceItemSum $this->rule = new $class(); $this->rule - ->setInvoice($this->invoice) + ->setEntity($this->invoice) ->init(); $this->calc_tax = true; diff --git a/tests/Feature/Scheduler/SchedulerTest.php b/tests/Feature/Scheduler/SchedulerTest.php index 370483683738..579186e7c597 100644 --- a/tests/Feature/Scheduler/SchedulerTest.php +++ b/tests/Feature/Scheduler/SchedulerTest.php @@ -480,14 +480,14 @@ class SchedulerTest extends TestCase $c = Client::factory()->create([ 'company_id' => $this->company->id, 'user_id' => $this->user->id, - 'number' => rand(1000, 100000), + 'number' => rand(1000, 10000000), 'name' => 'A fancy client' ]); $c2 = Client::factory()->create([ 'company_id' => $this->company->id, 'user_id' => $this->user->id, - 'number' => rand(1000, 100000), + 'number' => rand(1000, 10000000), 'name' => 'A fancy client' ]); diff --git a/tests/Unit/Tax/EuTaxTest.php b/tests/Unit/Tax/EuTaxTest.php index 6f41d282bbc3..b56c13050711 100644 --- a/tests/Unit/Tax/EuTaxTest.php +++ b/tests/Unit/Tax/EuTaxTest.php @@ -349,7 +349,7 @@ class EuTaxTest extends TestCase ]); $process = new Rule(); - $process->setInvoice($invoice); + $process->setEntity($invoice); $process->init(); $this->assertEquals('EU', $process->seller_region); @@ -403,7 +403,7 @@ class EuTaxTest extends TestCase ]); $process = new Rule(); - $process->setInvoice($invoice); + $process->setEntity($invoice); $process->init(); $this->assertEquals('EU', $process->seller_region); @@ -458,7 +458,7 @@ class EuTaxTest extends TestCase ]); $process = new Rule(); - $process->setInvoice($invoice); + $process->setEntity($invoice); $process->init(); $this->assertEquals('EU', $process->seller_region); @@ -513,7 +513,7 @@ class EuTaxTest extends TestCase ]); $process = new Rule(); - $process->setInvoice($invoice); + $process->setEntity($invoice); $process->init(); $this->assertInstanceOf(Rule::class, $process); @@ -564,7 +564,7 @@ class EuTaxTest extends TestCase ]); $process = new Rule(); - $process->setInvoice($invoice); + $process->setEntity($invoice); $process->init(); @@ -615,7 +615,7 @@ class EuTaxTest extends TestCase ]); $process = new Rule(); - $process->setInvoice($invoice); + $process->setEntity($invoice); $process->init(); @@ -666,7 +666,7 @@ class EuTaxTest extends TestCase ]); $process = new Rule(); - $process->setInvoice($invoice); + $process->setEntity($invoice); $process->init(); @@ -717,7 +717,7 @@ class EuTaxTest extends TestCase ]); $process = new Rule(); - $process->setInvoice($invoice); + $process->setEntity($invoice); $process->init(); $this->assertInstanceOf(Rule::class, $process); @@ -766,7 +766,7 @@ class EuTaxTest extends TestCase ]); $process = new Rule(); - $process->setInvoice($invoice); + $process->setEntity($invoice); $process->init(); diff --git a/tests/Unit/Tax/SumTaxTest.php b/tests/Unit/Tax/SumTaxTest.php index f1a4ad5b08fe..ec3b3047237f 100644 --- a/tests/Unit/Tax/SumTaxTest.php +++ b/tests/Unit/Tax/SumTaxTest.php @@ -102,10 +102,13 @@ class SumTaxTest extends TestCase $this->company->tax_data = $tax_data; $this->company->save(); + $tax_data = new TaxData($this->response); + $client = Client::factory()->create([ 'user_id' => $this->user->id, 'company_id' => $this->company->id, 'country_id' => 840, + 'tax_data' => $tax_data, ]); $invoice = InvoiceFactory::create($this->company->id, $this->user->id); @@ -114,7 +117,7 @@ class SumTaxTest extends TestCase $line_items = []; - $invoice->tax_data = new TaxData($this->response); + $invoice->tax_data = $tax_data; $line_item = new InvoiceItem(); $line_item->quantity = 1; @@ -131,7 +134,6 @@ class SumTaxTest extends TestCase $line_items = $invoice->line_items; - $this->assertEquals(10, $invoice->amount); $this->assertEquals("", $line_items[0]->tax_name1); $this->assertEquals(0, $line_items[0]->tax_rate1); @@ -152,19 +154,23 @@ class SumTaxTest extends TestCase $this->company->tax_data = $tax_data; $this->company->save(); - $client = Client::factory()->create([ - 'user_id' => $this->user->id, - 'company_id' => $this->company->id, - 'country_id' => 840, - ]); +$tax_data = new TaxData($this->response); - $invoice = InvoiceFactory::create($this->company->id, $this->user->id); - $invoice->client_id = $client->id; - $invoice->uses_inclusive_taxes = false; +$client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'country_id' => 840, + 'tax_data' => $tax_data, +]); - $line_items = []; +$invoice = InvoiceFactory::create($this->company->id, $this->user->id); +$invoice->client_id = $client->id; +$invoice->uses_inclusive_taxes = false; + +$line_items = []; + +$invoice->tax_data = $tax_data; - $invoice->tax_data = new TaxData($this->response); $line_item = new InvoiceItem; $line_item->quantity = 1; diff --git a/tests/Unit/Tax/UsTaxTest.php b/tests/Unit/Tax/UsTaxTest.php index f08b295b3829..5ed356723cd2 100644 --- a/tests/Unit/Tax/UsTaxTest.php +++ b/tests/Unit/Tax/UsTaxTest.php @@ -107,6 +107,7 @@ class UsTaxTest extends TestCase 'shipping_country_id' => 840, 'has_valid_vat_number' => false, 'postal_code' => $postal_code, + 'tax_data' => new Response($this->mock_response), ]); $invoice = Invoice::factory()->create([ @@ -309,6 +310,7 @@ class UsTaxTest extends TestCase 'shipping_country_id' => 276, 'has_valid_vat_number' => false, 'postal_code' => 'xx', + 'tax_data' => new Response($this->mock_response), ]); $invoice = Invoice::factory()->create([ @@ -353,18 +355,18 @@ class UsTaxTest extends TestCase { $invoice = $this->invoiceStub('92582'); - $client = $invoice->client; - $client->is_tax_exempt = false; - $client->save(); + $invoice->client->is_tax_exempt = false; + $invoice->client->tax_data = new Response($this->mock_response); - $company = $invoice->company; - $tax_data = $company->tax_data; + $invoice->client->push(); + + $tax_data = $invoice->company->tax_data; $tax_data->regions->US->has_sales_above_threshold = true; $tax_data->regions->US->tax_all_subregions = true; - $company->tax_data = $tax_data; - $company->save(); + $invoice->company->tax_data = $tax_data; + $invoice->company->push(); $invoice = $invoice->calc()->getInvoice()->service()->markSent()->save(); From 85b261ab21c9e91295c5ca419cba6536c0c9a0ef Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 24 Apr 2023 12:07:36 +1000 Subject: [PATCH 03/14] remove tax_data from transformers --- app/DataMapper/Tax/BaseRule.php | 2 +- app/Transformers/ClientTransformer.php | 2 +- app/Transformers/InvoiceTransformer.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index c212c870fd8b..779c40d82bc2 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -188,7 +188,7 @@ class BaseRule implements RuleInterface try { return USStates::getState(strlen($this->client->postal_code) > 1 ? $this->client->postal_code : $this->client->shipping_postal_code); } catch (\Exception $e) { - return 'CA'; + return $this->client->company->country()->iso_3166_2 == 'US' ? $this->client->company->tax_data->seller_subregion : 'CA'; } } diff --git a/app/Transformers/ClientTransformer.php b/app/Transformers/ClientTransformer.php index af7f3338a536..512fd9a15a4e 100644 --- a/app/Transformers/ClientTransformer.php +++ b/app/Transformers/ClientTransformer.php @@ -149,7 +149,7 @@ class ClientTransformer extends EntityTransformer 'number' => (string) $client->number ?: '', 'has_valid_vat_number' => (bool) $client->has_valid_vat_number, 'is_tax_exempt' => (bool) $client->is_tax_exempt, - 'tax_data' => $client->tax_data ?: '', + // 'tax_data' => $client->tax_data ?: '', ]; } } diff --git a/app/Transformers/InvoiceTransformer.php b/app/Transformers/InvoiceTransformer.php index 5e3b0a93955d..b76d3d6cfddb 100644 --- a/app/Transformers/InvoiceTransformer.php +++ b/app/Transformers/InvoiceTransformer.php @@ -149,7 +149,7 @@ class InvoiceTransformer extends EntityTransformer 'paid_to_date' => (float) $invoice->paid_to_date, 'subscription_id' => $this->encodePrimaryKey($invoice->subscription_id), 'auto_bill_enabled' => (bool) $invoice->auto_bill_enabled, - 'tax_data' => $invoice->tax_data ?: '', + // 'tax_data' => $invoice->tax_data ?: '', ]; } } From 9584fe0ee5d2518dd2a25a4874d68ab21a9042f1 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 24 Apr 2023 12:22:15 +1000 Subject: [PATCH 04/14] Fixes for cross region rules --- app/DataMapper/Tax/BaseRule.php | 1 + app/DataMapper/Tax/US/Rule.php | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index 779c40d82bc2..bf8481b1faaf 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -162,6 +162,7 @@ class BaseRule implements RuleInterface if(strlen($this->invoice->tax_data?->originDestination) == 0 && $this->client->company->tax_data->seller_subregion != $this->client_subregion) { $tax_data = $this->invoice->tax_data; $tax_data->originDestination = "D"; + $tax_data->geoState = $this->client_subregion; $this->invoice->tax_data = $tax_data; $this->invoice->saveQuietly(); } diff --git a/app/DataMapper/Tax/US/Rule.php b/app/DataMapper/Tax/US/Rule.php index 4b2789becba7..85b779020e93 100644 --- a/app/DataMapper/Tax/US/Rule.php +++ b/app/DataMapper/Tax/US/Rule.php @@ -143,10 +143,25 @@ class Rule extends BaseRule implements RuleInterface */ public function default(): self { - + + if($this->tax_data?->stateSalesTax == 0) { + + if($this->tax_data->originDestination == "O"){ + $tax_region = $this->client->company->tax_data->seller_subregion; + $this->tax_rate1 = $this->invoice->client->company->tax_data->regions->US->subregions->{$tax_region}->tax_rate; + $this->tax_name1 = "{$this->tax_data->geoState} Sales Tax"; + } else { + $this->tax_rate1 = $this->invoice->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_rate; + $this->tax_name1 = $this->invoice->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_name; + } + + return $this; + } + $this->tax_rate1 = $this->tax_data->taxSales * 100; $this->tax_name1 = "{$this->tax_data->geoState} Sales Tax"; + return $this; } From 2d48ffdc6fe34cb79ddc30421c9a3a0089f7a727 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 24 Apr 2023 12:48:07 +1000 Subject: [PATCH 05/14] Add in multiple methods to determine the USState --- app/DataProviders/USStates.php | 137 +++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/app/DataProviders/USStates.php b/app/DataProviders/USStates.php index 265574aab2a9..acfba3b7fb39 100644 --- a/app/DataProviders/USStates.php +++ b/app/DataProviders/USStates.php @@ -12,6 +12,8 @@ namespace App\DataProviders; +use Illuminate\Support\Facades\Http; + class USStates { protected static array $states = [ @@ -33871,6 +33873,141 @@ class USStates if(isset(self::$zip_code_map[$zip])) return self::$zip_code_map[$zip]; + $prefix_state = self::getStateFromThreeDigitPrefix($zip) + + if($prefix_state) + return $prefix_state; + + $zippo_response = self::getStateFromZippo($zip); + + if($zippo_response) + return $zippo_response; + throw new \Exception('Zip code not found'); } + + /* + { + "post code": "90210", + "country": "United States", + "country abbreviation": "US", + "places": [ + { + "place name": "Beverly Hills", + "longitude": "-118.4065", + "state": "California", + "state abbreviation": "CA", + "latitude": "34.0901" + } + ] + } + */ + public static function getStateFromZippo($zip): mixed + { + + $response = Http::get("https://api.zippopotam.us/us/{$zip}"); + + if($response->failed()) + return false; + + $data = $response->object(); + + if(isset($data->places[0])) { + return $data->places[0]->{'state abbreviation'}; + } + + return false; + + } + + public static function getStateFromThreeDigitPrefix($zip): mixed + { + + /* 000 to 999 */ + $zip_by_state = [ + '--', '--', '--', '--', '--', 'NY', 'PR', 'PR', 'VI', 'PR', 'MA', 'MA', 'MA', + 'MA', 'MA', 'MA', 'MA', 'MA', 'MA', 'MA', 'MA', 'MA', 'MA', 'MA', 'MA', 'MA', + 'MA', 'MA', 'RI', 'RI', 'NH', 'NH', 'NH', 'NH', 'NH', 'NH', 'NH', 'NH', 'NH', + 'ME', 'ME', 'ME', 'ME', 'ME', 'ME', 'ME', 'ME', 'ME', 'ME', 'ME', 'VT', 'VT', + 'VT', 'VT', 'VT', 'MA', 'VT', 'VT', 'VT', 'VT', 'CT', 'CT', 'CT', 'CT', 'CT', + 'CT', 'CT', 'CT', 'CT', 'CT', 'NJ', 'NJ', 'NJ', 'NJ', 'NJ', 'NJ', 'NJ', 'NJ', + 'NJ', 'NJ', 'NJ', 'NJ', 'NJ', 'NJ', 'NJ', 'NJ', 'NJ', 'NJ', 'NJ', 'NJ', 'AE', + 'AE', 'AE', 'AE', 'AE', 'AE', 'AE', 'AE', 'AE', '--', 'NY', 'NY', 'NY', 'NY', + 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', + 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', + 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', + 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'NY', 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', + 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', + 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', + 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', 'PA', '--', 'PA', 'PA', + 'PA', 'PA', 'DE', 'DE', 'DE', 'DC', 'VA', 'DC', 'DC', 'DC', 'DC', 'MD', 'MD', + 'MD', 'MD', 'MD', 'MD', 'MD', '--', 'MD', 'MD', 'MD', 'MD', 'MD', 'MD', 'VA', + 'VA', 'VA', 'VA', 'VA', 'VA', 'VA', 'VA', 'VA', 'VA', 'VA', 'VA', 'VA', 'VA', + 'VA', 'VA', 'VA', 'VA', 'VA', 'VA', 'VA', 'VA', 'VA', 'VA', 'VA', 'VA', 'VA', + 'WV', 'WV', 'WV', 'WV', 'WV', 'WV', 'WV', 'WV', 'WV', 'WV', 'WV', 'WV', 'WV', + 'WV', 'WV', 'WV', 'WV', 'WV', 'WV', 'WV', 'WV', 'WV', '--', 'NC', 'NC', 'NC', + 'NC', 'NC', 'NC', 'NC', 'NC', 'NC', 'NC', 'NC', 'NC', 'NC', 'NC', 'NC', 'NC', + 'NC', 'NC', 'NC', 'NC', 'SC', 'SC', 'SC', 'SC', 'SC', 'SC', 'SC', 'SC', 'SC', + 'SC', 'GA', 'GA', 'GA', 'GA', 'GA', 'GA', 'GA', 'GA', 'GA', 'GA', 'GA', 'GA', + 'GA', 'GA', 'GA', 'GA', 'GA', 'GA', 'GA', 'GA', 'FL', 'FL', 'FL', 'FL', 'FL', + 'FL', 'FL', 'FL', 'FL', 'FL', 'FL', 'FL', 'FL', 'FL', 'FL', 'FL', 'FL', 'FL', + 'FL', 'FL', 'AA', 'FL', 'FL', '--', 'FL', '--', 'FL', 'FL', '--', 'FL', 'AL', + 'AL', 'AL', '--', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', + 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'TN', 'TN', 'TN', 'TN', 'TN', 'TN', 'TN', + 'TN', 'TN', 'TN', 'TN', 'TN', 'TN', 'TN', 'TN', 'TN', 'MS', 'MS', 'MS', 'MS', + 'MS', 'MS', 'MS', 'MS', 'MS', 'MS', 'MS', 'MS', 'GA', '--', 'KY', 'KY', 'KY', + 'KY', 'KY', 'KY', 'KY', 'KY', 'KY', 'KY', 'KY', 'KY', 'KY', 'KY', 'KY', 'KY', + 'KY', 'KY', 'KY', '--', 'KY', 'KY', 'KY', 'KY', 'KY', 'KY', 'KY', 'KY', '--', + '--', 'OH', 'OH', 'OH', 'OH', 'OH', 'OH', 'OH', 'OH', 'OH', 'OH', 'OH', 'OH', + 'OH', 'OH', 'OH', 'OH', 'OH', 'OH', 'OH', 'OH', 'OH', 'OH', 'OH', 'OH', 'OH', + 'OH', 'OH', 'OH', 'OH', '--', 'IN', 'IN', 'IN', 'IN', 'IN', 'IN', 'IN', 'IN', + 'IN', 'IN', 'IN', 'IN', 'IN', 'IN', 'IN', 'IN', 'IN', 'IN', 'IN', 'IN', 'MI', + 'MI', 'MI', 'MI', 'MI', 'MI', 'MI', 'MI', 'MI', 'MI', 'MI', 'MI', 'MI', 'MI', + 'MI', 'MI', 'MI', 'MI', 'MI', 'MI', 'IA', 'IA', 'IA', 'IA', 'IA', 'IA', 'IA', + 'IA', 'IA', '--', 'IA', 'IA', 'IA', 'IA', 'IA', 'IA', 'IA', '--', '--', '--', + 'IA', 'IA', 'IA', 'IA', 'IA', 'IA', 'IA', 'IA', 'IA', '--', 'WI', 'WI', 'WI', + '--', 'WI', 'WI', '--', 'WI', 'WI', 'WI', 'WI', 'WI', 'WI', 'WI', 'WI', 'WI', + 'WI', 'WI', 'WI', 'WI', 'MN', 'MN', '--', 'MN', 'MN', 'MN', 'MN', 'MN', 'MN', + 'MN', 'MN', 'MN', 'MN', 'MN', 'MN', 'MN', 'MN', 'MN', '--', 'DC', 'SD', 'SD', + 'SD', 'SD', 'SD', 'SD', 'SD', 'SD', '--', '--', 'ND', 'ND', 'ND', 'ND', 'ND', + 'ND', 'ND', 'ND', 'ND', '--', 'MT', 'MT', 'MT', 'MT', 'MT', 'MT', 'MT', 'MT', + 'MT', 'MT', 'IL', 'IL', 'IL', 'IL', 'IL', 'IL', 'IL', 'IL', 'IL', 'IL', 'IL', + 'IL', 'IL', 'IL', 'IL', 'IL', 'IL', 'IL', 'IL', 'IL', 'IL', '--', 'IL', 'IL', + 'IL', 'IL', 'IL', 'IL', 'IL', 'IL', 'MO', 'MO', '--', 'MO', 'MO', 'MO', 'MO', + 'MO', 'MO', 'MO', 'MO', 'MO', '--', '--', 'MO', 'MO', 'MO', 'MO', 'MO', '--', + 'MO', 'MO', 'MO', 'MO', 'MO', 'MO', 'MO', 'MO', 'MO', '--', 'KS', 'KS', 'KS', + '--', 'KS', 'KS', 'KS', 'KS', 'KS', 'KS', 'KS', 'KS', 'KS', 'KS', 'KS', 'KS', + 'KS', 'KS', 'KS', 'KS', 'NE', 'NE', '--', 'NE', 'NE', 'NE', 'NE', 'NE', 'NE', + 'NE', 'NE', 'NE', 'NE', 'NE', '--', '--', '--', '--', '--', '--', 'LA', 'LA', + '--', 'LA', 'LA', 'LA', 'LA', 'LA', 'LA', '--', 'LA', 'LA', 'LA', 'LA', 'LA', + '--', 'AR', 'AR', 'AR', 'AR', 'AR', 'AR', 'AR', 'AR', 'AR', 'AR', 'AR', 'AR', + 'AR', 'AR', 'OK', 'OK', '--', 'TX', 'OK', 'OK', 'OK', 'OK', 'OK', 'OK', 'OK', + 'OK', '--', 'OK', 'OK', 'OK', 'OK', 'OK', 'OK', 'OK', 'TX', 'TX', 'TX', 'TX', + 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', + 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', + 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', + 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'TX', 'CO', 'CO', 'CO', 'CO', 'CO', 'CO', + 'CO', 'CO', 'CO', 'CO', 'CO', 'CO', 'CO', 'CO', 'CO', 'CO', 'CO', '--', '--', + '--', 'WY', 'WY', 'WY', 'WY', 'WY', 'WY', 'WY', 'WY', 'WY', 'WY', 'WY', 'WY', + 'ID', 'ID', 'ID', 'ID', 'ID', 'ID', 'ID', '--', 'UT', 'UT', '--', 'UT', 'UT', + 'UT', 'UT', 'UT', '--', '--', 'AZ', 'AZ', 'AZ', 'AZ', '--', 'AZ', 'AZ', 'AZ', + '--', 'AZ', 'AZ', '--', '--', 'AZ', 'AZ', 'AZ', '--', '--', '--', '--', 'NM', + 'NM', '--', 'NM', 'NM', 'NM', '--', 'NM', 'NM', 'NM', 'NM', 'NM', 'NM', 'NM', + 'NM', 'NM', '--', '--', '--', '--', 'NV', 'NV', '--', 'NV', 'NV', 'NV', '--', + 'NV', 'NV', '--', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', '--', + 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', + 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', '--', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', + 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', + 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', 'CA', + 'AP', 'AP', 'AP', 'AP', 'AP', 'HI', 'HI', 'GU', 'OR', 'OR', 'OR', 'OR', 'OR', + 'OR', 'OR', 'OR', 'OR', 'OR', 'WA', 'WA', 'WA', 'WA', 'WA', 'WA', 'WA', '--', + 'WA', 'WA', 'WA', 'WA', 'WA', 'WA', 'WA', 'AK', 'AK', 'AK', 'AK', 'AK' + ]; + + $prefix = substr($zip, 0, 3); + $index = intval($prefix); + /* converts prefix to integer */ + return $zip_by_state[$index] == "--" ? false : $zip_by_state[$index]; + + } } From 07c87aeb0cf1bc883bda572379b27bf7196da7ff Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 24 Apr 2023 12:48:51 +1000 Subject: [PATCH 06/14] remove tax_data from transformers --- app/DataProviders/USStates.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/DataProviders/USStates.php b/app/DataProviders/USStates.php index acfba3b7fb39..f66493667b86 100644 --- a/app/DataProviders/USStates.php +++ b/app/DataProviders/USStates.php @@ -33873,7 +33873,7 @@ class USStates if(isset(self::$zip_code_map[$zip])) return self::$zip_code_map[$zip]; - $prefix_state = self::getStateFromThreeDigitPrefix($zip) + $prefix_state = self::getStateFromThreeDigitPrefix($zip); if($prefix_state) return $prefix_state; From 056e0dc7a423885437634ed034723fb28b1d1a46 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 24 Apr 2023 12:50:08 +1000 Subject: [PATCH 07/14] remove tax_data from transformers --- app/DataMapper/Tax/US/Rule.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/DataMapper/Tax/US/Rule.php b/app/DataMapper/Tax/US/Rule.php index 85b779020e93..68cc9bd2c05a 100644 --- a/app/DataMapper/Tax/US/Rule.php +++ b/app/DataMapper/Tax/US/Rule.php @@ -153,6 +153,10 @@ class Rule extends BaseRule implements RuleInterface } else { $this->tax_rate1 = $this->invoice->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_rate; $this->tax_name1 = $this->invoice->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_name; + + if($this->client_region == 'US') + $this->tax_name1 = "{$this->tax_data->geoState} ".$this->tax_name1; + } return $this; From 6a38b7a14a2add10e97d87c23615b60bcc66b024 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 24 Apr 2023 12:50:35 +1000 Subject: [PATCH 08/14] Add in multiple methods to determine the USState --- app/DataMapper/Tax/US/Rule.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/DataMapper/Tax/US/Rule.php b/app/DataMapper/Tax/US/Rule.php index 68cc9bd2c05a..11b08ce41f81 100644 --- a/app/DataMapper/Tax/US/Rule.php +++ b/app/DataMapper/Tax/US/Rule.php @@ -155,8 +155,8 @@ class Rule extends BaseRule implements RuleInterface $this->tax_name1 = $this->invoice->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_name; if($this->client_region == 'US') - $this->tax_name1 = "{$this->tax_data->geoState} ".$this->tax_name1; - + $this->tax_name1 = "{$this->client_subregion} ".$this->tax_name1; + } return $this; From 4840207b3ba76a1985a421f585f82ccebc1633c0 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 24 Apr 2023 13:04:52 +1000 Subject: [PATCH 09/14] Update payment report export --- app/Export/CSV/PaymentExport.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Export/CSV/PaymentExport.php b/app/Export/CSV/PaymentExport.php index 41e090f520f0..78592b763c86 100644 --- a/app/Export/CSV/PaymentExport.php +++ b/app/Export/CSV/PaymentExport.php @@ -49,6 +49,7 @@ class PaymentExport extends BaseExport 'transaction_reference' => 'transaction_reference', 'type' => 'type_id', 'vendor' => 'vendor_id', + 'invoices' => 'invoices', ]; private array $decorate_keys = [ @@ -59,6 +60,7 @@ class PaymentExport extends BaseExport 'currency', 'exchange_currency', 'type', + 'invoices', ]; public function __construct(Company $company, array $input) @@ -154,6 +156,8 @@ class PaymentExport extends BaseExport $entity['gateway'] = $payment->gateway_type ? $payment->gateway_type->name : 'Unknown Type'; } + $entity['invoices'] = $payment->invoices()->exists() ? $payment->invoices->pluck('number')->implode(',') : ''; + return $entity; } } From 3a429605b72c62e925360d315cfdbe796b9d639e Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 24 Apr 2023 13:41:35 +1000 Subject: [PATCH 10/14] Report Controllers --- app/DataProviders/USStates.php | 2 +- .../Reports/ARDetailReportController.php | 84 ++++++++++++++++++ .../Reports/ARSummaryReportController.php | 84 ++++++++++++++++++ .../Reports/ClientBalanceReportController.php | 85 +++++++++++++++++++ .../Reports/ClientSalesReportController.php | 85 +++++++++++++++++++ .../Reports/TaxSummaryReportController.php | 85 +++++++++++++++++++ .../Reports/UserSalesReportController.php | 85 +++++++++++++++++++ lang/en/texts.php | 2 + routes/api.php | 15 +++- tests/Feature/Export/ArDetailReportTest.php | 2 + tests/Unit/Tax/EuTaxTest.php | 2 +- 11 files changed, 528 insertions(+), 3 deletions(-) create mode 100644 app/Http/Controllers/Reports/ARDetailReportController.php create mode 100644 app/Http/Controllers/Reports/ARSummaryReportController.php create mode 100644 app/Http/Controllers/Reports/ClientBalanceReportController.php create mode 100644 app/Http/Controllers/Reports/ClientSalesReportController.php create mode 100644 app/Http/Controllers/Reports/TaxSummaryReportController.php create mode 100644 app/Http/Controllers/Reports/UserSalesReportController.php diff --git a/app/DataProviders/USStates.php b/app/DataProviders/USStates.php index f66493667b86..d973bfe5f4e7 100644 --- a/app/DataProviders/USStates.php +++ b/app/DataProviders/USStates.php @@ -33868,7 +33868,7 @@ class USStates return self::$states; } - public static function getState(string $zip): string + public static function getState(?string $zip = '90210'): string { if(isset(self::$zip_code_map[$zip])) return self::$zip_code_map[$zip]; diff --git a/app/Http/Controllers/Reports/ARDetailReportController.php b/app/Http/Controllers/Reports/ARDetailReportController.php new file mode 100644 index 000000000000..4f6c460e7a31 --- /dev/null +++ b/app/Http/Controllers/Reports/ARDetailReportController.php @@ -0,0 +1,84 @@ +has('send_email') && $request->get('send_email')) { + SendToAdmin::dispatch(auth()->user()->company(), $request->all(), ARDetailReport::class, $this->filename); + + return response()->json(['message' => 'working...'], 200); + } + // expect a list of visible fields, or use the default + + $export = new ARDetailReport(auth()->user()->company(), $request->all()); + + $csv = $export->run(); + + $headers = [ + 'Content-Disposition' => 'attachment', + 'Content-Type' => 'text/csv', + ]; + + return response()->streamDownload(function () use ($csv) { + echo $csv; + }, $this->filename, $headers); + } +} diff --git a/app/Http/Controllers/Reports/ARSummaryReportController.php b/app/Http/Controllers/Reports/ARSummaryReportController.php new file mode 100644 index 000000000000..a9c9f6e68946 --- /dev/null +++ b/app/Http/Controllers/Reports/ARSummaryReportController.php @@ -0,0 +1,84 @@ +has('send_email') && $request->get('send_email')) { + SendToAdmin::dispatch(auth()->user()->company(), $request->all(), ARSummaryReport::class, $this->filename); + + return response()->json(['message' => 'working...'], 200); + } + // expect a list of visible fields, or use the default + + $export = new ARSummaryReport(auth()->user()->company(), $request->all()); + + $csv = $export->run(); + + $headers = [ + 'Content-Disposition' => 'attachment', + 'Content-Type' => 'text/csv', + ]; + + return response()->streamDownload(function () use ($csv) { + echo $csv; + }, $this->filename, $headers); + } +} diff --git a/app/Http/Controllers/Reports/ClientBalanceReportController.php b/app/Http/Controllers/Reports/ClientBalanceReportController.php new file mode 100644 index 000000000000..0202af3d2f68 --- /dev/null +++ b/app/Http/Controllers/Reports/ClientBalanceReportController.php @@ -0,0 +1,85 @@ +has('send_email') && $request->get('send_email')) { + SendToAdmin::dispatch(auth()->user()->company(), $request->all(), ClientBalanceReport::class, $this->filename); + + return response()->json(['message' => 'working...'], 200); + } + // expect a list of visible fields, or use the default + + $export = new ClientBalanceReport(auth()->user()->company(), $request->all()); + + $csv = $export->run(); + + $headers = [ + 'Content-Disposition' => 'attachment', + 'Content-Type' => 'text/csv', + ]; + + return response()->streamDownload(function () use ($csv) { + echo $csv; + }, $this->filename, $headers); + } +} diff --git a/app/Http/Controllers/Reports/ClientSalesReportController.php b/app/Http/Controllers/Reports/ClientSalesReportController.php new file mode 100644 index 000000000000..056b157775b3 --- /dev/null +++ b/app/Http/Controllers/Reports/ClientSalesReportController.php @@ -0,0 +1,85 @@ +has('send_email') && $request->get('send_email')) { + SendToAdmin::dispatch(auth()->user()->company(), $request->all(), ClientSalesReport::class, $this->filename); + + return response()->json(['message' => 'working...'], 200); + } + // expect a list of visible fields, or use the default + + $export = new ClientSalesReport(auth()->user()->company(), $request->all()); + + $csv = $export->run(); + + $headers = [ + 'Content-Disposition' => 'attachment', + 'Content-Type' => 'text/csv', + ]; + + return response()->streamDownload(function () use ($csv) { + echo $csv; + }, $this->filename, $headers); + } +} diff --git a/app/Http/Controllers/Reports/TaxSummaryReportController.php b/app/Http/Controllers/Reports/TaxSummaryReportController.php new file mode 100644 index 000000000000..776fa988d694 --- /dev/null +++ b/app/Http/Controllers/Reports/TaxSummaryReportController.php @@ -0,0 +1,85 @@ +has('send_email') && $request->get('send_email')) { + SendToAdmin::dispatch(auth()->user()->company(), $request->all(), TaxSummaryReport::class, $this->filename); + + return response()->json(['message' => 'working...'], 200); + } + // expect a list of visible fields, or use the default + + $export = new TaxSummaryReport(auth()->user()->company(), $request->all()); + + $csv = $export->run(); + + $headers = [ + 'Content-Disposition' => 'attachment', + 'Content-Type' => 'text/csv', + ]; + + return response()->streamDownload(function () use ($csv) { + echo $csv; + }, $this->filename, $headers); + } +} diff --git a/app/Http/Controllers/Reports/UserSalesReportController.php b/app/Http/Controllers/Reports/UserSalesReportController.php new file mode 100644 index 000000000000..582eb4e71727 --- /dev/null +++ b/app/Http/Controllers/Reports/UserSalesReportController.php @@ -0,0 +1,85 @@ +has('send_email') && $request->get('send_email')) { + SendToAdmin::dispatch(auth()->user()->company(), $request->all(), UserSalesReport::class, $this->filename); + + return response()->json(['message' => 'working...'], 200); + } + // expect a list of visible fields, or use the default + + $export = new UserSalesReport(auth()->user()->company(), $request->all()); + + $csv = $export->run(); + + $headers = [ + 'Content-Disposition' => 'attachment', + 'Content-Type' => 'text/csv', + ]; + + return response()->streamDownload(function () use ($csv) { + echo $csv; + }, $this->filename, $headers); + } +} diff --git a/lang/en/texts.php b/lang/en/texts.php index 1b9cc11b114b..78d7862dddb9 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5063,6 +5063,8 @@ $LANG = array( 'industry_Restaurant & Catering' => 'Restaurant & Catering', 'show_credits_table' => 'Show Credits Table', 'manual_payment' => 'Payment Manual', + 'tax_summary_report' => 'Tax Summary Report', + ); diff --git a/routes/api.php b/routes/api.php index 4eed06948c5f..350e547333e5 100644 --- a/routes/api.php +++ b/routes/api.php @@ -90,12 +90,18 @@ use App\Http\Controllers\Reports\PaymentReportController; use App\Http\Controllers\Reports\ProductReportController; use App\Http\Controllers\Reports\ProfitAndLossController; use App\Http\Controllers\Reports\ActivityReportController; +use App\Http\Controllers\Reports\ARDetailReportController; use App\Http\Controllers\Reports\DocumentReportController; +use App\Http\Controllers\Reports\ARSummaryReportController; use App\Http\Controllers\Reports\QuoteItemReportController; +use App\Http\Controllers\Reports\UserSalesReportController; +use App\Http\Controllers\Reports\TaxSummaryReportController; use App\Http\Controllers\Support\Messages\SendingController; +use App\Http\Controllers\Reports\ClientSalesReportController; use App\Http\Controllers\Reports\InvoiceItemReportController; use App\Http\Controllers\PaymentNotificationWebhookController; use App\Http\Controllers\Reports\ProductSalesReportController; +use App\Http\Controllers\Reports\ClientBalanceReportController; use App\Http\Controllers\Reports\ClientContactReportController; use App\Http\Controllers\Reports\RecurringInvoiceReportController; @@ -285,7 +291,14 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'] Route::post('reports/product_sales', ProductSalesReportController::class); Route::post('reports/tasks', TaskReportController::class); Route::post('reports/profitloss', ProfitAndLossController::class); - + + Route::post('reports/ar_detail_report', ARDetailReportController::class); + Route::post('reports/ar_summary_report', ARSummaryReportController::class); + Route::post('reports/client_balance_report', ClientBalanceReportController::class); + Route::post('reports/client_sales_report', ClientSalesReportController::class); + Route::post('reports/tax_summary_report', TaxSummaryReportController::class); + Route::post('reports/user_sales_report', UserSalesReportController::class); + Route::resource('task_schedulers', TaskSchedulerController::class); Route::post('task_schedulers/bulk', [TaskSchedulerController::class, 'bulk'])->name('task_schedulers.bulk'); diff --git a/tests/Feature/Export/ArDetailReportTest.php b/tests/Feature/Export/ArDetailReportTest.php index 0d053f34c710..8f523a52c551 100644 --- a/tests/Feature/Export/ArDetailReportTest.php +++ b/tests/Feature/Export/ArDetailReportTest.php @@ -22,6 +22,7 @@ use App\Services\Report\ARDetailReport; use App\Services\Report\UserSalesReport; use App\Utils\Traits\MakesHash; use Illuminate\Routing\Middleware\ThrottleRequests; +use Tests\MockAccountData; use Tests\TestCase; /** @@ -44,6 +45,7 @@ class ARDetailReportTest extends TestCase ); $this->withoutExceptionHandling(); + } public $company; diff --git a/tests/Unit/Tax/EuTaxTest.php b/tests/Unit/Tax/EuTaxTest.php index b56c13050711..537cfaa5fd19 100644 --- a/tests/Unit/Tax/EuTaxTest.php +++ b/tests/Unit/Tax/EuTaxTest.php @@ -399,7 +399,7 @@ class EuTaxTest extends TestCase 'status_id' => Invoice::STATUS_SENT, 'tax_data' => new Response([ 'geoState' => 'CA', - ]), + ]), ]); $process = new Rule(); From d5d6ef5edfa1e05b232a5d16c9e66219eb8091c6 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 24 Apr 2023 13:42:29 +1000 Subject: [PATCH 11/14] Update translations --- lang/en/texts.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lang/en/texts.php b/lang/en/texts.php index 78d7862dddb9..565fe7684e00 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5064,7 +5064,12 @@ $LANG = array( 'show_credits_table' => 'Show Credits Table', 'manual_payment' => 'Payment Manual', 'tax_summary_report' => 'Tax Summary Report', - + 'tax_category' => 'Tax Category', + 'physical_goods' => 'Physical Goods', + 'digital_products' => 'Digital Products', + 'services' => 'Services', + 'shipping' => 'Shipping', + 'tax_exempt' => 'Tax Exempt', ); From 1416e03263d7360c0baac25bc075c5ab31acc0d3 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 24 Apr 2023 13:43:29 +1000 Subject: [PATCH 12/14] Updated translations --- lang/fr_CA/texts.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lang/fr_CA/texts.php b/lang/fr_CA/texts.php index b747f2ea6650..c15a08f77ca9 100644 --- a/lang/fr_CA/texts.php +++ b/lang/fr_CA/texts.php @@ -1333,7 +1333,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'finish_setup' => 'Terminer la configuration', 'created_wepay_confirmation_required' => 'Veuillez vérifier vos courriel et confirmer votre adresse courriel avec WePay.', 'switch_to_wepay' => 'Changer pour WePay', - 'switch' => 'Changer', + 'switch' => 'Commutateur', 'restore_account_gateway' => 'Restaurer la passerelle de paiement', 'restored_account_gateway' => 'La passerelle de paiement a été restaurée', 'united_states' => 'États-Unis', @@ -3333,7 +3333,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'accent_color' => 'Couleur de mise en évidence', 'comma_sparated_list' => 'Liste séparée par virgule', 'single_line_text' => 'Ligne de texte simple', - 'multi_line_text' => 'Multiligne de texte', + 'multi_line_text' => 'Zone de texte multiligne', 'dropdown' => 'Liste déroulante', 'field_type' => 'Type de champ', 'recover_password_email_sent' => 'Un courriel a été envoyé pour la récupération du mot de passe', @@ -4609,7 +4609,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'add' => 'Ajouter', 'last_sent_template' => 'Modèle pour dernier envoi', 'enable_flexible_search' => 'Activer la recherche flexible', - 'enable_flexible_search_help' => 'Match non-contiguous characters, ie. "ct" matches "cat"', + 'enable_flexible_search_help' => 'Correspondance de caractères non contigus, ex. "ct" pour "cat"', 'vendor_details' => 'Informations du fournisseur', 'purchase_order_details' => 'Détails du bon de commande', 'qr_iban' => 'QR IBAN', @@ -4805,7 +4805,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'task_update_authorization_error' => 'Permission d\'accès insuffisante ou tâche verrouillée', 'cash_vs_accrual' => 'Comptabilité d\'exercice', 'cash_vs_accrual_help' => 'Activer pour comptabilité d\'exercice. Désactiver pour comptabilité d\'encaisse', - 'expense_paid_report' => 'Expensed reporting', + 'expense_paid_report' => 'Rapport des déboursés', 'expense_paid_report_help' => 'Activer pour un rapport de toutes les dépenses. Désactiver pour un rapport des dépenses payées seulement', 'online_payment_email_help' => 'Envoyer un courriel lorsque un paiement en ligne à été fait', 'manual_payment_email_help' => 'Envoyer un courriel lorsque un paiement a été saisi manuellement', From f470d10f9638b93d69e3b8974ccfd4bf35216054 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 24 Apr 2023 13:58:25 +1000 Subject: [PATCH 13/14] Minor fixes for reports --- app/Services/Report/ARDetailReport.php | 2 ++ app/Services/Report/ClientBalanceReport.php | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/Services/Report/ARDetailReport.php b/app/Services/Report/ARDetailReport.php index 8c324cafa42b..c283a0a84ab0 100644 --- a/app/Services/Report/ARDetailReport.php +++ b/app/Services/Report/ARDetailReport.php @@ -40,6 +40,7 @@ class ARDetailReport extends BaseExport public array $report_keys = [ 'date', + 'due_date', 'invoice_number', 'status', 'client_name', @@ -114,6 +115,7 @@ class ARDetailReport extends BaseExport $client = $invoice->client; return [ + $this->translateDate($invoice->date, $this->company->date_format(), $this->company->locale()), $this->translateDate($invoice->due_date, $this->company->date_format(), $this->company->locale()), $invoice->number, $invoice->stringStatus($invoice->status_id), diff --git a/app/Services/Report/ClientBalanceReport.php b/app/Services/Report/ClientBalanceReport.php index 498a018a2aea..d94679776106 100644 --- a/app/Services/Report/ClientBalanceReport.php +++ b/app/Services/Report/ClientBalanceReport.php @@ -36,6 +36,7 @@ class ClientBalanceReport extends BaseExport 'client_name', 'client_number', 'id_number', + 'invoices', 'invoice_balance', 'credit_balance', ]; @@ -68,7 +69,7 @@ class ClientBalanceReport extends BaseExport $this->csv->insertOne([]); $this->csv->insertOne([]); $this->csv->insertOne([]); - $this->csv->insertOne([ctrans('texts.customer_balance_report')]); + $this->csv->insertOne([ctrans('texts.client_balance_report')]); $this->csv->insertOne([ctrans('texts.created_on'),' ',$this->translateDate(now()->format('Y-m-d'), $this->company->date_format(), $this->company->locale())]); if (count($this->input['report_keys']) == 0) { From b5fd275dcd60cc4fbd191311b70f7156e060ceba Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 24 Apr 2023 14:55:56 +1000 Subject: [PATCH 14/14] Additions for Invoice Report --- app/Export/CSV/BaseExport.php | 58 ++++++- app/Export/CSV/InvoiceExport.php | 28 +++- .../Requests/Report/GenericReportRequest.php | 1 + tests/Feature/Export/ReportApiTest.php | 145 ++++++++++++++++++ 4 files changed, 227 insertions(+), 5 deletions(-) create mode 100644 tests/Feature/Export/ReportApiTest.php diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index 1c0a0294e42e..65b3bd50b363 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -12,8 +12,10 @@ namespace App\Export\CSV; use App\Models\Client; -use App\Utils\Traits\MakesHash; +use App\Models\Invoice; use Illuminate\Support\Carbon; +use App\Utils\Traits\MakesHash; +use Illuminate\Database\Eloquent\Builder; class BaseExport { @@ -46,6 +48,60 @@ class BaseExport return $query; } + protected function addInvoiceStatusFilter($query, $status): Builder + { + + $status_parameters = explode(',', $status); + + + if(in_array('all', $status_parameters)) + return $query; + + $query->where(function ($nested) use ($status_parameters) { + + $invoice_filters = []; + + if (in_array('draft', $status_parameters)) { + $invoice_filters[] = Invoice::STATUS_DRAFT; + } + + if (in_array('sent', $status_parameters)) { + $invoice_filters[] = Invoice::STATUS_SENT; + } + + if (in_array('paid', $status_parameters)) { + $invoice_filters[] = Invoice::STATUS_PAID; + } + + if (in_array('unpaid', $status_parameters)) { + $invoice_filters[] = Invoice::STATUS_SENT; + $invoice_filters[] = Invoice::STATUS_PARTIAL; + } + + if (count($invoice_filters) > 0) { + $nested->whereIn('status_id', $invoice_filters); + } + + if (in_array('overdue', $status_parameters)) { + $nested->orWhereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) + ->where('due_date', '<', Carbon::now()) + ->orWhere('partial_due_date', '<', Carbon::now()); + } + + if(in_array('viewed', $status_parameters)){ + + $nested->whereHas('invitations', function ($q){ + $q->whereNotNull('viewed_date')->whereNotNull('deleted_at'); + }); + + } + + + }); + + return $query; + } + protected function addDateRange($query) { $date_range = $this->input['date_range']; diff --git a/app/Export/CSV/InvoiceExport.php b/app/Export/CSV/InvoiceExport.php index 750cbd5f60e8..2141358fa134 100644 --- a/app/Export/CSV/InvoiceExport.php +++ b/app/Export/CSV/InvoiceExport.php @@ -11,13 +11,15 @@ namespace App\Export\CSV; -use App\Libraries\MultiDB; +use App\Utils\Ninja; +use App\Utils\Number; +use League\Csv\Writer; use App\Models\Company; use App\Models\Invoice; -use App\Transformers\InvoiceTransformer; -use App\Utils\Ninja; +use App\Libraries\MultiDB; +use App\Export\CSV\BaseExport; use Illuminate\Support\Facades\App; -use League\Csv\Writer; +use App\Transformers\InvoiceTransformer; class InvoiceExport extends BaseExport { @@ -63,6 +65,10 @@ class InvoiceExport extends BaseExport 'terms' => 'terms', 'total_taxes' => 'total_taxes', 'currency_id' => 'currency_id', + 'payment_number' => 'payment_number', + 'payment_date' => 'payment_date', + 'payment_amount' => 'payment_amount', + 'method' => 'method', ]; private array $decorate_keys = [ @@ -107,6 +113,10 @@ class InvoiceExport extends BaseExport $query = $this->addDateRange($query); + if(isset($this->input['status'])){ + $query = $this->addInvoiceStatusFilter($query, $this->input['status']); + } + $query->cursor() ->each(function ($invoice) { $this->csv->insertOne($this->buildRow($invoice)); @@ -151,7 +161,17 @@ class InvoiceExport extends BaseExport if (in_array('status_id', $this->input['report_keys'])) { $entity['status'] = $invoice->stringStatus($invoice->status_id); } + + $payment_exists = $invoice->payments()->exists(); + $entity['payment_number'] = $payment_exists ? $invoice->payments()->pluck('number')->implode(',') : ''; + + $entity['payment_date'] = $payment_exists ? $invoice->payments()->pluck('date')->implode(',') : ''; + + $entity['payment_amount'] = $payment_exists ? Number::formatMoney($invoice->payments()->sum('paymentables.amount'), $invoice->company) : ctrans('texts.unpaid'); + + $entity['method'] = $payment_exists ? $invoice->payments()->first()->translatedType() : ""; + return $entity; } } diff --git a/app/Http/Requests/Report/GenericReportRequest.php b/app/Http/Requests/Report/GenericReportRequest.php index 63837aa41341..3e95479d23b5 100644 --- a/app/Http/Requests/Report/GenericReportRequest.php +++ b/app/Http/Requests/Report/GenericReportRequest.php @@ -33,6 +33,7 @@ class GenericReportRequest extends Request 'start_date' => 'bail|required_if:date_range,custom|nullable|date', 'report_keys' => 'present|array', 'send_email' => 'required|bool', + 'status' => 'sometimes|string|nullable|in:all,draft,sent,viewed,paid,unpaid,overdue', ]; } diff --git a/tests/Feature/Export/ReportApiTest.php b/tests/Feature/Export/ReportApiTest.php new file mode 100644 index 000000000000..14fd1f57297b --- /dev/null +++ b/tests/Feature/Export/ReportApiTest.php @@ -0,0 +1,145 @@ +faker = \Faker\Factory::create(); + + $this->withoutMiddleware( + ThrottleRequests::class + ); + + $this->withoutExceptionHandling(); + + $this->makeTestData(); + + } + + public function testUserSalesReportApiRoute() + { + $data = [ + 'send_email' => false, + 'date_range' => 'all', + 'report_keys' => [], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/reports/user_sales_report', $data) + ->assertStatus(200); + + } + + + public function testTaxSummaryReportApiRoute() + { + $data = [ + 'send_email' => false, + 'date_range' => 'all', + 'report_keys' => [], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/reports/tax_summary_report', $data) + ->assertStatus(200); + + } + + + public function testClientSalesReportApiRoute() + { + $data = [ + 'send_email' => false, + 'date_range' => 'all', + 'report_keys' => [], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/reports/client_sales_report', $data) + ->assertStatus(200); + + } + + + public function testArDetailReportApiRoute() + { + $data = [ + 'send_email' => false, + 'date_range' => 'all', + 'report_keys' => [], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/reports/ar_detail_report', $data) + ->assertStatus(200); + + } + + public function testArSummaryReportApiRoute() + { + $data = [ + 'send_email' => false, + 'date_range' => 'all', + 'report_keys' => [], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/reports/ar_summary_report', $data) + ->assertStatus(200); + + } + + public function testClientBalanceReportApiRoute() + { + $data = [ + 'send_email' => false, + 'date_range' => 'all', + 'report_keys' => [], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/reports/client_balance_report', $data) + ->assertStatus(200); + + } + + +} \ No newline at end of file