Merge pull request #4660 from turbo124/v5-stable

5.0.45
This commit is contained in:
David Bomba 2021-01-10 09:16:37 +11:00 committed by GitHub
commit 472a9f755f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 62604 additions and 61796 deletions

View File

@ -1 +1 @@
5.0.44 5.0.45

View File

@ -227,7 +227,7 @@ class CreateSingleAccount extends Command
$settings = $client->settings; $settings = $client->settings;
$settings->currency_id = "1"; $settings->currency_id = "1";
$settings->use_credits_payment = "always"; // $settings->use_credits_payment = "always";
$client->settings = $settings; $client->settings = $settings;

View File

@ -258,8 +258,10 @@ class CompanySettings extends BaseSettings
public $client_portal_allow_over_payment = false; //@implemented public $client_portal_allow_over_payment = false; //@implemented
public $use_credits_payment = 'off'; //always, option, off //@implemented public $use_credits_payment = 'off'; //always, option, off //@implemented
public $hide_empty_columns_on_pdf = false;
public static $casts = [ public static $casts = [
'hide_empty_columns_on_pdf' => 'bool',
'enable_reminder_endless' => 'bool', 'enable_reminder_endless' => 'bool',
'use_credits_payment' => 'string', 'use_credits_payment' => 'string',
'recurring_invoice_number_pattern' => 'string', 'recurring_invoice_number_pattern' => 'string',

View File

@ -22,7 +22,8 @@ class ExpenseCategoryFactory
$expense->company_id = $company_id; $expense->company_id = $company_id;
$expense->name = ''; $expense->name = '';
$expense->is_deleted = false; $expense->is_deleted = false;
$expense->color = '#fff';
return $expense; return $expense;
} }
} }

View File

@ -43,7 +43,7 @@ class RecurringInvoiceToInvoiceFactory
$invoice->custom_value3 = $recurring_invoice->custom_value3; $invoice->custom_value3 = $recurring_invoice->custom_value3;
$invoice->custom_value4 = $recurring_invoice->custom_value4; $invoice->custom_value4 = $recurring_invoice->custom_value4;
$invoice->amount = $recurring_invoice->amount; $invoice->amount = $recurring_invoice->amount;
$invoice->balance = $recurring_invoice->balance; // $invoice->balance = $recurring_invoice->balance;
$invoice->user_id = $recurring_invoice->user_id; $invoice->user_id = $recurring_invoice->user_id;
$invoice->assigned_user_id = $recurring_invoice->assigned_user_id; $invoice->assigned_user_id = $recurring_invoice->assigned_user_id;
$invoice->company_id = $recurring_invoice->company_id; $invoice->company_id = $recurring_invoice->company_id;

View File

@ -21,6 +21,7 @@ class TaskStatusFactory
$task_status->user_id = $user_id; $task_status->user_id = $user_id;
$task_status->company_id = $company_id; $task_status->company_id = $company_id;
$task_status->name = ''; $task_status->name = '';
$task_status->color = '#fff';
return $task_status; return $task_status;
} }

View File

@ -29,3 +29,11 @@ function nlog($output, $context = []): void
\Illuminate\Support\Facades\Log::channel('invoiceninja')->info($output, $context); \Illuminate\Support\Facades\Log::channel('invoiceninja')->info($output, $context);
} }
} }
if (!function_exists('ray')) {
function ray($payload)
{
return true;
}
}

View File

@ -151,7 +151,7 @@ class InvoiceItemSum
$key = str_replace(' ', '', $tax_name.$tax_rate); $key = str_replace(' ', '', $tax_name.$tax_rate);
$group_tax = ['key' => $key, 'total' => $tax_total, 'tax_name' => $tax_name.' '.$tax_rate.'%']; $group_tax = ['key' => $key, 'total' => $tax_total, 'tax_name' => $tax_name.' '.floatval($tax_rate).'%'];
$this->tax_collection->push(collect($group_tax)); $this->tax_collection->push(collect($group_tax));
} }

View File

@ -111,19 +111,19 @@ class InvoiceSum
if ($this->invoice->tax_rate1 > 0) { if ($this->invoice->tax_rate1 > 0) {
$tax = $this->taxer($this->total, $this->invoice->tax_rate1); $tax = $this->taxer($this->total, $this->invoice->tax_rate1);
$this->total_taxes += $tax; $this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.$this->invoice->tax_rate1.'%', 'total' => $tax]; $this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.floatval($this->invoice->tax_rate1).'%', 'total' => $tax];
} }
if ($this->invoice->tax_rate2 > 0) { if ($this->invoice->tax_rate2 > 0) {
$tax = $this->taxer($this->total, $this->invoice->tax_rate2); $tax = $this->taxer($this->total, $this->invoice->tax_rate2);
$this->total_taxes += $tax; $this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.$this->invoice->tax_rate2.'%', 'total' => $tax]; $this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.floatval($this->invoice->tax_rate2).'%', 'total' => $tax];
} }
if ($this->invoice->tax_rate3 > 0) { if ($this->invoice->tax_rate3 > 0) {
$tax = $this->taxer($this->total, $this->invoice->tax_rate3); $tax = $this->taxer($this->total, $this->invoice->tax_rate3);
$this->total_taxes += $tax; $this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.$this->invoice->tax_rate3.'%', 'total' => $tax]; $this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.floatval($this->invoice->tax_rate3).'%', 'total' => $tax];
} }
return $this; return $this;

View File

@ -122,19 +122,19 @@ class InvoiceSumInclusive
$tax = $this->calcInclusiveLineTax($this->invoice->tax_rate1, $amount); $tax = $this->calcInclusiveLineTax($this->invoice->tax_rate1, $amount);
$this->total_taxes += $tax; $this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.$this->invoice->tax_rate1.'%', 'total' => $tax]; $this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.floatval($this->invoice->tax_rate1).'%', 'total' => $tax];
} }
if ($this->invoice->tax_rate2 > 0) { if ($this->invoice->tax_rate2 > 0) {
$tax = $this->calcInclusiveLineTax($this->invoice->tax_rate2, $amount); $tax = $this->calcInclusiveLineTax($this->invoice->tax_rate2, $amount);
$this->total_taxes += $tax; $this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.$this->invoice->tax_rate2.'%', 'total' => $tax]; $this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.floatval($this->invoice->tax_rate2).'%', 'total' => $tax];
} }
if ($this->invoice->tax_rate3 > 0) { if ($this->invoice->tax_rate3 > 0) {
$tax = $this->calcInclusiveLineTax($this->invoice->tax_rate3, $amount); $tax = $this->calcInclusiveLineTax($this->invoice->tax_rate3, $amount);
$this->total_taxes += $tax; $this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.$this->invoice->tax_rate3.'%', 'total' => $tax]; $this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.floatval($this->invoice->tax_rate3).'%', 'total' => $tax];
} }
return $this; return $this;

View File

