Merge remote-tracking branch 'origin/v5-develop' into v5-develop

# Conflicts:
#	resources/lang/en/texts.php
This commit is contained in:
Lars Kusch 2021-10-08 14:49:02 +02:00
commit 0d1596339e
76 changed files with 532712 additions and 530494 deletions

View File

@ -1 +1 @@
5.3.19 5.3.22

View File

@ -23,6 +23,7 @@ use App\Jobs\Util\SchedulerCheck;
use App\Jobs\Util\SendFailedEmails; use App\Jobs\Util\SendFailedEmails;
use App\Jobs\Util\UpdateExchangeRates; use App\Jobs\Util\UpdateExchangeRates;
use App\Jobs\Util\VersionCheck; use App\Jobs\Util\VersionCheck;
use App\Models\Account;
use App\Utils\Ninja; use App\Utils\Ninja;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@ -69,6 +70,10 @@ class Kernel extends ConsoleKernel
$schedule->job(new SchedulerCheck)->daily()->withoutOverlapping(); $schedule->job(new SchedulerCheck)->daily()->withoutOverlapping();
$schedule->call(function () {
Account::whereNotNull('id')->update(['is_scheduler_running' => true]);
})->everyFiveMinutes()->withoutOverlapping();
/* Run hosted specific jobs */ /* Run hosted specific jobs */
if (Ninja::isHosted()) { if (Ninja::isHosted()) {

View File

@ -28,9 +28,9 @@ class CloneQuoteToInvoiceFactory
unset($quote_array['invoice_id']); unset($quote_array['invoice_id']);
unset($quote_array['id']); unset($quote_array['id']);
unset($quote_array['invitations']); unset($quote_array['invitations']);
unset($quote_array['terms']); //unset($quote_array['terms']);
// unset($quote_array['public_notes']); //unset($quote_array['public_notes']);
unset($quote_array['footer']); //unset($quote_array['footer']);
unset($quote_array['design_id']); unset($quote_array['design_id']);
foreach ($quote_array as $key => $value) { foreach ($quote_array as $key => $value) {

View File

@ -67,9 +67,10 @@ class ContactResetPasswordController extends Controller
$account_id = $request->get('account_id'); $account_id = $request->get('account_id');
$account = Account::find($account_id); $account = Account::find($account_id);
$db = $account->companies->first()->db; $db = $account->companies->first()->db;
$company = $account->companies->first();
return $this->render('auth.passwords.reset')->with( return $this->render('auth.passwords.reset')->with(
['token' => $token, 'email' => $request->email, 'account' => $account, 'db' => $db] ['token' => $token, 'email' => $request->email, 'account' => $account, 'db' => $db, 'company' => $company]
); );
} }

View File

@ -19,7 +19,10 @@ use App\Http\Controllers\Controller;
use App\Jobs\Entity\CreateRawPdf; use App\Jobs\Entity\CreateRawPdf;
use App\Models\Client; use App\Models\Client;
use App\Models\ClientContact; use App\Models\ClientContact;
use App\Models\InvoiceInvitation;
use App\Models\Payment; use App\Models\Payment;
use App\Services\ClientPortal\InstantPayment;
use App\Utils\CurlUtils;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
@ -65,11 +68,17 @@ class InvitationController extends Controller
private function genericRouter(string $entity, string $invitation_key) private function genericRouter(string $entity, string $invitation_key)
{ {
if(!in_array($entity, ['invoice', 'credit', 'quote', 'recurring_invoice']))
return response()->json(['message' => 'Invalid resource request']);
$key = $entity.'_id'; $key = $entity.'_id';
$entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation'; $entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
$invitation = $entity_obj::whereRaw('BINARY `key`= ?', [$invitation_key]) $invitation = $entity_obj::where('key', $invitation_key)
->whereHas($entity, function ($query) {
$query->where('is_deleted',0);
})
->with('contact.client') ->with('contact.client')
->firstOrFail(); ->firstOrFail();
@ -133,6 +142,9 @@ class InvitationController extends Controller
private function returnRawPdf(string $entity, string $invitation_key) private function returnRawPdf(string $entity, string $invitation_key)
{ {
if(!in_array($entity, ['invoice', 'credit', 'quote', 'recurring_invoice']))
return response()->json(['message' => 'Invalid resource request']);
$key = $entity.'_id'; $key = $entity.'_id';
$entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation'; $entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
@ -181,4 +193,41 @@ class InvitationController extends Controller
return redirect()->route('client.payments.show', $payment->hashed_id); return redirect()->route('client.payments.show', $payment->hashed_id);
} }
public function payInvoice(Request $request, string $invitation_key)
{
$invitation = InvoiceInvitation::where('key', $invitation_key)
->with('contact.client')
->firstOrFail();
auth()->guard('contact')->login($invitation->contact, true);
$invoice = $invitation->invoice;
if($invoice->partial > 0)
$amount = round($invoice->partial, (int)$invoice->client->currency()->precision);
else
$amount = round($invoice->balance, (int)$invoice->client->currency()->precision);
$gateways = $invitation->contact->client->service()->getPaymentMethods($amount);
if(is_array($gateways))
{
$data = [
'company_gateway_id' => $gateways[0]['company_gateway_id'],
'payment_method_id' => $gateways[0]['gateway_type_id'],
'payable_invoices' => [
['invoice_id' => $invitation->invoice->hashed_id, 'amount' => $amount],
],
'signature' => false
];
$request->replace($data);
return (new InstantPayment($request))->run();
}
abort(404, "Invoice not found");
}
} }

View File

@ -23,6 +23,7 @@ use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentHash; use App\Models\PaymentHash;
use App\Models\SystemLog; use App\Models\SystemLog;
use App\Services\ClientPortal\InstantPayment;
use App\Services\Subscription\SubscriptionService; use App\Services\Subscription\SubscriptionService;
use App\Utils\Number; use App\Utils\Number;
use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesDates;
@ -79,235 +80,7 @@ class PaymentController extends Controller
*/ */
public function process(Request $request) public function process(Request $request)
{ {
$is_credit_payment = false; return (new InstantPayment($request))->run();
$tokens = [];
if ($request->input('company_gateway_id') == CompanyGateway::GATEWAY_CREDIT) {
$is_credit_payment = true;
}
$gateway = CompanyGateway::find($request->input('company_gateway_id'));
/**
* find invoices
*
* ['invoice_id' => xxx, 'amount' => 22.00]
*/
$payable_invoices = collect($request->payable_invoices);
$invoices = Invoice::whereIn('id', $this->transformKeys($payable_invoices->pluck('invoice_id')->toArray()))->withTrashed()->get();
$invoices->each(function($invoice){
$invoice->service()->removeUnpaidGatewayFees()->save();
});
/* pop non payable invoice from the $payable_invoices array */
$payable_invoices = $payable_invoices->filter(function ($payable_invoice) use ($invoices) {
return $invoices->where('hashed_id', $payable_invoice['invoice_id'])->first()->isPayable();
});
/*return early if no invoices*/
if ($payable_invoices->count() == 0) {
return redirect()
->route('client.invoices.index')
->with(['message' => 'No payable invoices selected.']);
}
$settings = auth()->user()->client->getMergedSettings();
// nlog($settings);
/* This loop checks for under / over payments and returns the user if a check fails */
foreach ($payable_invoices as $payable_invoice) {
/*Match the payable invoice to the Model Invoice*/
$invoice = $invoices->first(function ($inv) use ($payable_invoice) {
return $payable_invoice['invoice_id'] == $inv->hashed_id;
});
/*
* Check if company supports over & under payments.
* Determine the payable amount and the max payable. ie either partial or invoice balance
*/
$payable_amount = Number::roundValue(Number::parseFloat($payable_invoice['amount']), auth()->user()->client->currency()->precision);
$invoice_balance = Number::roundValue(($invoice->partial > 0 ? $invoice->partial : $invoice->balance), auth()->user()->client->currency()->precision);
/*If we don't allow under/over payments force the payable amount - prevents inspect element adjustments in JS*/
if ($settings->client_portal_allow_under_payment == false && $settings->client_portal_allow_over_payment == false) {
$payable_invoice['amount'] = Number::roundValue(($invoice->partial > 0 ? $invoice->partial : $invoice->balance), auth()->user()->client->currency()->precision);
}
if (!$settings->client_portal_allow_under_payment && $payable_amount < $invoice_balance) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.minimum_required_payment', ['amount' => $invoice_balance]));
}
if ($settings->client_portal_allow_under_payment) {
if ($invoice_balance < $settings->client_portal_under_payment_minimum && $payable_amount < $invoice_balance) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.minimum_required_payment', ['amount' => $invoice_balance]));
}
if ($invoice_balance < $settings->client_portal_under_payment_minimum) {
// Skip the under payment rule.
}
if ($invoice_balance >= $settings->client_portal_under_payment_minimum && $payable_amount < $settings->client_portal_under_payment_minimum) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.minimum_required_payment', ['amount' => $settings->client_portal_under_payment_minimum]));
}
}
/* If we don't allow over payments and the amount exceeds the balance */
if (!$settings->client_portal_allow_over_payment && $payable_amount > $invoice_balance) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.over_payments_disabled'));
}
}
/*Iterate through invoices and add gateway fees and other payment metadata*/
//$payable_invoices = $payable_invoices->map(function ($payable_invoice) use ($invoices, $settings) {
$payable_invoice_collection = collect();
foreach ($payable_invoices as $payable_invoice) {
// nlog($payable_invoice);
$payable_invoice['amount'] = Number::parseFloat($payable_invoice['amount']);
$invoice = $invoices->first(function ($inv) use ($payable_invoice) {
return $payable_invoice['invoice_id'] == $inv->hashed_id;
});
$payable_amount = Number::roundValue(Number::parseFloat($payable_invoice['amount']), auth()->user()->client->currency()->precision);
$invoice_balance = Number::roundValue($invoice->balance, auth()->user()->client->currency()->precision);
$payable_invoice['due_date'] = $this->formatDate($invoice->due_date, $invoice->client->date_format());
$payable_invoice['invoice_number'] = $invoice->number;
if (isset($invoice->po_number)) {
$additional_info = $invoice->po_number;
} elseif (isset($invoice->public_notes)) {
$additional_info = $invoice->public_notes;
} else {
$additional_info = $invoice->date;
}
$payable_invoice['additional_info'] = $additional_info;
$payable_invoice_collection->push($payable_invoice);
}
//});
if (request()->has('signature') && !is_null(request()->signature) && !empty(request()->signature)) {
$invoices->each(function ($invoice) use ($request) {
InjectSignature::dispatch($invoice, $request->signature);
});
}
$payable_invoices = $payable_invoice_collection;
$payment_method_id = $request->input('payment_method_id');
$invoice_totals = $payable_invoices->sum('amount');
$first_invoice = $invoices->first();
$credit_totals = $first_invoice->client->getSetting('use_credits_payment') == 'always' ? $first_invoice->client->service()->getCreditBalance() : 0;
$starting_invoice_amount = $first_invoice->balance;
if ($gateway) {
$first_invoice->service()->addGatewayFee($gateway, $payment_method_id, $invoice_totals)->save();
}
/**
* Gateway fee is calculated
* by adding it as a line item, and then subtract
* the starting and finishing amounts of the invoice.
*/
$fee_totals = $first_invoice->balance - $starting_invoice_amount;
if ($gateway) {
$tokens = auth()->user()->client->gateway_tokens()
->whereCompanyGatewayId($gateway->id)
->whereGatewayTypeId($payment_method_id)
->get();
}
if(!$is_credit_payment){
$credit_totals = 0;
}
$hash_data = ['invoices' => $payable_invoices->toArray(), 'credits' => $credit_totals, 'amount_with_fee' => max(0, (($invoice_totals + $fee_totals) - $credit_totals))];
if ($request->query('hash')) {
$hash_data['billing_context'] = Cache::get($request->query('hash'));
}
$payment_hash = new PaymentHash;
$payment_hash->hash = Str::random(32);
$payment_hash->data = $hash_data;
$payment_hash->fee_total = $fee_totals;
$payment_hash->fee_invoice_id = $first_invoice->id;
$payment_hash->save();
if($is_credit_payment){
$amount_with_fee = max(0, (($invoice_totals + $fee_totals) - $credit_totals));
}
else{
$credit_totals = 0;
$amount_with_fee = max(0, $invoice_totals + $fee_totals);
}
$totals = [
'credit_totals' => $credit_totals,
'invoice_totals' => $invoice_totals,
'fee_total' => $fee_totals,
'amount_with_fee' => $amount_with_fee,
];
$data = [
'payment_hash' => $payment_hash->hash,
'total' => $totals,
'invoices' => $payable_invoices,
'tokens' => $tokens,
'payment_method_id' => $payment_method_id,
'amount_with_fee' => $invoice_totals + $fee_totals,
];
if ($is_credit_payment || $totals <= 0) {
return $this->processCreditPayment($request, $data);
}
try {
return $gateway
->driver(auth()->user()->client)
->setPaymentMethod($payment_method_id)
->setPaymentHash($payment_hash)
->checkRequirements()
->processPaymentView($data);
} catch (\Exception $e) {
SystemLogger::dispatch(
$e->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_ERROR,
SystemLog::TYPE_FAILURE,
auth('contact')->user()->client,
auth('contact')->user()->client->company
);
throw new PaymentFailed($e->getMessage());
}
} }
public function response(PaymentResponseRequest $request) public function response(PaymentResponseRequest $request)

View File

