1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 05:02:36 +01:00

Merge pull request #8795 from turbo124/v5-develop

v5.7.11
This commit is contained in:
David Bomba 2023-09-09 22:50:05 +10:00 committed by GitHub
commit f55a6022ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 464 additions and 88 deletions

View File

@ -1 +1 @@
5.7.10
5.7.11

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()];
}

View File

@ -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']);
}

View File

@ -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,

View File

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

View File

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

View File

@ -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'];

View File

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

View File

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

View File

@ -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 {

View File

@ -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']],

View 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();
}
}

View File

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

View File

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

View File

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

View File

@ -39,6 +39,8 @@ trait PdfMaker
$pdf->addChromiumArguments(config('ninja.snappdf_chromium_arguments'));
}
$html = str_replace(['file:/', 'iframe', '&lt;object', '<object', '127.0.0.1', 'localhost'], ['','','','','',''], $html);
$generated = $pdf
->setHtml($html)
->generate();

View File

@ -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', ''),

View File

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

View File

@ -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 = [];