@ -76,12 +76,15 @@ class InvitationController extends Controller
{ {
switch ($entity_string) { switch ($entity_string) {
case 'invoice': case 'invoice':
$invitation->invoice->service()->markSent()->save();
event(new InvoiceWasViewed($invitation, $invitation->company, Ninja::eventVars())); event(new InvoiceWasViewed($invitation, $invitation->company, Ninja::eventVars()));
break; break;
case 'quote': case 'quote':
$invitation->quote->service()->markSent()->save();
event(new QuoteWasViewed($invitation, $invitation->company, Ninja::eventVars())); event(new QuoteWasViewed($invitation, $invitation->company, Ninja::eventVars()));
break; break;
case 'credit': case 'credit':
$invitation->credit->service()->markSent()->save();
event(new CreditWasViewed($invitation, $invitation->company, Ninja::eventVars())); event(new CreditWasViewed($invitation, $invitation->company, Ninja::eventVars()));
break; break;
default: default:

View File

@ -77,7 +77,9 @@ class InvoiceController extends Controller
return $this->downloadInvoicePDF((array) $transformed_ids); return $this->downloadInvoicePDF((array) $transformed_ids);
} }
return redirect()->back(); return redirect()
->back()
->with('message', ctrans('texts.no_action_provided'));
} }
private function makePayment(array $ids) private function makePayment(array $ids)
@ -86,16 +88,30 @@ class InvoiceController extends Controller
->whereClientId(auth()->user()->client->id) ->whereClientId(auth()->user()->client->id)
->get(); ->get();
$total = $invoices->sum('balance'); //filter invoices which are payable
$invoices = $invoices->filter(function ($invoice) { $invoices = $invoices->filter(function ($invoice) {
return $invoice->isPayable() && $invoice->balance > 0; return $invoice->isPayable() && $invoice->balance > 0;
}); });
//return early if no invoices.
if ($invoices->count() == 0) { if ($invoices->count() == 0) {
return back()->with(['warning' => 'No payable invoices selected']); return back()
->with('message', ctrans('texts.no_payable_invoices_selected'));
} }
//iterate and sum the payable amounts either partial or balance
$total = 0;
foreach($invoices as $invoice)
{
if($invoice->partial > 0)
$total += $invoice->partial;
else
$total += $invoice->balance;
}
//format data
$invoices->map(function ($invoice) { $invoices->map(function ($invoice) {
$invoice->service()->removeUnpaidGatewayFees()->save(); $invoice->service()->removeUnpaidGatewayFees()->save();
$invoice->balance = Number::formatValue($invoice->balance, $invoice->client->currency()); $invoice->balance = Number::formatValue($invoice->balance, $invoice->client->currency());
@ -104,6 +120,7 @@ class InvoiceController extends Controller
return $invoice; return $invoice;
}); });
//format totals
$formatted_total = Number::formatMoney($total, auth()->user()->client); $formatted_total = Number::formatMoney($total, auth()->user()->client);
$payment_methods = auth()->user()->client->getPaymentMethods($total); $payment_methods = auth()->user()->client->getPaymentMethods($total);
@ -117,6 +134,8 @@ class InvoiceController extends Controller
'total' => $total, 'total' => $total,
]; ];
// nlog($data);
return $this->render('invoices.payment', $data); return $this->render('invoices.payment', $data);
} }

View File

@ -12,14 +12,17 @@
namespace App\Http\Controllers\ClientPortal; namespace App\Http\Controllers\ClientPortal;
use App\Exceptions\PaymentFailed;
use App\Factory\PaymentFactory; use App\Factory\PaymentFactory;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Jobs\Invoice\InjectSignature; use App\Jobs\Invoice\InjectSignature;
use App\Jobs\Util\SystemLogger;
use App\Models\CompanyGateway; use App\Models\CompanyGateway;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentHash; use App\Models\PaymentHash;
use App\Models\SystemLog;
use App\Utils\Number; use App\Utils\Number;
use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
@ -83,8 +86,6 @@ class PaymentController extends Controller
$gateway = CompanyGateway::find($request->input('company_gateway_id')); $gateway = CompanyGateway::find($request->input('company_gateway_id'));
//refactor from here!
/** /**
* find invoices * find invoices
* *
@ -95,11 +96,13 @@ class PaymentController extends Controller
$invoices = Invoice::whereIn('id', $this->transformKeys($payable_invoices->pluck('invoice_id')->toArray()))->get(); $invoices = Invoice::whereIn('id', $this->transformKeys($payable_invoices->pluck('invoice_id')->toArray()))->get();
/* pop non payable invoice from the $payable_invoices array */ /* pop non payable invoice from the $payable_invoices array */
$payable_invoices = $payable_invoices->filter(function ($payable_invoice) use ($invoices) { $payable_invoices = $payable_invoices->filter(function ($payable_invoice) use ($invoices) {
return $invoices->where('hashed_id', $payable_invoice['invoice_id'])->first()->isPayable(); return $invoices->where('hashed_id', $payable_invoice['invoice_id'])->first()->isPayable();
}); });
/*return early if no invoices*/ /*return early if no invoices*/
if ($payable_invoices->count() == 0) { if ($payable_invoices->count() == 0) {
return redirect() return redirect()
->route('client.invoices.index') ->route('client.invoices.index')
@ -108,23 +111,34 @@ class PaymentController extends Controller
$settings = auth()->user()->client->getMergedSettings(); $settings = auth()->user()->client->getMergedSettings();
/*iterate through invoices and add gateway fees and other payment metadata*/ // nlog($settings);
$payable_invoices = $payable_invoices->map(function ($payable_invoice) use ($invoices, $settings) {
$payable_invoice['amount'] = Number::parseFloat($payable_invoice['amount']); /* This loop checks for under / over payments and returns the user if a check fails */
foreach($payable_invoices as $payable_invoice)
{
/*Match the payable invoice to the Model Invoice*/
$invoice = $invoices->first(function ($inv) use ($payable_invoice) { $invoice = $invoices->first(function ($inv) use ($payable_invoice) {
return $payable_invoice['invoice_id'] == $inv->hashed_id; return $payable_invoice['invoice_id'] == $inv->hashed_id;
}); });
// Check if company supports over & under payments. /*
// In case it doesn't this is where process should stop. * Check if company supports over & under payments.
* Determine the payable amount and the max payable. ie either partial or invoice balance
*/
$payable_amount = Number::roundValue(Number::parseFloat($payable_invoice['amount']), auth()->user()->client->currency()->precision); $payable_amount = Number::roundValue(Number::parseFloat($payable_invoice['amount']), auth()->user()->client->currency()->precision);
$invoice_balance = Number::roundValue($invoice->balance, auth()->user()->client->currency()->precision); $invoice_balance = Number::roundValue(($invoice->partial > 0 ? $invoice->partial : $invoice->balance), auth()->user()->client->currency()->precision);
/*If we don't allow under/over payments force the payable amount - prevents inspect element adjustments in JS*/
if ($settings->client_portal_allow_under_payment == false && $settings->client_portal_allow_over_payment == false) { if ($settings->client_portal_allow_under_payment == false && $settings->client_portal_allow_over_payment == false) {
$payable_invoice['amount'] = Number::roundValue(($invoice->partial > 0 ? $invoice->partial : $invoice->balance), auth()->user()->client->currency()->precision); $payable_invoice['amount'] = Number::roundValue(($invoice->partial > 0 ? $invoice->partial : $invoice->balance), auth()->user()->client->currency()->precision);
} // We don't allow either of these, reset the amount to default invoice (to prevent inspect element payments). }
/* If we DO allow under payments check the minimum amount is present else return */
if ($settings->client_portal_allow_under_payment) { if ($settings->client_portal_allow_under_payment) {
if ($payable_invoice['amount'] < $settings->client_portal_under_payment_minimum) { if ($payable_invoice['amount'] < $settings->client_portal_under_payment_minimum) {
@ -133,23 +147,44 @@ class PaymentController extends Controller
->with('message', ctrans('texts.minimum_required_payment', ['amount' => $settings->client_portal_under_payment_minimum])); ->with('message', ctrans('texts.minimum_required_payment', ['amount' => $settings->client_portal_under_payment_minimum]));
} }
} else { } else {
$payable_amount = Number::roundValue(Number::parseFloat($payable_invoice['amount']), auth()->user()->client->currency()->precision);
$invoice_balance = Number::roundValue($invoice->balance, auth()->user()->client->currency()->precision);
/*Double check!!*/
if ($payable_amount < $invoice_balance) { if ($payable_amount < $invoice_balance) {
return redirect() return redirect()
->route('client.invoices.index') ->route('client.invoices.index')
->with('message', ctrans('texts.under_payments_disabled')); ->with('message', ctrans('texts.under_payments_disabled'));
} }
} // Make sure 'amount' from form is not lower than 'amount' from invoice. }
if ($settings->client_portal_allow_over_payment == false) { /* If we don't allow over payments and the amount exceeds the balance */
if (!$settings->client_portal_allow_over_payment) {
if ($payable_amount > $invoice_balance) { if ($payable_amount > $invoice_balance) {
return redirect() return redirect()
->route('client.invoices.index') ->route('client.invoices.index')
->with('message', ctrans('texts.over_payments_disabled')); ->with('message', ctrans('texts.over_payments_disabled'));
} }
} // Make sure 'amount' from form is not higher than 'amount' from invoice. }
}
/*Iterate through invoices and add gateway fees and other payment metadata*/
//$payable_invoices = $payable_invoices->map(function ($payable_invoice) use ($invoices, $settings) {
$payable_invoice_collection = collect();
foreach($payable_invoices as $payable_invoice)
{
nlog($payable_invoice);
$payable_invoice['amount'] = Number::parseFloat($payable_invoice['amount']);
$invoice = $invoices->first(function ($inv) use ($payable_invoice) {
return $payable_invoice['invoice_id'] == $inv->hashed_id;
});
$payable_amount = Number::roundValue(Number::parseFloat($payable_invoice['amount']), auth()->user()->client->currency()->precision);
$invoice_balance = Number::roundValue($invoice->balance, auth()->user()->client->currency()->precision);
$payable_invoice['due_date'] = $this->formatDate($invoice->due_date, $invoice->client->date_format()); $payable_invoice['due_date'] = $this->formatDate($invoice->due_date, $invoice->client->date_format());
$payable_invoice['invoice_number'] = $invoice->number; $payable_invoice['invoice_number'] = $invoice->number;
@ -164,8 +199,9 @@ class PaymentController extends Controller
$payable_invoice['additional_info'] = $additional_info; $payable_invoice['additional_info'] = $additional_info;
return $payable_invoice; $payable_invoice_collection->push($payable_invoice);
}); }
//});
if (request()->has('signature') && !is_null(request()->signature) && !empty(request()->signature)) { if (request()->has('signature') && !is_null(request()->signature) && !empty(request()->signature)) {
$invoices->each(function ($invoice) use ($request) { $invoices->each(function ($invoice) use ($request) {
@ -173,6 +209,8 @@ class PaymentController extends Controller
}); });
} }
$payable_invoices = $payable_invoice_collection;
$payment_method_id = $request->input('payment_method_id'); $payment_method_id = $request->input('payment_method_id');
$invoice_totals = $payable_invoices->sum('amount'); $invoice_totals = $payable_invoices->sum('amount');
$first_invoice = $invoices->first(); $first_invoice = $invoices->first();
@ -196,7 +234,7 @@ class PaymentController extends Controller
$payment_hash = new PaymentHash; $payment_hash = new PaymentHash;
$payment_hash->hash = Str::random(128); $payment_hash->hash = Str::random(128);
$payment_hash->data = ['invoices' => $payable_invoices->toArray()]; $payment_hash->data = ['invoices' => $payable_invoices->toArray() , 'credits' => $credit_totals];
$payment_hash->fee_total = $fee_totals; $payment_hash->fee_total = $fee_totals;
$payment_hash->fee_invoice_id = $first_invoice->id; $payment_hash->fee_invoice_id = $first_invoice->id;
$payment_hash->save(); $payment_hash->save();
@ -221,12 +259,24 @@ class PaymentController extends Controller
return $this->processCreditPayment($request, $data); return $this->processCreditPayment($request, $data);
} }
return $gateway try {
->driver(auth()->user()->client) return $gateway
->setPaymentMethod($payment_method_id) ->driver(auth()->user()->client)
->setPaymentHash($payment_hash) ->setPaymentMethod($payment_method_id)
->checkRequirements() ->setPaymentHash($payment_hash)
->processPaymentView($data); ->checkRequirements()
->processPaymentView($data);
} catch(\Exception $e) {
SystemLogger::dispatch(
$e->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_ERROR,
SystemLog::TYPE_FAILURE,
auth('contact')->user()->client
);
throw new PaymentFailed($e->getMessage());
}
} }
public function response(PaymentResponseRequest $request) public function response(PaymentResponseRequest $request)
@ -235,12 +285,24 @@ class PaymentController extends Controller
$payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$request->payment_hash])->first(); $payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$request->payment_hash])->first();
return $gateway try {
->driver(auth()->user()->client) return $gateway
->setPaymentMethod($request->input('payment_method_id')) ->driver(auth()->user()->client)
->setPaymentHash($payment_hash) ->setPaymentMethod($request->input('payment_method_id'))
->checkRequirements() ->setPaymentHash($payment_hash)
->processPaymentResponse($request); ->checkRequirements()
->processPaymentResponse($request);
} catch(\Exception $e) {
SystemLogger::dispatch(
$e->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_FAILURE,
auth('contact')->user()->client
);
throw new PaymentFailed($e->getMessage());
}
} }
/** /**
@ -265,35 +327,7 @@ class PaymentController extends Controller
$payment_hash->save(); $payment_hash->save();
} }
/* Iterate through the invoices and apply credits to them */ $payment = $payment->service()->applyCredits($payment_hash)->save();
collect($payment_hash->invoices())->each(function ($payable_invoice) use ($payment, $payment_hash) {
$invoice = Invoice::find($this->decodePrimaryKey($payable_invoice->invoice_id));
$amount = $payable_invoice->amount;
$credits = $payment_hash->fee_invoice
->client
->service()
->getCredits();
foreach ($credits as $credit) {
//starting invoice balance
$invoice_balance = $invoice->balance;
//credit payment applied
$credit->service()->applyPayment($invoice, $amount, $payment);
//amount paid from invoice calculated
$remaining_balance = ($invoice_balance - $invoice->fresh()->balance);
//reduce the amount to be paid on the invoice from the NEXT credit
$amount -= $remaining_balance;
//break if the invoice is no longer PAYABLE OR there is no more amount to be applied
if (!$invoice->isPayable() || (int)$amount == 0) {
break;
}
}
});
return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]); return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]);
} }