@ -127,7 +127,7 @@ class EmailController extends BaseController
$entity_obj->invitations->each(function ($invitation) use ($data, $entity_string, $entity_obj, $template) { $entity_obj->invitations->each(function ($invitation) use ($data, $entity_string, $entity_obj, $template) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) { if (!$invitation->contact->trashed() && $invitation->contact->email) {
$entity_obj->service()->markSent()->save(); $entity_obj->service()->markSent()->save();

View File

@ -234,21 +234,24 @@ class MigrationController extends BaseController
public function startMigration(Request $request) public function startMigration(Request $request)
{ {
// v4 Laravel 6
// $companies = [];
// foreach($request->all() as $input){
// if($input instanceof UploadedFile)
// nlog('is file');
// else
// $companies[] = json_decode($input);
// }
nlog("Starting Migration"); nlog("Starting Migration");
$companies = json_decode($request->companies,1); if($request->companies){
//handle Laravel 5.5 UniHTTP
$companies = json_decode($request->companies,1);
}
else {
//handle Laravel 6 Guzzle
$companies = [];
foreach($request->all() as $input){
if($input instanceof UploadedFile)
nlog('is file');
else
$companies[] = json_decode($input,1);
}
}
if (app()->environment() === 'local') { if (app()->environment() === 'local') {
nlog($request->all()); nlog($request->all());
@ -267,22 +270,11 @@ class MigrationController extends BaseController
foreach($companies as $company) foreach($companies as $company)
{ {
if(!is_array($company))
continue;
$company = (array)$company; $company = (array)$company;
// v4 Laravel 6
// $input = $request->all();
// foreach ($input as $company) {
// if($company instanceof UploadedFile)
// continue;
// else
// $company = json_decode($company,1);
// if (!$company || !is_int($company['company_index'] || !$request->file($company['company_index'])->isValid())) {
// continue;
// }
$user = auth()->user(); $user = auth()->user();
$company_count = $user->account->companies()->count(); $company_count = $user->account->companies()->count();

View File

@ -216,7 +216,7 @@ class PreviewController extends BaseController
if(!$request->has('entity_id')) if(!$request->has('entity_id'))
$entity_obj->service()->fillDefaults()->save(); $entity_obj->service()->fillDefaults()->save();
$entity_obj->load('client'); $entity_obj->load('client.contacts','company');
App::forgetInstance('translator'); App::forgetInstance('translator');
$t = app('translator'); $t = app('translator');
@ -345,7 +345,7 @@ class PreviewController extends BaseController
$invoice->setRelation('invitations', $invitation); $invoice->setRelation('invitations', $invitation);
$invoice->setRelation('client', $client); $invoice->setRelation('client', $client);
$invoice->setRelation('company', auth()->user()->company()); $invoice->setRelation('company', auth()->user()->company());
$invoice->load('client'); $invoice->load('client.company');
// nlog(print_r($invoice->toArray(),1)); // nlog(print_r($invoice->toArray(),1));

View File

@ -41,8 +41,9 @@ class CreditsTable extends Component
->where('status_id', '<>', Credit::STATUS_DRAFT) ->where('status_id', '<>', Credit::STATUS_DRAFT)
->where('is_deleted', 0) ->where('is_deleted', 0)
->where(function ($query){ ->where(function ($query){
$query->whereDate('due_date', '<=', now()) $query->whereDate('due_date', '>=', now())
->orWhereNull('due_date'); ->orWhereNull('due_date')
->orWhere('due_date', '=', '');
}) })
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->withTrashed() ->withTrashed()

View File

@ -52,7 +52,7 @@ class QueryLogging
$timeEnd = microtime(true); $timeEnd = microtime(true);
$time = $timeEnd - $timeStart; $time = $timeEnd - $timeStart;
// nlog("Query count = {$count}"); // info("Query count = {$count}");
if($count > 175){ if($count > 175){
nlog("Query count = {$count}"); nlog("Query count = {$count}");

View File

@ -42,6 +42,9 @@ class SetInviteDb
$entity = $request->route('entity'); $entity = $request->route('entity');
} }
if($entity == "pay")
$entity = "invoice";
if ($request->getSchemeAndHttpHost() && config('ninja.db.multi_db_enabled') && ! MultiDB::findAndSetDbByInvitation($entity, $request->route('invitation_key'))) { if ($request->getSchemeAndHttpHost() && config('ninja.db.multi_db_enabled') && ! MultiDB::findAndSetDbByInvitation($entity, $request->route('invitation_key'))) {
if (request()->json) { if (request()->json) {
return response()->json($error, 403); return response()->json($error, 403);

View File

@ -1331,6 +1331,12 @@ class CompanyImport implements ShouldQueue
$new_obj->save(['timestamps' => false]); $new_obj->save(['timestamps' => false]);
$new_obj->number = $this->getNextQuoteNumber($client = Client::find($obj_array['client_id']), $new_obj); $new_obj->number = $this->getNextQuoteNumber($client = Client::find($obj_array['client_id']), $new_obj);
} }
elseif($class == 'App\Models\ClientContact'){
$new_obj = new ClientContact();
$new_obj->company_id = $this->company->id;
$new_obj->fill($obj_array);
$new_obj->save(['timestamps' => false]);
}
else{ else{
$new_obj = $class::firstOrNew( $new_obj = $class::firstOrNew(
[$match_key => $obj->{$match_key}, 'company_id' => $this->company->id], [$match_key => $obj->{$match_key}, 'company_id' => $this->company->id],

View File

@ -50,6 +50,8 @@ class AutoBill
MultiDB::setDb($this->db); MultiDB::setDb($this->db);
try{ try{
nlog("autobill {$this->invoice->id}");
$this->invoice->service()->autoBill()->save(); $this->invoice->service()->autoBill()->save();

View File

@ -25,6 +25,8 @@ class AutoBillCron
public $tries = 1; public $tries = 1;
private $counter = 1;
/** /**
* Create a new job instance. * Create a new job instance.
* *
@ -56,6 +58,7 @@ class AutoBillCron
->whereHas('company', function ($query) { ->whereHas('company', function ($query) {
$query->where('is_disabled',0); $query->where('is_disabled',0);
}) })
->orderBy('id', 'DESC')
->with('company'); ->with('company');
nlog($auto_bill_partial_invoices->count(). " partial invoices to auto bill"); nlog($auto_bill_partial_invoices->count(). " partial invoices to auto bill");
@ -72,6 +75,7 @@ class AutoBillCron
->whereHas('company', function ($query) { ->whereHas('company', function ($query) {
$query->where('is_disabled',0); $query->where('is_disabled',0);
}) })
->orderBy('id', 'DESC')
->with('company'); ->with('company');
nlog($auto_bill_invoices->count(). " full invoices to auto bill"); nlog($auto_bill_invoices->count(). " full invoices to auto bill");
@ -95,6 +99,7 @@ class AutoBillCron
->whereHas('company', function ($query) { ->whereHas('company', function ($query) {
$query->where('is_disabled',0); $query->where('is_disabled',0);
}) })
->orderBy('id', 'DESC')
->with('company'); ->with('company');
nlog($auto_bill_partial_invoices->count(). " partial invoices to auto bill db = {$db}"); nlog($auto_bill_partial_invoices->count(). " partial invoices to auto bill db = {$db}");
@ -111,19 +116,23 @@ class AutoBillCron
->whereHas('company', function ($query) { ->whereHas('company', function ($query) {
$query->where('is_disabled',0); $query->where('is_disabled',0);
}) })
->orderBy('id', 'DESC')
->with('company'); ->with('company');
nlog($auto_bill_invoices->count(). " full invoices to auto bill db = {$db}"); nlog($auto_bill_invoices->count(). " full invoices to auto bill db = {$db}");
$auto_bill_invoices->cursor()->each(function ($invoice) use($db){ $auto_bill_invoices->cursor()->each(function ($invoice) use($db){
nlog($this->counter);
AutoBill::dispatch($invoice, $db); AutoBill::dispatch($invoice, $db);
$this->counter++;
}); });
} }
nlog("fine"); nlog("Auto Bill - fine");
} }
} }

View File

@ -59,6 +59,8 @@ class CreateEntityPdf implements ShouldQueue
public $entity_string = ''; public $entity_string = '';
public $client;
/** /**
* Create a new job instance. * Create a new job instance.
* *
@ -69,15 +71,19 @@ class CreateEntityPdf implements ShouldQueue
$this->invitation = $invitation; $this->invitation = $invitation;
if ($invitation instanceof InvoiceInvitation) { if ($invitation instanceof InvoiceInvitation) {
// $invitation->load('contact.client.company','invoice.client','invoice.user.account');
$this->entity = $invitation->invoice; $this->entity = $invitation->invoice;
$this->entity_string = 'invoice'; $this->entity_string = 'invoice';
} elseif ($invitation instanceof QuoteInvitation) { } elseif ($invitation instanceof QuoteInvitation) {
// $invitation->load('contact.client.company','quote.client','quote.user.account');
$this->entity = $invitation->quote; $this->entity = $invitation->quote;
$this->entity_string = 'quote'; $this->entity_string = 'quote';
} elseif ($invitation instanceof CreditInvitation) { } elseif ($invitation instanceof CreditInvitation) {
// $invitation->load('contact.client.company','credit.client','credit.user.account');
$this->entity = $invitation->credit; $this->entity = $invitation->credit;
$this->entity_string = 'credit'; $this->entity_string = 'credit';
} elseif ($invitation instanceof RecurringInvoiceInvitation) { } elseif ($invitation instanceof RecurringInvoiceInvitation) {
// $invitation->load('contact.client.company','recurring_invoice');
$this->entity = $invitation->recurring_invoice; $this->entity = $invitation->recurring_invoice;
$this->entity_string = 'recurring_invoice'; $this->entity_string = 'recurring_invoice';
} }
@ -86,6 +92,8 @@ class CreateEntityPdf implements ShouldQueue
$this->contact = $invitation->contact; $this->contact = $invitation->contact;
$this->client = $invitation->contact->client;
$this->disk = Ninja::isHosted() ? config('filesystems.default') : $disk; $this->disk = Ninja::isHosted() ? config('filesystems.default') : $disk;
} }
@ -102,7 +110,7 @@ class CreateEntityPdf implements ShouldQueue
App::setLocale($this->contact->preferredLocale()); App::setLocale($this->contact->preferredLocale());
/* Set customized translations _NOW_ */ /* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->entity->client->getMergedSettings())); $t->replace(Ninja::transformTranslations($this->client->getMergedSettings()));
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') { if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
return (new Phantom)->generate($this->invitation); return (new Phantom)->generate($this->invitation);
@ -111,22 +119,22 @@ class CreateEntityPdf implements ShouldQueue
$entity_design_id = ''; $entity_design_id = '';
if ($this->entity instanceof Invoice) { if ($this->entity instanceof Invoice) {
$path = $this->entity->client->invoice_filepath($this->invitation); $path = $this->client->invoice_filepath($this->invitation);
$entity_design_id = 'invoice_design_id'; $entity_design_id = 'invoice_design_id';
} elseif ($this->entity instanceof Quote) { } elseif ($this->entity instanceof Quote) {
$path = $this->entity->client->quote_filepath($this->invitation); $path = $this->client->quote_filepath($this->invitation);
$entity_design_id = 'quote_design_id'; $entity_design_id = 'quote_design_id';
} elseif ($this->entity instanceof Credit) { } elseif ($this->entity instanceof Credit) {
$path = $this->entity->client->credit_filepath($this->invitation); $path = $this->client->credit_filepath($this->invitation);
$entity_design_id = 'credit_design_id'; $entity_design_id = 'credit_design_id';
} elseif ($this->entity instanceof RecurringInvoice) { } elseif ($this->entity instanceof RecurringInvoice) {
$path = $this->entity->client->recurring_invoice_filepath($this->invitation); $path = $this->client->recurring_invoice_filepath($this->invitation);
$entity_design_id = 'invoice_design_id'; $entity_design_id = 'invoice_design_id';
} }
$file_path = $path.$this->entity->numberFormatter().'.pdf'; $file_path = $path.$this->entity->numberFormatter().'.pdf';
$entity_design_id = $this->entity->design_id ? $this->entity->design_id : $this->decodePrimaryKey($this->entity->client->getSetting($entity_design_id)); $entity_design_id = $this->entity->design_id ? $this->entity->design_id : $this->decodePrimaryKey($this->client->getSetting($entity_design_id));
// if(!$this->company->account->hasFeature(Account::FEATURE_DIFFERENT_DESIGNS)) // if(!$this->company->account->hasFeature(Account::FEATURE_DIFFERENT_DESIGNS))
// $entity_design_id = 2; // $entity_design_id = 2;
@ -152,18 +160,18 @@ class CreateEntityPdf implements ShouldQueue
$state = [ $state = [
'template' => $template->elements([ 'template' => $template->elements([
'client' => $this->entity->client, 'client' => $this->client,
'entity' => $this->entity, 'entity' => $this->entity,
'pdf_variables' => (array) $this->entity->company->settings->pdf_variables, 'pdf_variables' => (array) $this->company->settings->pdf_variables,
'$product' => $design->design->product, '$product' => $design->design->product,
'variables' => $variables, 'variables' => $variables,
]), ]),
'variables' => $variables, 'variables' => $variables,
'options' => [ 'options' => [
'all_pages_header' => $this->entity->client->getSetting('all_pages_header'), 'all_pages_header' => $this->client->getSetting('all_pages_header'),
'all_pages_footer' => $this->entity->client->getSetting('all_pages_footer'), 'all_pages_footer' => $this->client->getSetting('all_pages_footer'),
], ],
'process_markdown' => $this->entity->client->company->markdown_enabled, 'process_markdown' => $this->client->company->markdown_enabled,
]; ];
$maker = new PdfMakerService($state); $maker = new PdfMakerService($state);

View File

@ -74,12 +74,15 @@ class EmailPayment implements ShouldQueue
MultiDB::setDb($this->company->db); MultiDB::setDb($this->company->db);
$this->payment->load('invoices');
$this->contact->load('client');
$email_builder = (new PaymentEmailEngine($this->payment, $this->contact))->build(); $email_builder = (new PaymentEmailEngine($this->payment, $this->contact))->build();
$invitation = null; $invitation = null;
if($this->payment->invoices()->exists()) if($this->payment->invoices && $this->payment->invoices->count() >=1)
$invitation = $this->payment->invoices()->first()->invitations()->first(); $invitation = $this->payment->invoices->first()->invitations()->first();
$nmo = new NinjaMailerObject; $nmo = new NinjaMailerObject;
$nmo->mailable = new TemplateEmail($email_builder, $this->contact, $invitation); $nmo->mailable = new TemplateEmail($email_builder, $this->contact, $invitation);

View File

@ -59,15 +59,15 @@ class SendRecurring implements ShouldQueue
public function handle() : void public function handle() : void
{ {
//reset all contacts here //reset all contacts here
$this->recurring_invoice->client->contacts()->update(['send_email' => false]); // $this->recurring_invoice->client->contacts()->update(['send_email' => false]);
$this->recurring_invoice->invitations->each(function ($invitation){ // $this->recurring_invoice->invitations->each(function ($invitation){
$contact = $invitation->contact; // $contact = $invitation->contact;
$contact->send_email = true; // $contact->send_email = true;
$contact->save(); // $contact->save();
}); // });
// Generate Standard Invoice // Generate Standard Invoice
$invoice = RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client); $invoice = RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client);
@ -153,7 +153,7 @@ class SendRecurring implements ShouldQueue
} }
//important catch all here - we should never leave contacts send_email to false incase they are permanently set to false in the future. //important catch all here - we should never leave contacts send_email to false incase they are permanently set to false in the future.
$this->recurring_invoice->client->contacts()->update(['send_email' => true]); // $this->recurring_invoice->client->contacts()->update(['send_email' => true]);
} }

View File

@ -237,9 +237,12 @@ class Import implements ShouldQueue
//company size check //company size check
if ($this->company->invoices()->count() > 500 || $this->company->products()->count() > 500 || $this->company->clients()->count() > 500) { if ($this->company->invoices()->count() > 500 || $this->company->products()->count() > 500 || $this->company->clients()->count() > 500) {
$this->company->is_large = true; $this->company->is_large = true;
$this->company->save();
} }
$this->company->client_registration_fields = \App\DataMapper\ClientRegistrationFields::generate();
$this->company->save();
$this->setInitialCompanyLedgerBalances(); $this->setInitialCompanyLedgerBalances();
// $this->fixClientBalances(); // $this->fixClientBalances();

View File

@ -38,8 +38,6 @@ class SchedulerCheck implements ShouldQueue
{ {
set_time_limit(0); set_time_limit(0);
Account::whereNotNull('id')->update(['is_scheduler_running' => true]);
if(config('ninja.app_version') != base_path('VERSION.txt')) if(config('ninja.app_version') != base_path('VERSION.txt'))
{ {

View File

@ -47,6 +47,7 @@ class PaymentEmailEngine extends BaseEmailEngine
$this->company = $payment->company; $this->company = $payment->company;
$this->client = $payment->client; $this->client = $payment->client;
$this->contact = $contact ?: $this->client->primary_contact()->first(); $this->contact = $contact ?: $this->client->primary_contact()->first();
$this->contact->load('client.company');
$this->settings = $this->client->getMergedSettings(); $this->settings = $this->client->getMergedSettings();
$this->template_data = $template_data; $this->template_data = $template_data;
$this->helpers = new Helpers(); $this->helpers = new Helpers();

View File

@ -119,6 +119,10 @@ class Activity extends StaticModel
'hashed_id', 'hashed_id',
]; ];
protected $with = [
'backup',
];
public function getHashedIdAttribute() public function getHashedIdAttribute()
{ {
return $this->encodePrimaryKey($this->id); return $this->encodePrimaryKey($this->id);

View File

@ -84,12 +84,12 @@ class Client extends BaseModel implements HasLocalePreference
]; ];
protected $with = [ protected $with = [
'gateway_tokens', // 'gateway_tokens',
'documents', // 'documents',
'contacts.company', // 'contacts.company',
// 'currency', // 'currency',
// 'primary_contact', // 'primary_contact',
'country', // 'country',
// 'contacts', // 'contacts',
// 'shipping_country', // 'shipping_country',
// 'company', // 'company',

View File

@ -93,6 +93,7 @@ class Gateway extends StaticModel
GatewayType::BANK_TRANSFER => ['refund' => false, 'token_billing' => true], GatewayType::BANK_TRANSFER => ['refund' => false, 'token_billing' => true],
GatewayType::KBC => ['refund' => false, 'token_billing' => false], GatewayType::KBC => ['refund' => false, 'token_billing' => false],
GatewayType::BANCONTACT => ['refund' => false, 'token_billing' => false], GatewayType::BANCONTACT => ['refund' => false, 'token_billing' => false],
GatewayType::IDEAL => ['refund' => false, 'token_billing' => false],
]; ];
case 15: case 15:
return [GatewayType::PAYPAL => ['refund' => true, 'token_billing' => false]]; //Paypal return [GatewayType::PAYPAL => ['refund' => true, 'token_billing' => false]]; //Paypal

View File

@ -27,6 +27,7 @@ class GatewayType extends StaticModel
const CREDIT = 10; const CREDIT = 10;
const KBC = 11; const KBC = 11;
const BANCONTACT = 12; const BANCONTACT = 12;
const IDEAL = 13;
public function gateway() public function gateway()
{ {
@ -63,7 +64,8 @@ class GatewayType extends StaticModel
return ctrans('texts.kbc_cbc'); return ctrans('texts.kbc_cbc');
case self::BANCONTACT: case self::BANCONTACT:
return ctrans('texts.bancontact'); return ctrans('texts.bancontact');
case self::IDEAL:
return ctrans('texts.ideal');
default: default:
return 'Undefined.'; return 'Undefined.';
break; break;

View File

@ -45,6 +45,7 @@ class PaymentType extends StaticModel
const MOLLIE_BANK_TRANSFER = 34; const MOLLIE_BANK_TRANSFER = 34;
const KBC = 35; const KBC = 35;
const BANCONTACT = 36; const BANCONTACT = 36;
const IDEAL = 37;
public static function parseCardType($cardName) public static function parseCardType($cardName)
{ {

View File

@ -449,6 +449,10 @@ class RecurringInvoice extends BaseModel
public function calculateDueDate($date) public function calculateDueDate($date)
{ {
//if nothing is set, assume we are using terms.
if(!$this->due_date_days)
return $this->calculateDateFromTerms($date);
switch ($this->due_date_days) { switch ($this->due_date_days) {
case 'terms': case 'terms':
return $this->calculateDateFromTerms($date); return $this->calculateDateFromTerms($date);

View File

@ -64,6 +64,10 @@ class Subscription extends BaseModel
'deleted_at' => 'timestamp', 'deleted_at' => 'timestamp',
]; ];
protected $with = [
'company',
];
public function service(): SubscriptionService public function service(): SubscriptionService
{ {
return new SubscriptionService($this); return new SubscriptionService($this);

View File

@ -34,9 +34,11 @@ class InvoiceObserver
->where('event_id', Webhook::EVENT_CREATE_INVOICE) ->where('event_id', Webhook::EVENT_CREATE_INVOICE)
->exists(); ->exists();
$invoice->load('client');
if ($subscriptions) { if ($subscriptions) {
$invoice->load('client');
WebhookHandler::dispatch(Webhook::EVENT_CREATE_INVOICE, $invoice, $invoice->company); WebhookHandler::dispatch(Webhook::EVENT_CREATE_INVOICE, $invoice, $invoice->company);
} }
} }
@ -53,11 +55,14 @@ class InvoiceObserver
->where('event_id', Webhook::EVENT_UPDATE_INVOICE) ->where('event_id', Webhook::EVENT_UPDATE_INVOICE)
->exists(); ->exists();
$invoice->load('client');
if ($subscriptions) { if ($subscriptions) {
$invoice->load('client');
WebhookHandler::dispatch(Webhook::EVENT_UPDATE_INVOICE, $invoice, $invoice->company); WebhookHandler::dispatch(Webhook::EVENT_UPDATE_INVOICE, $invoice, $invoice->company);
} }
} }
@ -75,6 +80,9 @@ class InvoiceObserver
->exists(); ->exists();
if ($subscriptions) { if ($subscriptions) {
$invoice->load('client');
WebhookHandler::dispatch(Webhook::EVENT_DELETE_INVOICE, $invoice, $invoice->company); WebhookHandler::dispatch(Webhook::EVENT_DELETE_INVOICE, $invoice, $invoice->company);
} }
} }

View File

@ -30,9 +30,9 @@ class QuoteObserver
->where('event_id', Webhook::EVENT_CREATE_QUOTE) ->where('event_id', Webhook::EVENT_CREATE_QUOTE)
->exists(); ->exists();
$quote->load('client');
if ($subscriptions) { if ($subscriptions) {
$quote->load('client');
WebhookHandler::dispatch(Webhook::EVENT_CREATE_QUOTE, $quote, $quote->company); WebhookHandler::dispatch(Webhook::EVENT_CREATE_QUOTE, $quote, $quote->company);
} }
} }
@ -49,10 +49,10 @@ class QuoteObserver
->where('event_id', Webhook::EVENT_UPDATE_QUOTE) ->where('event_id', Webhook::EVENT_UPDATE_QUOTE)
->exists(); ->exists();
$quote->load('client');
if ($subscriptions) { if ($subscriptions) {
$quote->load('client');
WebhookHandler::dispatch(Webhook::EVENT_UPDATE_QUOTE, $quote, $quote->company); WebhookHandler::dispatch(Webhook::EVENT_UPDATE_QUOTE, $quote, $quote->company);
} }
@ -71,6 +71,7 @@ class QuoteObserver
->exists(); ->exists();
if ($subscriptions) { if ($subscriptions) {
$quote->load('client');
WebhookHandler::dispatch(Webhook::EVENT_DELETE_QUOTE, $quote, $quote->company); WebhookHandler::dispatch(Webhook::EVENT_DELETE_QUOTE, $quote, $quote->company);
} }
} }

View File

@ -395,7 +395,7 @@ class BaseDriver extends AbstractPaymentDriver
$invoices->first()->invitations->each(function ($invitation) use ($nmo) { $invoices->first()->invitations->each(function ($invitation) use ($nmo) {
if ($invitation->contact->send_email && $invitation->contact->email) { if ($invitation->contact->email) {
$nmo->to_user = $invitation->contact; $nmo->to_user = $invitation->contact;
NinjaMailerJob::dispatch($nmo); NinjaMailerJob::dispatch($nmo);
@ -459,7 +459,7 @@ class BaseDriver extends AbstractPaymentDriver
$invoices->first()->invitations->each(function ($invitation) use ($nmo){ $invoices->first()->invitations->each(function ($invitation) use ($nmo){
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) { if (!$invitation->contact->trashed() && $invitation->contact->email) {
$nmo->to_user = $invitation->contact; $nmo->to_user = $invitation->contact;
NinjaMailerJob::dispatch($nmo); NinjaMailerJob::dispatch($nmo);
@ -492,84 +492,84 @@ class BaseDriver extends AbstractPaymentDriver
public function checkRequirements() public function checkRequirements()
{ {
if ($this->company_gateway->require_billing_address) { if ($this->company_gateway->require_billing_address) {
if ($this->checkRequiredResource(auth()->user('contact')->client->address1)) { if ($this->checkRequiredResource($this->client->address1)) {
$this->required_fields[] = 'billing_address1'; $this->required_fields[] = 'billing_address1';
} }
if ($this->checkRequiredResource(auth()->user('contact')->client->address2)) { if ($this->checkRequiredResource($this->client->address2)) {
$this->required_fields[] = 'billing_address2'; $this->required_fields[] = 'billing_address2';
} }
if ($this->checkRequiredResource(auth()->user('contact')->client->city)) { if ($this->checkRequiredResource($this->client->city)) {
$this->required_fields[] = 'billing_city'; $this->required_fields[] = 'billing_city';
} }
if ($this->checkRequiredResource(auth()->user('contact')->client->state)) { if ($this->checkRequiredResource($this->client->state)) {
$this->required_fields[] = 'billing_state'; $this->required_fields[] = 'billing_state';
} }
if ($this->checkRequiredResource(auth()->user('contact')->client->postal_code)) { if ($this->checkRequiredResource($this->client->postal_code)) {
$this->required_fields[] = 'billing_postal_code'; $this->required_fields[] = 'billing_postal_code';
} }
if ($this->checkRequiredResource(auth()->user('contact')->client->country_id)) { if ($this->checkRequiredResource($this->client->country_id)) {
$this->required_fields[] = 'billing_country'; $this->required_fields[] = 'billing_country';
} }
} }
if ($this->company_gateway->require_shipping_address) { if ($this->company_gateway->require_shipping_address) {
if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_address1)) { if ($this->checkRequiredResource($this->client->shipping_address1)) {
$this->required_fields[] = 'shipping_address1'; $this->required_fields[] = 'shipping_address1';
} }
if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_address2)) { if ($this->checkRequiredResource($this->client->shipping_address2)) {
$this->required_fields[] = 'shipping_address2'; $this->required_fields[] = 'shipping_address2';
} }
if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_city)) { if ($this->checkRequiredResource($this->client->shipping_city)) {
$this->required_fields[] = 'shipping_city'; $this->required_fields[] = 'shipping_city';
} }
if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_state)) { if ($this->checkRequiredResource($this->client->shipping_state)) {
$this->required_fields[] = 'shipping_state'; $this->required_fields[] = 'shipping_state';
} }
if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_postal_code)) { if ($this->checkRequiredResource($this->client->shipping_postal_code)) {
$this->required_fields[] = 'shipping_postal_code'; $this->required_fields[] = 'shipping_postal_code';
} }
if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_country_id)) { if ($this->checkRequiredResource($this->client->shipping_country_id)) {
$this->required_fields[] = 'shipping_country'; $this->required_fields[] = 'shipping_country';
} }
} }
if ($this->company_gateway->require_client_name) { if ($this->company_gateway->require_client_name) {
if ($this->checkRequiredResource(auth()->user('contact')->client->name)) { if ($this->checkRequiredResource($this->client->name)) {
$this->required_fields[] = 'name'; $this->required_fields[] = 'name';
} }
} }
if ($this->company_gateway->require_client_phone) { if ($this->company_gateway->require_client_phone) {
if ($this->checkRequiredResource(auth()->user('contact')->client->phone)) { if ($this->checkRequiredResource($this->client->phone)) {
$this->required_fields[] = 'phone'; $this->required_fields[] = 'phone';
} }
} }
if ($this->company_gateway->require_contact_email) { if ($this->company_gateway->require_contact_email) {
if ($this->checkRequiredResource(auth()->user('contact')->email)) { if ($this->checkRequiredResource($this->email)) {
$this->required_fields[] = 'contact_email'; $this->required_fields[] = 'contact_email';
} }
} }
if ($this->company_gateway->require_contact_name) { // if ($this->company_gateway->require_contact_name) {
if ($this->checkRequiredResource(auth()->user('contact')->first_name)) { // if ($this->checkRequiredResource($this->first_name)) {
$this->required_fields[] = 'contact_first_name'; // $this->required_fields[] = 'contact_first_name';
} // }
if ($this->checkRequiredResource(auth()->user('contact')->last_name)) { // if ($this->checkRequiredResource($this->last_name)) {
$this->required_fields[] = 'contact_last_name'; // $this->required_fields[] = 'contact_last_name';
} // }
} // }
if ($this->company_gateway->require_postal_code) { if ($this->company_gateway->require_postal_code) {
// In case "require_postal_code" is true, we don't need billing address. // In case "require_postal_code" is true, we don't need billing address.
@ -580,7 +580,7 @@ class BaseDriver extends AbstractPaymentDriver
} }
} }
if ($this->checkRequiredResource(auth()->user('contact')->client->postal_code)) { if ($this->checkRequiredResource($this->client->postal_code)) {
$this->required_fields[] = 'postal_code'; $this->required_fields[] = 'postal_code';
} }
} }

View File

@ -0,0 +1,215 @@
<?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://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\Mollie;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Http\Requests\Request;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\Common\MethodInterface;
use App\PaymentDrivers\MolliePaymentDriver;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class IDEAL implements MethodInterface
{
protected MolliePaymentDriver $mollie;
public function __construct(MolliePaymentDriver $mollie)
{
$this->mollie = $mollie;
$this->mollie->init();
}
/**
* Show the authorization page for iDEAL.
*
* @param array $data
* @return View
*/
public function authorizeView(array $data): View
{
return render('gateways.mollie.ideal.authorize', $data);
}
/**
* Handle the authorization for iDEAL.
*
* @param Request $request
* @return RedirectResponse
*/
public function authorizeResponse(Request $request): RedirectResponse
{
return redirect()->route('client.payment_methods.index');
}
/**
* Show the payment page for iDEAL.
*
* @param array $data
* @return Redirector|RedirectResponse
*/
public function paymentView(array $data)
{
$this->mollie->payment_hash
->withData('gateway_type_id', GatewayType::IDEAL)
->withData('client_id', $this->mollie->client->id);
try {
$payment = $this->mollie->gateway->payments->create([
'method' => 'ideal',
'amount' => [
'currency' => $this->mollie->client->currency()->code,
'value' => $this->mollie->convertToMollieAmount((float) $this->mollie->payment_hash->data->amount_with_fee),
],
'description' => \sprintf('Invoices: %s', collect($data['invoices'])->pluck('invoice_number')),
'redirectUrl' => route('client.payments.response', [
'company_gateway_id' => $this->mollie->company_gateway->id,
'payment_hash' => $this->mollie->payment_hash->hash,
'payment_method_id' => GatewayType::IDEAL,
]),
'webhookUrl' => $this->mollie->company_gateway->webhookUrl(),
'metadata' => [
'client_id' => $this->mollie->client->hashed_id,
],
]);
$this->mollie->payment_hash->withData('payment_id', $payment->id);
return redirect(
$payment->getCheckoutUrl()
);
} catch (\Mollie\Api\Exceptions\ApiException | \Exception $exception) {
return $this->processUnsuccessfulPayment($exception);
}
}
/**
* Handle unsuccessful payment.
*
* @param Exception $exception
* @throws PaymentFailed
* @return void
*/
public function processUnsuccessfulPayment(\Exception $exception): void
{
PaymentFailureMailer::dispatch(
$this->mollie->client,
$exception->getMessage(),
$this->mollie->client->company,
$this->mollie->payment_hash->data->amount_with_fee
);
SystemLogger::dispatch(
$exception->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_MOLLIE,
$this->mollie->client,
$this->mollie->client->company,
);
throw new PaymentFailed($exception->getMessage(), $exception->getCode());
}
/**
* Handle the payments for the iDEAL.
*
* @param PaymentResponseRequest $request
* @return mixed
*/
public function paymentResponse(PaymentResponseRequest $request)
{
if (!\property_exists($this->mollie->payment_hash->data, 'payment_id')) {
return $this->processUnsuccessfulPayment(
new PaymentFailed('Whoops, something went wrong. Missing required [payment_id] parameter. Please contact administrator. Reference hash: ' . $this->mollie->payment_hash->hash)
);
}
try {
$payment = $this->mollie->gateway->payments->get(
$this->mollie->payment_hash->data->payment_id
);
if ($payment->status === 'paid') {
return $this->processSuccessfulPayment($payment);
}
if ($payment->status === 'open') {
return $this->processOpenPayment($payment);
}
if ($payment->status === 'failed') {
return $this->processUnsuccessfulPayment(
new PaymentFailed(ctrans('texts.status_failed'))
);
}
return $this->processUnsuccessfulPayment(
new PaymentFailed(ctrans('texts.status_voided'))
);
} catch (\Mollie\Api\Exceptions\ApiException | \Exception $exception) {
return $this->processUnsuccessfulPayment($exception);
}
}
/**
* Handle the successful payment for iDEAL.
*
* @param string $status
* @param ResourcesPayment $payment
* @return RedirectResponse
*/
public function processSuccessfulPayment(\Mollie\Api\Resources\Payment $payment, string $status = 'paid'): RedirectResponse
{
$data = [
'gateway_type_id' => GatewayType::IDEAL,
'amount' => array_sum(array_column($this->mollie->payment_hash->invoices(), 'amount')) + $this->mollie->payment_hash->fee_total,
'payment_type' => PaymentType::IDEAL,
'transaction_reference' => $payment->id,
];
$payment_record = $this->mollie->createPayment(
$data,
$status === 'paid' ? Payment::STATUS_COMPLETED : Payment::STATUS_PENDING
);
SystemLogger::dispatch(
['response' => $payment, 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_MOLLIE,
$this->mollie->client,
$this->mollie->client->company,
);
return redirect()->route('client.payments.show', ['payment' => $this->mollie->encodePrimaryKey($payment_record->id)]);
}
/**
* Handle 'open' payment status for IDEAL.
*
* @param ResourcesPayment $payment
* @return RedirectResponse
*/
public function processOpenPayment(\Mollie\Api\Resources\Payment $payment): RedirectResponse
{
return $this->processSuccessfulPayment($payment, 'open');
}
}

View File

@ -27,6 +27,7 @@ use App\Models\SystemLog;
use App\PaymentDrivers\Mollie\Bancontact; use App\PaymentDrivers\Mollie\Bancontact;
use App\PaymentDrivers\Mollie\BankTransfer; use App\PaymentDrivers\Mollie\BankTransfer;
use App\PaymentDrivers\Mollie\CreditCard; use App\PaymentDrivers\Mollie\CreditCard;
use App\PaymentDrivers\Mollie\IDEAL;
use App\PaymentDrivers\Mollie\KBC; use App\PaymentDrivers\Mollie\KBC;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
@ -70,6 +71,7 @@ class MolliePaymentDriver extends BaseDriver
GatewayType::BANCONTACT => Bancontact::class, GatewayType::BANCONTACT => Bancontact::class,
GatewayType::BANK_TRANSFER => BankTransfer::class, GatewayType::BANK_TRANSFER => BankTransfer::class,
GatewayType::KBC => KBC::class, GatewayType::KBC => KBC::class,
GatewayType::IDEAL => IDEAL::class,
]; ];
const SYSTEM_LOG_TYPE = SystemLog::TYPE_MOLLIE; const SYSTEM_LOG_TYPE = SystemLog::TYPE_MOLLIE;
@ -93,6 +95,7 @@ class MolliePaymentDriver extends BaseDriver
$types[] = GatewayType::BANCONTACT; $types[] = GatewayType::BANCONTACT;
$types[] = GatewayType::BANK_TRANSFER; $types[] = GatewayType::BANK_TRANSFER;
$types[] = GatewayType::KBC; $types[] = GatewayType::KBC;
$types[] = GatewayType::IDEAL;
return $types; return $types;
} }

View File

@ -70,13 +70,9 @@ class ActivityRepository extends BaseRepository
*/ */
public function createBackup($entity, $activity) public function createBackup($entity, $activity)
{ {
if ($entity instanceof User || $entity->company->is_disabled)
if($entity instanceof User){
}
else if ($entity->company->is_disabled) {
return; return;
}
$backup = new Backup(); $backup = new Backup();
@ -85,6 +81,7 @@ class ActivityRepository extends BaseRepository
|| get_class($entity) == Credit::class || get_class($entity) == Credit::class
|| get_class($entity) == RecurringInvoice::class || get_class($entity) == RecurringInvoice::class
) { ) {
$entity->load('company', 'client');
$contact = $entity->client->primary_contact()->first(); $contact = $entity->client->primary_contact()->first();
$backup->html_backup = $this->generateHtml($entity); $backup->html_backup = $this->generateHtml($entity);
$backup->amount = $entity->amount; $backup->amount = $entity->amount;
@ -92,7 +89,6 @@ class ActivityRepository extends BaseRepository
$backup->activity_id = $activity->id; $backup->activity_id = $activity->id;
$backup->json_backup = ''; $backup->json_backup = '';
//$backup->json_backup = $entity->toJson();
$backup->save(); $backup->save();
} }
@ -121,6 +117,8 @@ class ActivityRepository extends BaseRepository
$entity_design_id = 'credit_design_id'; $entity_design_id = 'credit_design_id';
} }
$entity->load('client','client.company');
$entity_design_id = $entity->design_id ? $entity->design_id : $this->decodePrimaryKey($entity->client->getSetting($entity_design_id)); $entity_design_id = $entity->design_id ? $entity->design_id : $this->decodePrimaryKey($entity->client->getSetting($entity_design_id));
$design = Design::find($entity_design_id); $design = Design::find($entity_design_id);

View File

@ -174,10 +174,6 @@ class BaseRepository
if(array_key_exists('client_id', $data)) if(array_key_exists('client_id', $data))
$model->client_id = $data['client_id']; $model->client_id = $data['client_id'];
//pickup changes here to recalculate reminders
//if($model instanceof Invoice && ($model->isDirty('date') || $model->isDirty('due_date')))
// $model->service()->setReminder()->save();
$client = Client::where('id', $model->client_id)->withTrashed()->first(); $client = Client::where('id', $model->client_id)->withTrashed()->first();
$state = []; $state = [];
@ -210,7 +206,10 @@ class BaseRepository
$model->custom_surcharge_tax3 = $client->company->custom_surcharge_taxes3; $model->custom_surcharge_tax3 = $client->company->custom_surcharge_taxes3;
$model->custom_surcharge_tax4 = $client->company->custom_surcharge_taxes4; $model->custom_surcharge_tax4 = $client->company->custom_surcharge_taxes4;
$model->saveQuietly(); if(!$model->id)
$model->save();
else
$model->saveQuietly();
/* Model now persisted, now lets do some child tasks */ /* Model now persisted, now lets do some child tasks */
@ -309,10 +308,6 @@ class BaseRepository
/* Perform model specific tasks */ /* Perform model specific tasks */
if ($model instanceof Invoice) { if ($model instanceof Invoice) {
nlog("Finished amount = " . $state['finished_amount']);
nlog("Starting amount = " . $state['starting_amount']);
nlog("Diff = " . ($state['finished_amount'] - $state['starting_amount']));
if (($state['finished_amount'] != $state['starting_amount']) && ($model->status_id != Invoice::STATUS_DRAFT)) { if (($state['finished_amount'] != $state['starting_amount']) && ($model->status_id != Invoice::STATUS_DRAFT)) {

View File

@ -0,0 +1,291 @@
<?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\ClientPortal;
use App\Exceptions\PaymentFailed;
use App\Factory\PaymentFactory;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Jobs\Invoice\InjectSignature;
use App\Jobs\Util\SystemLogger;
use App\Models\CompanyGateway;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\SystemLog;
use App\Services\Subscription\SubscriptionService;
use App\Utils\Number;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Illuminate\View\View;
class InstantPayment
{
use MakesHash;
use MakesDates;
public Request $request;
public function __construct(Request $request)
{
$this->request = $request;
}
public function run()
{
$is_credit_payment = false;
$tokens = [];
if ($this->request->input('company_gateway_id') == CompanyGateway::GATEWAY_CREDIT) {
$is_credit_payment = true;
}
$gateway = CompanyGateway::find($this->request->input('company_gateway_id'));
/**
* find invoices
*
* ['invoice_id' => xxx, 'amount' => 22.00]
*/
$payable_invoices = collect($this->request->payable_invoices);
$invoices = Invoice::whereIn('id', $this->transformKeys($payable_invoices->pluck('invoice_id')->toArray()))->withTrashed()->get();
$invoices->each(function($invoice){
$invoice->service()->removeUnpaidGatewayFees()->save();
});
/* pop non payable invoice from the $payable_invoices array */
$payable_invoices = $payable_invoices->filter(function ($payable_invoice) use ($invoices) {
return $invoices->where('hashed_id', $payable_invoice['invoice_id'])->first()->isPayable();
});
/*return early if no invoices*/
if ($payable_invoices->count() == 0) {
return redirect()
->route('client.invoices.index')
->with(['message' => 'No payable invoices selected.']);
}
$client = $invoices->first()->client;
$settings = $client->getMergedSettings();
/* This loop checks for under / over payments and returns the user if a check fails */
foreach ($payable_invoices as $payable_invoice) {
/*Match the payable invoice to the Model Invoice*/
$invoice = $invoices->first(function ($inv) use ($payable_invoice) {
return $payable_invoice['invoice_id'] == $inv->hashed_id;
});
/*
* Check if company supports over & under payments.
* Determine the payable amount and the max payable. ie either partial or invoice balance
*/
$payable_amount = Number::roundValue(Number::parseFloat($payable_invoice['amount'], $client->currency()->precision));
$invoice_balance = Number::roundValue(($invoice->partial > 0 ? $invoice->partial : $invoice->balance), $client->currency()->precision);
/*If we don't allow under/over payments force the payable amount - prevents inspect element adjustments in JS*/
if ($settings->client_portal_allow_under_payment == false && $settings->client_portal_allow_over_payment == false) {
$payable_invoice['amount'] = Number::roundValue(($invoice->partial > 0 ? $invoice->partial : $invoice->balance), $client->currency()->precision);
}
if (!$settings->client_portal_allow_under_payment && $payable_amount < $invoice_balance) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.minimum_required_payment', ['amount' => $invoice_balance]));
}
if ($settings->client_portal_allow_under_payment) {
if ($invoice_balance < $settings->client_portal_under_payment_minimum && $payable_amount < $invoice_balance) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.minimum_required_payment', ['amount' => $invoice_balance]));
}
if ($invoice_balance < $settings->client_portal_under_payment_minimum) {
// Skip the under payment rule.
}
if ($invoice_balance >= $settings->client_portal_under_payment_minimum && $payable_amount < $settings->client_portal_under_payment_minimum) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.minimum_required_payment', ['amount' => $settings->client_portal_under_payment_minimum]));
}
}
/* If we don't allow over payments and the amount exceeds the balance */
if (!$settings->client_portal_allow_over_payment && $payable_amount > $invoice_balance) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.over_payments_disabled'));
}
}
/*Iterate through invoices and add gateway fees and other payment metadata*/
//$payable_invoices = $payable_invoices->map(function ($payable_invoice) use ($invoices, $settings) {
$payable_invoice_collection = collect();
foreach ($payable_invoices as $payable_invoice) {
// nlog($payable_invoice);
$payable_invoice['amount'] = Number::parseFloat($payable_invoice['amount']);
$invoice = $invoices->first(function ($inv) use ($payable_invoice) {
return $payable_invoice['invoice_id'] == $inv->hashed_id;
});
$payable_amount = Number::roundValue(Number::parseFloat($payable_invoice['amount'], $client->currency()->precision));
$invoice_balance = Number::roundValue($invoice->balance, $client->currency()->precision);
$payable_invoice['due_date'] = $this->formatDate($invoice->due_date, $invoice->client->date_format());
$payable_invoice['invoice_number'] = $invoice->number;
if (isset($invoice->po_number)) {
$additional_info = $invoice->po_number;
} elseif (isset($invoice->public_notes)) {
$additional_info = $invoice->public_notes;
} else {
$additional_info = $invoice->date;
}
$payable_invoice['additional_info'] = $additional_info;
$payable_invoice_collection->push($payable_invoice);
}
if ($this->request->has('signature') && !is_null($this->request->signature) && !empty($this->request->signature)) {
$invoices->each(function ($invoice){
InjectSignature::dispatch($invoice, $this->request->signature);
});
}
$payable_invoices = $payable_invoice_collection;
$payment_method_id = $this->request->input('payment_method_id');
$invoice_totals = $payable_invoices->sum('amount');
$first_invoice = $invoices->first();
$credit_totals = $first_invoice->client->getSetting('use_credits_payment') == 'always' ? $first_invoice->client->service()->getCreditBalance() : 0;
$starting_invoice_amount = $first_invoice->balance;
if ($gateway) {
$first_invoice->service()->addGatewayFee($gateway, $payment_method_id, $invoice_totals)->save();
}
/**
* Gateway fee is calculated
* by adding it as a line item, and then subtract
* the starting and finishing amounts of the invoice.
*/
$fee_totals = $first_invoice->balance - $starting_invoice_amount;
if ($gateway) {
$tokens = $client->gateway_tokens()
->whereCompanyGatewayId($gateway->id)
->whereGatewayTypeId($payment_method_id)
->get();
}
if(!$is_credit_payment){
$credit_totals = 0;
}
$hash_data = ['invoices' => $payable_invoices->toArray(), 'credits' => $credit_totals, 'amount_with_fee' => max(0, (($invoice_totals + $fee_totals) - $credit_totals))];
if ($this->request->query('hash')) {
$hash_data['billing_context'] = Cache::get($this->request->query('hash'));
}
$payment_hash = new PaymentHash;
$payment_hash->hash = Str::random(32);
$payment_hash->data = $hash_data;
$payment_hash->fee_total = $fee_totals;
$payment_hash->fee_invoice_id = $first_invoice->id;
$payment_hash->save();
if($is_credit_payment){
$amount_with_fee = max(0, (($invoice_totals + $fee_totals) - $credit_totals));
}
else{
$credit_totals = 0;
$amount_with_fee = max(0, $invoice_totals + $fee_totals);
}
$totals = [
'credit_totals' => $credit_totals,
'invoice_totals' => $invoice_totals,
'fee_total' => $fee_totals,
'amount_with_fee' => $amount_with_fee,
];
$data = [
'payment_hash' => $payment_hash->hash,
'total' => $totals,
'invoices' => $payable_invoices,
'tokens' => $tokens,
'payment_method_id' => $payment_method_id,
'amount_with_fee' => $invoice_totals + $fee_totals,
];
if ($is_credit_payment || $totals <= 0) {
return $this->processCreditPayment($this->request, $data);
}
try {
return $gateway
->driver($client)
->setPaymentMethod($payment_method_id)
->setPaymentHash($payment_hash)
->checkRequirements()
->processPaymentView($data);
} catch (\Exception $e) {
SystemLogger::dispatch(
$e->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_ERROR,
SystemLog::TYPE_FAILURE,
$client,
$client->company
);
throw new PaymentFailed($e->getMessage());
}
}
public function processCreditPayment(Request $request, array $data)
{
return render('gateways.credit.index', $data);
}
}

View File

@ -44,7 +44,7 @@ class SendEmail
} }
$this->credit->invitations->each(function ($invitation) { $this->credit->invitations->each(function ($invitation) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) { if (!$invitation->contact->trashed() && $invitation->contact->email) {
$email_builder = (new CreditEmail())->build($invitation, $this->reminder_template); $email_builder = (new CreditEmail())->build($invitation, $this->reminder_template);
// EmailCredit::dispatchNow($email_builder, $invitation, $invitation->company); // EmailCredit::dispatchNow($email_builder, $invitation, $invitation->company);

View File

@ -304,7 +304,7 @@ class AutoBillInvoice extends AbstractService
{ {
//get all client gateway tokens and set the is_default one to the first record //get all client gateway tokens and set the is_default one to the first record
$gateway_tokens = $this->client->gateway_tokens()->orderBy('is_default', 'DESC'); $gateway_tokens = $this->client->gateway_tokens()->orderBy('is_default', 'DESC')->get();
// $gateway_tokens = $this->client->gateway_tokens; // $gateway_tokens = $this->client->gateway_tokens;
$filtered_gateways = $gateway_tokens->filter(function ($gateway_token) use($amount) { $filtered_gateways = $gateway_tokens->filter(function ($gateway_token) use($amount) {
@ -312,7 +312,7 @@ class AutoBillInvoice extends AbstractService
$company_gateway = $gateway_token->gateway; $company_gateway = $gateway_token->gateway;
//check if fees and limits are set //check if fees and limits are set
if (isset($company_gateway->fees_and_limits) && property_exists($company_gateway->fees_and_limits, $gateway_token->gateway_type_id)) if (isset($company_gateway->fees_and_limits) && !is_array($company_gateway->fees_and_limits) && property_exists($company_gateway->fees_and_limits, $gateway_token->gateway_type_id))
{ {
//if valid we keep this gateway_token //if valid we keep this gateway_token
if ($this->invoice->client->validGatewayForAmount($company_gateway->fees_and_limits->{$gateway_token->gateway_type_id}, $amount)) if ($this->invoice->client->validGatewayForAmount($company_gateway->fees_and_limits->{$gateway_token->gateway_type_id}, $amount))

View File

@ -322,6 +322,8 @@ class InvoiceService
public function deletePdf() public function deletePdf()
{ {
$this->invoice->load('invitations');
$this->invoice->invitations->each(function ($invitation){ $this->invoice->invitations->each(function ($invitation){
Storage::disk(config('filesystems.default'))->delete($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf'); Storage::disk(config('filesystems.default'))->delete($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf');
@ -452,13 +454,13 @@ class InvoiceService
if (! $this->invoice->design_id) if (! $this->invoice->design_id)
$this->invoice->design_id = $this->decodePrimaryKey($settings->invoice_design_id); $this->invoice->design_id = $this->decodePrimaryKey($settings->invoice_design_id);
if (!isset($this->invoice->footer)) if (!isset($this->invoice->footer) || empty($this->invoice->footer))
$this->invoice->footer = $settings->invoice_footer; $this->invoice->footer = $settings->invoice_footer;
if (!isset($this->invoice->terms)) if (!isset($this->invoice->terms) || empty($this->invoice->terms))
$this->invoice->terms = $settings->invoice_terms; $this->invoice->terms = $settings->invoice_terms;
if (!isset($this->invoice->public_notes)) if (!isset($this->invoice->public_notes) || empty($this->invoice->public_notes))
$this->invoice->public_notes = $this->invoice->client->public_notes; $this->invoice->public_notes = $this->invoice->client->public_notes;
/* If client currency differs from the company default currency, then insert the client exchange rate on the model.*/ /* If client currency differs from the company default currency, then insert the client exchange rate on the model.*/
@ -473,8 +475,10 @@ class InvoiceService
if ($this->invoice->status_id == Invoice::STATUS_PAID && $this->invoice->client->getSetting('auto_archive_invoice')) { if ($this->invoice->status_id == Invoice::STATUS_PAID && $this->invoice->client->getSetting('auto_archive_invoice')) {
/* Throws: Payment amount xxx does not match invoice totals. */ /* Throws: Payment amount xxx does not match invoice totals. */
$base_repository = new BaseRepository(); $base_repository = new BaseRepository();
$base_repository->archive($this->invoice); $base_repository->archive($this->invoice);
} }
return $this; return $this;

View File

@ -44,7 +44,7 @@ class SendEmail extends AbstractService
} }
$this->invoice->invitations->each(function ($invitation) { $this->invoice->invitations->each(function ($invitation) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) { if (!$invitation->contact->trashed() && $invitation->contact->email) {
EmailEntity::dispatchNow($invitation, $invitation->company, $this->reminder_template); EmailEntity::dispatchNow($invitation, $invitation->company, $this->reminder_template);
} }
}); });

View File

@ -32,8 +32,10 @@ class SendEmail
*/ */
public function run() public function run()
{ {
$this->payment->load('company', 'client.contacts');
$this->payment->client->contacts->each(function ($contact) { $this->payment->client->contacts->each(function ($contact) {
if ($contact->send_email && $contact->email) { if ($contact->email) {
EmailPayment::dispatchNow($this->payment, $this->payment->company, $contact); EmailPayment::dispatchNow($this->payment, $this->payment->company, $contact);
} }
}); });

View File

@ -77,8 +77,6 @@ class PdfMaker
$this->updateVariables($this->data['variables']); $this->updateVariables($this->data['variables']);
} }
$this->processOptions();
return $this; return $this;
} }

View File

@ -163,144 +163,6 @@ trait PdfMakerUtilities
return $element; return $element;
} }
public function processOptions()
{
if (!isset($this->options['all_pages_header']) || $this->options['all_pages_header'] == false) {
return;
}
if (!isset($this->options['all_pages_footer']) || $this->options['all_pages_footer'] == false) {
return;
}
$this->insertPrintCSS();
$this->wrapIntoTable();
}
public function insertPrintCSS()
{
$css = <<<'EOT'
table.page-container {
page-break-after: always;
min-width: 100%;
}
thead.page-header {
display: table-header-group;
}
tfoot.page-footer {
display: table-footer-group;
}
EOT;
$css_node = $this->document->createTextNode($css);
$style = $this->document->getElementsByTagName('style')->item(0);
if ($style) {
return $style->appendChild($css_node);
}
$head = $this->document->getElementsByTagName('head')->item(0);
if ($head) {
$style_node = $this->document->createElement('style', $css);
return $head->appendChild($style_node);
}
return $this;
}
public function wrapIntoTable()
{
$markup = <<<'EOT'
<table class="page-container" id="page-container">
<thead class="page-report">
<tr>
<th class="page-report-cell" id="repeat-header">
<!-- Repeating header goes here.. -->
</th>
</tr>
</thead>
<tfoot class="report-footer">
<tr>
<td class="report-footer-cell" id="repeat-footer">
<!-- Repeating footer goes here -->
</td>
</tr>
</tfoot>
<tbody class="report-content">
<tr>
<td class="report-content-cell" id="repeat-content">
<!-- Rest of the content goes here -->
</td>
</tr>
</tbody>
</table>
EOT;
$document = new DOMDocument();
$document->loadHTML($markup);
$table = $document->getElementById('page-container');
$body = $this->document->getElementsByTagName('body')
->item(0);
$body->appendChild(
$this->document->importNode($table, true)
);
for ($i = 0; $i < $body->childNodes->length; $i++) {
$element = $body->childNodes->item($i);
if ($element->nodeType !== 1) {
continue;
}
if (
$element->getAttribute('id') == 'header' ||
$element->getAttribute('id') == 'footer' ||
$element->getAttribute('id') === 'page-container'
) {
continue;
}
$clone = $element->cloneNode(true);
$element->parentNode->removeChild($element);
$this->document->getElementById('repeat-content')->appendChild($clone);
}
// info($this->data['options']);
if (
$header = $this->document->getElementById('header') &&
isset($this->data['options']['all_pages_header']) &&
$this->data['options']['all_pages_header']
) {
$header = $this->document->getElementById('header');
$clone = $header->cloneNode(true);
$header->parentNode->removeChild($header);
$this->document->getElementById('repeat-header')->appendChild($clone);
}
if (
$footer = $this->document->getElementById('footer') &&
isset($this->data['options']['all_pages_footer']) &&
$this->data['options']['all_pages_footer']
) {
$footer = $this->document->getElementById('footer');
$clone = $footer->cloneNode(true);
$footer->parentNode->removeChild($footer);
$this->document->getElementById('repeat-footer')->appendChild($clone);
}
}
public function getEmptyElements(array &$elements, array $variables) public function getEmptyElements(array &$elements, array $variables)
{ {
foreach ($elements as &$element) { foreach ($elements as &$element) {

View File

@ -39,7 +39,7 @@ class ConvertQuote
{ {
$invoice = CloneQuoteToInvoiceFactory::create($quote, $quote->user_id); $invoice = CloneQuoteToInvoiceFactory::create($quote, $quote->user_id);
$invoice->design_id = $this->decodePrimaryKey($this->client->getSetting('invoice_design_id')); $invoice->design_id = $this->decodePrimaryKey($this->client->getSetting('invoice_design_id'));
$invoice = $this->invoice_repo->save([], $invoice); $invoice = $this->invoice_repo->save($invoice->toArray(), $invoice);
$invoice->fresh(); $invoice->fresh();

View File

@ -42,7 +42,7 @@ class SendEmail
} }
$this->quote->invitations->each(function ($invitation) { $this->quote->invitations->each(function ($invitation) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) { if (!$invitation->contact->trashed() && $invitation->contact->email) {
EmailEntity::dispatchNow($invitation, $invitation->company, $this->reminder_template); EmailEntity::dispatchNow($invitation, $invitation->company, $this->reminder_template);
} }
}); });

View File

@ -165,6 +165,7 @@ class CompanyTransformer extends EntityTransformer
'markdown_enabled' => (bool) $company->markdown_enabled, 'markdown_enabled' => (bool) $company->markdown_enabled,
'use_comma_as_decimal_place' => (bool) $company->use_comma_as_decimal_place, 'use_comma_as_decimal_place' => (bool) $company->use_comma_as_decimal_place,
'report_include_drafts' => (bool) $company->report_include_drafts, 'report_include_drafts' => (bool) $company->report_include_drafts,
'client_registration_fields' => (array) $company->client_registration_fields,
]; ];
} }

View File

@ -49,6 +49,7 @@ class HtmlEngine
public function __construct($invitation) public function __construct($invitation)
{ {
$this->invitation = $invitation; $this->invitation = $invitation;
// $invitation->load('contact.client.company', 'company');
$this->entity_string = $this->resolveEntityString(); $this->entity_string = $this->resolveEntityString();
@ -58,7 +59,9 @@ class HtmlEngine
$this->contact = $invitation->contact; $this->contact = $invitation->contact;
$this->client = $this->entity->client; $this->client = $invitation->contact->client;
$this->client->load('country','company');
$this->entity->load('client');
$this->settings = $this->client->getMergedSettings(); $this->settings = $this->client->getMergedSettings();
@ -113,22 +116,25 @@ class HtmlEngine
$data['$total_tax_values'] = ['value' => $this->totalTaxValues(), 'label' => ctrans('texts.taxes')]; $data['$total_tax_values'] = ['value' => $this->totalTaxValues(), 'label' => ctrans('texts.taxes')];
$data['$line_tax_labels'] = ['value' => $this->lineTaxLabels(), 'label' => ctrans('texts.taxes')]; $data['$line_tax_labels'] = ['value' => $this->lineTaxLabels(), 'label' => ctrans('texts.taxes')];
$data['$line_tax_values'] = ['value' => $this->lineTaxValues(), 'label' => ctrans('texts.taxes')]; $data['$line_tax_values'] = ['value' => $this->lineTaxValues(), 'label' => ctrans('texts.taxes')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.date')]; $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.date')];
$data['$invoice.date'] = &$data['$date']; $data['$invoice.date'] = &$data['$date'];
$data['$invoiceDate'] = &$data['$date']; $data['$invoiceDate'] = &$data['$date'];
$data['$due_date'] = ['value' => $this->translateDate($this->entity->due_date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')]; $data['$due_date'] = ['value' => $this->translateDate($this->entity->due_date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')];
$data['$dueDate'] = &$data['$due_date']; $data['$dueDate'] = &$data['$due_date'];
$data['$payment_due'] = ['value' => $this->translateDate($this->entity->due_date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.payment_due')]; $data['$payment_due'] = ['value' => $this->translateDate($this->entity->due_date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.payment_due')];
$data['$invoice.due_date'] = &$data['$due_date']; $data['$invoice.due_date'] = &$data['$due_date'];
$data['$invoice.number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.invoice_number')]; $data['$invoice.number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.invoice_number')];
$data['$invoice.po_number'] = ['value' => $this->entity->po_number ?: '&nbsp;', 'label' => ctrans('texts.po_number')]; $data['$invoice.po_number'] = ['value' => $this->entity->po_number ?: '&nbsp;', 'label' => ctrans('texts.po_number')];
$data['$poNumber'] = &$data['$invoice.po_number']; $data['$poNumber'] = &$data['$invoice.po_number'];
$data['$entity.datetime'] = ['value' => $this->formatDatetime($this->entity->created_at, $this->entity->client->date_format(), $this->entity->client->locale()), 'label' => ctrans('texts.date')]; $data['$entity.datetime'] = ['value' => $this->formatDatetime($this->entity->created_at, $this->client->date_format(), $this->client->locale()), 'label' => ctrans('texts.date')];
$data['$invoice.datetime'] = &$data['$entity.datetime']; $data['$invoice.datetime'] = &$data['$entity.datetime'];
$data['$quote.datetime'] = &$data['$entity.datetime']; $data['$quote.datetime'] = &$data['$entity.datetime'];
$data['$credit.datetime'] = &$data['$entity.datetime']; $data['$credit.datetime'] = &$data['$entity.datetime'];
$data['$payment_button'] = ['value' => '<a class="button" href="'.$this->invitation->getPaymentLink().'">'.ctrans('texts.pay_now').'</a>', 'label' => ctrans('texts.pay_now')];
$data['$payment_link'] = ['value' => $this->invitation->getPaymentLink(), 'label' => ctrans('texts.pay_now')];
if ($this->entity_string == 'invoice' || $this->entity_string == 'recurring_invoice') { if ($this->entity_string == 'invoice' || $this->entity_string == 'recurring_invoice') {
$data['$entity'] = ['value' => '', 'label' => ctrans('texts.invoice')]; $data['$entity'] = ['value' => '', 'label' => ctrans('texts.invoice')];
@ -140,11 +146,11 @@ class HtmlEngine
$data['$viewLink'] = &$data['$view_link']; $data['$viewLink'] = &$data['$view_link'];
$data['$viewButton'] = &$data['$view_link']; $data['$viewButton'] = &$data['$view_link'];
$data['$view_button'] = &$data['$view_link']; $data['$view_button'] = &$data['$view_link'];
$data['$paymentButton'] = &$data['$view_link']; $data['$paymentButton'] = &$data['$payment_button'];
$data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_invoice')]; $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')]; $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.invoice_date')];
if($this->entity->project()->exists()) { if($this->entity->project) {
$data['$project.name'] = ['value' => $this->entity->project->name, 'label' => ctrans('texts.project_name')]; $data['$project.name'] = ['value' => $this->entity->project->name, 'label' => ctrans('texts.project_name')];
} }
} }
@ -161,7 +167,7 @@ class HtmlEngine
$data['$view_button'] = &$data['$view_link']; $data['$view_button'] = &$data['$view_link'];
$data['$approveButton'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_quote').'</a>', 'label' => ctrans('texts.approve')]; $data['$approveButton'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_quote').'</a>', 'label' => ctrans('texts.approve')];
$data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_quote')]; $data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_quote')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.quote_date')]; $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.quote_date')];
} }
if ($this->entity_string == 'credit') { if ($this->entity_string == 'credit') {
@ -176,7 +182,7 @@ class HtmlEngine
$data['$viewLink'] = &$data['$view_link']; $data['$viewLink'] = &$data['$view_link'];
$data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_credit')]; $data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_credit')];
// $data['$view_link'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_credit')]; // $data['$view_link'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_credit')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.credit_date')]; $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.credit_date')];
} }
$data['$portal_url'] = ['value' => $this->invitation->getPortalLink(), 'label' =>'']; $data['$portal_url'] = ['value' => $this->invitation->getPortalLink(), 'label' =>''];
@ -197,7 +203,7 @@ class HtmlEngine
if ($this->entity->partial > 0) { if ($this->entity->partial > 0) {
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.partial_due')]; $data['$balance_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.partial_due')];
$data['$balance_due_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.partial_due')]; $data['$balance_due_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.partial_due')];
$data['$due_date'] = ['value' => $this->translateDate($this->entity->partial_due_date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')]; $data['$due_date'] = ['value' => $this->translateDate($this->entity->partial_due_date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')];
} else { } else {
@ -230,7 +236,7 @@ class HtmlEngine
$data['$credit.number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.credit_number')]; $data['$credit.number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.credit_number')];
$data['$credit.total'] = &$data['$credit.total']; $data['$credit.total'] = &$data['$credit.total'];
$data['$credit.po_number'] = &$data['$invoice.po_number']; $data['$credit.po_number'] = &$data['$invoice.po_number'];
$data['$credit.date'] = ['value' => $this->translateDate($this->entity->date, $this->entity->client->date_format(), $this->entity->client->locale()), 'label' => ctrans('texts.credit_date')]; $data['$credit.date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()), 'label' => ctrans('texts.credit_date')];
$data['$balance'] = ['value' => Number::formatMoney($this->entity_calc->getBalance(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.balance')]; $data['$balance'] = ['value' => Number::formatMoney($this->entity_calc->getBalance(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.balance')];
$data['$credit.balance'] = &$data['$balance']; $data['$credit.balance'] = &$data['$balance'];
@ -257,13 +263,13 @@ class HtmlEngine
$data['$entity_issued_to'] = ['value' => '', 'label' => ctrans("texts.{$this->entity_string}_issued_to")]; $data['$entity_issued_to'] = ['value' => '', 'label' => ctrans("texts.{$this->entity_string}_issued_to")];
$data['$your_entity'] = ['value' => '', 'label' => ctrans("texts.your_{$this->entity_string}")]; $data['$your_entity'] = ['value' => '', 'label' => ctrans("texts.your_{$this->entity_string}")];
$data['$quote.date'] = ['value' => $this->translateDate($this->entity->date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.quote_date')]; $data['$quote.date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.quote_date')];
$data['$quote.number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.quote_number')]; $data['$quote.number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.quote_number')];
$data['$quote.po_number'] = &$data['$invoice.po_number']; $data['$quote.po_number'] = &$data['$invoice.po_number'];
$data['$quote.quote_number'] = &$data['$quote.number']; $data['$quote.quote_number'] = &$data['$quote.number'];
$data['$quote_no'] = &$data['$quote.number']; $data['$quote_no'] = &$data['$quote.number'];
$data['$quote.quote_no'] = &$data['$quote.number']; $data['$quote.quote_no'] = &$data['$quote.number'];
$data['$quote.valid_until'] = ['value' => $this->translateDate($this->entity->due_date, $this->client->date_format(), $this->entity->client->locale()), 'label' => ctrans('texts.valid_until')]; $data['$quote.valid_until'] = ['value' => $this->translateDate($this->entity->due_date, $this->client->date_format(), $this->client->locale()), 'label' => ctrans('texts.valid_until')];
$data['$valid_until'] = &$data['$quote.valid_until']; $data['$valid_until'] = &$data['$quote.valid_until'];
$data['$credit_amount'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.credit_amount')]; $data['$credit_amount'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.credit_amount')];
$data['$credit_balance'] = ['value' => Number::formatMoney($this->entity->balance, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.credit_balance')]; $data['$credit_balance'] = ['value' => Number::formatMoney($this->entity->balance, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.credit_balance')];
@ -457,7 +463,8 @@ class HtmlEngine
$data['$auto_bill'] = &$data['$autoBill']; $data['$auto_bill'] = &$data['$autoBill'];
/*Payment Aliases*/ /*Payment Aliases*/
$data['$paymentLink'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_payment').'</a>', 'label' => ctrans('texts.view_payment')]; $data['$paymentLink'] = &$data['$payment_link'];
$data['$payment_url'] = &$data['$payment_link'];
$data['$portalButton'] = &$data['$paymentLink']; $data['$portalButton'] = &$data['$paymentLink'];
$data['$dir'] = ['value' => optional($this->client->language())->locale === 'ar' ? 'rtl' : 'ltr', 'label' => '']; $data['$dir'] = ['value' => optional($this->client->language())->locale === 'ar' ? 'rtl' : 'ltr', 'label' => ''];

View File

@ -82,11 +82,20 @@ class Phantom
$url = config('ninja.app_url').'/phantom/'.$entity.'/'.$invitation->key.'?phantomjs_secret='.config('ninja.phantomjs_secret'); $url = config('ninja.app_url').'/phantom/'.$entity.'/'.$invitation->key.'?phantomjs_secret='.config('ninja.phantomjs_secret');
info($url); info($url);
$key = config('ninja.phantomjs_key'); $key = config( 'ninja.phantomjs_key' );
$secret = config('ninja.phantomjs_key'); $phantom_url = "https://phantomjscloud.com/api/browser/v2/{$key}/";
$pdf = CurlUtils::post( $phantom_url, json_encode( [
$phantom_url = "https://phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$url}%22,renderType:%22pdf%22%7D"; 'url' => $url,
$pdf = CurlUtils::get($phantom_url); 'renderType' => 'pdf',
'outputAsJson' => false,
'renderSettings' => [
'emulateMedia' => 'print',
'pdfOptions' => [
'preferCSSPageSize' => true,
'printBackground' => true,
],
],
] ) );
$this->checkMime($pdf, $invitation, $entity); $this->checkMime($pdf, $invitation, $entity);
@ -100,14 +109,20 @@ class Phantom
public function convertHtmlToPdf($html) public function convertHtmlToPdf($html)
{ {
$hash = Str::random(32); $key = config( 'ninja.phantomjs_key' );
Cache::put($hash, $html, 300); $phantom_url = "https://phantomjscloud.com/api/browser/v2/{$key}/";
$pdf = CurlUtils::post( $phantom_url, json_encode( [
$url = route('tmp_pdf', ['hash' => $hash]); 'content' => $html,
info($url); 'renderType' => 'pdf',
$key = config('ninja.phantomjs_key'); 'outputAsJson' => false,
$phantom_url = "https://phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$url}%22,renderType:%22pdf%22%7D"; 'renderSettings' => [
$pdf = CurlUtils::get($phantom_url); 'emulateMedia' => 'print',
'pdfOptions' => [
'preferCSSPageSize' => true,
'printBackground' => true,
],
],
] ) );
$response = Response::make($pdf, 200); $response = Response::make($pdf, 200);
$response->header('Content-Type', 'application/pdf'); $response->header('Content-Type', 'application/pdf');

View File

@ -43,6 +43,17 @@ trait Inviteable
return $status; return $status;
} }
public function getPaymentLink()
{
if(Ninja::isHosted()){
$domain = $this->company->domain();
}
else
$domain = config('ninja.app_url');
return $domain.'/client/pay/'.$this->key;
}
public function getLink() :string public function getLink() :string
{ {
$entity_type = Str::snake(class_basename($this->entityType())); $entity_type = Str::snake(class_basename($this->entityType()));

View File

@ -270,7 +270,7 @@ trait MakesInvoiceValues
if (! is_array($items)) { if (! is_array($items)) {
$data; $data;
} }
$locale_info = localeconv(); $locale_info = localeconv();
foreach ($items as $key => $item) { foreach ($items as $key => $item) {
@ -296,11 +296,7 @@ trait MakesInvoiceValues
$data[$key][$table_type.'.notes'] = Helpers::processReservedKeywords($item->notes, $this->client); $data[$key][$table_type.'.notes'] = Helpers::processReservedKeywords($item->notes, $this->client);
$data[$key][$table_type.'.description'] = Helpers::processReservedKeywords($item->notes, $this->client); $data[$key][$table_type.'.description'] = Helpers::processReservedKeywords($item->notes, $this->client);
/* need to test here as this is new - 18/09/2021*/
if(!array_key_exists($table_type.'.gross_line_total', $data[$key]))
$data[$key][$table_type.'.gross_line_total'] = 0;
$data[$key][$table_type . ".{$_table_type}1"] = $helpers->formatCustomFieldValue($this->client->company->custom_fields, "{$_table_type}1", $item->custom_value1, $this->client); $data[$key][$table_type . ".{$_table_type}1"] = $helpers->formatCustomFieldValue($this->client->company->custom_fields, "{$_table_type}1", $item->custom_value1, $this->client);
$data[$key][$table_type . ".{$_table_type}2"] = $helpers->formatCustomFieldValue($this->client->company->custom_fields, "{$_table_type}2", $item->custom_value2, $this->client); $data[$key][$table_type . ".{$_table_type}2"] = $helpers->formatCustomFieldValue($this->client->company->custom_fields, "{$_table_type}2", $item->custom_value2, $this->client);
$data[$key][$table_type . ".{$_table_type}3"] = $helpers->formatCustomFieldValue($this->client->company->custom_fields, "{$_table_type}3", $item->custom_value3, $this->client); $data[$key][$table_type . ".{$_table_type}3"] = $helpers->formatCustomFieldValue($this->client->company->custom_fields, "{$_table_type}3", $item->custom_value3, $this->client);
@ -314,8 +310,12 @@ trait MakesInvoiceValues
$data[$key][$table_type.'.cost'] = Number::formatMoney($item->cost, $this->client); $data[$key][$table_type.'.cost'] = Number::formatMoney($item->cost, $this->client);
$data[$key][$table_type.'.line_total'] = Number::formatMoney($item->line_total, $this->client); $data[$key][$table_type.'.line_total'] = Number::formatMoney($item->line_total, $this->client);
if(property_exists($item, 'gross_line_total'))
$data[$key][$table_type.'.gross_line_total'] = Number::formatMoney($item->gross_line_total, $this->client);
else
$data[$key][$table_type.'.gross_line_total'] = 0;
if (isset($item->discount) && $item->discount > 0) { if (isset($item->discount) && $item->discount > 0) {
if ($item->is_amount_discount) { if ($item->is_amount_discount) {
$data[$key][$table_type.'.discount'] = Number::formatMoney($item->discount, $this->client); $data[$key][$table_type.'.discount'] = Number::formatMoney($item->discount, $this->client);

406
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,28 @@
<?php
use App\Models\GatewayType;
use App\Models\PaymentType;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddIdealToPaymentTypes extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('payment_types', function (Blueprint $table) {
$type = new PaymentType();
$type->id = 37;
$type->name = 'iDEAL';
$type->gateway_type_id = GatewayType::IDEAL;
$type->save();
});
}
}

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class UpdatedBoldAndModernDesigns extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
\Illuminate\Support\Facades\Artisan::call('ninja:design-update');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -3,38 +3,38 @@ const MANIFEST = 'flutter-app-manifest';
const TEMP = 'flutter-temp-cache'; const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache'; const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = { const RESOURCES = {
"favicon.png": "dca91c54388f52eded692718d5a98b8b", "version.json": "9ec5e3813adc4bfd8713556c5059e97d",
"/": "0d9690c2b925c794b94e0778817e5c19", "favicon.ico": "51636d3a390451561744c42188ccd628",
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
"assets/fonts/MaterialIcons-Regular.otf": "4e6447691c9509f7acdbf8a931a85ca1",
"assets/NOTICES": "9eb7e2eb2888ea5bae5f536720db37cd", "assets/NOTICES": "9eb7e2eb2888ea5bae5f536720db37cd",
"assets/assets/images/icon.png": "090f69e23311a4b6d851b3880ae52541",
"assets/assets/images/google_logo.png": "0f118259ce403274f407f5e982e681c3",
"assets/assets/images/logo_light.png": "e5f46d5a78e226e7a9553d4ca6f69219", "assets/assets/images/logo_light.png": "e5f46d5a78e226e7a9553d4ca6f69219",
"assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024",
"assets/assets/images/payment_types/mastercard.png": "6f6cdc29ee2e22e06b1ac029cb52ef71",
"assets/assets/images/payment_types/solo.png": "2030c3ccaccf5d5e87916a62f5b084d6",
"assets/assets/images/payment_types/paypal.png": "8e06c094c1871376dfea1da8088c29d1",
"assets/assets/images/payment_types/discover.png": "6c0a386a00307f87db7bea366cca35f5",
"assets/assets/images/payment_types/switch.png": "4fa11c45327f5fdc20205821b2cfd9cc",
"assets/assets/images/payment_types/ach.png": "7433f0aff779dc98a649b7a2daf777cf",
"assets/assets/images/payment_types/laser.png": "b4e6e93dd35517ac429301119ff05868", "assets/assets/images/payment_types/laser.png": "b4e6e93dd35517ac429301119ff05868",
"assets/assets/images/payment_types/jcb.png": "07e0942d16c5592118b72e74f2f7198c", "assets/assets/images/payment_types/discover.png": "6c0a386a00307f87db7bea366cca35f5",
"assets/assets/images/payment_types/other.png": "d936e11fa3884b8c9f1bd5c914be8629", "assets/assets/images/payment_types/mastercard.png": "6f6cdc29ee2e22e06b1ac029cb52ef71",
"assets/assets/images/payment_types/amex.png": "c49a4247984b3732a4af50a3390aa978",
"assets/assets/images/payment_types/ach.png": "7433f0aff779dc98a649b7a2daf777cf",
"assets/assets/images/payment_types/carteblanche.png": "d936e11fa3884b8c9f1bd5c914be8629", "assets/assets/images/payment_types/carteblanche.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/other.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/visa.png": "3ddc4a4d25c946e8ad7e6998f30fd4e3", "assets/assets/images/payment_types/visa.png": "3ddc4a4d25c946e8ad7e6998f30fd4e3",
"assets/assets/images/payment_types/maestro.png": "e533b92bfb50339fdbfa79e3dfe81f08", "assets/assets/images/payment_types/maestro.png": "e533b92bfb50339fdbfa79e3dfe81f08",
"assets/assets/images/payment_types/jcb.png": "07e0942d16c5592118b72e74f2f7198c",
"assets/assets/images/payment_types/solo.png": "2030c3ccaccf5d5e87916a62f5b084d6",
"assets/assets/images/payment_types/switch.png": "4fa11c45327f5fdc20205821b2cfd9cc",
"assets/assets/images/payment_types/paypal.png": "8e06c094c1871376dfea1da8088c29d1",
"assets/assets/images/payment_types/unionpay.png": "7002f52004e0ab8cc0b7450b0208ccb2", "assets/assets/images/payment_types/unionpay.png": "7002f52004e0ab8cc0b7450b0208ccb2",
"assets/assets/images/payment_types/amex.png": "c49a4247984b3732a4af50a3390aa978", "assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024",
"assets/assets/images/google_logo.png": "0f118259ce403274f407f5e982e681c3",
"assets/assets/images/logo_dark.png": "a233ed1d4d0f7414bf97a9a10f11fb0a", "assets/assets/images/logo_dark.png": "a233ed1d4d0f7414bf97a9a10f11fb0a",
"assets/assets/images/icon.png": "090f69e23311a4b6d851b3880ae52541",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "174c02fc4609e8fc4389f5d21f16a296",
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f", "assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
"assets/fonts/MaterialIcons-Regular.otf": "4e6447691c9509f7acdbf8a931a85ca1",
"assets/AssetManifest.json": "38d9aea341601f3a5c6fa7b5a1216ea5", "assets/AssetManifest.json": "38d9aea341601f3a5c6fa7b5a1216ea5",
"version.json": "9ec5e3813adc4bfd8713556c5059e97d", "assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "174c02fc4609e8fc4389f5d21f16a296",
"manifest.json": "ef43d90e57aa7682d7e2cfba2f484a40",
"favicon.ico": "51636d3a390451561744c42188ccd628",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed", "icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"main.dart.js": "9542568225b6ad9e1ffbc87c3e6f74a2" "icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"main.dart.js": "7905763d1c2f393f06c8c5b95449e54b",
"manifest.json": "ef43d90e57aa7682d7e2cfba2f484a40",
"/": "2b102108c1e1cc925a193f954e3a2b68"
}; };
// The application shell files that are downloaded before a service worker can // The application shell files that are downloaded before a service worker can

204444
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

210264
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

216217
public/main.last.dart.js vendored

File diff suppressed because one or more lines are too long

216052
public/main.next.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

201726
public/main.wasm.dart.js vendored

File diff suppressed because one or more lines are too long

View File

@ -4316,8 +4316,8 @@ $LANG = array(
'payment_method_cannot_be_preauthorized' => 'This payment method cannot be preauthorized.', 'payment_method_cannot_be_preauthorized' => 'This payment method cannot be preauthorized.',
'kbc_cbc' => 'KBC/CBC', 'kbc_cbc' => 'KBC/CBC',
'bancontact' => 'Bancontact', 'bancontact' => 'Bancontact',
'sepa_mandat' => 'By providing your IBAN and confirming this payment, you are authorizing :company and Stripe, our payment service provider, to send instructions to your bank to debit your account and your bank to debit your account in accordance with those instructions. You are entitled to a refund from your bank under the terms and conditions of your agreement with your bank. A refund must be claimed within 8 weeks starting from the date on which your account was debited.', 'sepa_mandat' => 'By providing your IBAN and confirming this payment, you are authorizing Rocketship Inc. and Stripe, our payment service provider, to send instructions to your bank to debit your account and your bank to debit your account in accordance with those instructions. You are entitled to a refund from your bank under the terms and conditions of your agreement with your bank. A refund must be claimed within 8 weeks starting from the date on which your account was debited.',
'bank_account_holder' => 'Bank Account Holder', 'ideal' => 'iDEAL',
); );
return $LANG; return $LANG;

View File

@ -13,31 +13,51 @@
zoom: 80%; zoom: 80%;
} }
body, html {
margin: 0;
padding: 0;
}
@page { @page {
margin: -0.25cm !important; margin: 0 !important;
size: $page_size $page_layout; size: $page_size $page_layout;
} }
p { p {
margin: 0; margin: 0;
padding: 0; padding: 0;
/* page-break-after: always; */
} }
.header-wrapper { #spacer-table > * > tr > td {
padding: 0;
}
#spacer-table{
width: 100%;
}
#header {
display: grid; display: grid;
grid-template-columns: 1.5fr 1fr 1fr; grid-template-columns: 1.5fr 1fr 1fr;
gap: 20px; gap: 20px;
background-color: #2d2c2a; background-color: #2d2c2a;
padding: 3rem;
color: white; color: white;
min-width: 100%;
line-height: var(--line-height); line-height: var(--line-height);
position: fixed;
top: 0;
} }
#header, #header-spacer {
height: 160px;
padding: 3rem;
margin-bottom: 3rem;
}
.company-logo { .company-logo {
height: 100%; height: 100%;
padding-right: 120px; max-width: 100%;
object-fit: contain;
object-position: left center;
} }
#company-details, #company-details,
@ -46,6 +66,12 @@
flex-direction: column; flex-direction: column;
} }
#company-details,
#company-address,
.logo-container {
max-height: 160px;
}
#client-details { #client-details {
margin: 2rem; margin: 2rem;
display: flex; display: flex;
@ -61,7 +87,6 @@
display: grid; display: grid;
grid-template-columns: 1.5fr 1fr; grid-template-columns: 1.5fr 1fr;
padding-left: 1rem; padding-left: 1rem;
padding-top: 3rem;
} }
.entity-details-wrapper { .entity-details-wrapper {
@ -86,7 +111,11 @@
table-layout: fixed; table-layout: fixed;
overflow-wrap: break-word; overflow-wrap: break-word;
margin-top: 3rem; margin-top: 3rem;
/* margin-bottom: 200px; */ margin-bottom: 200px;
}
[data-ref="table"]:last-child{
margin-bottom:0;
} }
.task-time-details { .task-time-details {
@ -138,23 +167,23 @@
gap: 80px; gap: 80px;
} }
#table-totals .totals-table-right-side>* { #table-totals .totals-table-right-side > * {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
#table-totals>.totals-table-right-side>*> :nth-child(1) { #table-totals > .totals-table-right-side > * > :nth-child(1) {
text-align: "$dir_text_align"; text-align: "$dir_text_align";
margin-top: .75rem; margin-top: .75rem;
} }
#table-totals>.totals-table-right-side> * > :not([hidden]) ~ :not([hidden]) { #table-totals > .totals-table-right-side > * > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0; --tw-space-y-reverse: 0;
margin-top: calc(.75rem * calc(1 - var(--tw-space-y-reverse))); margin-top: calc(.75rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(.75rem * var(--tw-space-y-reverse)); margin-bottom: calc(.75rem * var(--tw-space-y-reverse));
} }
#table-totals>.totals-table-right-side>*> :nth-child(2) { #table-totals > .totals-table-right-side > * > :nth-child(2) {
text-align: right; text-align: right;
} }
@ -186,51 +215,40 @@
font-size: 1.5rem; font-size: 1.5rem;
} }
.footer-wrapper { #footer {
margin-top: 1rem; margin-top: 1rem;
background-color: #2d2c2a; background-color: #2d2c2a;
height: 160px;
min-width: 100%; min-width: 100%;
position: fixed; position: fixed;
bottom: 0; bottom: 0;
padding: 1rem 3rem;
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
gap: 15px; gap: 15px;
color: white; color: white;
} }
#footer, #footer-spacer {
height: 160px;
padding: 1rem 3rem;
margin-top: 3rem;
}
[data-ref="total_table-footer"] { [data-ref="total_table-footer"] {
padding-top: 0.5rem padding-top: 0.5rem
} }
/** Repeating header & footer styling. */
table { table {
width: 100%; width: 100%;
} }
table.print-content {} table[data-ref="table"] th,
table[data-ref="table"] td {
table.print-content th,
table.print-content td {
padding: .2rem .4rem; padding: .2rem .4rem;
text-align: left; text-align: left;
vertical-align: top; vertical-align: top;
border-top: 1px solid #dee2e6; border-top: 1px solid #dee2e6;
} }
@media print {
.print-footer {
position: fixed;
bottom: 0;
left: 0;
}
.no-print {
display: none
}
}
/** Markdown-specific styles. **/ /** Markdown-specific styles. **/
#product-table h3, #product-table h3,
#task-table h3, #task-table h3,
@ -239,9 +257,9 @@
margin-bottom: 0; margin-bottom: 0;
} }
[data-ref="product_table-product.description-th"] { [data-ref="product_table-product.description-th"] {
width: 23%; width: 23%;
} }
[data-ref="statement-totals"] { [data-ref="statement-totals"] {
margin-top: 1rem; margin-top: 1rem;
@ -253,10 +271,6 @@
white-space: nowrap; white-space: nowrap;
} }
.logo-container {
max-height: 160px;
}
#statement-invoice-table-totals > p { #statement-invoice-table-totals > p {
margin-right: 2rem; margin-right: 2rem;
margin-top: 1rem; margin-top: 1rem;
@ -264,101 +278,94 @@
/** Useful snippets, uncomment to enable. **/ /** Useful snippets, uncomment to enable. **/
/** Hide company logo **/ /** Hide company logo **/
/* .company-logo { display: none } */ /* .company-logo { display: none } */
/* Hide company details */ /* Hide company details */
/* #company-details > * { display: none } */ /* #company-details > * { display: none } */
/* Hide company address */ /* Hide company address */
/* #company-address > * { display: none } */ /* #company-address > * { display: none } */
/* Hide public notes */ /* Hide public notes */
/* [data-ref="total_table-public_notes"] { display: none } */ /* [data-ref="total_table-public_notes"] { display: none } */
/* Hide terms label */ /* Hide terms label */
/* [data-ref="total_table-terms-label"] { display: none } */ /* [data-ref="total_table-terms-label"] { display: none } */
/* Hide totals table */ /* Hide totals table */
/* #table-totals { display: none } */ /* #table-totals { display: none } */
/* Hide totals table left side */ /* Hide totals table left side */
/* #table-totals div:first-child > * { display: none !important } */ /* #table-totals div:first-child > * { display: none !important } */
/* Hide totals table right side */ /* Hide totals table right side */
/* .totals-table-right-side { display: none } */ /* .totals-table-right-side { display: none } */
/** For more info, please check our docs: https://invoiceninja.github.io **/ /** For more info, please check our docs: https://invoiceninja.github.io **/
/** To find out selectors on your own: https://invoiceninja.github.io/docs/custom-fields/#snippets **/ /** To find out selectors on your own: https://invoiceninja.github.io/docs/custom-fields/#snippets **/
</style> </style>
<table> <div id="header">
<!-- Start Header --> <div class="logo-container">
<thead> <img class="company-logo" src="$company.logo" alt="$company.name logo"/>
<tr> </div>
<td> <div id="company-details"></div>
<div class="header-wrapper" id="header"> <div id="company-address"></div>
<div class="logo-container"> </div>
<img class="company-logo" src="$company.logo" alt="$company.name logo"/> <div id="body">
</div> <table id="spacer-table" cellspacing="0" >
<div id="company-details"></div> <thead>
<div id="company-address"></div> <tr>
</div> <td>
</td> <div id="header-spacer"></div>
</tr> </td>
</thead> </tr>
<!-- End Header --> </thead>
<tr> <tbody>
<td id="body"> <tr>
<div class="client-entity-wrapper"> <td>
<div class="client-wrapper-left-side"> <div class="client-entity-wrapper">
<h4 class="entity-label">$entity_label</h4> <div class="client-wrapper-left-side">
<div id="client-details" cellspacing="0"></div> <h4 class="entity-label">$entity_label</h4>
</div> <div id="client-details" cellspacing="0"></div>
</div>
<div class="entity-details-wrapper-right-side"> <div class="entity-details-wrapper-right-side">
<div class="entity-details-wrapper"> <div class="entity-details-wrapper">
<table id="entity-details" dir="$dir"></table> <table id="entity-details" dir="$dir"></table>
</div>
</div> </div>
</div> </div>
</div>
<!-- Start Print Content --> <table id="product-table" cellspacing="0" data-ref="table"></table>
<table id="product-table" cellspacing="0" class="print-content" data-ref="table"></table>
<table id="task-table" cellspacing="0" class="print-content" data-ref="table"></table> <table id="task-table" cellspacing="0" data-ref="table"></table>
<table id="delivery-note-table" cellspacing="0" class="print-content" data-ref="table"></table> <table id="delivery-note-table" cellspacing="0" data-ref="table"></table>
<table id="statement-invoice-table" cellspacing="0" class="print-content" data-ref="table"></table> <table id="statement-invoice-table" cellspacing="0" data-ref="table"></table>
<div id="statement-invoice-table-totals" data-ref="statement-totals"></div> <div id="statement-invoice-table-totals" data-ref="statement-totals"></div>
<table id="statement-payment-table" cellspacing="0" class="print-content" data-ref="table"></table> <table id="statement-payment-table" cellspacing="0" data-ref="table"></table>
<div id="statement-payment-table-totals" data-ref="statement-totals"></div> <div id="statement-payment-table-totals" data-ref="statement-totals"></div>
<table id="statement-aging-table" cellspacing="0" class="print-content" data-ref="table"></table> <table id="statement-aging-table" cellspacing="0" data-ref="table"></table>
<div id="statement-aging-table-totals" data-ref="statement-totals"></div> <div id="statement-aging-table-totals" data-ref="statement-totals"></div>
<!-- End Print Content --> <div id="table-totals" cellspacing="0"></div>
</td> </td>
</tr> </tr>
<tr> </tbody>
<td> <tfoot>
<div id="table-totals" cellspacing="0"></div> <tr>
</td> <td>
</tr> <div id="footer-spacer"></div>
<!-- Start Space For Footer --> </td>
<tfoot> </tr>
<tr> </tfoot>
<td style="height: 180px"> </table>
<!-- Leave this empty and don't remove it. This space is where footer placed on print. --> </div>
</td> <div id="footer">
</tr>
</tfoot>
<!-- End Space For Footer -->
</table>
<!-- Start Footer -->
<div class="footer-wrapper print-footer" id="footer">
<div> <div>
<p data-ref="total_table-footer">$entity_footer</p> <p data-ref="total_table-footer">$entity_footer</p>
@ -366,14 +373,13 @@
// Clear up space a bit, if [product-table, tasks-table, delivery-note-table] isn't present. // Clear up space a bit, if [product-table, tasks-table, delivery-note-table] isn't present.
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
let tables = [ let tables = [
'product-table', 'task-table', 'delivery-note-table', 'product-table', 'task-table', 'delivery-note-table', 'statement-invoice-table-totals', 'statement-payment-table-totals','statement-invoice-table-totals',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals', 'statement-invoice-table', 'statement-payment-table', 'statement-aging-table', 'statement-aging-table-totals', 'statement-payment-table-totals'
]; ];
tables.forEach((tableIdentifier) => { tables.forEach((tableIdentifier) => {
document.getElementById(tableIdentifier).childElementCount === 0 const el =document.getElementById(tableIdentifier);
? document.getElementById(tableIdentifier).style.display = 'none' if(el && el.childElementCount === 0)el.remove()
: '';
}); });
}); });
</script> </script>
@ -381,4 +387,3 @@
<div> <!-- #2 column --> </div> <div> <!-- #2 column --> </div>
<div> <!-- #3 column --> </div> <div> <!-- #3 column --> </div>
</div> </div>
<!-- End Footer -->

