mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-10 05:02:36 +01:00
commit
f55a6022ce
@ -1 +1 @@
|
||||
5.7.10
|
||||
5.7.11
|
@ -107,7 +107,7 @@ class Kernel extends ConsoleKernel
|
||||
$schedule->job(new AdjustEmailQuota)->dailyAt('23:30')->withoutOverlapping();
|
||||
|
||||
/* Pulls in bank transactions from third party services */
|
||||
$schedule->job(new BankTransactionSync)->dailyAt('04:10')->withoutOverlapping()->name('bank-trans-sync-job')->onOneServer();
|
||||
$schedule->job(new BankTransactionSync)->everyFourHours()->withoutOverlapping()->name('bank-trans-sync-job')->onOneServer();
|
||||
|
||||
$schedule->command('ninja:check-data --database=db-ninja-01')->dailyAt('02:10')->withoutOverlapping()->name('check-data-db-1-job')->onOneServer();
|
||||
|
||||
|
@ -363,7 +363,15 @@ class BaseRule implements RuleInterface
|
||||
|
||||
public function override($item): self
|
||||
{
|
||||
$this->tax_rate1 = $item->tax_rate1;
|
||||
$this->tax_name1 = $item->tax_name1;
|
||||
$this->tax_rate2 = $item->tax_rate2;
|
||||
$this->tax_name2 = $item->tax_name2;
|
||||
$this->tax_rate3 = $item->tax_rate3;
|
||||
$this->tax_name3 = $item->tax_name3;
|
||||
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
public function calculateRates(): self
|
||||
|
@ -49,6 +49,10 @@ class Rule extends BaseRule implements RuleInterface
|
||||
|
||||
$this->tax_rate1 = $item->tax_rate1;
|
||||
$this->tax_name1 = $item->tax_name1;
|
||||
$this->tax_rate2 = $item->tax_rate2;
|
||||
$this->tax_name2 = $item->tax_name2;
|
||||
$this->tax_rate3 = $item->tax_rate3;
|
||||
$this->tax_name3 = $item->tax_name3;
|
||||
|
||||
return $this;
|
||||
|
||||
|
@ -22,9 +22,10 @@ class SwitchCompanyController extends Controller
|
||||
|
||||
public function __invoke(string $contact)
|
||||
{
|
||||
$client_contact = ClientContact::where('email', auth()->user()->email)
|
||||
->where('id', $this->transformKeys($contact))
|
||||
->first();
|
||||
$client_contact = ClientContact::query()
|
||||
->where('email', auth()->user()->email)
|
||||
->where('id', $this->transformKeys($contact))
|
||||
->firstOrFail();
|
||||
|
||||
auth()->guard('contact')->loginUsingId($client_contact->id, true);
|
||||
|
||||
|
@ -115,7 +115,7 @@ class CompanyUserController extends BaseController
|
||||
$auth_user = auth()->user();
|
||||
$company = $auth_user->company();
|
||||
|
||||
$company_user = CompanyUser::whereUserId($user->id)->whereCompanyId($company->id)->first();
|
||||
$company_user = CompanyUser::query()->where('user_id', $user->id)->where('company_id',$company->id)->first();
|
||||
|
||||
if (! $company_user) {
|
||||
throw new ModelNotFoundException(ctrans('texts.company_user_not_found'));
|
||||
@ -128,6 +128,11 @@ class CompanyUserController extends BaseController
|
||||
} else {
|
||||
$company_user->settings = $request->input('company_user')['settings'];
|
||||
$company_user->notifications = $request->input('company_user')['notifications'];
|
||||
|
||||
if(isset($request->input('company_user')['react_settings'])) {
|
||||
$company_user->react_settings = $request->input('company_user')['react_settings'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$company_user->save();
|
||||
|
@ -120,36 +120,45 @@ class ImportController extends Controller
|
||||
|
||||
|
||||
foreach($headers as $key => $value) {
|
||||
|
||||
$hit = false;
|
||||
$unsetKey = false;
|
||||
// array_multisort(array_column($translated_keys, 'label'), SORT_ASC, $translated_keys);
|
||||
|
||||
foreach($translated_keys as $tkey => $tvalue)
|
||||
{
|
||||
|
||||
if($this->testMatch($value, $tvalue['label'])) {
|
||||
$hit = $available_keys[$tvalue['key']];
|
||||
$unsetKey = $tkey;
|
||||
$hit = $tvalue['key'];
|
||||
$hints[$key] = $hit;
|
||||
unset($translated_keys[$tkey]);
|
||||
break;
|
||||
}
|
||||
else {
|
||||
$hints[$key] = null;
|
||||
}
|
||||
// elseif($this->testMatch($value, $tvalue['index'])) {
|
||||
// $hit = $available_keys[$tvalue['key']];
|
||||
// $unsetKey = $tkey;
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
if($hit) {
|
||||
$hints[$key] = $hit;
|
||||
unset($translated_keys[$unsetKey]);
|
||||
} else {
|
||||
$hints[$key] = null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
nlog($translated_keys);
|
||||
//second pass using the index of the translation here
|
||||
foreach($headers as $key => $value)
|
||||
{
|
||||
if(isset($hints[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach($translated_keys as $tkey => $tvalue)
|
||||
{
|
||||
if($this->testMatch($value, $tvalue['index'])) {
|
||||
$hit = $tvalue['key'];
|
||||
$hints[$key] = $hit;
|
||||
unset($translated_keys[$tkey]);
|
||||
break;
|
||||
} else {
|
||||
$hints[$key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $hints;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ use App\Models\Quote;
|
||||
use App\Models\Client;
|
||||
use App\Models\Account;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Project;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Factory\QuoteFactory;
|
||||
use App\Filters\QuoteFilters;
|
||||
@ -33,6 +34,7 @@ use App\Transformers\QuoteTransformer;
|
||||
use App\Utils\Traits\GeneratesCounter;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Transformers\InvoiceTransformer;
|
||||
use App\Transformers\ProjectTransformer;
|
||||
use App\Factory\CloneQuoteToInvoiceFactory;
|
||||
use App\Factory\CloneQuoteToProjectFactory;
|
||||
use App\Http\Requests\Quote\EditQuoteRequest;
|
||||
@ -555,7 +557,7 @@ class QuoteController extends BaseController
|
||||
}
|
||||
});
|
||||
|
||||
return $this->listResponse(Quote::withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
|
||||
return $this->listResponse(Quote::query()->withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
|
||||
}
|
||||
|
||||
if ($action == 'bulk_print' && $user->can('view', $quotes->first())) {
|
||||
@ -585,7 +587,7 @@ class QuoteController extends BaseController
|
||||
}
|
||||
});
|
||||
|
||||
return $this->listResponse(Quote::withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
|
||||
return $this->listResponse(Quote::query()->withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
|
||||
}
|
||||
|
||||
/*
|
||||
@ -683,7 +685,14 @@ class QuoteController extends BaseController
|
||||
private function performAction(Quote $quote, $action, $bulk = false)
|
||||
{
|
||||
switch ($action) {
|
||||
case 'convert':
|
||||
case 'convert_to_project':
|
||||
|
||||
$this->entity_type = Project::class;
|
||||
$this->entity_transformer = ProjectTransformer::class;
|
||||
|
||||
return $this->itemResponse($quote->service()->convertToProject());
|
||||
|
||||
case 'convert':
|
||||
case 'convert_to_invoice':
|
||||
|
||||
$this->entity_type = Invoice::class;
|
||||
@ -691,8 +700,6 @@ class QuoteController extends BaseController
|
||||
|
||||
return $this->itemResponse($quote->service()->convertToInvoice());
|
||||
|
||||
break;
|
||||
|
||||
case 'clone_to_invoice':
|
||||
|
||||
$this->entity_type = Invoice::class;
|
||||
@ -701,19 +708,19 @@ class QuoteController extends BaseController
|
||||
$invoice = CloneQuoteToInvoiceFactory::create($quote, auth()->user()->id);
|
||||
|
||||
return $this->itemResponse($invoice);
|
||||
break;
|
||||
|
||||
case 'clone_to_quote':
|
||||
$quote = CloneQuoteFactory::create($quote, auth()->user()->id);
|
||||
|
||||
return $this->itemResponse($quote);
|
||||
break;
|
||||
|
||||
case 'approve':
|
||||
if (! in_array($quote->status_id, [Quote::STATUS_SENT, Quote::STATUS_DRAFT])) {
|
||||
return response()->json(['message' => ctrans('texts.quote_unapprovable')], 400);
|
||||
}
|
||||
|
||||
return $this->itemResponse($quote->service()->approveWithNoCoversion()->save());
|
||||
break;
|
||||
|
||||
case 'history':
|
||||
// code...
|
||||
break;
|
||||
@ -725,16 +732,14 @@ class QuoteController extends BaseController
|
||||
echo Storage::get($file);
|
||||
}, basename($file), ['Content-Type' => 'application/pdf']);
|
||||
|
||||
|
||||
break;
|
||||
case 'restore':
|
||||
$this->quote_repo->restore($quote);
|
||||
|
||||
if (! $bulk) {
|
||||
return $this->itemResponse($quote);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'archive':
|
||||
$this->quote_repo->archive($quote);
|
||||
|
||||
@ -752,16 +757,11 @@ class QuoteController extends BaseController
|
||||
|
||||
break;
|
||||
case 'email':
|
||||
$quote->service()->sendEmail();
|
||||
|
||||
return response()->json(['message'=> ctrans('texts.sent_message')], 200);
|
||||
break;
|
||||
|
||||
case 'send_email':
|
||||
|
||||
$quote->service()->sendEmail();
|
||||
|
||||
return response()->json(['message'=> ctrans('texts.sent_message')], 200);
|
||||
break;
|
||||
|
||||
case 'mark_sent':
|
||||
$quote->service()->markSent()->save();
|
||||
|
@ -25,7 +25,10 @@ class UpdateCompanyUserRequest extends Request
|
||||
*/
|
||||
public function authorize() : bool
|
||||
{
|
||||
return auth()->user()->isAdmin() || (auth()->user()->id == $this->user->id);
|
||||
/** @var \App\Models\User $auth_user */
|
||||
$auth_user = auth()->user();
|
||||
|
||||
return $auth_user->isAdmin() || ($auth_user->id == $this->user->id);
|
||||
}
|
||||
|
||||
public function rules()
|
||||
|
@ -30,9 +30,11 @@ class BulkActionQuoteRequest extends Request
|
||||
{
|
||||
$input = $this->all();
|
||||
|
||||
$rules = [];
|
||||
$rules = [
|
||||
'action' => 'sometimes|in:convert_to_invoice,convert_to_project,email,bulk_download,bulk_print,clone_to_invoice,approve,download,restore,archive,delete,send_email,mark_sent',
|
||||
];
|
||||
|
||||
if ($input['action'] == 'convert_to_invoice') {
|
||||
if (in_array($input['action'], ['convert,convert_to_invoice']) ) {
|
||||
$rules['action'] = [new ConvertableQuoteRule()];
|
||||
}
|
||||
|
||||
|
@ -28,23 +28,30 @@ class StoreTaskRequest extends Request
|
||||
*/
|
||||
public function authorize() : bool
|
||||
{
|
||||
return auth()->user()->can('create', Task::class);
|
||||
/** @var \App\Models\User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $user->can('create', Task::class);
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
|
||||
/** @var \App\Models\User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
$rules = [];
|
||||
|
||||
if (isset($this->number)) {
|
||||
$rules['number'] = Rule::unique('tasks')->where('company_id', auth()->user()->company()->id);
|
||||
$rules['number'] = Rule::unique('tasks')->where('company_id', $user->company()->id);
|
||||
}
|
||||
|
||||
if (isset($this->client_id)) {
|
||||
$rules['client_id'] = 'bail|required|exists:clients,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
|
||||
$rules['client_id'] = 'bail|required|exists:clients,id,company_id,'.$user->company()->id.',is_deleted,0';
|
||||
}
|
||||
|
||||
if (isset($this->project_id)) {
|
||||
$rules['project_id'] = 'bail|required|exists:projects,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
|
||||
$rules['project_id'] = 'bail|required|exists:projects,id,company_id,'.$user->company()->id.',is_deleted,0';
|
||||
}
|
||||
|
||||
$rules['timelog'] = ['bail','array',function ($attribute, $values, $fail) {
|
||||
@ -77,9 +84,9 @@ class StoreTaskRequest extends Request
|
||||
|
||||
public function prepareForValidation()
|
||||
{
|
||||
$input = $this->all();
|
||||
$input = $this->decodePrimaryKeys($this->all());
|
||||
|
||||
$input = $this->decodePrimaryKeys($this->all());
|
||||
|
||||
if (array_key_exists('status_id', $input) && is_string($input['status_id'])) {
|
||||
$input['status_id'] = $this->decodePrimaryKey($input['status_id']);
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ class EntitySentObject
|
||||
);
|
||||
$mail_obj->data = [
|
||||
'title' => $mail_obj->subject,
|
||||
'message' => ctrans(
|
||||
'content' => ctrans(
|
||||
$this->template_body,
|
||||
[
|
||||
'amount' => $mail_obj->amount,
|
||||
@ -98,7 +98,7 @@ class EntitySentObject
|
||||
$mail_obj->markdown = 'email.admin.generic';
|
||||
$mail_obj->tag = $this->company->company_key;
|
||||
}
|
||||
|
||||
nlog($mail_obj);
|
||||
return $mail_obj;
|
||||
}
|
||||
|
||||
@ -186,7 +186,7 @@ class EntitySentObject
|
||||
|
||||
return [
|
||||
'title' => $this->getSubject(),
|
||||
'message' => $this->getMessage(),
|
||||
'content' => $this->getMessage(),
|
||||
'url' => $this->invitation->getAdminLink($this->use_react_url),
|
||||
'button' => ctrans("texts.view_{$this->entity_type}"),
|
||||
'signature' => $settings->email_signature,
|
||||
|
@ -226,4 +226,15 @@ class Task extends BaseModel
|
||||
{
|
||||
return ctrans('texts.task');
|
||||
}
|
||||
|
||||
public function getRate(): float
|
||||
{
|
||||
if($this->project && $this->project->task_rate > 0)
|
||||
return $this->project->task_rate;
|
||||
|
||||
if($this->client)
|
||||
return $this->client->getSetting('default_task_rate');
|
||||
|
||||
return $this->company->settings->default_task_rate ?? 0;
|
||||
}
|
||||
}
|
||||
|
@ -12,14 +12,16 @@
|
||||
|
||||
namespace App\PaymentDrivers\Authorize;
|
||||
|
||||
use App\Jobs\Util\SystemLogger;
|
||||
use App\Models\Payment;
|
||||
use App\Models\SystemLog;
|
||||
use App\Jobs\Util\SystemLogger;
|
||||
use App\PaymentDrivers\AuthorizePaymentDriver;
|
||||
use net\authorize\api\contract\v1\CreateTransactionRequest;
|
||||
use net\authorize\api\contract\v1\CustomerProfilePaymentType;
|
||||
use net\authorize\api\contract\v1\PaymentType;
|
||||
use net\authorize\api\contract\v1\CreditCardType;
|
||||
use net\authorize\api\contract\v1\PaymentProfileType;
|
||||
use net\authorize\api\contract\v1\TransactionRequestType;
|
||||
use net\authorize\api\contract\v1\CreateTransactionRequest;
|
||||
use net\authorize\api\contract\v1\CustomerProfilePaymentType;
|
||||
use net\authorize\api\controller\CreateTransactionController;
|
||||
|
||||
/**
|
||||
@ -43,24 +45,42 @@ class RefundTransaction
|
||||
|
||||
$transaction_details = $this->authorize_transaction->getTransactionDetails($payment->transaction_reference);
|
||||
|
||||
$creditCard = $transaction_details->getTransaction()->getPayment()->getCreditCard();
|
||||
$creditCardNumber = $creditCard->getCardNumber();
|
||||
$creditCardExpiry = $creditCard->getExpirationDate();
|
||||
$transaction_status = $transaction_details->getTransaction()->getTransactionStatus();
|
||||
|
||||
$transaction_type = $transaction_status == 'capturedPendingSettlement' ? 'voidTransaction' : 'refundTransaction';
|
||||
|
||||
if($transaction_type == 'voidTransaction') {
|
||||
$amount = $transaction_details->getTransaction()->getAuthAmount();
|
||||
}
|
||||
|
||||
$this->authorize->init();
|
||||
|
||||
// Set the transaction's refId
|
||||
$refId = 'ref'.time();
|
||||
|
||||
$paymentProfile = new PaymentProfileType();
|
||||
$paymentProfile->setPaymentProfileId($transaction_details->getTransaction()->getProfile()->getCustomerPaymentProfileId());
|
||||
// $paymentProfile = new PaymentProfileType();
|
||||
// $paymentProfile->setPaymentProfileId($transaction_details->getTransaction()->getProfile()->getCustomerPaymentProfileId());
|
||||
|
||||
// set customer profile
|
||||
$customerProfile = new CustomerProfilePaymentType();
|
||||
$customerProfile->setCustomerProfileId($transaction_details->getTransaction()->getProfile()->getCustomerProfileId());
|
||||
$customerProfile->setPaymentProfile($paymentProfile);
|
||||
// // // set customer profile
|
||||
// $customerProfile = new CustomerProfilePaymentType();
|
||||
// $customerProfile->setCustomerProfileId($transaction_details->getTransaction()->getProfile()->getCustomerProfileId());
|
||||
// $customerProfile->setPaymentProfile($paymentProfile);
|
||||
|
||||
$creditCard = new CreditCardType();
|
||||
$creditCard->setCardNumber($creditCardNumber);
|
||||
$creditCard->setExpirationDate($creditCardExpiry);
|
||||
$paymentOne = new PaymentType();
|
||||
$paymentOne->setCreditCard($creditCard);
|
||||
|
||||
//create a transaction
|
||||
$transactionRequest = new TransactionRequestType();
|
||||
$transactionRequest->setTransactionType('refundTransaction');
|
||||
$transactionRequest->setTransactionType($transaction_type);
|
||||
$transactionRequest->setAmount($amount);
|
||||
$transactionRequest->setProfile($customerProfile);
|
||||
// $transactionRequest->setProfile($customerProfile);
|
||||
$transactionRequest->setPayment($paymentOne);
|
||||
$transactionRequest->setRefTransId($payment->transaction_reference);
|
||||
|
||||
$request = new CreateTransactionRequest();
|
||||
@ -83,6 +103,7 @@ class RefundTransaction
|
||||
'transaction_response' => $tresponse->getResponseCode(),
|
||||
'payment_id' => $payment->id,
|
||||
'amount' => $amount,
|
||||
'voided' => $transaction_status == 'capturedPendingSettlement' ? true : false,
|
||||
];
|
||||
|
||||
SystemLogger::dispatch($data, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_AUTHORIZE, $this->authorize->client, $this->authorize->client->company);
|
||||
@ -166,4 +187,5 @@ class RefundTransaction
|
||||
|
||||
SystemLogger::dispatch($data, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_AUTHORIZE, $this->authorize->client, $this->authorize->client->company);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -45,7 +45,11 @@ class TaskRepository extends BaseRepository
|
||||
$task->saveQuietly();
|
||||
|
||||
if ($this->new_task && ! $task->status_id) {
|
||||
$this->setDefaultStatus($task);
|
||||
$task->status_id = $this->setDefaultStatus($task);
|
||||
}
|
||||
|
||||
if($this->new_task && (!$task->rate || $task->rate <= 0)) {
|
||||
$task->rate = $task->getRate();
|
||||
}
|
||||
|
||||
$task->number = empty($task->number) || ! array_key_exists('number', $data) ? $this->trySaving($task) : $data['number'];
|
||||
|
@ -11,8 +11,10 @@
|
||||
|
||||
namespace App\Services\Credit;
|
||||
|
||||
use App\Jobs\Entity\EmailEntity;
|
||||
use App\Utils\Ninja;
|
||||
use App\Models\ClientContact;
|
||||
use App\Jobs\Entity\EmailEntity;
|
||||
use App\Events\Credit\CreditWasEmailed;
|
||||
|
||||
class SendEmail
|
||||
{
|
||||
@ -40,12 +42,17 @@ class SendEmail
|
||||
$this->reminder_template = $this->credit->calculateTemplate('credit');
|
||||
}
|
||||
|
||||
$this->credit->service()->markSent()->save();
|
||||
|
||||
$this->credit->invitations->each(function ($invitation) {
|
||||
if (! $invitation->contact->trashed() && $invitation->contact->email) {
|
||||
EmailEntity::dispatch($invitation, $invitation->company, $this->reminder_template)->delay(2);
|
||||
}
|
||||
});
|
||||
|
||||
$this->credit->service()->markSent()->save();
|
||||
if ($this->credit->invitations->count() >= 1) {
|
||||
event(new CreditWasEmailed($this->credit->invitations->first(), $this->credit->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), 'credit'));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -11,10 +11,12 @@
|
||||
|
||||
namespace App\Services\Invoice;
|
||||
|
||||
use App\Jobs\Entity\EmailEntity;
|
||||
use App\Models\ClientContact;
|
||||
use App\Utils\Ninja;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\ClientContact;
|
||||
use App\Jobs\Entity\EmailEntity;
|
||||
use App\Services\AbstractService;
|
||||
use App\Events\Invoice\InvoiceWasEmailed;
|
||||
|
||||
class SendEmail extends AbstractService
|
||||
{
|
||||
@ -36,5 +38,10 @@ class SendEmail extends AbstractService
|
||||
EmailEntity::dispatch($invitation, $invitation->company, $this->reminder_template)->delay(10);
|
||||
}
|
||||
});
|
||||
|
||||
if ($this->invoice->invitations->count() >= 1) {
|
||||
event(new InvoiceWasEmailed($this->invoice->invitations->first(), $this->invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $this->reminder_template ?? 'invoice'));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,10 @@ class RefundPayment
|
||||
|
||||
private $activity_repository;
|
||||
|
||||
private bool $refund_failed = false;
|
||||
|
||||
private string $refund_failed_message = '';
|
||||
|
||||
public function __construct($payment, $refund_data)
|
||||
{
|
||||
$this->payment = $payment;
|
||||
@ -50,12 +54,14 @@ class RefundPayment
|
||||
|
||||
public function run()
|
||||
{
|
||||
$this->payment = $this->calculateTotalRefund() //sets amount for the refund (needed if we are refunding multiple invoices in one payment)
|
||||
$this->payment = $this
|
||||
->calculateTotalRefund() //sets amount for the refund (needed if we are refunding multiple invoices in one payment)
|
||||
->processGatewayRefund() //process the gateway refund if needed
|
||||
->setStatus() //sets status of payment
|
||||
->updateCreditables() //return the credits first
|
||||
->updatePaymentables() //update the paymentable items
|
||||
->adjustInvoices()
|
||||
->processGatewayRefund() //process the gateway refund if needed
|
||||
->finalize()
|
||||
->save();
|
||||
|
||||
if (array_key_exists('email_receipt', $this->refund_data) && $this->refund_data['email_receipt'] == 'true') {
|
||||
@ -71,9 +77,28 @@ class RefundPayment
|
||||
return $this->payment;
|
||||
}
|
||||
|
||||
private function finalize(): self
|
||||
{
|
||||
if($this->refund_failed)
|
||||
throw new PaymentRefundFailed($this->refund_failed_message);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the refund through the gateway.
|
||||
*
|
||||
* @var array $response
|
||||
* [
|
||||
* 'transaction_reference' => (string),
|
||||
* 'transaction_response' => (string),
|
||||
* 'success' => (bool),
|
||||
* 'description' => (string),
|
||||
* 'code' => (string),
|
||||
* 'payment_id' => (int),
|
||||
* 'amount' => (float),
|
||||
* ];
|
||||
*
|
||||
* @return $this
|
||||
* @throws PaymentRefundFailed
|
||||
*/
|
||||
@ -83,16 +108,27 @@ class RefundPayment
|
||||
if ($this->payment->company_gateway) {
|
||||
$response = $this->payment->company_gateway->driver($this->payment->client)->refund($this->payment, $this->total_refund);
|
||||
|
||||
if($response['amount'] ?? false)
|
||||
$this->total_refund = $response['amount'];
|
||||
|
||||
if($response['voided'] ?? false)
|
||||
{
|
||||
//When a transaction is voided - all invoices attached to the payment need to be reversed, this
|
||||
//block prevents the edge case where a partial refund was attempted.
|
||||
$this->refund_data['invoices'] = $this->payment->invoices->map(function ($invoice){
|
||||
return [
|
||||
'invoice_id' => $invoice->id,
|
||||
'amount' => $invoice->pivot->amount,
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
$this->payment->refunded += $this->total_refund;
|
||||
|
||||
if ($response['success'] == false) {
|
||||
$this->payment->save();
|
||||
|
||||
if (array_key_exists('description', $response)) {
|
||||
throw new PaymentRefundFailed($response['description']);
|
||||
} else {
|
||||
throw new PaymentRefundFailed();
|
||||
}
|
||||
$this->refund_failed = true;
|
||||
$this->refund_failed_message = $response['description'] ?? '';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -932,7 +932,9 @@ class Design extends BaseDesign
|
||||
} elseif (Str::startsWith($variable, '$custom_surcharge')) {
|
||||
$_variable = ltrim($variable, '$'); // $custom_surcharge1 -> custom_surcharge1
|
||||
|
||||
$visible = intval($this->entity->{$_variable}) != 0;
|
||||
//07/09/2023 don't show custom values if they are empty
|
||||
// $visible = intval($this->entity->{$_variable}) != 0;
|
||||
$visible = intval(str_replace(['0','.'],'', $this->entity->{$_variable})) != 0;
|
||||
|
||||
$elements[1]['elements'][] = ['element' => 'div', 'elements' => [
|
||||
['element' => 'span', 'content' => $variable . '_label', 'properties' => ['hidden' => !$visible, 'data-ref' => 'totals_table-' . substr($variable, 1) . '-label']],
|
||||
|
76
app/Services/Quote/ConvertQuoteToProject.php
Normal file
76
app/Services/Quote/ConvertQuoteToProject.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Services\Quote;
|
||||
|
||||
use App\DataMapper\InvoiceItem;
|
||||
use App\Models\Quote;
|
||||
use App\Factory\ProjectFactory;
|
||||
use App\Factory\TaskFactory;
|
||||
use App\Models\Project;
|
||||
use App\Repositories\TaskRepository;
|
||||
use App\Utils\Traits\GeneratesCounter;
|
||||
|
||||
class ConvertQuoteToProject
|
||||
{
|
||||
use GeneratesCounter;
|
||||
|
||||
public function __construct(private Quote $quote)
|
||||
{
|
||||
}
|
||||
|
||||
public function run(): Project
|
||||
{
|
||||
|
||||
$quote_items = collect($this->quote->line_items)->filter(function ($item){
|
||||
return $item->type_id == '2';
|
||||
});
|
||||
|
||||
$project = ProjectFactory::create($this->quote->company_id, $this->quote->user_id);
|
||||
$project->name = ctrans('texts.quote_number_short'). " " . $this->quote->number . "[{$this->quote->client->present()->name()}]";
|
||||
$project->client_id = $this->quote->client_id;
|
||||
$project->public_notes = $this->quote->public_notes;
|
||||
$project->private_notes = $this->quote->private_notes;
|
||||
$project->budgeted_hours = $quote_items->sum('quantity') ?? 0;
|
||||
$project->task_rate = $this->quote->client->getSetting('default_task_rate');
|
||||
$project->saveQuietly();
|
||||
$project->number = $this->getNextProjectNumber($project);
|
||||
$project->saveQuietly();
|
||||
|
||||
$this->quote->project_id = $project->id;
|
||||
$this->quote->saveQuietly();
|
||||
|
||||
$task_status = $this->quote->company->task_statuses()
|
||||
->whereNull('deleted_at')
|
||||
->orderBy('id', 'asc')
|
||||
->first();
|
||||
|
||||
|
||||
$task_repo = new TaskRepository();
|
||||
|
||||
$quote_items->each(function($item) use($task_repo, $task_status){
|
||||
|
||||
$task = TaskFactory::create($this->quote->company_id, $this->quote->user_id);
|
||||
$task->client_id = $this->quote->client_id;
|
||||
$task->project_id = $this->quote->project_id;
|
||||
$task->description = $item->notes;
|
||||
$task->status_id = $task_status->id;
|
||||
$task->rate = $item->unit_cost;
|
||||
$task_repo->save([], $task);
|
||||
|
||||
});
|
||||
|
||||
event('eloquent.created: App\Models\Project', $project);
|
||||
|
||||
return $project->fresh();
|
||||
}
|
||||
}
|
@ -13,13 +13,14 @@ namespace App\Services\Quote;
|
||||
|
||||
use App\Utils\Ninja;
|
||||
use App\Models\Quote;
|
||||
use App\Jobs\Util\UnlinkFile;
|
||||
use App\Models\Project;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Exceptions\QuoteConversion;
|
||||
use App\Jobs\Entity\CreateEntityPdf;
|
||||
use App\Repositories\QuoteRepository;
|
||||
use App\Events\Quote\QuoteWasApproved;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Services\Quote\ConvertQuoteToProject;
|
||||
|
||||
class QuoteService
|
||||
{
|
||||
@ -41,6 +42,13 @@ class QuoteService
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function convertToProject(): Project
|
||||
{
|
||||
$project = (new ConvertQuoteToProject($this->quote))->run();
|
||||
|
||||
return $project;
|
||||
}
|
||||
|
||||
public function convert() :self
|
||||
{
|
||||
if ($this->quote->invoice_id) {
|
||||
|
@ -11,8 +11,10 @@
|
||||
|
||||
namespace App\Services\Quote;
|
||||
|
||||
use App\Jobs\Entity\EmailEntity;
|
||||
use App\Utils\Ninja;
|
||||
use App\Models\ClientContact;
|
||||
use App\Jobs\Entity\EmailEntity;
|
||||
use App\Events\Quote\QuoteWasEmailed;
|
||||
|
||||
class SendEmail
|
||||
{
|
||||
@ -42,15 +44,15 @@ class SendEmail
|
||||
$this->reminder_template = $this->quote->calculateTemplate('quote');
|
||||
}
|
||||
|
||||
|
||||
$this->quote->service()->markSent()->save();
|
||||
|
||||
$this->quote->invitations->each(function ($invitation) {
|
||||
if (! $invitation->contact->trashed() && $invitation->contact->email) {
|
||||
EmailEntity::dispatch($invitation, $invitation->company, $this->reminder_template);
|
||||
|
||||
// MailEntity::dispatch($invitation, $invitation->company->db, $mo);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -663,6 +663,12 @@ class HtmlEngine
|
||||
$data['$payment.custom3'] = ['value' => '', 'label' => ctrans('texts.payment')];
|
||||
$data['$payment.custom4'] = ['value' => '', 'label' => ctrans('texts.payment')];
|
||||
|
||||
$data['$payment.amount'] = ['value' => '', 'label' => ctrans('texts.payment')];
|
||||
$data['$payment.date'] = ['value' => '', 'label' => ctrans('texts.payment_date')];
|
||||
$data['$payment.number'] = ['value' => '', 'label' => ctrans('texts.payment_number')];
|
||||
$data['$payment.transaction_reference'] = ['value' => '', 'label' => ctrans('texts.transaction_reference')];
|
||||
|
||||
|
||||
if ($this->entity_string == 'invoice' && $this->entity->payments()->exists()) {
|
||||
$payment_list = '<br><br>';
|
||||
|
||||
@ -672,7 +678,6 @@ class HtmlEngine
|
||||
|
||||
$data['$payments'] = ['value' => $payment_list, 'label' => ctrans('texts.payments')];
|
||||
|
||||
|
||||
$payment = $this->entity->payments()->first();
|
||||
|
||||
$data['$payment.custom1'] = ['value' => $payment->custom_value1, 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'payment1')];
|
||||
@ -680,6 +685,11 @@ class HtmlEngine
|
||||
$data['$payment.custom3'] = ['value' => $payment->custom_value3, 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'payment3')];
|
||||
$data['$payment.custom4'] = ['value' => $payment->custom_value4, 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'payment4')];
|
||||
|
||||
$data['$payment.amount'] = ['value' => Number::formatMoney($payment->amount, $this->client), 'label' => ctrans('texts.payment')];
|
||||
$data['$payment.date'] = ['value' => $this->formatDate($payment->date, $this->client->date_format()), 'label' => ctrans('texts.payment_date')];
|
||||
$data['$payment.number'] = ['value' => $payment->number, 'label' => ctrans('texts.payment_number')];
|
||||
$data['$payment.transaction_reference'] = ['value' => $payment->transaction_reference, 'label' => ctrans('texts.transaction_reference')];
|
||||
|
||||
}
|
||||
|
||||
if (($this->entity_string == 'invoice' || $this->entity_string == 'recurring_invoice') && isset($this->company?->custom_fields?->company1)) {
|
||||
|
@ -39,6 +39,8 @@ trait PdfMaker
|
||||
$pdf->addChromiumArguments(config('ninja.snappdf_chromium_arguments'));
|
||||
}
|
||||
|
||||
$html = str_replace(['file:/', 'iframe', '<object', '<object', '127.0.0.1', 'localhost'], ['','','','','',''], $html);
|
||||
|
||||
$generated = $pdf
|
||||
->setHtml($html)
|
||||
->generate();
|
||||
|
@ -15,8 +15,8 @@ return [
|
||||
'require_https' => env('REQUIRE_HTTPS', true),
|
||||
'app_url' => rtrim(env('APP_URL', ''), '/'),
|
||||
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
|
||||
'app_version' => env('APP_VERSION','5.7.10'),
|
||||
'app_tag' => env('APP_TAG','5.7.10'),
|
||||
'app_version' => env('APP_VERSION','5.7.11'),
|
||||
'app_tag' => env('APP_TAG','5.7.11'),
|
||||
'minimum_client_version' => '5.0.16',
|
||||
'terms_version' => '1.0.1',
|
||||
'api_secret' => env('API_SECRET', ''),
|
||||
|
@ -13,10 +13,12 @@ namespace Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
use App\Models\Quote;
|
||||
use App\Models\Client;
|
||||
use App\Models\Project;
|
||||
use Tests\MockAccountData;
|
||||
use App\Models\ClientContact;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\DataMapper\ClientSettings;
|
||||
use App\Exceptions\QuoteConversion;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
@ -52,6 +54,72 @@ class QuoteTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testQuoteToProjectConversion2()
|
||||
{
|
||||
$settings = ClientSettings::defaults();
|
||||
$settings->default_task_rate = 41;
|
||||
|
||||
$c = Client::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $this->company->id,
|
||||
'settings' => $settings,
|
||||
]);
|
||||
|
||||
$q = Quote::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $this->company->id,
|
||||
'client_id' => $c->id,
|
||||
'status_id' => 2,
|
||||
'date' => now(),
|
||||
'line_items' =>[
|
||||
[
|
||||
'type_id' => 2,
|
||||
'unit_cost' => 200,
|
||||
'quantity' => 2,
|
||||
'notes' => 'Test200',
|
||||
],
|
||||
[
|
||||
'type_id' => 2,
|
||||
'unit_cost' => 100,
|
||||
'quantity' => 1,
|
||||
'notes' => 'Test100',
|
||||
],
|
||||
[
|
||||
'type_id' => 1,
|
||||
'unit_cost' => 10,
|
||||
'quantity' => 1,
|
||||
'notes' => 'Test',
|
||||
],
|
||||
|
||||
],
|
||||
]);
|
||||
|
||||
$q->calc()->getQuote();
|
||||
$q->fresh();
|
||||
|
||||
$p = $q->service()->convertToProject();
|
||||
|
||||
$this->assertEquals(3, $p->budgeted_hours);
|
||||
$this->assertEquals(2, $p->tasks()->count());
|
||||
|
||||
$t = $p->tasks()->where('description', 'Test200')->first();
|
||||
|
||||
$this->assertEquals(200, $t->rate);
|
||||
|
||||
$t = $p->tasks()->where('description', 'Test100')->first();
|
||||
|
||||
$this->assertEquals(100, $t->rate);
|
||||
|
||||
|
||||
}
|
||||
|
||||
public function testQuoteToProjectConversion()
|
||||
{
|
||||
$project = $this->quote->service()->convertToProject();
|
||||
|
||||
$this->assertInstanceOf('\App\Models\Project', $project);
|
||||
}
|
||||
|
||||
public function testQuoteConversion()
|
||||
{
|
||||
$invoice = $this->quote->service()->convertToInvoice();
|
||||
@ -62,7 +130,6 @@ class QuoteTest extends TestCase
|
||||
|
||||
$invoice = $this->quote->service()->convertToInvoice();
|
||||
|
||||
|
||||
}
|
||||
|
||||
public function testQuoteDownloadPDF()
|
||||
|
@ -11,8 +11,10 @@
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\DataMapper\ClientSettings;
|
||||
use Tests\TestCase;
|
||||
use App\Models\Task;
|
||||
use App\Models\Client;
|
||||
use App\Models\Project;
|
||||
use Tests\MockAccountData;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
@ -101,7 +103,88 @@ class TaskApiTest extends TestCase
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function testTaskClientRateSet()
|
||||
{
|
||||
$settings = ClientSettings::defaults();
|
||||
$settings->default_task_rate = 41;
|
||||
|
||||
$c = Client::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $this->company->id,
|
||||
'settings' => $settings,
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'client_id' => $c->hashed_id,
|
||||
'description' => 'Test Task',
|
||||
'time_log' => '[[1681165417,1681165432,"sumtin",true],[1681165446,0]]',
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson("/api/v1/tasks", $data);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$arr = $response->json();
|
||||
|
||||
$this->assertEquals(41, $arr['data']['rate']);
|
||||
}
|
||||
|
||||
public function testTaskProjectRateSet()
|
||||
{
|
||||
|
||||
$p = Project::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $this->company->id,
|
||||
'client_id' => $this->client->id,
|
||||
'name' => 'proggy',
|
||||
'task_rate' => 101,
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'project_id' => $p->hashed_id,
|
||||
'client_id' => $this->client->id,
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $this->company->id,
|
||||
'description' => 'Test Task',
|
||||
'time_log' => '[[1681165417,1681165432,"sumtin",true],[1681165446,0]]',
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson("/api/v1/tasks", $data);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$arr = $response->json();
|
||||
|
||||
$this->assertEquals(101, $arr['data']['rate']);
|
||||
}
|
||||
|
||||
public function testStatusSet()
|
||||
{
|
||||
|
||||
$data = [
|
||||
'client_id' => $this->client->id,
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $this->company->id,
|
||||
'description' => 'Test Task',
|
||||
'time_log' => '[[1681165417,1681165432,"sumtin",true],[1681165446,0]]',
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson("/api/v1/tasks");
|
||||
|
||||
$response->assertStatus(200);
|
||||
$arr = $response->json();
|
||||
|
||||
$this->assertNotEmpty($arr['data']['status_id']);
|
||||
}
|
||||
|
||||
public function testStartDate()
|
||||
{
|
||||
$x = [];
|
||||
|
Loading…
Reference in New Issue
Block a user