View File

@ -508,10 +508,7 @@ class InvoiceController extends BaseController
*/ */
public function bulk() public function bulk()
{ {
/*
* WIP!
*/
$action = request()->input('action'); $action = request()->input('action');
$ids = request()->input('ids'); $ids = request()->input('ids');

View File

@ -174,8 +174,11 @@ class TaskStatusController extends BaseController
*/ */
public function store(StoreTaskStatusRequest $request) public function store(StoreTaskStatusRequest $request)
{ {
nlog($request->all());
$task_status = TaskStatusFactory::create(auth()->user()->company()->id, auth()->user()->id); $task_status = TaskStatusFactory::create(auth()->user()->company()->id, auth()->user()->id);
$task_status->fill($request->all()); $task_status->fill($request->all());
$task_status->save(); $task_status->save();
return $this->itemResponse($task_status->fresh()); return $this->itemResponse($task_status->fresh());

View File

@ -90,6 +90,11 @@ class UpdateClientRequest extends Request
$input['group_settings_id'] = $this->decodePrimaryKey($input['group_settings_id']); $input['group_settings_id'] = $this->decodePrimaryKey($input['group_settings_id']);
} }
/* If the user removes the currency we must always set the default */
if (array_key_exists('settings', $input) && ! array_key_exists('currency_id', $input['settings'])) {
$input['settings']['currency_id'] = (string) auth()->user()->company()->settings->currency_id;
}
$input = $this->decodePrimaryKeys($input); $input = $this->decodePrimaryKeys($input);
if (array_key_exists('settings', $input)) { if (array_key_exists('settings', $input)) {

View File

@ -57,7 +57,7 @@ class UpdateCompanyRequest extends Request
protected function prepareForValidation() protected function prepareForValidation()
{ {
$input = $this->all(); $input = $this->all();
// nlog($input);
if (array_key_exists('settings', $input)) { if (array_key_exists('settings', $input)) {
$input['settings'] = $this->filterSaveableSettings($input['settings']); $input['settings'] = $this->filterSaveableSettings($input['settings']);
} }

View File

@ -62,6 +62,9 @@ class StoreExpenseRequest extends Request
$input['currency_id'] = (string)auth()->user()->company()->settings->currency_id; $input['currency_id'] = (string)auth()->user()->company()->settings->currency_id;
} }
if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff';
$this->replace($input); $this->replace($input);
} }

View File

@ -66,6 +66,10 @@ class UpdateExpenseRequest extends Request
$input['category_id'] = $this->decodePrimaryKey($input['category_id']); $input['category_id'] = $this->decodePrimaryKey($input['category_id']);
} }
if (array_key_exists('documents', $input)) {
unset($input['documents']);
}
if (! array_key_exists('currency_id', $input) || strlen($input['currency_id']) == 0) { if (! array_key_exists('currency_id', $input) || strlen($input['currency_id']) == 0) {
$input['currency_id'] = (string)auth()->user()->company()->settings->currency_id; $input['currency_id'] = (string)auth()->user()->company()->settings->currency_id;
} }

View File

@ -42,6 +42,9 @@ class StoreExpenseCategoryRequest extends Request
$input = $this->decodePrimaryKeys($input); $input = $this->decodePrimaryKeys($input);
if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff';
$this->replace($input); $this->replace($input);
} }
} }

View File