View File

@ -9,38 +9,54 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
font-size: "7px"; font-size: "7px";
margin: 0;
padding: 0;
zoom: 80%; zoom: 80%;
} }
body, html {
margin: 0;
padding: 0;
}
@page { @page {
margin: -0.22cm; margin: 0 !important;
size: A4 portrait; size: $page_size $page_layout;
} }
p { p {
margin: 0; margin: 0;
padding: 0; padding: 0;
/* page-break-after: always; */
} }
.header-container { #spacer-table > * > tr > td {
background-color: var(--primary-color); padding: 0;
color: white; }
display: grid;
grid-template-columns: 1.5fr 1fr; #spacer-table{
padding: 3rem; width: 100%;
min-width: 100%; }
height: 160px;
#header {
background-color: var(--primary-color);
color: white;
display: grid;
grid-template-columns: 1.5fr 1fr;
position:fixed;
top: 0;
width: 100%;
}
#header, #header-spacer {
height: 160px;
padding: 3rem;
margin-bottom: 3rem;
} }
.company-name { .company-name {
text-align: left; text-align: left;
} }
.header-container .company-name { #header .company-name {
font-size: 2rem; font-size: 2rem;
} }
#entity-details { #entity-details {
@ -55,7 +71,7 @@
} }
.logo-client-wrapper { .logo-client-wrapper {
margin: 3rem 2rem; margin: 0 2rem 3rem;
display: grid; display: grid;
grid-template-columns: 1.5fr 1fr; grid-template-columns: 1.5fr 1fr;
} }
@ -121,15 +137,18 @@
text-align: right; text-align: right;
} }
.footer-wrapper { #footer {
margin-top: 1rem; background-color: var(--primary-color);
background-color: var(--primary-color); width: 100%;
padding-left: 3rem; position: fixed;
padding-right: 3rem; bottom: 0;
height: 220px; }
width: 100%;
position: fixed;
bottom: 0; #footer, #footer-spacer {
height: 220px;
padding: 1rem 3rem;
margin-top: 1rem;
} }
.footer-content { .footer-content {
@ -204,54 +223,21 @@
font-size: 1.3rem; font-size: 1.3rem;
} }
table.page-container {
page-break-after: always;
min-width: 100%;
}
thead.page-header {
display: table-header-group;
}
tfoot.page-footer {
display: table-footer-group;
}
.page-content-cell {
padding: 1rem;
}
[data-ref="total_table-footer"] { [data-ref="total_table-footer"] {
margin-top: 2rem; margin-top: 2rem;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
/** Repeating header & footer styling. */
table { table {
width: 100%; width: 100%;
} }
table.print-content { table[data-ref="table"] th,
} table[data-ref="table"] td {
padding: 0.2rem 0.4rem;
table.print-content th, text-align: left;
table.print-content td { vertical-align: top;
padding: 0.2rem 0.4rem; border-top: 1px solid #dee2e6;
text-align: left;
vertical-align: top;
border-top: 1px solid #dee2e6;
}
@media print {
.print-footer {
position: fixed;
bottom: 0;
left: 0;
}
.no-print {
display: none;
}
} }
/** Markdown-specific styles. **/ /** Markdown-specific styles. **/
@ -306,70 +292,57 @@
/** For more info, please check our docs: https://invoiceninja.github.io **/ /** For more info, please check our docs: https://invoiceninja.github.io **/
/** To find out selectors on your own: https://invoiceninja.github.io/docs/custom-fields/#snippets **/ /** To find out selectors on your own: https://invoiceninja.github.io/docs/custom-fields/#snippets **/
</style> </style>
<div id="header">
<h1 class="company-name">$company.name</h1>
<table id="entity-details" cellspacing="0" dir="$dir"></table>
</div>
<div id="body">
<table id="spacer-table" cellspacing="0">
<thead>
<tr>
<td>
<div id="header-spacer"></div>
</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="logo-client-wrapper">
<img class="company-logo" src="$company.logo" alt="$company.name logo"/>
<div id="client-details"></div>
</div>
<div class="table-wrapper">
<table id="product-table" cellspacing="0" data-ref="table"></table>
<table> <table id="task-table" cellspacing="0" data-ref="table"></table>
<!-- Start Header -->
<thead>
<tr>
<td>
<div class="header-container" id="header">
<h1 class="company-name">$company.name</h1>
<table id="entity-details" cellspacing="0" dir="$dir"></table>
</div>
</td>
</tr>
</thead>
<!-- End Header -->
<tr>
<td id="body">
<div class="logo-client-wrapper">
<img
class="company-logo"
src="$company.logo"
alt="$company.name logo"
/>
<div id="client-details"></div> <table id="delivery-note-table" cellspacing="0" data-ref="table"></table>
</div>
<!-- Start Print Content --> <table id="statement-invoice-table" cellspacing="0" data-ref="table"></table>
<div class="table-wrapper"> <div id="statement-invoice-table-totals" data-ref="statement-totals"></div>
<table id="product-table" cellspacing="0" class="print-content" data-ref="table"></table>
<table id="task-table" cellspacing="0" class="print-content" data-ref="table"></table> <table id="statement-payment-table" cellspacing="0" data-ref="table"></table>
<div id="statement-payment-table-totals" data-ref="statement-totals"></div>
<table id="delivery-note-table" cellspacing="0" class="print-content" data-ref="table"></table> <table id="statement-aging-table" cellspacing="0" data-ref="table"></table>
<div id="statement-aging-table-totals" data-ref="statement-totals"></div>
</div>
<div id="table-totals" cellspacing="0"></div>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>
<div id="footer-spacer"></div>
</td>
</tr>
</tfoot>
</table>
</div>
<table id="statement-invoice-table" cellspacing="0" class="print-content" data-ref="table"></table> <div id="footer">
<div id="statement-invoice-table-totals" data-ref="statement-totals"></div>
<table id="statement-payment-table" cellspacing="0" class="print-content" data-ref="table"></table>
<div id="statement-payment-table-totals" data-ref="statement-totals"></div>
<table id="statement-aging-table" cellspacing="0" class="print-content" data-ref="table"></table>
<div id="statement-aging-table-totals" data-ref="statement-totals"></div>
</div>
<!-- End Print Content -->
</td>
</tr>
<tr>
<td>
<div id="table-totals" cellspacing="0"></div>
</td>
</tr>
<!-- Start Space For Footer -->
<tfoot>
<tr>
<td style="height: 230px">
<!-- Leave this empty and don't remove it. This space is where footer placed on print -->
</td>
</tr>
</tfoot>
<!-- End Space For Footer -->
</table>
<!-- Start Footer -->
<div class="footer-wrapper print-footer" id="footer">
<div class="footer-content"> <div class="footer-content">
<div> <div>
<p data-ref="total_table-footer">$entity_footer</p> <p data-ref="total_table-footer">$entity_footer</p>
@ -378,14 +351,14 @@
// Clear up space a bit, if [product-table, tasks-table, delivery-note-table] isn't present. // Clear up space a bit, if [product-table, tasks-table, delivery-note-table] isn't present.
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
let tables = [ let tables = [
'product-table', 'task-table', 'delivery-note-table', 'product-table', 'task-table', 'delivery-note-table', 'statement-invoice-table-totals', 'statement-payment-table-totals','statement-invoice-table-totals',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals', 'statement-invoice-table', 'statement-payment-table', 'statement-aging-table', 'statement-aging-table-totals', 'statement-payment-table-totals'
]; ];
tables.forEach((tableIdentifier) => { tables.forEach((tableIdentifier) => {
document.getElementById(tableIdentifier).childElementCount === 0 document.getElementById(tableIdentifier).childElementCount === 0
? document.getElementById(tableIdentifier).style.display = 'none' ? document.getElementById(tableIdentifier).remove()
: ''; : '';
}); });
}); });
</script> </script>
@ -396,4 +369,3 @@
</div> </div>
</div> </div>
</div> </div>
<!-- End Footer -->

