diff --git a/app/Export/CSV/ProductSalesExport.php b/app/Export/CSV/ProductSalesExport.php new file mode 100644 index 000000000000..fee3c5f53e9a --- /dev/null +++ b/app/Export/CSV/ProductSalesExport.php @@ -0,0 +1,130 @@ + 'custom_value1', + 'custom_value2' => 'custom_value2', + 'custom_value3' => 'custom_value3', + 'custom_value4' => 'custom_value4', + 'product_key' => 'product_key', + 'notes' => 'notes', + 'cost' => 'cost', + 'price' => 'price', + 'quantity' => 'quantity', + 'tax_rate1' => 'tax_rate1', + 'tax_rate2' => 'tax_rate2', + 'tax_rate3' => 'tax_rate3', + 'tax_name1' => 'tax_name1', + 'tax_name2' => 'tax_name2', + 'tax_name3' => 'tax_name3', + 'is_amount_discount' => 'is_amount_discount', + 'discount' => 'discount', + 'line_total' => 'line_total', + 'gross_line_total' => 'gross_line_total', + ]; + + private array $decorate_keys = [ + 'client', + 'currency', + ]; + + public function __construct(Company $company, array $input) + { + $this->company = $company; + $this->input = $input; + } + + public function run() + { + MultiDB::setDb($this->company->db); + App::forgetInstance('translator'); + App::setLocale($this->company->locale()); + $t = app('translator'); + $t->replace(Ninja::transformTranslations($this->company->settings)); + + //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()); + + $query = Invoice::query() + ->withTrashed() + ->where('company_id', $this->company->id) + ->where('is_deleted', 0) + ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL, Invoice::STATUS_PAID]); + + $query = $this->addDateRange($query); + + $query->cursor() + ->each(function ($invoice) { + + foreach($invoice->line_items as $item) + $this->csv->insertOne($this->buildRow($invoice, $item)); + + }); + + return $this->csv->toString(); + } + + private function buildRow($invoice, $invoice_item) :array + { + $transformed_entity = (array)$invoice_item; + + $entity = []; + + foreach (array_values($this->input['report_keys']) as $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($invoice, $entity); + } + + private function decorateAdvancedFields(Invoice $invoice, $entity) :array + { + $entity['client'] = $invoice->client->present()->name(); + $entity['currency'] = $invoice->client->currency()->code; + + return $entity; + } +} diff --git a/app/Http/Controllers/Reports/ProductSalesReportController.php b/app/Http/Controllers/Reports/ProductSalesReportController.php new file mode 100644 index 000000000000..1f0bf4a9aa76 --- /dev/null +++ b/app/Http/Controllers/Reports/ProductSalesReportController.php @@ -0,0 +1,89 @@ +has('send_email') && $request->get('send_email')) { + SendToAdmin::dispatch(auth()->user()->company(), $request->all(), ProductSalesExport::class, $this->filename); + + return response()->json(['message' => 'working...'], 200); + } + // expect a list of visible fields, or use the default + + $export = new ProductSalesExport(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/Requests/Report/ProductSalesReportRequest.php b/app/Http/Requests/Report/ProductSalesReportRequest.php new file mode 100644 index 000000000000..6234cc4466e4 --- /dev/null +++ b/app/Http/Requests/Report/ProductSalesReportRequest.php @@ -0,0 +1,71 @@ +user()->isAdmin(); + } + + public function rules() + { + + return [ + 'date_range' => 'bail|required|string', + 'end_date' => 'bail|required_if:date_range,custom|nullable|date', + 'start_date' => 'bail|required_if:date_range,custom|nullable|date', + 'report_keys' => 'bail|present|array', + 'send_email' => 'bail|required|bool', + 'client_id' => 'bail|sometimes|exists:clients,id,company_id,'.auth()->user()->company()->id.',is_deleted,0', + ]; + } + + public function prepareForValidation() + { + $input = $this->all(); + + if (! array_key_exists('date_range', $input)) { + $input['date_range'] = 'all'; + } + + if (! array_key_exists('report_keys', $input)) { + $input['report_keys'] = []; + } + + if (! array_key_exists('send_email', $input)) { + $input['send_email'] = true; + } + + if (array_key_exists('date_range', $input) && $input['date_range'] != 'custom') { + $input['start_date'] = null; + $input['end_date'] = null; + } + + if(array_key_exists('client_id', $input) && strlen($input['client_id']) >=1) + $input['client_id'] = $this->decodePrimaryKey($input['client_id']); + + $this->replace($input); + } +} diff --git a/routes/api.php b/routes/api.php index e20d1315f290..62d2cb6b9586 100644 --- a/routes/api.php +++ b/routes/api.php @@ -70,6 +70,7 @@ use App\Http\Controllers\Reports\InvoiceItemReportController; use App\Http\Controllers\Reports\InvoiceReportController; use App\Http\Controllers\Reports\PaymentReportController; use App\Http\Controllers\Reports\ProductReportController; +use App\Http\Controllers\Reports\ProductSalesReportController; use App\Http\Controllers\Reports\ProfitAndLossController; use App\Http\Controllers\Reports\QuoteItemReportController; use App\Http\Controllers\Reports\QuoteReportController; @@ -270,6 +271,7 @@ Route::group(['middleware' => ['throttle:300,1', 'api_db', 'token_auth', 'locale Route::post('reports/recurring_invoices', RecurringInvoiceReportController::class); Route::post('reports/payments', PaymentReportController::class); Route::post('reports/products', ProductReportController::class); + Route::post('reports/product_sales', ProductSalesReportController::class); Route::post('reports/tasks', TaskReportController::class); Route::post('reports/profitloss', ProfitAndLossController::class); diff --git a/tests/Feature/Export/ProductSalesReportTest.php b/tests/Feature/Export/ProductSalesReportTest.php new file mode 100644 index 000000000000..6917bdd33ec7 --- /dev/null +++ b/tests/Feature/Export/ProductSalesReportTest.php @@ -0,0 +1,212 @@ +faker = \Faker\Factory::create(); + + $this->withoutMiddleware( + ThrottleRequests::class + ); + + $this->withoutExceptionHandling(); + } + + public $company; + + public $user; + + public $payload; + + public $account; + + /** + * start_date - Y-m-d + end_date - Y-m-d + date_range - + all + last7 + last30 + this_month + last_month + this_quarter + last_quarter + this_year + custom + is_income_billed - true = Invoiced || false = Payments + expense_billed - true = Expensed || false = Expenses marked as paid + include_tax - true tax_included || false - tax_excluded + */ + private function buildData() + { + $this->account = Account::factory()->create([ + 'hosted_client_count' => 1000, + 'hosted_company_count' => 1000, + ]); + + $this->account->num_users = 3; + $this->account->save(); + + $this->user = User::factory()->create([ + 'account_id' => $this->account->id, + 'confirmation_code' => 'xyz123', + 'email' => $this->faker->unique()->safeEmail(), + ]); + + $settings = CompanySettings::defaults(); + $settings->client_online_payment_notification = false; + $settings->client_manual_payment_notification = false; + + $this->company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + ]); + + $this->payload = [ + 'start_date' => '2000-01-01', + 'end_date' => '2030-01-11', + 'date_range' => 'custom', + 'is_income_billed' => true, + 'include_tax' => false, + ]; + } + + public function testProductSalesInstance() + { + $this->buildData(); + + $pl = new ProductSalesExport($this->company, $this->payload); + + $this->assertInstanceOf(ProductSalesExport::class, $pl); + + $this->account->delete(); + } + + public function testSimpleReport() + { + $this->buildData(); + + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'is_deleted' => 0, + ]); + + $this->payload = [ + 'start_date' => '2000-01-01', + 'end_date' => '2030-01-11', + 'date_range' => 'custom', + 'client_id' => $client->id, + 'report_keys' => [] + ]; + + $i = Invoice::factory()->create([ + 'client_id' => $client->id, + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'amount' => 0, + 'balance' => 0, + 'status_id' => 2, + 'total_taxes' => 1, + 'date' => now()->format('Y-m-d'), + 'terms' => 'nada', + 'discount' => 0, + 'tax_rate1' => 0, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name1' => '', + 'tax_name2' => '', + 'tax_name3' => '', + 'uses_inclusive_taxes' => false, + 'line_items' => $this->buildLineItems(), + ]); + + $i = $i->calc()->getInvoice(); + + $pl = new ProductSalesExport($this->company, $this->payload); + $response = $pl->run(); + + $this->assertIsString($response); +nlog($response); + + // $this->account->delete(); + } + + + private function buildLineItems() + { + $line_items = []; + + $item = InvoiceItemFactory::create(); + $item->quantity = 1; + $item->cost = 10; + $item->product_key = 'test'; + $item->notes = 'test_product'; + // $item->task_id = $this->encodePrimaryKey($this->task->id); + // $item->expense_id = $this->encodePrimaryKey($this->expense->id); + + $line_items[] = $item; + + + $item = InvoiceItemFactory::create(); + $item->quantity = 1; + $item->cost = 10; + $item->product_key = 'pumpkin'; + $item->notes = 'test_pumpkin'; + // $item->task_id = $this->encodePrimaryKey($this->task->id); + // $item->expense_id = $this->encodePrimaryKey($this->expense->id); + + $line_items[] = $item; + + + return $line_items; + } + +} \ No newline at end of file