mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-07 16:04:32 -04:00
commit
7372cf4ffc
@ -1 +1 @@
|
||||
5.5.50
|
||||
5.5.51
|
@ -22,6 +22,7 @@ use App\Jobs\Ninja\CompanySizeCheck;
|
||||
use App\Jobs\Ninja\QueueSize;
|
||||
use App\Jobs\Ninja\SystemMaintenance;
|
||||
use App\Jobs\Ninja\TaskScheduler;
|
||||
use App\Jobs\Invoice\InvoiceCheckLateWebhook;
|
||||
use App\Jobs\Quote\QuoteCheckExpired;
|
||||
use App\Jobs\Subscription\CleanStaleInvoiceOrder;
|
||||
use App\Jobs\Util\DiskCleanup;
|
||||
@ -78,6 +79,9 @@ class Kernel extends ConsoleKernel
|
||||
/* Fires notifications for expired Quotes */
|
||||
$schedule->job(new QuoteCheckExpired)->dailyAt('05:10')->withoutOverlapping()->name('quote-expired-job')->onOneServer();
|
||||
|
||||
/* Fires webhooks for overdue Invoice */
|
||||
$schedule->job(new InvoiceCheckLateWebhook)->dailyAt('07:00')->withoutOverlapping()->name('invoice-overdue-job')->onOneServer();
|
||||
|
||||
/* Performs auto billing */
|
||||
$schedule->job(new AutoBillCron)->dailyAt('06:20')->withoutOverlapping()->name('auto-bill-job')->onOneServer();
|
||||
|
||||
|
138
app/Export/CSV/ProductSalesExport.php
Normal file
138
app/Export/CSV/ProductSalesExport.php
Normal file
@ -0,0 +1,138 @@
|
||||
<?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;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
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',
|
||||
'status' => 'status',
|
||||
'date' => 'date',
|
||||
'currency' => 'currency',
|
||||
'client' => 'client',
|
||||
];
|
||||
|
||||
private array $decorate_keys = [
|
||||
'client',
|
||||
'currency',
|
||||
'date',
|
||||
];
|
||||
|
||||
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;
|
||||
$entity['status'] = $invoice->stringStatus($invoice->status_id);
|
||||
$entity['date'] = Carbon::parse($invoice->date)->format($this->company->date_format());
|
||||
|
||||
return $entity;
|
||||
}
|
||||
}
|
@ -69,25 +69,54 @@ class ExpenseFilters extends QueryFilters
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
if (in_array('logged', $status_parameters)) {
|
||||
$this->builder->where('amount', '>', 0);
|
||||
}
|
||||
$this->builder->whereNested(function ($query) use($status_parameters){
|
||||
|
||||
if (in_array('pending', $status_parameters)) {
|
||||
$this->builder->whereNull('invoice_id')->whereNotNull('payment_date');
|
||||
}
|
||||
if (in_array('logged', $status_parameters)) {
|
||||
|
||||
if (in_array('invoiced', $status_parameters)) {
|
||||
$this->builder->whereNotNull('invoice_id');
|
||||
}
|
||||
$query->orWhere(function ($query){
|
||||
$query->where('amount', '>', 0)
|
||||
->whereNull('invoice_id')
|
||||
->whereNull('payment_date');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
if (in_array('paid', $status_parameters)) {
|
||||
$this->builder->whereNotNull('payment_date');
|
||||
}
|
||||
if (in_array('pending', $status_parameters)) {
|
||||
|
||||
if (in_array('unpaid', $status_parameters)) {
|
||||
$this->builder->whereNull('payment_date');
|
||||
}
|
||||
$query->orWhere(function ($query){
|
||||
$query->where('should_be_invoiced',true)
|
||||
->whereNull('invoice_id');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
if (in_array('invoiced', $status_parameters)) {
|
||||
|
||||
$query->orWhere(function ($query){
|
||||
$query->whereNotNull('invoice_id');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
if (in_array('paid', $status_parameters)) {
|
||||
|
||||
$query->orWhere(function ($query){
|
||||
$query->whereNotNull('payment_date');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
if (in_array('unpaid', $status_parameters)) {
|
||||
|
||||
$query->orWhere(function ($query){
|
||||
$query->whereNull('payment_date');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// nlog($this->builder->toSql());
|
||||
|
||||
return $this->builder;
|
||||
}
|
||||
@ -212,8 +241,6 @@ class ExpenseFilters extends QueryFilters
|
||||
*/
|
||||
public function entityFilter()
|
||||
{
|
||||
|
||||
//return $this->builder->whereCompanyId(auth()->user()->company()->id);
|
||||
return $this->builder->company();
|
||||
}
|
||||
}
|
||||
|
@ -218,6 +218,15 @@ abstract class QueryFilters
|
||||
return $this->builder->where('client_id', $this->decodePrimaryKey($client_id));
|
||||
}
|
||||
|
||||
public function vendor_id(string $vendor_id = '') :Builder
|
||||
{
|
||||
if (strlen($vendor_id) == 0) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
return $this->builder->where('vendor_id', $this->decodePrimaryKey($vendor_id));
|
||||
}
|
||||
|
||||
public function filter_deleted_clients($value)
|
||||
{
|
||||
if ($value == 'true') {
|
||||
|
@ -85,14 +85,19 @@ class QuoteFilters extends QueryFilters
|
||||
}
|
||||
|
||||
if (in_array('expired', $status_parameters)) {
|
||||
$this->builder->orWhere('status_id', Quote::STATUS_SENT)
|
||||
$this->builder->orWhere(function ($query){
|
||||
$query->where('status_id', Quote::STATUS_SENT)
|
||||
->whereNotNull('due_date')
|
||||
->where('due_date', '<=', now()->toDateString());
|
||||
});
|
||||
}
|
||||
|
||||
if (in_array('upcoming', $status_parameters)) {
|
||||
$this->builder->orWhere('status_id', Quote::STATUS_SENT)
|
||||
$this->builder->orWhere(function ($query){
|
||||
$query->where('status_id', Quote::STATUS_SENT)
|
||||
->where('due_date', '>=', now()->toDateString())
|
||||
->orderBy('due_date', 'DESC');
|
||||
});
|
||||
}
|
||||
|
||||
return $this->builder;
|
||||
|
129
app/Filters/SubscriptionFilters.php
Normal file
129
app/Filters/SubscriptionFilters.php
Normal file
@ -0,0 +1,129 @@
|
||||
<?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\Filters;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Webhook;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
/**
|
||||
* SubscriptionFilters.
|
||||
*/
|
||||
class SubscriptionFilters extends QueryFilters
|
||||
{
|
||||
/**
|
||||
* Filter based on search text.
|
||||
*
|
||||
* @param string query filter
|
||||
* @return Builder
|
||||
* @deprecated
|
||||
*/
|
||||
public function filter(string $filter = '') : Builder
|
||||
{
|
||||
if (strlen($filter) == 0) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
return $this->builder->where(function ($query) use ($filter) {
|
||||
$query->where('name', 'like', '%'.$filter.'%');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the list based on the status
|
||||
* archived, active, deleted.
|
||||
*
|
||||
* @param string filter
|
||||
* @return Builder
|
||||
*/
|
||||
public function status(string $filter = '') : Builder
|
||||
{
|
||||
if (strlen($filter) == 0) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
$table = 'subscriptions';
|
||||
$filters = explode(',', $filter);
|
||||
|
||||
return $this->builder->where(function ($query) use ($filters, $table) {
|
||||
$query->whereNull($table.'.id');
|
||||
|
||||
if (in_array(parent::STATUS_ACTIVE, $filters)) {
|
||||
$query->orWhereNull($table.'.deleted_at');
|
||||
}
|
||||
|
||||
if (in_array(parent::STATUS_ARCHIVED, $filters)) {
|
||||
$query->orWhere(function ($query) use ($table) {
|
||||
$query->whereNotNull($table.'.deleted_at');
|
||||
|
||||
if (! in_array($table, ['users'])) {
|
||||
$query->where($table.'.is_deleted', '=', 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (in_array(parent::STATUS_DELETED, $filters)) {
|
||||
$query->orWhere($table.'.is_deleted', '=', 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the list based on $sort.
|
||||
*
|
||||
* @param string sort formatted as column|asc
|
||||
* @return Builder
|
||||
*/
|
||||
public function sort(string $sort) : Builder
|
||||
{
|
||||
$sort_col = explode('|', $sort);
|
||||
|
||||
return $this->builder->orderBy($sort_col[0], $sort_col[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base query.
|
||||
*
|
||||
* @param int company_id
|
||||
* @param User $user
|
||||
* @return Builder
|
||||
* @deprecated
|
||||
*/
|
||||
public function baseQuery(int $company_id, User $user) : Builder
|
||||
{
|
||||
$query = DB::table('subscriptions')
|
||||
->join('companies', 'companies.id', '=', 'subscriptions.company_id')
|
||||
->where('subscriptions.company_id', '=', $company_id);
|
||||
|
||||
/*
|
||||
* If the user does not have permissions to view all invoices
|
||||
* limit the user to only the invoices they have created
|
||||
*/
|
||||
if (Gate::denies('view-list', Webhook::class)) {
|
||||
$query->where('subscriptions.user_id', '=', $user->id);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the query by the users company ID.
|
||||
*
|
||||
* @return Illuminate\Database\Query\Builder
|
||||
*/
|
||||
public function entityFilter()
|
||||
{
|
||||
return $this->builder->company();
|
||||
}
|
||||
}
|
@ -112,6 +112,7 @@ class ActivityController extends BaseController
|
||||
'purchase_order' => $activity->purchase_order ? $activity->purchase_order : '',
|
||||
'subscription' => $activity->subscription ? $activity->subscription : '',
|
||||
'vendor_contact' => $activity->vendor_contact ? $activity->vendor_contact : '',
|
||||
'recurring_expense' => $activity->recurring_expense ? $activity->recurring_expense : '',
|
||||
];
|
||||
|
||||
return array_merge($arr, $activity->toArray());
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ namespace App\Http\Controllers;
|
||||
use App\Events\Subscription\SubscriptionWasCreated;
|
||||
use App\Events\Subscription\SubscriptionWasUpdated;
|
||||
use App\Factory\SubscriptionFactory;
|
||||
use App\Filters\SubscriptionFilters;
|
||||
use App\Http\Requests\Subscription\CreateSubscriptionRequest;
|
||||
use App\Http\Requests\Subscription\DestroySubscriptionRequest;
|
||||
use App\Http\Requests\Subscription\EditSubscriptionRequest;
|
||||
@ -80,9 +81,9 @@ class SubscriptionController extends BaseController
|
||||
* ),
|
||||
* )
|
||||
*/
|
||||
public function index(): \Illuminate\Http\Response
|
||||
public function index(SubscriptionFilters $filters): \Illuminate\Http\Response
|
||||
{
|
||||
$subscriptions = Subscription::query()->company();
|
||||
$subscriptions = Subscription::filter($filters);
|
||||
|
||||
return $this->listResponse($subscriptions);
|
||||
}
|
||||
|
71
app/Http/Requests/Report/ProductSalesReportRequest.php
Normal file
71
app/Http/Requests/Report/ProductSalesReportRequest.php
Normal 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);
|
||||
}
|
||||
}
|
@ -61,9 +61,9 @@ class InvoiceTransformer extends BaseTransformer
|
||||
'discount' => $this->getFreshbookQuantityFloat($record, 'Discount Percentage'),
|
||||
'is_amount_discount' => false,
|
||||
'tax_name1' => $this->getString($record, 'Tax 1 Type'),
|
||||
'tax_rate1' => $this->getFreshbookQuantityFloat($record, 'Tax 1 Amount'),
|
||||
'tax_rate1' => $this->calcTaxRate($record, 'Tax 1 Amount'),
|
||||
'tax_name2' => $this->getString($record, 'Tax 2 Type'),
|
||||
'tax_rate2' => $this->getFreshbookQuantityFloat($record, 'Tax 2 Amount'),
|
||||
'tax_rate2' => $this->calcTaxRate($record, 'Tax 2 Amount'),
|
||||
];
|
||||
$transformed['amount'] += $this->getFreshbookQuantityFloat($record, 'Line Total');
|
||||
}
|
||||
@ -79,6 +79,27 @@ class InvoiceTransformer extends BaseTransformer
|
||||
return $transformed;
|
||||
}
|
||||
|
||||
//Line Subtotal
|
||||
public function calcTaxRate($record, $field)
|
||||
{
|
||||
if(isset($record['Line Subtotal']) && $record['Line Subtotal'] > 0)
|
||||
return ($record[$field] / $record['Line Subtotal']) * 100;
|
||||
|
||||
$tax_amount1 = isset($record['Tax 1 Amount']) ? $record['Tax 1 Amount'] : 0;
|
||||
|
||||
$tax_amount2 = isset($record['Tax 2 Amount']) ? $record['Tax 2 Amount'] : 0;
|
||||
|
||||
$line_total = isset($record['Line Total']) ? $record['Line Total'] : 0;
|
||||
|
||||
$subtotal = $line_total - $tax_amount2 - $tax_amount1;
|
||||
|
||||
if($subtotal > 0)
|
||||
return $record[$field] / $subtotal * 100;
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
|
||||
/** @return float */
|
||||
public function getFreshbookQuantityFloat($data, $field)
|
||||
{
|
||||
|
@ -1112,6 +1112,9 @@ class CompanyImport implements ShouldQueue
|
||||
|
||||
foreach((object)$this->getObject("documents") as $document)
|
||||
{
|
||||
//todo enable this for v5.5.51
|
||||
if(!$this->transformDocumentId($document->documentable_id, $document->documentable_type))
|
||||
continue;
|
||||
|
||||
$new_document = new Document();
|
||||
$new_document->user_id = $this->transformId('users', $document->user_id);
|
||||
@ -1152,6 +1155,10 @@ class CompanyImport implements ShouldQueue
|
||||
{
|
||||
try{
|
||||
Storage::disk(config('filesystems.default'))->put($new_document->url, $file);
|
||||
|
||||
$new_document->disk = config('filesystems.default');
|
||||
$new_document->save();
|
||||
|
||||
}
|
||||
catch(\Exception $e)
|
||||
{
|
||||
@ -1279,6 +1286,9 @@ class CompanyImport implements ShouldQueue
|
||||
case Payment::class:
|
||||
return $this->transformId('payments', $id);
|
||||
break;
|
||||
case Project::class:
|
||||
return $this->transformId('projects', $id);
|
||||
break;
|
||||
case Product::class:
|
||||
return $this->transformId('products', $id);
|
||||
break;
|
||||
@ -1294,7 +1304,7 @@ class CompanyImport implements ShouldQueue
|
||||
|
||||
|
||||
default:
|
||||
# code...
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -106,6 +106,24 @@ class CSVIngest implements ShouldQueue
|
||||
$new_contact->is_primary = true;
|
||||
$new_contact->save();
|
||||
}
|
||||
|
||||
Client::with('contacts')->where('company_id', $this->company->id)->cursor()->each(function ($client){
|
||||
|
||||
$contact = $client->contacts()->first();
|
||||
$contact->is_primary = true;
|
||||
$contact->save();
|
||||
|
||||
});
|
||||
|
||||
Vendor::with('contacts')->where('company_id', $this->company->id)->cursor()->each(function ($vendor){
|
||||
|
||||
$contact = $vendor->contacts()->first();
|
||||
$contact->is_primary = true;
|
||||
$contact->save();
|
||||
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
private function bootEngine()
|
||||
|
111
app/Jobs/Invoice/InvoiceCheckLateWebhook.php
Normal file
111
app/Jobs/Invoice/InvoiceCheckLateWebhook.php
Normal file
@ -0,0 +1,111 @@
|
||||
<?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\Jobs\Invoice;
|
||||
|
||||
use App\Jobs\Util\WebhookHandler;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Webhook;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class InvoiceCheckLateWebhook implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct() {}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
nlog("sending overdue webhooks for invoices");
|
||||
|
||||
if (! config('ninja.db.multi_db_enabled')){
|
||||
|
||||
$company_ids = Webhook::where('event_id', Webhook::EVENT_LATE_INVOICE)
|
||||
->where('is_deleted', 0)
|
||||
->pluck('company_id');
|
||||
|
||||
Invoice::query()
|
||||
->where('is_deleted', false)
|
||||
->whereNull('deleted_at')
|
||||
->whereNotNull('due_date')
|
||||
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
|
||||
->where('balance', '>', 0)
|
||||
->whereIn('company_id', $company_ids)
|
||||
->whereHas('client', function ($query) {
|
||||
$query->where('is_deleted', 0)
|
||||
->where('deleted_at', null);
|
||||
})
|
||||
->whereHas('company', function ($query){
|
||||
$query->where('is_disabled', 0);
|
||||
})
|
||||
->whereBetween('due_date', [now()->subDay()->startOfDay(), now()->startOfDay()->subSecond()])
|
||||
->cursor()
|
||||
->each(function ($invoice){
|
||||
|
||||
WebhookHandler::dispatch(Webhook::EVENT_LATE_INVOICE, $invoice, $invoice->company, 'client')->delay(now()->addSeconds(2));
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
foreach (MultiDB::$dbs as $db)
|
||||
{
|
||||
|
||||
MultiDB::setDB($db);
|
||||
|
||||
$company_ids = Webhook::where('event_id', Webhook::EVENT_LATE_INVOICE)
|
||||
->where('is_deleted', 0)
|
||||
->pluck('company_id');
|
||||
|
||||
Invoice::query()
|
||||
->where('is_deleted', false)
|
||||
->whereNull('deleted_at')
|
||||
->whereNotNull('due_date')
|
||||
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
|
||||
->where('balance', '>', 0)
|
||||
->whereIn('company_id', $company_ids)
|
||||
->whereHas('client', function ($query) {
|
||||
$query->where('is_deleted', 0)
|
||||
->where('deleted_at', null);
|
||||
})
|
||||
->whereHas('company', function ($query){
|
||||
$query->where('is_disabled', 0);
|
||||
})
|
||||
->whereBetween('due_date', [now()->subDay()->startOfDay(), now()->startOfDay()->subSecond()])
|
||||
->cursor()
|
||||
->each(function ($invoice){
|
||||
|
||||
WebhookHandler::dispatch(Webhook::EVENT_LATE_INVOICE, $invoice, $invoice->company, 'client')->delay(now()->addSeconds(2));
|
||||
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -349,6 +349,26 @@ class Import implements ShouldQueue
|
||||
}
|
||||
|
||||
$account = $this->company->account;
|
||||
|
||||
/* If the user has upgraded their account, do not wipe their payment plan*/
|
||||
if($account->isPaid() || (isset($data['plan']) && $data['plan'] == 'white_label'))
|
||||
{
|
||||
if(isset($data['plan']))
|
||||
unset($data['plan']);
|
||||
|
||||
if(isset($data['plan_term']))
|
||||
unset($data['plan_term']);
|
||||
|
||||
if(isset($data['plan_paid']))
|
||||
unset($data['plan_paid']);
|
||||
|
||||
if(isset($data['plan_started']))
|
||||
unset($data['plan_started']);
|
||||
|
||||
if(isset($data['plan_expires']))
|
||||
unset($data['plan_expires']);
|
||||
}
|
||||
|
||||
$account->fill($data);
|
||||
$account->save();
|
||||
|
||||
|
@ -214,21 +214,33 @@ class Activity extends StaticModel
|
||||
'backup',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getHashedIdAttribute()
|
||||
{
|
||||
return $this->encodePrimaryKey($this->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getEntityType()
|
||||
{
|
||||
return self::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function backup()
|
||||
{
|
||||
return $this->hasOne(Backup::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function history()
|
||||
{
|
||||
return $this->hasOne(Backup::class);
|
||||
@ -266,6 +278,9 @@ class Activity extends StaticModel
|
||||
return $this->belongsTo(Invoice::class)->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function vendor()
|
||||
{
|
||||
return $this->belongsTo(Vendor::class)->withTrashed();
|
||||
@ -279,6 +294,9 @@ class Activity extends StaticModel
|
||||
return $this->belongsTo(RecurringInvoice::class)->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function credit()
|
||||
{
|
||||
return $this->belongsTo(Credit::class)->withTrashed();
|
||||
@ -308,31 +326,57 @@ class Activity extends StaticModel
|
||||
return $this->belongsTo(Payment::class)->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function expense()
|
||||
{
|
||||
return $this->belongsTo(Expense::class)->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function recurring_expense()
|
||||
{
|
||||
return $this->belongsTo(RecurringExpense::class)->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function purchase_order()
|
||||
{
|
||||
return $this->belongsTo(PurchaseOrder::class)->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function vendor_contact()
|
||||
{
|
||||
return $this->belongsTo(VendorContact::class)->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function task()
|
||||
{
|
||||
return $this->belongsTo(Task::class)->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function company()
|
||||
{
|
||||
return $this->belongsTo(Company::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function resolveRouteBinding($value, $field = null)
|
||||
{
|
||||
if (is_numeric($value)) {
|
||||
|
@ -102,21 +102,23 @@ class Gateway extends StaticModel
|
||||
break;
|
||||
case 20:
|
||||
return [
|
||||
GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true],
|
||||
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'charge.failed', 'payment_intent.payment_failed']],
|
||||
GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => ['payment_intent.succeeded']],
|
||||
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']],
|
||||
GatewayType::ALIPAY => ['refund' => false, 'token_billing' => false],
|
||||
GatewayType::APPLE_PAY => ['refund' => false, 'token_billing' => false],
|
||||
GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], //Stripe
|
||||
GatewayType::SEPA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']],
|
||||
GatewayType::PRZELEWY24 => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']],
|
||||
GatewayType::GIROPAY => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']],
|
||||
GatewayType::EPS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']],
|
||||
GatewayType::BANCONTACT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']],
|
||||
GatewayType::BECS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']],
|
||||
GatewayType::IDEAL => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']],
|
||||
GatewayType::ACSS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']],
|
||||
GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']],
|
||||
GatewayType::KLARNA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']],
|
||||
GatewayType::FPX => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], ];
|
||||
GatewayType::SEPA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']],
|
||||
GatewayType::PRZELEWY24 => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']],
|
||||
GatewayType::GIROPAY => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']],
|
||||
GatewayType::EPS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']],
|
||||
GatewayType::BANCONTACT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']],
|
||||
GatewayType::BECS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']],
|
||||
GatewayType::IDEAL => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']],
|
||||
GatewayType::ACSS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']],
|
||||
GatewayType::FPX => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']],
|
||||
];
|
||||
break;
|
||||
case 39:
|
||||
return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']]]; //Checkout
|
||||
break;
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Filterable;
|
||||
use App\Models\RecurringInvoice;
|
||||
use App\Services\Subscription\SubscriptionService;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@ -19,7 +20,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Subscription extends BaseModel
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
use HasFactory, SoftDeletes, Filterable;
|
||||
|
||||
protected $hidden = [
|
||||
'id',
|
||||
|
@ -627,7 +627,9 @@ class StripePaymentDriver extends BaseDriver
|
||||
|
||||
public function processWebhookRequest(PaymentWebhookRequest $request)
|
||||
{
|
||||
|
||||
// if($request->type === 'payment_intent.requires_action')
|
||||
// nlog($request->all());
|
||||
|
||||
//payment_intent.succeeded - this will confirm or cancel the payment
|
||||
if ($request->type === 'payment_intent.succeeded') {
|
||||
PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(5, 10)));
|
||||
|
57
app/Services/Schedule/ScheduleService.php
Normal file
57
app/Services/Schedule/ScheduleService.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?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\Services\Schedule;
|
||||
|
||||
class ScheduleService
|
||||
{
|
||||
|
||||
public function __construct(public Scheduler $scheduler) {}
|
||||
|
||||
public function scheduleStatement()
|
||||
{
|
||||
|
||||
//Is it for one client
|
||||
//Is it for all clients
|
||||
//Is it for all clients excluding these clients
|
||||
|
||||
//Frequency
|
||||
|
||||
//show aging
|
||||
//show payments
|
||||
//paid/unpaid
|
||||
|
||||
//When to send? 1st of month
|
||||
//End of month
|
||||
//This date
|
||||
|
||||
}
|
||||
|
||||
public function scheduleReport()
|
||||
{
|
||||
//Report type
|
||||
//same schema as ScheduleStatement
|
||||
}
|
||||
|
||||
public function scheduleEntitySend()
|
||||
{
|
||||
//Entity
|
||||
//Entity Id
|
||||
//When
|
||||
}
|
||||
|
||||
public function projectStatus()
|
||||
{
|
||||
//Project ID
|
||||
//Tasks - task statuses
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
@ -40,10 +39,6 @@ trait AppSetup
|
||||
foreach ($cached_tables as $name => $class) {
|
||||
if (! Cache::has($name) || $force) {
|
||||
|
||||
// check that the table exists in case the migration is pending
|
||||
if (! Schema::hasTable((new $class())->getTable())) {
|
||||
continue;
|
||||
}
|
||||
if ($name == 'payment_terms') {
|
||||
$orderBy = 'num_days';
|
||||
} elseif ($name == 'fonts') {
|
||||
|
@ -14,8 +14,8 @@ return [
|
||||
'require_https' => env('REQUIRE_HTTPS', true),
|
||||
'app_url' => rtrim(env('APP_URL', ''), '/'),
|
||||
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
|
||||
'app_version' => '5.5.50',
|
||||
'app_tag' => '5.5.50',
|
||||
'app_version' => '5.5.51',
|
||||
'app_tag' => '5.5.51',
|
||||
'minimum_client_version' => '5.0.16',
|
||||
'terms_version' => '1.0.1',
|
||||
'api_secret' => env('API_SECRET', ''),
|
||||
|
@ -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);
|
||||
|
||||
|
212
tests/Feature/Export/ProductSalesReportTest.php
Normal file
212
tests/Feature/Export/ProductSalesReportTest.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -37,12 +37,12 @@ class PdfCreatorTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testCreditPdfCreated()
|
||||
{
|
||||
$credit_path = (new CreateEntityPdf($this->credit->invitations->first()))->handle();
|
||||
// public function testCreditPdfCreated()
|
||||
// {
|
||||
// $credit_path = (new CreateEntityPdf($this->credit->invitations->first()))->handle();
|
||||
|
||||
$this->assertTrue(Storage::exists($credit_path));
|
||||
}
|
||||
// $this->assertTrue(Storage::exists($credit_path));
|
||||
// }
|
||||
|
||||
public function testInvoicePdfCreated()
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user