View File

@ -13,9 +13,13 @@
<div class="{{ $account && !$account->isPaid() ? 'col-span-2' : 'col-span-3' }} h-screen flex"> <div class="{{ $account && !$account->isPaid() ? 'col-span-2' : 'col-span-3' }} h-screen flex">
<div class="m-auto w-1/2 md:w-1/3 lg:w-1/4"> <div class="m-auto w-1/2 md:w-1/3 lg:w-1/4">
@if($account && !$account->isPaid()) @if($account && !$account->isPaid())
<div> <div>
<img src="{{ asset('images/invoiceninja-black-logo-2.png') }}" class="border-b border-gray-100 h-18 pb-4" alt="Invoice Ninja logo"> <img src="{{ asset('images/invoiceninja-black-logo-2.png') }}" class="border-b border-gray-100 h-18 pb-4" alt="Invoice Ninja logo">
</div> </div>
@elseif(isset($company) && !is_null($company))
<div>
<img src="{{ asset($company->present()->logo()) }}" class="h-14 mb-10" alt="{{ $company->present()->name() }} logo">
</div>
@endif @endif
<div class="flex flex-col"> <div class="flex flex-col">
<h1 class="text-center text-3xl">{{ ctrans('texts.password_recovery') }}</h1> <h1 class="text-center text-3xl">{{ ctrans('texts.password_recovery') }}</h1>