@ -13,6 +13,7 @@ namespace App\Http\Requests\ExpenseCategory;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Utils\Traits\ChecksEntityStatus; use App\Utils\Traits\ChecksEntityStatus;
use Illuminate\Validation\Rule;
class UpdateExpenseCategoryRequest extends Request class UpdateExpenseCategoryRequest extends Request
{ {
@ -33,9 +34,21 @@ class UpdateExpenseCategoryRequest extends Request
$rules = []; $rules = [];
if ($this->input('name')) { if ($this->input('name')) {
$rules['name'] = 'unique:expense_categories,name,'.$this->id.',id,company_id,'.$this->expense_category->company_id; // $rules['name'] = 'unique:expense_categories,name,'.$this->id.',id,company_id,'.$this->expense_category->company_id;
$rules['name'] = Rule::unique('expense_categories')->where('company_id', auth()->user()->company()->id)->ignore($this->expense_category->id);
} }
return $rules; return $rules;
} }
protected function prepareForValidation()
{
$input = $this->all();
if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff';
$this->replace($input);
}
} }

View File

@ -67,6 +67,10 @@ class UpdateInvoiceRequest extends Request
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
if (array_key_exists('documents', $input)) {
unset($input['documents']);
}
$this->replace($input); $this->replace($input);
} }

View File

@ -49,6 +49,10 @@ class StoreProjectRequest extends Request
{ {
$input = $this->decodePrimaryKeys($this->all()); $input = $this->decodePrimaryKeys($this->all());
if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff';
$this->replace($input); $this->replace($input);
} }

View File

@ -48,6 +48,9 @@ class UpdateProjectRequest extends Request
unset($input['client_id']); unset($input['client_id']);
} }
if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff';
$this->replace($input); $this->replace($input);
} }
} }

View File

@ -73,6 +73,10 @@ class UpdateQuoteRequest extends Request
$input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']);
} }
if (array_key_exists('documents', $input)) {
unset($input['documents']);
}
$input['id'] = $this->quote->id; $input['id'] = $this->quote->id;
$this->replace($input); $this->replace($input);

View File

@ -91,6 +91,10 @@ class UpdateRecurringInvoiceRequest extends Request
$input['auto_bill_enabled'] = $this->setAutoBillFlag($input['auto_bill']); $input['auto_bill_enabled'] = $this->setAutoBillFlag($input['auto_bill']);
} }
if (array_key_exists('documents', $input)) {
unset($input['documents']);
}
$this->replace($input); $this->replace($input);
} }

View File

@ -50,6 +50,9 @@ class UpdateTaskRequest extends Request
$input['status_id'] = $this->decodePrimaryKey($input['status_id']); $input['status_id'] = $this->decodePrimaryKey($input['status_id']);
} }
if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff';
$this->replace($input); $this->replace($input);
} }
} }

View File

@ -32,6 +32,9 @@ class StoreTaskStatusRequest extends Request
{ {
$input = $this->all(); $input = $this->all();
if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff';
$this->replace($input); $this->replace($input);
} }

View File

@ -13,6 +13,7 @@ namespace App\Http\Requests\TaskStatus;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
class UpdateTaskStatusRequest extends Request class UpdateTaskStatusRequest extends Request
{ {
@ -33,9 +34,21 @@ class UpdateTaskStatusRequest extends Request
$rules = []; $rules = [];
if ($this->input('name')) { if ($this->input('name')) {
$rules['name'] = 'unique:task_statuses,name,'.$this->id.',id,company_id,'.$this->task_status->company_id; //$rules['name'] = 'unique:task_statuses,name,'.$this->id.',id,company_id,'.$this->task_status->company_id;
$rules['name'] = Rule::unique('task_statuses')->where('company_id', auth()->user()->company()->id)->ignore($this->task_status->id);
} }
return $rules; return $rules;
} }
protected function prepareForValidation()
{
$input = $this->all();
if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff';
$this->replace($input);
}
} }

View File

@ -168,8 +168,12 @@ class CreateEntityPdf implements ShouldQueue
return $file_path; return $file_path;
} }
public function failed(\Exception $exception) public function failed($e)
{ {
nlog("help!");
} }
// public function failed(\Exception $exception)
// {
// nlog("help!");
// }
} }

View File

@ -58,7 +58,7 @@ class SendRecurring implements ShouldQueue
$invoice = RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client); $invoice = RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client);
$invoice->date = now()->format('Y-m-d'); $invoice->date = now()->format('Y-m-d');
$invoice = $invoice->service() $invoice = $invoice->service()
->markSent() ->markSent()
->applyNumber() ->applyNumber()

View File

@ -11,6 +11,7 @@
namespace App\Jobs\Util; namespace App\Jobs\Util;
use Illuminate\Http\UploadedFile;
use App\DataMapper\Analytics\MigrationFailure; use App\DataMapper\Analytics\MigrationFailure;
use App\DataMapper\CompanySettings; use App\DataMapper\CompanySettings;
use App\Exceptions\MigrationValidatorFailed; use App\Exceptions\MigrationValidatorFailed;
@ -124,7 +125,7 @@ class Import implements ShouldQueue
'task_statuses', 'task_statuses',
'expenses', 'expenses',
'tasks', 'tasks',
// 'documents', 'documents',
]; ];
/** /**
@ -964,56 +965,41 @@ class Import implements ShouldQueue
$entity = Expense::where('id', $expense_id)->withTrashed()->first(); $entity = Expense::where('id', $expense_id)->withTrashed()->first();
} }
$this->saveDocument(file_get_contents($resource['url']), $entity, $is_public = true); $file_url = $resource['url'];
$file_name = basename($file_url);
$file_path = sys_get_temp_dir().'/'.$file_name;
file_put_contents($file_path, file_get_contents($file_url), LOCK_EX);
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$file_info = $finfo->file($file_path);
nlog($resource['url']);
nlog($file_url);
nlog($file_name);
nlog($file_path);
nlog($file_info);
nlog(filesize($file_path));
$uploaded_file = new UploadedFile(
$file_path,
$file_name,
$file_info,
filesize($file_path),
0,
false
);
$this->saveDocument($uploaded_file, $entity, $is_public = true);
$uploaded_file = null;
$finfo = null;
$file_url = null;
$file_name = null;
$file_path = null;
$file_info = null;
} }
// foreach ($data as $resource) {
// $modified = $resource;
// if (array_key_exists('invoice_id', $resource) && $resource['invoice_id'] && ! array_key_exists('invoices', $this->ids)) {
// throw new ResourceDependencyMissing('Processing documents failed, because of missing dependency - invoices.');
// }
// if (array_key_exists('expense_id', $resource) && $resource['expense_id'] && ! array_key_exists('expenses', $this->ids)) {
// throw new ResourceDependencyMissing('Processing documents failed, because of missing dependency - expenses.');
// }
// /* Remove because of polymorphic joins. */
// unset($modified['invoice_id']);
// unset($modified['expense_id']);
// if (array_key_exists('invoice_id', $resource) && $resource['invoice_id'] && array_key_exists('invoices', $this->ids)) {
// $modified['documentable_id'] = $this->transformId('invoices', $resource['invoice_id']);
// $modified['documentable_type'] = Invoice::class;
// }
// if (array_key_exists('expense_id', $resource) && $resource['expense_id'] && array_key_exists('expenses', $this->ids)) {
// $modified['documentable_id'] = $this->transformId('expenses', $resource['expense_id']);
// $modified['documentable_type'] = Expense::class;
// }
// $modified['user_id'] = $this->processUserId($resource);
// $modified['company_id'] = $this->company->id;
// $document = Document::create($modified);
// // $entity = $modified['documentable_type']::find($modified['documentable_id']);
// // $entity->documents()->save($modified);
// $old_user_key = array_key_exists('user_id', $resource) ?? $this->user->id;
// $this->ids['documents'] = [
// "documents_{$old_user_key}" => [
// 'old' => $resource['id'],
// 'new' => $document->id,
// ],
// ];
// }
// Document::reguard();
// /*Improve memory handling by setting everything to null when we have finished*/
// $data = null;
} }
private function processPaymentTerms(array $data) :void private function processPaymentTerms(array $data) :void
@ -1157,12 +1143,11 @@ class Import implements ShouldQueue
$old_user_key = array_key_exists('user_id', $resource) ?? $this->user->id; $old_user_key = array_key_exists('user_id', $resource) ?? $this->user->id;
$key = "expense_categories_{$resource['id']}"; $key = "expense_categories_{$resource['id']}";
$this->ids['expense_categories'][$key] = [ $this->ids['expense_categories'][$key] = [
'old' => $resource['id'], 'old' => $resource['id'],
'new' => $expense_category->id, 'new' => $expense_category->id,
]; ];
// $this->ids['expense_categories'] = [ // $this->ids['expense_categories'] = [
@ -1290,12 +1275,13 @@ class Import implements ShouldQueue
$old_user_key = array_key_exists('user_id', $resource) ?? $this->user->id; $old_user_key = array_key_exists('user_id', $resource) ?? $this->user->id;
$this->ids['expenses'] = [ $key = "expenses_{$resource['id']}";
"expenses_{$old_user_key}" => [
'old' => $resource['id'], $this->ids['expenses'][$key] = [
'new' => $expense->id, 'old' => $resource['id'],
], 'new' => $expense->id,
]; ];
} }
Expense::reguard(); Expense::reguard();

