1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 13:12:50 +01:00

Product Sales Report

This commit is contained in:
David Bomba 2023-01-04 01:30:50 +11:00
parent 5790dc8a7b
commit fd51303617
5 changed files with 504 additions and 0 deletions

View File

@ -0,0 +1,130 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Export\CSV;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\Company;
use App\Models\Credit;
use App\Models\Document;
use App\Models\Invoice;
use App\Models\Product;
use App\Transformers\ProductTransformer;
use App\Utils\Ninja;
use Illuminate\Support\Facades\App;
use League\Csv\Writer;
class ProductSalesExport extends BaseExport
{
private Company $company;
protected array $input;
protected $date_key = 'created_at';
protected array $entity_keys = [
'custom_value1' => '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;
}
}

View File

@ -0,0 +1,89 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers\Reports;
use App\Export\CSV\ProductExport;
use App\Export\CSV\ProductSalesExport;
use App\Http\Controllers\BaseController;
use App\Http\Requests\Report\GenericReportRequest;
use App\Http\Requests\Report\ProductSalesReportRequest;
use App\Jobs\Report\SendToAdmin;
use App\Models\Client;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Response;
class ProductSalesReportController extends BaseController
{
use MakesHash;
private string $filename = 'product_sales.csv';
public function __construct()
{
parent::__construct();
}
/**
* @OA\Post(
* path="/api/v1/reports/product_sales",
* operationId="getProductSalesReport",
* tags={"reports"},
* summary="Product Salesreports",
* description="Export product sales reports",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(ref="#/components/schemas/GenericReportSchema")
* ),
* @OA\Response(
* response=200,
* description="success",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function __invoke(ProductSalesReportRequest $request)
{
if ($request->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);
}
}

View File

@ -0,0 +1,71 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\Report;
use App\Http\Requests\Request;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
class ProductSalesReportRequest extends Request
{
use MakesHash;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->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);
}
}

View File

@ -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);

View File

@ -0,0 +1,212 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Feature\Export;
use App\DataMapper\ClientSettings;
use App\DataMapper\CompanySettings;
use App\Export\CSV\ProductSalesExport;
use App\Factory\ExpenseCategoryFactory;
use App\Factory\ExpenseFactory;
use App\Factory\InvoiceFactory;
use App\Factory\InvoiceItemFactory;
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;
use App\Utils\Traits\MakesHash;
use Database\Factories\ClientContactFactory;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Facades\Storage;
use League\Csv\Writer;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Services\Report\ProductSalesExport
*/
class ProductSalesReportTest extends TestCase
{
use MakesHash;
public $faker;
protected function setUp() :void
{
parent::setUp();
$this->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;
}
}