View File

@ -13,11 +13,14 @@
<div class="{{ $account && !$account->isPaid() ? 'col-span-2' : 'col-span-3' }} h-screen flex"> <div class="{{ $account && !$account->isPaid() ? 'col-span-2' : 'col-span-3' }} h-screen flex">
<div class="m-auto w-1/2 md:w-1/3 lg:w-1/4"> <div class="m-auto w-1/2 md:w-1/3 lg:w-1/4">
@if($account && !$account->isPaid()) @if($account && !$account->isPaid())
<div> <div>
<img src="{{ asset('images/invoiceninja-black-logo-2.png') }}" class="border-b border-gray-100 h-18 pb-4" alt="Invoice Ninja logo"> <img src="{{ asset('images/invoiceninja-black-logo-2.png') }}" class="border-b border-gray-100 h-18 pb-4" alt="Invoice Ninja logo">
</div> </div>
@elseif(isset($company) && !is_null($company))
<div>
<img src="{{ asset($company->present()->logo()) }}" class="h-14 mb-10" alt="{{ $company->present()->name() }} logo">
</div>
@endif @endif
<div class="flex flex-col"> <div class="flex flex-col">
<h1 class="text-center text-3xl">{{ ctrans('texts.password_recovery') }}</h1> <h1 class="text-center text-3xl">{{ ctrans('texts.password_recovery') }}</h1>