View File

@ -88,7 +88,18 @@ class CreditEmailEngine extends BaseEmailEngine
->setViewText(ctrans('texts.view_credit')); ->setViewText(ctrans('texts.view_credit'));
if ($this->client->getSetting('pdf_email_attachment') !== false) { if ($this->client->getSetting('pdf_email_attachment') !== false) {
$this->setAttachments([$this->credit->pdf_file_path()]); $this->setAttachments(['path' => $this->credit->pdf_file_path(), 'name' => basename($this->credit->pdf_file_path())]);
}
//attach third party documents
if($this->client->getSetting('document_email_attachment') !== false){
// Storage::url
foreach($this->credit->documents as $document){
// $this->setAttachments(['path'=>$document->filePath(),'name'=>$document->name]);
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => $document->type]]);
}
} }
return $this; return $this;

View File

@ -98,6 +98,19 @@ class InvoiceEmailEngine extends BaseEmailEngine
if ($this->client->getSetting('pdf_email_attachment') !== false) { if ($this->client->getSetting('pdf_email_attachment') !== false) {
$this->setAttachments([$this->invoice->pdf_file_path()]); $this->setAttachments([$this->invoice->pdf_file_path()]);
// $this->setAttachments(['path' => $this->invoice->pdf_file_path(), 'name' => basename($this->invoice->pdf_file_path())]);
}
//attach third party documents
if($this->client->getSetting('document_email_attachment') !== false){
// Storage::url
foreach($this->invoice->documents as $document){
// $this->setAttachments(['path'=>$document->filePath(),'name'=>$document->name]);
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => $document->type]]);
}
} }
return $this; return $this;

View File

@ -88,9 +88,22 @@ class QuoteEmailEngine extends BaseEmailEngine
->setViewText(ctrans('texts.view_quote')); ->setViewText(ctrans('texts.view_quote'));
if ($this->client->getSetting('pdf_email_attachment') !== false) { if ($this->client->getSetting('pdf_email_attachment') !== false) {
$this->setAttachments([$this->invitation->pdf_file_path()]); // $this->setAttachments([$this->quote->pdf_file_path()]);
$this->setAttachments(['path' => $this->quote->pdf_file_path(), 'name' => basename($this->quote->pdf_file_path())]);
} }
//attach third party documents
if($this->client->getSetting('document_email_attachment') !== false){
// Storage::url
foreach($this->quote->documents as $document){
// $this->setAttachments(['path'=>$document->filePath(),'name'=>$document->name]);
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => $document->type]]);
}
}
return $this; return $this;
} }
} }

View File

@ -84,8 +84,15 @@ class TemplateEmail extends Mailable
//conditionally attach files //conditionally attach files
if ($settings->pdf_email_attachment !== false && ! empty($this->build_email->getAttachments())) { if ($settings->pdf_email_attachment !== false && ! empty($this->build_email->getAttachments())) {
//hosted | plan check here
foreach ($this->build_email->getAttachments() as $file) { foreach ($this->build_email->getAttachments() as $file) {
$this->attach($file);
if(is_string($file))
$this->attach($file);
elseif(is_array($file))
$this->attach($file['path'], ['as' => $file['name'], 'mime' => $file['mime']]);
} }
} }

View File

@ -509,14 +509,13 @@ class Client extends BaseModel implements HasLocalePreference
$payment_methods_intersect->push([$gateway->id => $type]); $payment_methods_intersect->push([$gateway->id => $type]);
} }
} else { } else {
$payment_methods_intersect->push([$gateway->id => $type]); $payment_methods_intersect->push([$gateway->id => NULL]);
} }
} }
} }
//handle custom gateways as they are not unique'd()--------------------------------------------------------- //handle custom gateways as they are not unique'd()---------------------------------------------------------
$payment_urls = []; $payment_urls = [];
foreach ($payment_methods_intersect as $key => $child_array) { foreach ($payment_methods_intersect as $key => $child_array) {
@ -525,11 +524,22 @@ class Client extends BaseModel implements HasLocalePreference
$fee_label = $gateway->calcGatewayFeeLabel($amount, $this); $fee_label = $gateway->calcGatewayFeeLabel($amount, $this);
$payment_urls[] = [ if(!$gateway_type_id){
'label' => $gateway->getTypeAlias($gateway_type_id) . $fee_label,
'company_gateway_id' => $gateway_id, $payment_urls[] = [
'gateway_type_id' => $gateway_type_id, 'label' => $gateway->getConfigField('name') . $fee_label,
]; 'company_gateway_id' => $gateway_id,
'gateway_type_id' => GatewayType::CREDIT_CARD,
];
}
else
{
$payment_urls[] = [
'label' => $gateway->getTypeAlias($gateway_type_id) . $fee_label,
'company_gateway_id' => $gateway_id,
'gateway_type_id' => $gateway_type_id,
];
}
} }
} }

View File

@ -45,7 +45,6 @@ class Company extends BaseModel
protected $presenter = CompanyPresenter::class; protected $presenter = CompanyPresenter::class;
protected $fillable = [ protected $fillable = [
'hide_empty_columns_on_pdf',
'calculate_expense_tax_by_amount', 'calculate_expense_tax_by_amount',
'invoice_expense_documents', 'invoice_expense_documents',
'invoice_task_documents', 'invoice_task_documents',
@ -83,6 +82,8 @@ class Company extends BaseModel
'is_disabled', 'is_disabled',
'default_task_is_date_based', 'default_task_is_date_based',
'enable_product_discount', 'enable_product_discount',
'expense_inclusive_taxes',
'expense_amount_is_pretax',
]; ];
protected $hidden = [ protected $hidden = [
@ -378,6 +379,11 @@ class Company extends BaseModel
return $this->hasMany(SystemLog::class)->orderBy('id', 'DESC')->take(50); return $this->hasMany(SystemLog::class)->orderBy('id', 'DESC')->take(50);
} }
public function system_log_relation()
{
return $this->hasMany(SystemLog::class)->orderBy('id', 'DESC');
}
public function tokens_hashed() public function tokens_hashed()
{ {
return $this->hasMany(CompanyToken::class); return $this->hasMany(CompanyToken::class);

View File

@ -11,6 +11,7 @@
namespace App\Models; namespace App\Models;
use App\Models\GatewayType;
use App\PaymentDrivers\BasePaymentDriver; use App\PaymentDrivers\BasePaymentDriver;
use App\Utils\Number; use App\Utils\Number;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@ -58,10 +59,12 @@ class CompanyGateway extends BaseModel
16 => ['card' => 'images/credit_cards/Test-Discover-Icon.png', 'text' => 'Discover'], 16 => ['card' => 'images/credit_cards/Test-Discover-Icon.png', 'text' => 'Discover'],
]; ];
// public function getFeesAndLimitsAttribute() public $gateway_consts = [
// { '38f2c48af60c7dd69e04248cbb24c36e' => 300,
// return json_decode($this->attributes['fees_and_limits']); 'd14dd26a37cecc30fdd65700bfb55b23' => 301,
// } '3758e7f7c6f4cecf0f4f348b9a00f456' => 304,
'3b6621f970ab18887c4f6dca78d3f8bb' => 305,
];
protected $touches = []; protected $touches = [];
@ -70,6 +73,15 @@ class CompanyGateway extends BaseModel
return self::class; return self::class;
} }
public function system_logs()
{
return $this->company
->system_log_relation
->where('type_id', $this->gateway_consts[$this->gateway->key])
->take(50);
}
public function company() public function company()
{ {
return $this->belongsTo(Company::class); return $this->belongsTo(Company::class);
@ -254,10 +266,6 @@ class CompanyGateway extends BaseModel
{ {
$label = ''; $label = '';
if (! $this->feesEnabled()) {
return $label;
}
$fee = $this->calcGatewayFee($amount, $gateway_type_id); $fee = $this->calcGatewayFee($amount, $gateway_type_id);
if ($fee > 0) { if ($fee > 0) {
@ -268,7 +276,7 @@ class CompanyGateway extends BaseModel
return $label; return $label;
} }
public function calcGatewayFee($amount, $include_taxes = false, $gateway_type_id = GatewayType::CREDIT_CARD) public function calcGatewayFee($amount, $gateway_type_id, $include_taxes = false)
{ {
$fees_and_limits = $this->getFeesAndLimits($gateway_type_id); $fees_and_limits = $this->getFeesAndLimits($gateway_type_id);

View File

@ -51,6 +51,11 @@ class Expense extends BaseModel
'custom_value3', 'custom_value3',
'custom_value4', 'custom_value4',
'number', 'number',
'tax_amount1',
'tax_amount2',
'tax_amount3',
'uses_inclusive_taxes',
'amount_is_pretax',
]; ];
protected $casts = [ protected $casts = [

View File

@ -27,6 +27,11 @@ class PaymentHash extends Model
return $this->data->invoices; return $this->data->invoices;
} }
public function credits_total()
{
return isset($this->data->credits) ? $this->data->credits : 0;
}
public function payment() public function payment()
{ {
return $this->belongsTo(Payment::class)->withTrashed(); return $this->belongsTo(Payment::class)->withTrashed();

View File

@ -29,5 +29,10 @@ class TaskStatus extends BaseModel
*/ */
protected $dates = ['deleted_at']; protected $dates = ['deleted_at'];
protected $fillable = ['name','color','status_order']; protected $fillable = [
'name',
'color',
'status_order',
];
} }

View File

@ -213,6 +213,9 @@ class BaseDriver extends AbstractPaymentDriver
$this->attachInvoices($payment, $this->payment_hash); $this->attachInvoices($payment, $this->payment_hash);
if($this->payment_hash->credits_total() > 0)
$payment = $payment->service()->applyCredits($this->payment_hash)->save();
$payment->service()->updateInvoicePayment($this->payment_hash); $payment->service()->updateInvoicePayment($this->payment_hash);
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars())); event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars()));

