1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-15 15:42:51 +01:00

Merge branch 'v5-develop' into v5-stable

This commit is contained in:
David Bomba 2021-08-06 20:12:55 +10:00
commit 15d04dc253
44 changed files with 205603 additions and 204657 deletions

View File

@ -1 +1 @@
5.2.16
5.2.17

View File

@ -29,7 +29,7 @@ class RecurringInvoiceToInvoiceFactory
$invoice->public_notes = $recurring_invoice->public_notes;
$invoice->private_notes = $recurring_invoice->private_notes;
//$invoice->date = now()->format($client->date_format());
$invoice->due_date = $recurring_invoice->calculateDueDate($recurring_invoice->next_send_date);
//$invoice->due_date = $recurring_invoice->calculateDueDate(now());
$invoice->is_deleted = $recurring_invoice->is_deleted;
$invoice->line_items = $recurring_invoice->line_items;
$invoice->tax_name1 = $recurring_invoice->tax_name1;

View File

@ -76,6 +76,11 @@ class ClientFilters extends QueryFilters
return $this->builder->where('id_number', $id_number);
}
public function number(string $number):Builder
{
return $this->builder->where('number', $number);
}
/**
* Filter based on search text.
*

View File

@ -19,6 +19,7 @@ use App\Factory\RecurringInvoiceFactory;
use App\Http\Requests\Invoice\StoreInvoiceRequest;
use App\Http\Requests\Preview\PreviewInvoiceRequest;
use App\Jobs\Util\PreviewPdf;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Credit;
@ -30,8 +31,8 @@ use App\Repositories\CreditRepository;
use App\Repositories\InvoiceRepository;
use App\Repositories\QuoteRepository;
use App\Repositories\RecurringInvoiceRepository;
use App\Services\PdfMaker\Design as PdfMakerDesign;
use App\Services\PdfMaker\Design as PdfDesignModel;
use App\Services\PdfMaker\Design as PdfMakerDesign;
use App\Services\PdfMaker\Design;
use App\Services\PdfMaker\PdfMaker;
use App\Utils\HostedPDF\NinjaPdf;
@ -168,25 +169,29 @@ class PreviewController extends BaseController
public function live(PreviewInvoiceRequest $request)
{
$company = auth()->user()->company();
MultiDB::setDb($company->db);
if($request->input('entity') == 'invoice'){
$repo = new InvoiceRepository();
$factory = InvoiceFactory::create(auth()->user()->company()->id, auth()->user()->id);
$entity_obj = InvoiceFactory::create($company->id, auth()->user()->id);
$class = Invoice::class;
}
elseif($request->input('entity') == 'quote'){
$repo = new QuoteRepository();
$factory = QuoteFactory::create(auth()->user()->company()->id, auth()->user()->id);
$entity_obj = QuoteFactory::create($company->id, auth()->user()->id);
$class = Quote::class;
}
elseif($request->input('entity') == 'credit'){
$repo = new CreditRepository();
$factory = CreditFactory::create(auth()->user()->company()->id, auth()->user()->id);
$entity_obj = CreditFactory::create($company->id, auth()->user()->id);
$class = Credit::class;
}
elseif($request->input('entity') == 'recurring_invoice'){
$repo = new RecurringInvoiceRepository();
$factory = RecurringInvoiceFactory::create(auth()->user()->company()->id, auth()->user()->id);
$entity_obj = RecurringInvoiceFactory::create($company->id, auth()->user()->id);
$class = RecurringInvoice::class;
}
@ -197,13 +202,15 @@ class PreviewController extends BaseController
if($request->has('entity_id')){
$entity_obj = $class::withTrashed()->whereId($this->decodePrimaryKey($request->input('entity_id')))->company()->first();
$entity_obj = $repo->save($request->all(), $entity_obj);
$entity_obj = $class::on(config('database.default'))
->where('id', $this->decodePrimaryKey($request->input('entity_id')))
->where('company_id', $company->id)
->withTrashed()
->first();
}
else {
$entity_obj = $repo->save($request->all(), $factory);
}
$entity_obj = $repo->save($request->all(), $entity_obj);
$entity_obj->load('client');
@ -218,7 +225,7 @@ class PreviewController extends BaseController
/* Catch all in case migration doesn't pass back a valid design */
if(!$design)
$design = Design::find(2);
$design = \App\Models\Design::find(2);
if ($design->is_custom) {
$options = [
@ -277,7 +284,7 @@ class PreviewController extends BaseController
return (new NinjaPdf())->build($maker->getCompiledHTML(true));
}
$file_path = PreviewPdf::dispatchNow($maker->getCompiledHTML(true), auth()->user()->company());
$file_path = PreviewPdf::dispatchNow($maker->getCompiledHTML(true), $company);
if(Ninja::isHosted())
@ -302,7 +309,7 @@ class PreviewController extends BaseController
$t = app('translator');
$t->replace(Ninja::transformTranslations(auth()->user()->company()->settings));
DB::connection(config('database.default'))->beginTransaction();
DB::connection(auth()->user()->company()->db)->beginTransaction();
$client = Client::factory()->create([
'user_id' => auth()->user()->id,
@ -377,7 +384,7 @@ class PreviewController extends BaseController
$file_path = PreviewPdf::dispatchNow($maker->getCompiledHTML(true), auth()->user()->company());
DB::connection(config('database.default'))->rollBack();
DB::connection(auth()->user()->company()->db)->rollBack();
$response = Response::make($file_path, 200);
$response->header('Content-Type', 'application/pdf');

View File

@ -158,6 +158,37 @@ class RequiredClientInfo extends Component
}
}
public function showCopyBillingCheckbox(): bool
{
$fields = [];
collect($this->fields)->map(function ($field) use (&$fields) {
if (! array_key_exists('filled', $field)) {
$fields[] = $field['name'];
}
});
foreach ($fields as $field) {
if (Str::startsWith($field, 'client_shipping')) {
return true;
}
}
return false;
}
public function handleCopyBilling(): void
{
$this->emit('update-shipping-data', [
'client_shipping_address_line_1' => $this->contact->client->address1,
'client_shipping_address_line_2' => $this->contact->client->address2,
'client_shipping_city' => $this->contact->client->city,
'client_shipping_state' => $this->contact->client->state,
'client_shipping_postal_code' => $this->contact->client->postal_code,
'client_shipping_country_id' => $this->contact->client->country_id,
]);
}
public function render()
{
count($this->fields) > 0

View File

@ -74,9 +74,9 @@ class PaymentWebhookRequest extends Request
{
// For testing purposes we'll slow down the webhook processing by 2 seconds
// to make sure webhook request doesn't came before our processing.
if (app()->environment() !== 'production') {
//if (app()->environment() !== 'production') {
sleep(2);
}
//}
// Some gateways, like Checkout, we can dynamically pass payment hash,
// which we will resolve here and get payment information from it.

View File

@ -37,7 +37,7 @@ class PreviewInvoiceRequest extends Request
{
$rules = [];
$rules['client_id'] = 'bail|required|exists:clients,id,company_id,'.auth()->user()->company()->id;
// $rules['client_id'] = 'bail|required|exists:clients,id,company_id,'.auth()->user()->company()->id;
$rules['number'] = ['nullable'];

View File

@ -43,7 +43,7 @@ class AutoBillCron
set_time_limit(0);
/* Get all invoices where the send date is less than NOW + 30 minutes() */
nlog("Performing Autobilling ".Carbon::now()->format('Y-m-d h:i:s'));
info("Performing Autobilling ".Carbon::now()->format('Y-m-d h:i:s'));
if (! config('ninja.db.multi_db_enabled')) {
@ -95,7 +95,7 @@ class AutoBillCron
private function runAutoBiller(Invoice $invoice)
{
nlog("Firing autobill for {$invoice->company_id} - {$invoice->number}");
info("Firing autobill for {$invoice->company_id} - {$invoice->number}");
$invoice->service()->autoBill()->save();
}
}

View File

@ -119,7 +119,7 @@ class EmailEntity implements ShouldQueue
$nmo->reminder_template = $this->reminder_template;
$nmo->entity = $this->entity;
NinjaMailerJob::dispatch($nmo);
NinjaMailerJob::dispatchNow($nmo);
/* Mark entity sent */
$this->entity->service()->markSent()->save();

View File

@ -60,7 +60,10 @@ class SendRecurring implements ShouldQueue
$invoice = RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client);
$invoice->date = now()->format('Y-m-d');
$invoice->due_date = $this->recurring_invoice->calculateDueDate(now()->format('Y-m-d'));
if($invoice->client->getSetting('auto_email_invoice'))
{
$invoice = $invoice->service()
->markSent()
->applyNumber()
@ -68,6 +71,14 @@ class SendRecurring implements ShouldQueue
->fillDefaults()
->save();
}
else{
$invoice = $invoice->service()
->fillDefaults()
->save();
}
nlog("updating recurring invoice dates");
/* Set next date here to prevent a recurring loop forming */
$this->recurring_invoice->next_send_date = $this->recurring_invoice->nextSendDate();
@ -93,7 +104,7 @@ class SendRecurring implements ShouldQueue
nlog("Invoice {$invoice->number} created");
$invoice->invitations->each(function ($invitation) use ($invoice) {
if ($invitation->contact && strlen($invitation->contact->email) >=1) {
if ($invitation->contact && strlen($invitation->contact->email) >=1 && $invoice->client->getSetting('auto_email_invoice')) {
try{
EmailEntity::dispatch($invitation, $invoice->company);

View File

@ -35,11 +35,14 @@ use App\Http\ValidationRules\ValidCompanyGatewayFeesAndLimitsRule;
use App\Http\ValidationRules\ValidUserForCompany;
use App\Jobs\Company\CreateCompanyTaskStatuses;
use App\Jobs\Company\CreateCompanyToken;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Jobs\Ninja\CheckCompanyData;
use App\Jobs\Ninja\CompanySizeCheck;
use App\Jobs\Util\VersionCheck;
use App\Libraries\MultiDB;
use App\Mail\MigrationCompleted;
use App\Mail\Migration\StripeConnectMigration;
use App\Models\Activity;
use App\Models\Client;
use App\Models\ClientContact;
@ -87,6 +90,7 @@ use Illuminate\Http\UploadedFile;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
@ -214,27 +218,15 @@ class Import implements ShouldQueue
$this->{$method}($data[$import]);
}
// if(Ninja::isHosted() && array_key_exists('ninja_tokens', $data))
$this->processNinjaTokens($data['ninja_tokens']);
$task_statuses = [
['name' => ctrans('texts.backlog'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now(), 'status_order' => 1],
['name' => ctrans('texts.ready_to_do'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now(), 'status_order' => 2],
['name' => ctrans('texts.in_progress'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now(), 'status_order' => 3],
['name' => ctrans('texts.done'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now(), 'status_order' => 4],
// $this->fixData();
];
$this->setInitialCompanyLedgerBalances();
// $this->fixClientBalances();
$check_data = CheckCompanyData::dispatchNow($this->company, md5(time()));
try{
Mail::to($this->user->email, $this->user->name())
->send(new MigrationCompleted($this->company, implode("<br>",$check_data)));
}
catch(\Exception $e) {
nlog($e->getMessage());
}
/*After a migration first some basic jobs to ensure the system is up to date*/
VersionCheck::dispatch();
TaskStatus::insert($task_statuses);
$account = $this->company->account;
$account->default_company_id = $this->company->id;
@ -246,19 +238,35 @@ class Import implements ShouldQueue
$this->company->save();
}
$this->setInitialCompanyLedgerBalances();
// $this->fixClientBalances();
$check_data = CheckCompanyData::dispatchNow($this->company, md5(time()));
// if(Ninja::isHosted() && array_key_exists('ninja_tokens', $data))
$this->processNinjaTokens($data['ninja_tokens']);
// $this->fixData();
try{
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($this->company->settings));
Mail::to($this->user->email, $this->user->name())
->send(new MigrationCompleted($this->company, implode("<br>",$check_data)));
}
catch(\Exception $e) {
nlog($e->getMessage());
}
/*After a migration first some basic jobs to ensure the system is up to date*/
VersionCheck::dispatch();
// CreateCompanyPaymentTerms::dispatchNow($sp035a66, $spaa9f78);
// CreateCompanyTaskStatuses::dispatchNow($this->company, $this->user);
$task_statuses = [
['name' => ctrans('texts.backlog'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now(), 'status_order' => 1],
['name' => ctrans('texts.ready_to_do'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now(), 'status_order' => 2],
['name' => ctrans('texts.in_progress'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now(), 'status_order' => 3],
['name' => ctrans('texts.done'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now(), 'status_order' => 4],
];
TaskStatus::insert($task_statuses);
info('Completed🚀🚀🚀🚀🚀 at '.now());
unlink($this->file_path);
@ -1289,7 +1297,8 @@ class Import implements ShouldQueue
}
if(!$entity)
throw new Exception("Resource invoice/quote document not available.");
continue;
// throw new Exception("Resource invoice/quote document not available.");
}
@ -1381,9 +1390,21 @@ class Import implements ShouldQueue
$modified['fees_and_limits'] = $this->cleanFeesAndLimits($modified['fees_and_limits']);
}
/* On Hosted platform we need to advise Stripe users to connect with Stripe Connect */
if(Ninja::isHosted() && $modified['gateway_key'] == 'd14dd26a37cecc30fdd65700bfb55b23'){
$nmo = new NinjaMailerObject;
$nmo->mailable = new StripeConnectMigration($this->company);
$nmo->company = $this->company;
$nmo->settings = $this->company->settings;
$nmo->to_user = $this->user;
NinjaMailerJob::dispatch($nmo);
$modified['gateway_key'] = 'd14dd26a47cecc30fdd65700bfb67b34';
$modified['fees_and_limits'] = [];
//why do we set this to a blank array?
//$modified['fees_and_limits'] = [];
}
$company_gateway = CompanyGateway::create($modified);

View File

@ -133,6 +133,7 @@ class PaymentEmailEngine extends BaseEmailEngine
$data['$email'] = ['value' => isset($this->contact) ? $this->contact->email : 'no contact email on record', 'label' => ctrans('texts.email')];
$data['$client_name'] = ['value' => $this->client->present()->name() ?: '&nbsp;', 'label' => ctrans('texts.client_name')];
$data['$client.name'] = &$data['$client_name'];
$data['$client'] = &$data['$client_name'];
$data['$client.address1'] = &$data['$address1'];
$data['$client.address2'] = &$data['$address2'];
$data['$client_address'] = ['value' => $this->client->present()->address() ?: '&nbsp;', 'label' => ctrans('texts.address')];
@ -164,6 +165,7 @@ class PaymentEmailEngine extends BaseEmailEngine
$data['$contact.custom2'] = ['value' => isset($this->contact) ? $this->contact->custom_value2 : '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'contact1')];
$data['$contact.custom3'] = ['value' => isset($this->contact) ? $this->contact->custom_value3 : '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'contact1')];
$data['$contact.custom4'] = ['value' => isset($this->contact) ? $this->contact->custom_value4 : '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'contact1')];
$data['$firstName'] = &$data['$contact.first_name'];
$data['$company.city_state_postal'] = ['value' => $this->company->present()->cityStateZip($this->settings->city, $this->settings->state, $this->settings->postal_code, false) ?: '&nbsp;', 'label' => ctrans('texts.city_state_postal')];
$data['$company.postal_city_state'] = ['value' => $this->company->present()->cityStateZip($this->settings->city, $this->settings->state, $this->settings->postal_code, true) ?: '&nbsp;', 'label' => ctrans('texts.postal_city_state')];
@ -191,7 +193,11 @@ class PaymentEmailEngine extends BaseEmailEngine
$data['$company4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company4', $this->settings->custom_value4, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company4')];
$data['$view_link'] = ['value' => '<a class="button" href="'.$this->payment->getLink().'">'.ctrans('texts.view_payment').'</a>', 'label' => ctrans('texts.view_payment')];
$data['$paymentLink'] = &$data['$view_link'];
$data['$portalButton'] = &$data['$view_link'];
$data['$view_url'] = ['value' => $this->payment->getLink(), 'label' => ctrans('texts.view_payment')];
$data['$signature'] = ['value' => $this->settings->email_signature ?: '&nbsp;', 'label' => ''];
$data['$invoices'] = ['value' => $this->formatInvoices(), 'label' => ctrans('texts.invoices')];

View File

@ -0,0 +1,55 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Mail\Migration;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class StripeConnectMigration extends Mailable
{
// use Queueable, SerializesModels;
public $company;
public $settings;
public $logo;
public $whitelabel;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct($company)
{
$this->company = $company;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
$this->settings = $this->company->settings;
$this->logo = $this->company->present()->logo();
$this->whitelabel = $this->company->account->isPaid();
return $this->from(config('mail.from.address'), config('mail.from.name'))
->subject(ctrans('texts.stripe_connect_migration_title'))
->view('email.migration.stripe_connect');
}
}

View File

@ -15,7 +15,11 @@ use App\DataMapper\ClientSettings;
use App\DataMapper\CompanySettings;
use App\DataMapper\FeesAndLimits;
use App\Models\CompanyGateway;
use App\Models\Expense;
use App\Models\Presenters\ClientPresenter;
use App\Models\Project;
use App\Models\Quote;
use App\Models\Task;
use App\Services\Client\ClientService;
use App\Utils\Traits\AppSetup;
use App\Utils\Traits\GeneratesCounter;
@ -153,6 +157,16 @@ class Client extends BaseModel implements HasLocalePreference
return $this->hasMany(ClientGatewayToken::class);
}
public function expenses()
{
return $this->hasMany(Expense::class)->withTrashed();
}
public function projects()
{
return $this->hasMany(Project::class)->withTrashed();
}
/**
* Retrieves the specific payment token per
* gateway - per payment method.
@ -217,6 +231,16 @@ class Client extends BaseModel implements HasLocalePreference
return $this->hasMany(Invoice::class)->withTrashed();
}
public function quotes()
{
return $this->hasMany(Quote::class)->withTrashed();
}
public function tasks()
{
return $this->hasMany(Task::class)->withTrashed();
}
public function recurring_invoices()
{
return $this->hasMany(RecurringInvoice::class)->withTrashed();
@ -774,7 +798,7 @@ class Client extends BaseModel implements HasLocalePreference
public function payments()
{
return $this->hasMany(Payment::class);
return $this->hasMany(Payment::class)->withTrashed();
}
public function timezone_offset()

View File

@ -95,8 +95,12 @@ class ACH
return render('gateways.stripe.ach.verify', $data);
}
public function processVerification($request, ClientGatewayToken $token)
public function processVerification(Request $request, ClientGatewayToken $token)
{
$request->validate([
'transactions.*' => ['integer', 'min:1'],
]);
if (isset($token->meta->state) && $token->meta->state === 'authorized') {
return redirect()
->route('client.payment_methods.show', $token->hashed_id)
@ -105,7 +109,7 @@ class ACH
$this->stripe->init();
$bank_account = Customer::retrieveSource($request->customer, ['source' => $request->source], $this->stripe->stripe_connect_auth);
$bank_account = Customer::retrieveSource($request->customer, $request->source, [], $this->stripe->stripe_connect_auth);
try {
$bank_account->verify(['amounts' => request()->transactions]);
@ -183,7 +187,7 @@ class ACH
return $this->processUnsuccessfulPayment($state);
} catch (Exception $e) {
if ($e instanceof CardException) {
return redirect()->route('client.payment_methods.verification', ['payment_method' => ClientGatewayToken::first()->hashed_id, 'method' => GatewayType::BANK_TRANSFER]);
return redirect()->route('client.payment_methods.verification', ['payment_method' => $source->hashed_id, 'method' => GatewayType::BANK_TRANSFER]);
}
throw new PaymentFailed($e->getMessage(), $e->getCode());

View File

@ -125,7 +125,7 @@ class ActivityRepository extends BaseRepository
$design = Design::find($entity_design_id);
if(!$entity->invitations()->exists()){
if(!$entity->invitations()->exists() || !$design){
nlog("No invitations for entity {$entity->id} - {$entity->number}");
return;
}

View File

@ -162,7 +162,7 @@ class PaymentRepository extends BaseRepository {
if ( ! $is_existing_payment && ! $this->import_mode ) {
if (array_key_exists('email_receipt', $data) && $data['email_receipt'] == true)
if (array_key_exists('email_receipt', $data) && $data['email_receipt'] == 'true')
$payment->service()->sendEmail();
elseif(!array_key_exists('email_receipt', $data) && $payment->client->getSetting('client_manual_payment_notification'))
$payment->service()->sendEmail();

View File

@ -12,6 +12,7 @@
namespace App\Services\Client;
use App\Models\Client;
use App\Services\Client\Merge;
use App\Services\Client\PaymentMethod;
use App\Utils\Number;
use Illuminate\Database\Eloquent\Collection;
@ -77,6 +78,13 @@ class ClientService
return (new PaymentMethod($this->client, $amount))->run();
}
public function merge(Client $mergable_client)
{
$this->client = (new Merge($this->client, $mergable_client))->run();
return $this;
}
public function save() :Client
{
$this->client->save();

View File

@ -0,0 +1,102 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Client;
use App\Factory\CompanyLedgerFactory;
use App\Models\Activity;
use App\Models\Client;
use App\Models\CompanyGateway;
use App\Models\CompanyLedger;
use App\Models\GatewayType;
use App\Models\Invoice;
use App\Models\Payment;
use App\Services\AbstractService;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
class Merge extends AbstractService
{
public $client;
public $mergable_client;
public function __construct(Client $client, Client $mergable_client)
{
$this->client = $client;
$this->mergable_client = $mergable_client;
}
public function run()
{
$this->client->balance += $this->mergable_client->balance;
$this->client->paid_to_date += $this->mergable_client->paid_to_date;
$this->client->save();
$this->updateLedger($this->mergable_client->balance);
$this->mergable_client->activities()->update(['client_id' => $this->client->id]);
$this->mergable_client->contacts()->update(['client_id' => $this->client->id]);
$this->mergable_client->gateway_tokens()->update(['client_id' => $this->client->id]);
$this->mergable_client->credits()->update(['client_id' => $this->client->id]);
$this->mergable_client->expenses()->update(['client_id' => $this->client->id]);
$this->mergable_client->invoices()->update(['client_id' => $this->client->id]);
$this->mergable_client->payments()->update(['client_id' => $this->client->id]);
$this->mergable_client->projects()->update(['client_id' => $this->client->id]);
$this->mergable_client->quotes()->update(['client_id' => $this->client->id]);
$this->mergable_client->recurring_invoices()->update(['client_id' => $this->client->id]);
$this->mergable_client->tasks()->update(['client_id' => $this->client->id]);
$this->mergable_client->documents()->update(['documentable_id' => $this->client->id]);
/* Loop through contacts an only merge distinct contacts by email */
$this->mergable_client->contacts->each(function ($contact){
$exist = $this->client->contacts->contains(function ($client_contact) use($contact){
return $client_contact->email == $contact->email;
});
if($exist)
{
$contact->delete();
$contact->save();
}
});
$this->mergable_client->forceDelete();
return $this->client;
}
private function updateLedger($adjustment)
{
$balance = 0;
$company_ledger = CompanyLedger::whereClientId($this->client->id)
->orderBy('id', 'DESC')
->first();
if ($company_ledger) {
$balance = $company_ledger->balance;
}
$company_ledger = CompanyLedgerFactory::create($this->client->company_id, $this->client->user_id);
$company_ledger->client_id = $this->client->id;
$company_ledger->adjustment = $adjustment;
$company_ledger->notes = "Balance update after merging " . $this->mergable_client->present()->name();
$company_ledger->balance = $balance + $adjustment;
$company_ledger->activity_id = Activity::UPDATE_CLIENT;
$company_ledger->save();
}
}

View File

@ -74,6 +74,8 @@ class PaymentMethod
->company
->company_gateways
->whereIn('id', $transformed_ids)
->where('is_deleted', false)
->whereNull('deleted_at')
->where('gateway_key', '!=', '54faab2ab6e3223dbe848b1686490baa')
->sortby(function ($model) use ($transformed_ids) { //company gateways are sorted in order of priority
return array_search($model->id, $transformed_ids);// this closure sorts for us
@ -85,6 +87,7 @@ class PaymentMethod
->company
->company_gateways
->where('gateway_key', '!=', '54faab2ab6e3223dbe848b1686490baa')
->whereNull('deleted_at')
->where('is_deleted', false);
}
@ -107,6 +110,8 @@ class PaymentMethod
->company
->company_gateways
->whereIn('id', $transformed_ids)
->where('is_deleted', false)
->whereNull('deleted_at')
->where('gateway_key', '54faab2ab6e3223dbe848b1686490baa')
->sortby(function ($model) use ($transformed_ids) { //company gateways are sorted in order of priority
return array_search($model->id, $transformed_ids);// this closure sorts for us
@ -118,6 +123,7 @@ class PaymentMethod
->company
->company_gateways
->where('gateway_key', '54faab2ab6e3223dbe848b1686490baa')
->whereNull('deleted_at')
->where('is_deleted', false);
}

View File

@ -99,6 +99,10 @@ class AutoBillInvoice extends AbstractService
->setPaymentHash($payment_hash)
->tokenBilling($gateway_token, $payment_hash);
if($payment){
info("Auto Bill payment captured for ".$this->invoice->number);
}
return $this->invoice;
}

View File

@ -160,6 +160,7 @@ class Design extends BaseDesign
if ($this->type == 'delivery_note') {
$elements = [
['element' => 'p', 'content' => ctrans('texts.delivery_note'), 'properties' => ['data-ref' => 'delivery_note-label', 'style' => 'font-weight: bold; text-transform: uppercase']],
['element' => 'p', 'content' => $this->entity->client->name, 'show_empty' => false, 'properties' => ['data-ref' => 'delivery_note-client.name']],
['element' => 'p', 'content' => $this->entity->client->shipping_address1, 'show_empty' => false, 'properties' => ['data-ref' => 'delivery_note-client.shipping_address1']],
['element' => 'p', 'content' => $this->entity->client->shipping_address2, 'show_empty' => false, 'properties' => ['data-ref' => 'delivery_note-client.shipping_address2']],

View File

@ -12,6 +12,8 @@
namespace App\Services\PdfMaker;
use League\CommonMark\CommonMarkConverter;
class PdfMaker
{
use PdfMakerUtilities;
@ -36,6 +38,9 @@ class PdfMaker
private $options;
/** @var CommonMarkConverter */
protected $commonmark;
public function __construct(array $data)
{
$this->data = $data;
@ -43,6 +48,10 @@ class PdfMaker
if (array_key_exists('options', $data)) {
$this->options = $data['options'];
}
$this->commonmark = new CommonMarkConverter([
'allow_unsafe_links' => false,
]);
}
public function design(Design $design)

View File

@ -91,6 +91,12 @@ trait PdfMakerUtilities
foreach ($children as $child) {
$contains_html = false;
if ($child['element'] !== 'script') {
$child['content'] = $this->commonmark->convertToHtml($child['content'] ?? '');
}
// $child['content'] = array_key_exists('content', $child) ? nl2br($child['content']) : '';
if (isset($child['content'])) {
if (isset($child['is_empty']) && $child['is_empty'] === true) {
continue;

View File

@ -39,7 +39,7 @@ class UserService
$nmo->to_user = $this->user;
$nmo->settings = $company->settings;
NinjaMailerJob::dispatch($nmo);
NinjaMailerJob::dispatch($nmo, true);
Ninja::registerNinjaUser($this->user);

View File

@ -130,6 +130,7 @@ class HtmlEngine
$data['$terms'] = &$data['$entity.terms'];
$data['$view_link'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_invoice').'</a>', 'label' => ctrans('texts.view_invoice')];
$data['$viewLink'] = &$data['$view_link'];
$data['$viewButton'] = &$data['$view_link'];
$data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_invoice')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.invoice_date')];
@ -422,6 +423,10 @@ class HtmlEngine
$data['$autoBill'] = ['value' => ctrans('texts.auto_bill_notification_placeholder'), 'label' => ''];
$data['$auto_bill'] = &$data['$autoBill'];
/*Payment Aliases*/
$data['$paymentLink'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_payment').'</a>', 'label' => ctrans('texts.view_payment')];
$data['$portalButton'] = &$data['$paymentLink'];
$arrKeysLength = array_map('strlen', array_keys($data));
array_multisort($arrKeysLength, SORT_DESC, $data);

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.2.16',
'app_tag' => '5.2.16',
'app_version' => '5.2.17',
'app_tag' => '5.2.17',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),

View File

@ -0,0 +1,40 @@
<?php
use App\Models\PaymentType;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Eloquent\Model;
class AddZellePaymentType extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Model::unguard();
$pt = PaymentType::where('name', 'Zelle')->first();
if(!$pt){
$payment_type = new PaymentType();
$payment_type->id = 33;
$payment_type->name = 'Zelle';
$payment_type->save();
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -70,6 +70,7 @@ class PaymentTypesSeeder extends Seeder
['name' => 'GoCardless', 'gateway_type_id' => self::GATEWAY_TYPE_GOCARDLESS],
['name' => 'Crypto', 'gateway_type_id' => self::GATEWAY_TYPE_CRYPTO],
['name' => 'Credit', 'gateway_type_id' => self::GATEWAY_TYPE_CREDIT],
['name' => 'Zelle'],
];
$x = 1;

View File

@ -32,7 +32,7 @@ const RESOURCES = {
"manifest.json": "ce1b79950eb917ea619a0a30da27c6a3",
"version.json": "3f9e03374a3e78d2cab3afd8723d0993",
"favicon.ico": "51636d3a390451561744c42188ccd628",
"main.dart.js": "2791c77ee5c4768ffdc2ef8aea29967c",
"main.dart.js": "02238eed5a325865e74b8a1e7afd03a6",
"/": "d389ab59423a76b2aaaa683ed382c78e"
};

197389
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

212086
public/main.foss.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3948,8 +3948,8 @@ https://invoiceninja.github.io/docs/migration/#troubleshooting',
'download_timeframe' => 'Use this link to download your files, the link will expire in 1 hour.',
'new_signup' => 'Neue Registrierung',
'new_signup_text' => 'Ein neuer Benutzer wurde von :user - :email von der IP: :ip erstellt',
'notification_payment_paid_subject' => 'Neue Zahlung von :Kunde',
'notification_partial_payment_paid_subject' => 'Neue Anzahlung von :Kunde',
'notification_payment_paid_subject' => 'Neue Zahlung von :client',
'notification_partial_payment_paid_subject' => 'Neue Anzahlung von :client',
'notification_payment_paid' => 'A payment of :amount was made by client :client towards :invoice',
'notification_partial_payment_paid' => 'A partial payment of :amount was made by client :client towards :invoice',
'notification_bot' => 'Benachrichtigungs-Bot',

View File

@ -4287,6 +4287,8 @@ $LANG = array(
'company_deleted' => 'Company deleted',
'company_deleted_body' => 'Company [ :company ] was deleted by :user',
'back_to' => 'Back to :url',
'stripe_connect_migration_title' => 'Connect your Stripe Account',
'stripe_connect_migration_desc' => 'Invoice Ninja v5 uses Stripe Connect to link your Stripe account to Invoice Ninja. This provides an additional layer of security for your account. Now that you data has migrated, you will need to Authorize Stripe to accept payments in v5.<br><br>To do this, navigate to Settings > Online Payments > Configure Gateways. Click on Stripe Connect and then under Settings click Setup Gateway. This will take you to Stripe to authorize Invoice Ninja and on your return your account will be successfully linked!',
);
return $LANG;

View File

@ -1,6 +1,7 @@
@component('email.template.admin', ['logo' => $logo, 'settings' => $settings])
<div class="center">
<h1>{{ ctrans('texts.max_companies') }}</h1>
<p>{{ ctrans('texts.max_companies_desc') }}</p>
</div>
@endcomponent

View File

@ -0,0 +1,7 @@
@component('email.template.admin', ['logo' => $logo, 'settings' => $settings])
<div class="center">
<h1>{{ ctrans('texts.stripe_connect_migration_title') }}</h1>
<p>{{ ctrans('texts.stripe_connect_migration_desc') }}</p>
</div>
@endcomponent

View File

@ -35,6 +35,16 @@
@endif
@endforeach
@if($this->showCopyBillingCheckbox())
@component('portal.ninja2020.components.general.card-element-single')
<div class="flex justify-end">
<button type="button" class="bg-gray-100 px-2 py-1 text-sm rounded" wire:click="handleCopyBilling">
{{ ctrans('texts.copy_billing') }}
</button>
</div>
@endcomponent
@endif
@component('portal.ninja2020.components.general.card-element-single')
<div class="flex flex-col items-end">
<button class="button button-primary bg-primary">

View File

@ -11,11 +11,23 @@
<input type="hidden" name="source" value="{{ $token->token }}">
@component('portal.ninja2020.components.general.card-element', ['title' => '#1 ' . ctrans('texts.amount_cents')])
<input type="text" name="transactions[]" class="w-full input" required dusk="verification-1st">
<input type="text" name="transactions[]" class="w-full input" required dusk="verification-1st" value="{{ old('transactions.0') }}">
@error('transactions.0')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
@endcomponent
@component('portal.ninja2020.components.general.card-element', ['title' => '#2 ' . ctrans('texts.amount_cents')])
<input type="text" name="transactions[]" class="w-full input" required dusk="verification-2nd">
<input type="text" name="transactions[]" class="w-full input" required dusk="verification-2nd" value="{{ old('transactions.1') }}">
@error('transactions.1')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
@endcomponent
@component('portal.ninja2020.gateways.includes.pay_now', ['type' => 'submit'])

View File

@ -58,6 +58,16 @@
.scrollIntoView({behavior: "smooth"});
});
Livewire.on('update-shipping-data', (event) => {
for (field in event) {
let element = document.querySelector(`input[name=${field}]`);
if (element) {
element.value = event[field];
}
}
});
document.addEventListener('DOMContentLoaded', function() {
let toggleWithToken = document.querySelector('.toggle-payment-with-token');

View File

@ -103,4 +103,31 @@ class ACHTest extends DuskTestCase
->assertSee('Payment method has been successfully removed.');
});
}
public function testIntegerAndMinimumValueOnVerification()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.payment_methods.index')
->press('Add Payment Method')
->clickLink('Bank Account')
->type('#account-holder-name', 'John Doe')
->select('#country', 'US')
->select('#currency', 'USD')
->type('#routing-number', '110000000')
->type('#account-number', '000123456789')
->check('#accept-terms')
->press('Add Payment Method')
->waitForText('ACH (Verification)', 60)
->type('@verification-1st', '0.1')
->type('@verification-2nd', '0')
->press('Complete Verification')
->assertSee('The transactions.0 must be an integer')
->assertSee('The transactions.1 must be at least 1')
->type('@verification-1st', '32')
->type('@verification-2nd', '45')
->press('Complete Verification')
->assertSee('Bank Transfer');
});
}
}

View File

@ -0,0 +1,180 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Feature\Client;
use App\DataMapper\ClientSettings;
use App\DataMapper\CompanySettings;
use App\Http\Livewire\CreditsTable;
use App\Models\Account;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\Credit;
use App\Models\User;
use App\Utils\Traits\AppSetup;
use Faker\Factory;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Livewire\Livewire;
use Tests\TestCase;
class ClientMergeTest extends TestCase
{
use DatabaseTransactions;
use AppSetup;
private $user;
private $company;
private $account;
public $client;
private $primary_contact;
public function setUp(): void
{
parent::setUp();
$this->faker = Factory::create();
$this->buildCache(true);
}
public function testSearchingForContacts()
{
$account = Account::factory()->create();
$this->user = User::factory()->create([
'account_id' => $account->id,
'email' => $this->faker->safeEmail
]);
$this->company = Company::factory()->create([
'account_id' => $account->id
]);
$this->client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id
]);
$this->primary_contact = ClientContact::factory()->create([
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'company_id' => $this->company->id,
'is_primary' => 1,
]);
ClientContact::factory()->count(2)->create([
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'company_id' => $this->company->id,
]);
ClientContact::factory()->create([
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'company_id' => $this->company->id,
'email' => 'search@gmail.com'
]);
$this->assertEquals(4, $this->client->contacts->count());
$this->assertTrue($this->client->contacts->contains(function ($contact) {
return $contact->email == 'search@gmail.com';
}));
$this->assertFalse($this->client->contacts->contains(function ($contact) {
return $contact->email == 'false@gmail.com';
}));
}
public function testMergeClients()
{
$account = Account::factory()->create();
$user = User::factory()->create([
'account_id' => $account->id,
'email' => $this->faker->safeEmail
]);
$company = Company::factory()->create([
'account_id' => $account->id
]);
$client = Client::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id
]);
$primary_contact = ClientContact::factory()->create([
'user_id' => $user->id,
'client_id' => $client->id,
'company_id' => $company->id,
'is_primary' => 1,
]);
ClientContact::factory()->count(2)->create([
'user_id' => $user->id,
'client_id' => $client->id,
'company_id' => $company->id,
]);
ClientContact::factory()->create([
'user_id' => $user->id,
'client_id' => $client->id,
'company_id' => $company->id,
'email' => 'search@gmail.com'
]);
//4contacts
$mergable_client = Client::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id
]);
$primary_contact = ClientContact::factory()->create([
'user_id' => $user->id,
'client_id' => $mergable_client->id,
'company_id' => $company->id,
'is_primary' => 1,
]);
ClientContact::factory()->count(2)->create([
'user_id' => $user->id,
'client_id' => $mergable_client->id,
'company_id' => $company->id,
]);
ClientContact::factory()->create([
'user_id' => $user->id,
'client_id' => $mergable_client->id,
'company_id' => $company->id,
'email' => 'search@gmail.com'
]);
//4 contacts
$this->assertEquals(4, $client->contacts->count());
$this->assertEquals(4, $mergable_client->contacts->count());
$client = $client->service()->merge($mergable_client)->save();
// nlog($client->contacts->fresh()->toArray());
// $this->assertEquals(7, $client->fresh()->contacts->count());
}
}