View File

@ -14,6 +14,7 @@
@csrf @csrf
<div class="grid grid-cols-12 gap-4 mt-10"> <div class="grid grid-cols-12 gap-4 mt-10">
@if($company->client_registration_fields)
@foreach($company->client_registration_fields as $field) @foreach($company->client_registration_fields as $field)
@if($field['required']) @if($field['required'])
<div class="col-span-12 md:col-span-6"> <div class="col-span-12 md:col-span-6">
@ -96,6 +97,7 @@
@endif @endif
@endif @endif
@endforeach @endforeach
@endif
</div> </div>
<div class="flex justify-between items-center mt-8"> <div class="flex justify-between items-center mt-8">

View File

@ -0,0 +1,8 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'iDEAL', 'card_title' =>
'iDEAL'])
@section('gateway_content')
@component('portal.ninja2020.components.general.card-element-single')
{{ __('texts.payment_method_cannot_be_preauthorized') }}
@endcomponent
@endsection

View File

@ -67,7 +67,7 @@
{{ ctrans('texts.invoice_date') }} {{ ctrans('texts.invoice_date') }}
</dt> </dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2"> <dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $invoice->date }} {{ $invoice->formatDate($invoice->date, $invoice->client->date_format()) }}
</dd> </dd>
@endif @endif
</div> </div>