View File

@ -158,10 +158,13 @@ class PayPalExpressPaymentDriver extends BaseDriver
} }
if (!$response->isSuccessful()) { if (!$response->isSuccessful()) {
PaymentFailureMailer::dispatch($this->client, $response->getMessage(), $this->client->company, $response['PAYMENTINFO_0_AMT']);
$data = $response->getData();
PaymentFailureMailer::dispatch($this->client, $response->getMessage(), $this->client->company, $this->payment_hash->data->amount);
$message = [ $message = [
'server_response' => $response->getMessage(), 'server_response' => $data['L_LONGMESSAGE0'],
'data' => $this->payment_hash->data, 'data' => $this->payment_hash->data,
]; ];

View File

@ -97,7 +97,7 @@ class InvoiceMigrationRepository extends BaseRepository
} }
foreach ($data['invitations'] as $invitation) { foreach ($data['invitations'] as $invitation) {
nlog($invitation); // nlog($invitation);
$new_invitation = $invitation_factory_class::create($model->company_id, $model->user_id); $new_invitation = $invitation_factory_class::create($model->company_id, $model->user_id);
$new_invitation->{$lcfirst_resource_id} = $model->id; $new_invitation->{$lcfirst_resource_id} = $model->id;

View File

@ -41,7 +41,7 @@ class AddGatewayFee extends AbstractService
public function run() public function run()
{ {
$gateway_fee = round($this->company_gateway->calcGatewayFee($this->amount, $this->invoice->uses_inclusive_taxes, $this->gateway_type_id), $this->invoice->client->currency()->precision); $gateway_fee = round($this->company_gateway->calcGatewayFee($this->amount, $this->gateway_type_id, $this->invoice->uses_inclusive_taxes), $this->invoice->client->currency()->precision);
if ((int)$gateway_fee == 0) { if ((int)$gateway_fee == 0) {
return $this->invoice; return $this->invoice;

View File

@ -14,7 +14,6 @@ namespace App\Services\Invoice;
use App\DataMapper\InvoiceItem; use App\DataMapper\InvoiceItem;
use App\Events\Payment\PaymentWasCreated; use App\Events\Payment\PaymentWasCreated;
use App\Factory\PaymentFactory; use App\Factory\PaymentFactory;
use App\Models\Client;
use App\Models\Credit; use App\Models\Credit;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
@ -82,7 +81,7 @@ class AutoBillInvoice extends AbstractService
} }
/* $gateway fee */ /* $gateway fee */
$fee = $gateway_token->gateway->calcGatewayFee($amount, $this->invoice->uses_inclusive_taxes); $fee = $gateway_token->gateway->calcGatewayFee($amount, $gateway_token->gateway_type_id, $this->invoice->uses_inclusive_taxes);
//todo determine exact fee as per PaymentController //todo determine exact fee as per PaymentController

View File

@ -15,9 +15,12 @@ use App\Factory\PaymentFactory;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentHash; use App\Models\PaymentHash;
use App\Utils\Traits\MakesHash;
class PaymentService class PaymentService
{ {
use MakesHash;
private $payment; private $payment;
public function __construct($payment) public function __construct($payment)
@ -97,6 +100,43 @@ class PaymentService
return $this; return $this;
} }
public function applyCredits($payment_hash)
{
/* Iterate through the invoices and apply credits to them */
collect($payment_hash->invoices())->each(function ($payable_invoice) use ($payment_hash) {
$invoice = Invoice::find($this->decodePrimaryKey($payable_invoice->invoice_id));
$amount = $payable_invoice->amount;
$credits = $payment_hash->fee_invoice
->client
->service()
->getCredits();
foreach ($credits as $credit) {
//starting invoice balance
$invoice_balance = $invoice->balance;
//credit payment applied
$credit->service()->applyPayment($invoice, $amount, $this->payment);
//amount paid from invoice calculated
$remaining_balance = ($invoice_balance - $invoice->fresh()->balance);
//reduce the amount to be paid on the invoice from the NEXT credit
$amount -= $remaining_balance;
//break if the invoice is no longer PAYABLE OR there is no more amount to be applied
if (!$invoice->isPayable() || (int)$amount == 0) {
break;
}
}
});
return $this;
}
public function save() public function save()
{ {
$this->payment->save(); $this->payment->save();

View File

@ -39,6 +39,7 @@ class UpdateInvoicePayment
$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($paid_invoices, 'invoice_id')))->get(); $invoices = Invoice::whereIn('id', $this->transformKeys(array_column($paid_invoices, 'invoice_id')))->get();
collect($paid_invoices)->each(function ($paid_invoice) use ($invoices) { collect($paid_invoices)->each(function ($paid_invoice) use ($invoices) {
$invoice = $invoices->first(function ($inv) use ($paid_invoice) { $invoice = $invoices->first(function ($inv) use ($paid_invoice) {
return $paid_invoice->invoice_id == $inv->hashed_id; return $paid_invoice->invoice_id == $inv->hashed_id;
}); });
@ -49,6 +50,11 @@ class UpdateInvoicePayment
$paid_amount = $paid_invoice->amount; $paid_amount = $paid_invoice->amount;
} }
/* Need to determine here is we have an OVER payment - if YES only apply the max invoice amount */
if($paid_amount > $invoice->partial && $paid_amount > $invoice->balance)
$paid_amount = $invoice->balance;
/* Updates the company ledger */
$this->payment $this->payment
->ledger() ->ledger()
->updatePaymentBalance($paid_amount * -1); ->updatePaymentBalance($paid_amount * -1);

View File

@ -88,7 +88,7 @@ trait PdfMakerUtilities
return $processed; return $processed;
} }
public function updateElementProperty($element, string $attribute, string $value) public function updateElementProperty($element, string $attribute, ?string $value)
{ {
// We have exception for "hidden" property. // We have exception for "hidden" property.
// hidden="true" or hidden="false" will both hide the element, // hidden="true" or hidden="false" will both hide the element,

View File

@ -12,6 +12,8 @@
namespace App\Transformers; namespace App\Transformers;
use App\Models\CompanyGateway; use App\Models\CompanyGateway;
use App\Models\SystemLog;
use App\Transformers\SystemLogTransformer;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use stdClass; use stdClass;
@ -33,6 +35,7 @@ class CompanyGatewayTransformer extends EntityTransformer
* @var array * @var array
*/ */
protected $availableIncludes = [ protected $availableIncludes = [
'system_logs',
'gateway', 'gateway',
]; ];
@ -81,4 +84,11 @@ class CompanyGatewayTransformer extends EntityTransformer
return $this->includeItem($company_gateway->gateway, $transformer, Gateway::class); return $this->includeItem($company_gateway->gateway, $transformer, Gateway::class);
} }
public function includeSystemLogs(CompanyGateway $company_gateway)
{
$transformer = new SystemLogTransformer($this->serializer);
return $this->includeCollection($company_gateway->system_logs(), $transformer, SystemLog::class);
}
} }

View File

@ -149,7 +149,9 @@ class CompanyTransformer extends EntityTransformer
'default_task_is_date_based' => (bool)$company->default_task_is_date_based, 'default_task_is_date_based' => (bool)$company->default_task_is_date_based,
'enable_product_discount' => (bool)$company->enable_product_discount, 'enable_product_discount' => (bool)$company->enable_product_discount,
'calculate_expense_tax_by_amount' =>(bool)$company->calculate_expense_tax_by_amount, 'calculate_expense_tax_by_amount' =>(bool)$company->calculate_expense_tax_by_amount,
'hide_empty_columns_on_pdf' => (bool) $company->hide_empty_columns_on_pdf, 'hide_empty_columns_on_pdf' => false, //@deprecate
'expense_inclusive_taxes' => (bool)$company->expense_inclusive_taxes,
'expense_amount_is_pretax' =>( bool)$company->expense_amount_is_pretax,
]; ];
} }