View File

@ -39,7 +39,7 @@
<input type="checkbox" class="form-checkbox mr-1" <input type="checkbox" class="form-checkbox mr-1"
name="send_logs" {{ old('send_logs' ? 'checked': '') }}> name="send_logs" {{ old('send_logs' ? 'checked': '') }}>
<span>{{ ctrans('texts.send_fail_logs_to_our_server') }}</span> <span>{{ ctrans('texts.send_fail_logs_to_our_server') }}</span>
<a class="button-link mt-1 block" href="https://www.invoiceninja.com/privacy-policy/">Read more <a class="button-link mt-1 block" target="_blank" href="https://www.invoiceninja.com/privacy-policy/">Read more
about how we use this.</a> about how we use this.</a>
</dd> </dd>
</div> </div>

View File

@ -10,7 +10,12 @@
<div class="col-span-12 md:col-start-4 md:col-span-6 mt-4 md:mt-10"> <div class="col-span-12 md:col-start-4 md:col-span-6 mt-4 md:mt-10">
<h1 class="text-center text-2xl font-semibold">Invoice Ninja Setup</h1> <h1 class="text-center text-2xl font-semibold">Invoice Ninja Setup</h1>
<p class="text-sm text-center">{{ ctrans('texts.if_you_need_help') }} <p class="text-sm text-center">{{ ctrans('texts.if_you_need_help') }}
<a href="https://www.invoiceninja.com/forums/forum/support/" class="button-link">{{ ctrans('texts.support_forum') }}</a> <a
target="_blank"
href="https://www.invoiceninja.com/forums/forum/support/"
class="button-link underline">
{{ ctrans('texts.support_forum') }}
</a>
</p> </p>
@if($errors->any()) @if($errors->any())