View File

@ -91,6 +91,11 @@ class ExpenseTransformer extends EntityTransformer
'archived_at' => (int) $expense->deleted_at, 'archived_at' => (int) $expense->deleted_at,
'created_at' => (int) $expense->created_at, 'created_at' => (int) $expense->created_at,
'project_id' => $this->encodePrimaryKey($expense->project_id), 'project_id' => $this->encodePrimaryKey($expense->project_id),
'tax_amount1' => (float) $expense->tax_amount1,
'tax_amount2' => (float) $expense->tax_amount2,
'tax_amount3' => (float) $expense->tax_amount3,
'uses_inclusive_taxes' => (bool) $expense->uses_inclusive_taxes,
'amount_is_pretax' => (bool) $expense->amount_is_pretax,
]; ];
} }
} }

View File

@ -113,6 +113,11 @@ class HtmlEngine
$data['$invoice.due_date'] = &$data['$due_date']; $data['$invoice.due_date'] = &$data['$due_date'];
$data['$invoice.number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.invoice_number')]; $data['$invoice.number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.invoice_number')];
$data['$invoice.po_number'] = ['value' => $this->entity->po_number ?: '&nbsp;', 'label' => ctrans('texts.po_number')]; $data['$invoice.po_number'] = ['value' => $this->entity->po_number ?: '&nbsp;', 'label' => ctrans('texts.po_number')];
$data['$entity.datetime'] = ['value' => $this->formatDatetime($this->entity->created_at, $this->entity->client->date_format()), 'label' => ctrans('texts.date')];
$data['$invoice.datetime'] = &$data['$entity.datetime'];
$data['$quote.datetime'] = &$data['$entity.datetime'];
$data['$credit.datetime'] = &$data['$entity.datetime'];
// $data['$line_taxes'] = ['value' => $this->makeLineTaxes() ?: '&nbsp;', 'label' => ctrans('texts.taxes')]; // $data['$line_taxes'] = ['value' => $this->makeLineTaxes() ?: '&nbsp;', 'label' => ctrans('texts.taxes')];
// $data['$invoice.line_taxes'] = &$data['$line_taxes']; // $data['$invoice.line_taxes'] = &$data['$line_taxes'];

View File

@ -71,6 +71,17 @@ trait MakesDates
return $date->format($format); return $date->format($format);
} }
/**
* Formats a datedate.
* @param Carbon|string $date Carbon object or date string
* @param string $format The date display format
* @return string The formatted date
*/
public function formatDatetime($date, string $format) :string
{
return Carbon::createFromTimestamp($date)->format($format . " g:i a");
}
/** /**
* Formats a date. * Formats a date.
* @param Carbon/String $date Carbon object or date string * @param Carbon/String $date Carbon object or date string

View File

@ -604,7 +604,7 @@ trait MakesInvoiceValues
if ($item->is_amount_discount) { if ($item->is_amount_discount) {
$data[$key][$table_type.'.discount'] = Number::formatMoney($item->discount, $this->client); $data[$key][$table_type.'.discount'] = Number::formatMoney($item->discount, $this->client);
} else { } else {
$data[$key][$table_type.'.discount'] = $item->discount.'%'; $data[$key][$table_type.'.discount'] = floatval($item->discount).'%';
} }
} else { } else {
$data[$key][$table_type.'.discount'] = ''; $data[$key][$table_type.'.discount'] = '';

View File

@ -68,6 +68,7 @@
"webpatser/laravel-countries": "dev-master#75992ad" "webpatser/laravel-countries": "dev-master#75992ad"
}, },
"require-dev": { "require-dev": {
"php": "^7.4",
"anahkiasen/former": "^4.2", "anahkiasen/former": "^4.2",
"barryvdh/laravel-debugbar": "^3.4", "barryvdh/laravel-debugbar": "^3.4",
"darkaonline/l5-swagger": "^8.0", "darkaonline/l5-swagger": "^8.0",
@ -78,6 +79,7 @@
"mockery/mockery": "^1.3.1", "mockery/mockery": "^1.3.1",
"nunomaduro/collision": "^5.0", "nunomaduro/collision": "^5.0",
"phpunit/phpunit": "^9.0", "phpunit/phpunit": "^9.0",
"spatie/laravel-ray": "^1.3",
"vimeo/psalm": "^4.0", "vimeo/psalm": "^4.0",
"wildbit/postmark-php": "^4.0" "wildbit/postmark-php": "^4.0"
}, },

521
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@ return [
'require_https' => env('REQUIRE_HTTPS', true), 'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', ''), 'app_domain' => env('APP_DOMAIN', ''),
'app_version' => '5.0.44', 'app_version' => '5.0.45',
'minimum_client_version' => '5.0.16', 'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1', 'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false), 'api_secret' => env('API_SECRET', false),

View File

@ -41,8 +41,8 @@ class InvoiceFactory extends Factory
'tax_rate2' => 17.5, 'tax_rate2' => 17.5,
//'tax_name3' => 'THIRDTAX', //'tax_name3' => 'THIRDTAX',
//'tax_rate3' => 5, //'tax_rate3' => 5,
'custom_value1' => $this->faker->date, // 'custom_value1' => $this->faker->date,
'custom_value2' => rand(0, 1) ? 'yes' : 'no', //'custom_value2' => rand(0, 1) ? 'yes' : 'no',
// 'custom_value3' => $this->faker->numberBetween(1,4), // 'custom_value3' => $this->faker->numberBetween(1,4),
// 'custom_value4' => $this->faker->numberBetween(1,4), // 'custom_value4' => $this->faker->numberBetween(1,4),
'is_deleted' => false, 'is_deleted' => false,

View File

@ -31,6 +31,7 @@ class TaskStatusFactory extends Factory
{ {
return [ return [
'name' => $this->faker->text(7), 'name' => $this->faker->text(7),
'color' => '#fff',
]; ];
} }
} }

View File

@ -144,6 +144,9 @@ class ImproveDecimalResolution extends Migration
$table->integer('status_order')->nullable(); $table->integer('status_order')->nullable();
}); });
Schema::table('companies', function (Blueprint $table) {
$table->dropColumn('hide_empty_columns_on_pdf');
});
} }
/** /**

View File

@ -0,0 +1,35 @@
<?php
use App\Models\Currency;
use App\Utils\Traits\AppSetup;
use Illuminate\Database\Migrations\Migration;
class UpdateSingaporeDollarSymbol extends Migration
{
use AppSetup;
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$currency = Currency::find(13);
if ($currency) {
$currency->update(['symbol' => '$']);
}
$this->buildCache(true);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ExpensesTableAdditionalFields extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('expenses', function (Blueprint $table) {
$table->decimal('tax_amount1', 20, 6)->default();
$table->decimal('tax_amount2', 20, 6)->default();
$table->decimal('tax_amount3', 20, 6)->default();
$table->boolean('uses_inclusive_taxes')->default(0);
$table->boolean('amount_is_pretax')->default(1);
});
Schema::table('companies', function (Blueprint $table) {
$table->boolean('expense_inclusive_taxes')->default(0);
$table->boolean('expense_amount_is_pretax')->default(1);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -31,7 +31,7 @@ const RESOURCES = {
"assets/fonts/MaterialIcons-Regular.otf": "1288c9e28052e028aba623321f7826ac", "assets/fonts/MaterialIcons-Regular.otf": "1288c9e28052e028aba623321f7826ac",
"/": "23224b5e03519aaa87594403d54412cf", "/": "23224b5e03519aaa87594403d54412cf",
"version.json": "1a0a7ed91cd721a8aad41df0c5f792b3", "version.json": "1a0a7ed91cd721a8aad41df0c5f792b3",
"main.dart.js": "dfd8a0dd3626cf7b7858e5cbd6e56a33", "main.dart.js": "3da75d88201ca57797d16afdf5b37442",
"favicon.png": "dca91c54388f52eded692718d5a98b8b" "favicon.png": "dca91c54388f52eded692718d5a98b8b"
}; };

123128
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

View File

@ -3362,4 +3362,8 @@ return [
'currency_albanian_lek' => 'Albanian Lek', 'currency_albanian_lek' => 'Albanian Lek',
'endless' => 'Endless', 'endless' => 'Endless',
'minimum_payment' => 'Minimum Payment',
'no_action_provided' => 'No action provided. If you believe this is wrong, please contact the support.',
'no_payable_invoices_selected' => 'No payable invoices selected. Make sure you are not trying to pay draft invoice or invoice with zero balance due.',
]; ];

View File

@ -99,8 +99,8 @@
function invokeServiceWorkerUpdateFlow() { function invokeServiceWorkerUpdateFlow() {
// you have a better UI here, reloading is not a great user experince here. // you have a better UI here, reloading is not a great user experince here.
const confirmed = confirm('New version of the app is available. Refresh now'); const confirmed = alert('New version of the app is available. Refresh now');
if (confirmed) { if (confirmed == true) {
window.location.reload(); window.location.reload();
} }
} }

View File

@ -12,6 +12,7 @@ namespace Tests\Feature;
use App\DataMapper\FeesAndLimits; use App\DataMapper\FeesAndLimits;
use App\Models\CompanyGateway; use App\Models\CompanyGateway;
use App\Models\GatewayType;
use App\Utils\Traits\CompanyGatewayFeesAndLimitsSaver; use App\Utils\Traits\CompanyGatewayFeesAndLimitsSaver;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -197,7 +198,7 @@ class CompanyGatewayApiTest extends TestCase
$company_gateway = CompanyGateway::find($id); $company_gateway = CompanyGateway::find($id);
$this->assertEquals(10, $company_gateway->calcGatewayFee(10)); $this->assertEquals(10, $company_gateway->calcGatewayFee(10, GatewayType::CREDIT_CARD));
} }
public function testFeesAndLimitsFeePercentCalcuation() public function testFeesAndLimitsFeePercentCalcuation()
@ -230,7 +231,7 @@ class CompanyGatewayApiTest extends TestCase
$company_gateway = CompanyGateway::find($id); $company_gateway = CompanyGateway::find($id);
$this->assertEquals(0.2, $company_gateway->calcGatewayFee(10)); $this->assertEquals(0.2, $company_gateway->calcGatewayFee(10, GatewayType::CREDIT_CARD));
} }
public function testFeesAndLimitsFeePercentAndAmountCalcuation() public function testFeesAndLimitsFeePercentAndAmountCalcuation()
@ -263,7 +264,7 @@ class CompanyGatewayApiTest extends TestCase
$company_gateway = CompanyGateway::find($id); $company_gateway = CompanyGateway::find($id);
$this->assertEquals(10.2, $company_gateway->calcGatewayFee(10)); $this->assertEquals(10.2, $company_gateway->calcGatewayFee(10, GatewayType::CREDIT_CARD));
} }
public function testFeesAndLimitsFeePercentAndAmountAndTaxCalcuation() public function testFeesAndLimitsFeePercentAndAmountAndTaxCalcuation()
@ -296,7 +297,7 @@ class CompanyGatewayApiTest extends TestCase
$company_gateway = CompanyGateway::find($id); $company_gateway = CompanyGateway::find($id);
$this->assertEquals(11, $company_gateway->calcGatewayFee(10, true)); $this->assertEquals(11, $company_gateway->calcGatewayFee(10, GatewayType::CREDIT_CARD, true));
} }
public function testFeesAndLimitsFeePercentAndAmountAndTaxCalcuationInclusiveTaxes() public function testFeesAndLimitsFeePercentAndAmountAndTaxCalcuationInclusiveTaxes()
@ -329,7 +330,7 @@ class CompanyGatewayApiTest extends TestCase
$company_gateway = CompanyGateway::find($id); $company_gateway = CompanyGateway::find($id);
$this->assertEquals(10, $company_gateway->calcGatewayFee(10)); $this->assertEquals(10, $company_gateway->calcGatewayFee(10, GatewayType::CREDIT_CARD));
} }
public function testFeesAndLimitsFeePercentAndAmountAndDoubleTaxCalcuation() public function testFeesAndLimitsFeePercentAndAmountAndDoubleTaxCalcuation()
@ -364,7 +365,7 @@ class CompanyGatewayApiTest extends TestCase
$company_gateway = CompanyGateway::find($id); $company_gateway = CompanyGateway::find($id);
$this->assertEquals(12, $company_gateway->calcGatewayFee(10, true)); $this->assertEquals(12, $company_gateway->calcGatewayFee(10, GatewayType::CREDIT_CARD, true));
} }
public function testFeesAndLimitsFeePercentAndAmountAndDoubleTaxCalcuationWithFeeCap() public function testFeesAndLimitsFeePercentAndAmountAndDoubleTaxCalcuationWithFeeCap()
@ -400,6 +401,6 @@ class CompanyGatewayApiTest extends TestCase
$company_gateway = CompanyGateway::find($id); $company_gateway = CompanyGateway::find($id);
$this->assertEquals(1.2, $company_gateway->calcGatewayFee(10, true)); $this->assertEquals(1.2, $company_gateway->calcGatewayFee(10, GatewayType::CREDIT_CARD, true));
} }
} }

View File

@ -102,10 +102,10 @@ class CompanyGatewayResolutionTest extends TestCase
*/ */
public function testGatewayResolution() public function testGatewayResolution()
{ {
$fee = $this->cg->calcGatewayFee(10, false, GatewayType::CREDIT_CARD); $fee = $this->cg->calcGatewayFee(10, GatewayType::CREDIT_CARD, false);
$this->assertEquals(0.2, $fee); $this->assertEquals(0.2, $fee);
$fee = $this->cg->calcGatewayFee(10, false, GatewayType::BANK_TRANSFER); // $fee = $this->cg->calcGatewayFee(10, GatewayType::CREDIT_CARD, false);
$this->assertEquals(0.1, $fee); // $this->assertEquals(0.1, $fee);
} }
/** /**

View File

@ -181,7 +181,7 @@ class CompanyGatewayTest extends TestCase
$total = 10.93; $total = 10.93;
$total_invoice_count = 5; $total_invoice_count = 5;
$total_gateway_fee = round($cg->calcGatewayFee($total, true, GatewayType::CREDIT_CARD), 2); $total_gateway_fee = round($cg->calcGatewayFee($total, GatewayType::CREDIT_CARD, true), 2);
$this->assertEquals(1.58, $total_gateway_fee); $this->assertEquals(1.58, $total_gateway_fee);

View File

@ -540,7 +540,7 @@ trait MockAccountData
$data[1]['fee_tax_rate2'] = ''; $data[1]['fee_tax_rate2'] = '';
$data[1]['fee_tax_name3'] = ''; $data[1]['fee_tax_name3'] = '';
$data[1]['fee_tax_rate3'] = 0; $data[1]['fee_tax_rate3'] = 0;
$data[1]['fee_cap'] = '';
$cg = new CompanyGateway; $cg = new CompanyGateway;
$cg->company_id = $this->company->id; $cg->company_id = $this->company->id;
$cg->user_id = $this->user->id; $cg->user_id = $this->user->id;