View File

@ -103,6 +103,7 @@ Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'clie
Route::get('quote/{invitation_key}/download_pdf', 'QuoteController@downloadPdf')->name('quote.download_invitation_key'); Route::get('quote/{invitation_key}/download_pdf', 'QuoteController@downloadPdf')->name('quote.download_invitation_key');
Route::get('credit/{invitation_key}/download_pdf', 'CreditController@downloadPdf')->name('credit.download_invitation_key'); Route::get('credit/{invitation_key}/download_pdf', 'CreditController@downloadPdf')->name('credit.download_invitation_key');
Route::get('{entity}/{invitation_key}/download', 'ClientPortal\InvitationController@routerForDownload'); Route::get('{entity}/{invitation_key}/download', 'ClientPortal\InvitationController@routerForDownload');
Route::get('pay/{invitation_key}', 'ClientPortal\InvitationController@payInvoice')->name('pay.invoice');
// Route::get('{entity}/{client_hash}/{invitation_key}', 'ClientPortal\InvitationController@routerForIframe')->name('invoice.client_hash_and_invitation_key'); //should never need this // Route::get('{entity}/{client_hash}/{invitation_key}', 'ClientPortal\InvitationController@routerForIframe')->name('invoice.client_hash_and_invitation_key'); //should never need this
}); });

View File

@ -0,0 +1,106 @@
<?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\Browser\ClientPortal\Gateways\Mollie;
use App\Models\CompanyGateway;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\ClientPortal\Login;
class IDEALTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->disableCompanyGateways();
CompanyGateway::where('gateway_key', '1bd651fb213ca0c9d66ae3c336dc77e8')->restore();
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
}
public function testSuccessfulPayment(): void
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('iDEAL')
->waitForText('Test profile')
->press('ABN AMRO')
->radio('final_state', 'paid')
->press('Continue')
->waitForText('Details of the payment')
->assertSee('Completed');
});
}
public function testOpenPayment(): void
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('iDEAL')
->waitForText('Test profile')
->press('ABN AMRO')
->radio('final_state', 'open')
->press('Continue')
->waitForText('Details of the payment')
->assertSee('Pending');
});
}
public function testFailedPayment(): void
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('iDEAL')
->waitForText('Test profile')
->press('ABN AMRO')
->radio('final_state', 'failed')
->press('Continue')
->waitForText('Failed.');
});
}
public function testCancelledPayment(): void
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('iDEAL')
->waitForText('Test profile')
->press('ABN AMRO')
->radio('final_state', 'canceled')
->press('Continue')
->waitForText('Cancelled.');
});
}
}

View File

@ -93,9 +93,9 @@ class CreditsTest extends TestCase
$this->actingAs($client->contacts->first(), 'contact'); $this->actingAs($client->contacts->first(), 'contact');
Livewire::test(CreditsTable::class, ['company' => $company]) Livewire::test(CreditsTable::class, ['company' => $company])
->assertSee('testing-number-01') ->assertDontSee('testing-number-01')
->assertSee('testing-number-02') ->assertSee('testing-number-02')
->assertDontSee('testing-number-03'); ->assertSee('testing-number-03');
} }
public function testShowingCreditsWithNullDueDate() public function testShowingCreditsWithNullDueDate()
@ -122,6 +122,7 @@ class CreditsTest extends TestCase
'client_id' => $client->id, 'client_id' => $client->id,
'number' => 'testing-number-01', 'number' => 'testing-number-01',
'status_id' => Credit::STATUS_SENT, 'status_id' => Credit::STATUS_SENT,
'due_date' => null,
]); ]);
Credit::factory()->create([ Credit::factory()->create([
@ -142,12 +143,21 @@ class CreditsTest extends TestCase
'status_id' => Credit::STATUS_SENT, 'status_id' => Credit::STATUS_SENT,
]); ]);
Credit::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'client_id' => $client->id,
'number' => 'testing-number-04',
'due_date' => '',
'status_id' => Credit::STATUS_SENT,
]);
$this->actingAs($client->contacts->first(), 'contact'); $this->actingAs($client->contacts->first(), 'contact');
Livewire::test(CreditsTable::class, ['company' => $company]) Livewire::test(CreditsTable::class, ['company' => $company])
->assertSee('testing-number-01') ->assertSee('testing-number-01')
->assertSee('testing-number-02') ->assertSee('testing-number-02')
->assertDontSee('testing-number-03'); ->assertSee('testing-number-03');
} }
} }