mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2026-03-03 02:57:01 +00:00
Merge pull request #11564 from turbo124/v5-develop
Improvements for SwissQR Codes
This commit is contained in:
@@ -114,13 +114,19 @@ class RecurringInvoiceToInvoiceFactory
|
||||
|
||||
$end_date = $end_date->format('Y-m-d');
|
||||
|
||||
$einvoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice();
|
||||
// $einvoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice();
|
||||
// $ip = new \InvoiceNinja\EInvoice\Models\Peppol\PeriodType\InvoicePeriod();
|
||||
// $ip->StartDate = new \DateTime($start_date);
|
||||
// $ip->EndDate = new \DateTime($end_date);
|
||||
// $einvoice->InvoicePeriod = [$ip];
|
||||
|
||||
$ip = new \InvoiceNinja\EInvoice\Models\Peppol\PeriodType\InvoicePeriod();
|
||||
$ip->StartDate = new \DateTime($start_date);
|
||||
$ip->EndDate = new \DateTime($end_date);
|
||||
$einvoice->InvoicePeriod = [$ip];
|
||||
// 2026-01-12 - To prevent storing datetime objects in the database, we manually build the InvoicePeriod object
|
||||
$einvoice = new \stdClass();
|
||||
|
||||
$invoice_period = new \stdClass();
|
||||
$invoice_period->StartDate = $start_date;
|
||||
$invoice_period->EndDate = $end_date;
|
||||
$einvoice->InvoicePeriod = [$invoice_period];
|
||||
|
||||
$stub = new \stdClass();
|
||||
$stub->Invoice = $einvoice;
|
||||
|
||||
@@ -338,6 +338,33 @@ abstract class QueryFilters
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Filter by created at date range
|
||||
*
|
||||
* @param string $date_range
|
||||
* @return Builder
|
||||
*/
|
||||
public function created_between(string $date_range = ''): Builder
|
||||
{
|
||||
$parts = explode(",", $date_range);
|
||||
|
||||
if (count($parts) != 2 || !in_array('created_at', \Illuminate\Support\Facades\Schema::getColumnListing($this->builder->getModel()->getTable()))) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
$start_date = Carbon::parse($parts[0]);
|
||||
$end_date = Carbon::parse($parts[1]);
|
||||
|
||||
return $this->builder->whereBetween('created_at', [$start_date, $end_date]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by date range
|
||||
*
|
||||
|
||||
@@ -297,10 +297,10 @@ class Nordigen
|
||||
* getTransactions
|
||||
*
|
||||
* @param string $accountId
|
||||
* @param string $dateFrom
|
||||
* @param ?string $dateFrom
|
||||
* @return array
|
||||
*/
|
||||
public function getTransactions(Company $company, string $accountId, string $dateFrom = null): array
|
||||
public function getTransactions(Company $company, string $accountId, ?string $dateFrom = null): array
|
||||
{
|
||||
$transactionResponse = $this->client->account($accountId)->getAccountTransactions($dateFrom);
|
||||
|
||||
@@ -310,7 +310,7 @@ class Nordigen
|
||||
|
||||
public function disabledAccountEmail(BankIntegration $bank_integration): void
|
||||
{
|
||||
$cache_key = "email_quota:{$bank_integration->company->company_key}:{$bank_integration->id}";
|
||||
$cache_key = "email_quota:{$bank_integration->account->key}:bank_integration_notified";
|
||||
|
||||
if (Cache::has($cache_key)) {
|
||||
return;
|
||||
|
||||
@@ -68,10 +68,12 @@ class SwissQrGenerator
|
||||
// Add creditor information
|
||||
// Who will receive the payment and to which bank account?
|
||||
$qrBill->setCreditor(
|
||||
QrBill\DataGroup\Element\CombinedAddress::create(
|
||||
$this->company->present()->name(),
|
||||
$this->company->present()->address1(),
|
||||
$this->company->present()->getCompanyCityState(),
|
||||
QrBill\DataGroup\Element\StructuredAddress::createWithStreet(
|
||||
substr($this->company->present()->name(), 0, 70),
|
||||
$this->company->settings->address1 ? substr($this->company->settings->address1, 0, 70) : ' ',
|
||||
$this->company->settings->address2 ? substr($this->company->settings->address2, 0, 16) : ' ',
|
||||
$this->company->settings->postal_code ? substr($this->company->settings->postal_code, 0, 16) : ' ',
|
||||
$this->company->settings->city ? substr($this->company->settings->city, 0, 35) : ' ',
|
||||
'CH'
|
||||
)
|
||||
);
|
||||
@@ -170,7 +172,7 @@ class SwissQrGenerator
|
||||
$output = new QrBill\PaymentPart\Output\HtmlOutput\HtmlOutput($qrBill, $this->resolveLanguage());
|
||||
|
||||
$html = $output
|
||||
->setPrintable(false)
|
||||
// ->setPrintable(false)
|
||||
->getPaymentPart();
|
||||
|
||||
// return $html;
|
||||
|
||||
@@ -78,6 +78,11 @@ class YodleeController extends BaseController
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
if ($bi = BankIntegration::where('bank_account_id', $account['id'])->where('company_id', $company->id)->first()) {
|
||||
|
||||
if($bi->deleted_at){
|
||||
continue;
|
||||
}
|
||||
|
||||
$bi->disabled_upstream = false;
|
||||
$bi->balance = $account['current_balance'];
|
||||
$bi->currency = $account['account_currency'];
|
||||
|
||||
@@ -286,7 +286,7 @@ class BankIntegrationController extends BaseController
|
||||
$bank_integration->bank_account_status = "429 Rate limit reached, check back later....";
|
||||
$bank_integration->save();
|
||||
return;
|
||||
} elseif (is_array($account) && isset($account['account_status']) && !in_array($account['account_status'], ['READY', 'PROCESSING','DISCOVERED'])) {
|
||||
} elseif (is_array($account) && isset($account['account_status']) && !in_array($account['account_status'], ['READY', 'PROCESSING', 'DISCOVERED'])) {
|
||||
$bank_integration->disabled_upstream = true;
|
||||
$bank_integration->save();
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ class EInvoiceController extends BaseController
|
||||
|
||||
match ($request->entity) {
|
||||
'invoices' => $data = $el->checkInvoice($request->getEntity()),
|
||||
'recurring_invoices' => $data = $el->checkRecurringInvoice($request->getEntity()),
|
||||
'clients' => $data = $el->checkClient($request->getEntity()),
|
||||
'companies' => $data = $el->checkCompany($request->getEntity()),
|
||||
default => $data['passes'] = false,
|
||||
@@ -87,7 +88,24 @@ class EInvoiceController extends BaseController
|
||||
$pm->CardAccount = $card_account;
|
||||
}
|
||||
|
||||
if (isset($payment_means['iban'])) {
|
||||
if (isset($payment_means['code']) && $payment_means['code'] == '58') {
|
||||
$fib = new FinancialInstitutionBranch();
|
||||
$fi = new FinancialInstitution();
|
||||
$bic_id = new ID();
|
||||
$bic_id->value = $payment_means['bic_swift'];
|
||||
$fi->ID = $bic_id;
|
||||
$fib->FinancialInstitution = $fi;
|
||||
$pfa = new PayeeFinancialAccount();
|
||||
$iban_id = new ID();
|
||||
$iban_id->value = $payment_means['iban'];
|
||||
$pfa->ID = $iban_id;
|
||||
$pfa->Name = $payment_means['account_holder'] ?? 'SEPA_CREDIT_TRANSFER';
|
||||
$pfa->FinancialInstitutionBranch = $fib;
|
||||
|
||||
$pm->PayeeFinancialAccount = $pfa;
|
||||
|
||||
}
|
||||
else if (isset($payment_means['iban'])) {
|
||||
$fib = new FinancialInstitutionBranch();
|
||||
$fi = new FinancialInstitution();
|
||||
$bic_id = new ID();
|
||||
|
||||
@@ -232,7 +232,7 @@ class SearchController extends Controller
|
||||
'name' => $result['_source']['name'],
|
||||
'type' => '/expense',
|
||||
'id' => $result['_source']['hashed_id'],
|
||||
'path' => "/expenses/{$result['_source']['hashed_id']}"
|
||||
'path' => "/expenses/{$result['_source']['hashed_id']}/edit"
|
||||
];
|
||||
|
||||
break;
|
||||
|
||||
@@ -12,31 +12,32 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Utils\Ninja;
|
||||
use App\Models\CompanyUser;
|
||||
use App\Factory\UserFactory;
|
||||
use App\Filters\UserFilters;
|
||||
use Illuminate\Http\Response;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Events\User\UserWasCreated;
|
||||
use App\Events\User\UserWasDeleted;
|
||||
use App\Events\User\UserWasUpdated;
|
||||
use App\Factory\UserFactory;
|
||||
use App\Filters\UserFilters;
|
||||
use App\Http\Controllers\Traits\VerifiesUserEmail;
|
||||
use App\Http\Requests\User\BulkUserRequest;
|
||||
use App\Http\Requests\User\CreateUserRequest;
|
||||
use App\Http\Requests\User\DestroyUserRequest;
|
||||
use App\Http\Requests\User\DetachCompanyUserRequest;
|
||||
use App\Http\Requests\User\DisconnectUserMailerRequest;
|
||||
use App\Http\Requests\User\EditUserRequest;
|
||||
use App\Http\Requests\User\ReconfirmUserRequest;
|
||||
use App\Http\Requests\User\ShowUserRequest;
|
||||
use App\Http\Requests\User\StoreUserRequest;
|
||||
use App\Http\Requests\User\UpdateUserRequest;
|
||||
use App\Jobs\Company\CreateCompanyToken;
|
||||
use App\Jobs\User\UserEmailChanged;
|
||||
use App\Models\CompanyUser;
|
||||
use App\Models\User;
|
||||
use App\Repositories\UserRepository;
|
||||
use App\Transformers\UserTransformer;
|
||||
use App\Utils\Ninja;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Http\Response;
|
||||
use App\Jobs\Company\CreateCompanyToken;
|
||||
use App\Http\Requests\User\BulkUserRequest;
|
||||
use App\Http\Requests\User\EditUserRequest;
|
||||
use App\Http\Requests\User\ShowUserRequest;
|
||||
use App\Http\Requests\User\PurgeUserRequest;
|
||||
use App\Http\Requests\User\StoreUserRequest;
|
||||
use App\Http\Requests\User\CreateUserRequest;
|
||||
use App\Http\Requests\User\UpdateUserRequest;
|
||||
use App\Http\Requests\User\DestroyUserRequest;
|
||||
use App\Http\Requests\User\ReconfirmUserRequest;
|
||||
use App\Http\Controllers\Traits\VerifiesUserEmail;
|
||||
use App\Http\Requests\User\DetachCompanyUserRequest;
|
||||
use App\Http\Requests\User\DisconnectUserMailerRequest;
|
||||
|
||||
/**
|
||||
* Class UserController.
|
||||
@@ -350,4 +351,12 @@ class UserController extends BaseController
|
||||
|
||||
}
|
||||
|
||||
public function purge(PurgeUserRequest $request, User $user)
|
||||
{
|
||||
$this->user_repo->purge($user, auth()->user());
|
||||
|
||||
return response()->noContent();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -194,6 +194,10 @@ class UpdateCompanyRequest extends Request
|
||||
$input['session_timeout'] = 0;
|
||||
}
|
||||
|
||||
if($company->settings->e_invoice_type == 'VERIFACTU') {
|
||||
$input['calculate_taxes'] = false;
|
||||
}
|
||||
|
||||
$this->replace($input);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,9 @@ use App\Models\Client;
|
||||
use App\Models\Company;
|
||||
use App\Models\Invoice;
|
||||
use App\Http\Requests\Request;
|
||||
use App\Services\EDocument\Standards\Validation\Peppol\EntityLevel;
|
||||
use Illuminate\Validation\Rule;
|
||||
use App\Models\RecurringInvoice;
|
||||
use App\Services\EDocument\Standards\Validation\Peppol\EntityLevel;
|
||||
|
||||
class ValidateEInvoiceRequest extends Request
|
||||
{
|
||||
@@ -50,7 +51,7 @@ class ValidateEInvoiceRequest extends Request
|
||||
$user = auth()->user();
|
||||
|
||||
return [
|
||||
'entity' => 'required|bail|in:invoices,clients,companies',
|
||||
'entity' => 'required|bail|in:invoices,recurring_invoices,clients,companies',
|
||||
'entity_id' => ['required','bail', Rule::exists($this->entity, 'id')
|
||||
->when($this->entity != 'companies', function ($q) use ($user) {
|
||||
$q->where('company_id', $user->company()->id);
|
||||
@@ -81,6 +82,7 @@ class ValidateEInvoiceRequest extends Request
|
||||
|
||||
match ($this->entity) {
|
||||
'invoices' => $class = Invoice::class,
|
||||
'recurring_invoices' => $class = RecurringInvoice::class,
|
||||
'clients' => $class = Client::class,
|
||||
'companies' => $class = Company::class,
|
||||
default => $class = Invoice::class,
|
||||
|
||||
28
app/Http/Requests/User/PurgeUserRequest.php
Normal file
28
app/Http/Requests/User/PurgeUserRequest.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests\User;
|
||||
|
||||
use App\Http\Requests\Request;
|
||||
|
||||
class PurgeUserRequest extends Request
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return auth()->user()->isOwner() && auth()->user()->id !== $this->user->id;
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,10 @@ class ProcessBankTransactionsNordigen implements ShouldQueue
|
||||
// UPDATE TRANSACTIONS
|
||||
try {
|
||||
$this->processTransactions();
|
||||
|
||||
// Perform Matching
|
||||
BankMatchingService::dispatch($this->company->id, $this->company->db);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
nlog("Nordigen: {$this->bank_integration->nordigen_account_id} - exited abnormally => " . $e->getMessage());
|
||||
|
||||
@@ -109,11 +113,9 @@ class ProcessBankTransactionsNordigen implements ShouldQueue
|
||||
|
||||
$this->bank_integration->company->notification(new GenericNinjaAdminNotification($content))->ninja();
|
||||
|
||||
throw $e;
|
||||
// throw $e;
|
||||
}
|
||||
|
||||
// Perform Matching
|
||||
BankMatchingService::dispatch($this->company->id, $this->company->db);
|
||||
}
|
||||
|
||||
// const DISCOVERED = 'DISCOVERED'; // Account was discovered but not yet processed
|
||||
@@ -163,8 +165,10 @@ class ProcessBankTransactionsNordigen implements ShouldQueue
|
||||
private function processTransactions()
|
||||
{
|
||||
//Get transaction count object
|
||||
$transactions = [];
|
||||
|
||||
$transactions = $this->nordigen->getTransactions($this->company, $this->bank_integration->nordigen_account_id, $this->from_date);
|
||||
|
||||
|
||||
//if no transactions, update the from_date and move on
|
||||
if (count($transactions) == 0) {
|
||||
|
||||
@@ -189,7 +193,12 @@ class ProcessBankTransactionsNordigen implements ShouldQueue
|
||||
|
||||
foreach ($transactions as $transaction) {
|
||||
|
||||
if (BankTransaction::where('nordigen_transaction_id', $transaction['nordigen_transaction_id'])->where('company_id', $this->company->id)->where('bank_integration_id', $this->bank_integration->id)->where('is_deleted', 0)->withTrashed()->exists()) {
|
||||
if (BankTransaction::where('nordigen_transaction_id', $transaction['nordigen_transaction_id'])
|
||||
->where('company_id', $this->company->id)
|
||||
->where('bank_integration_id', $this->bank_integration->id)
|
||||
->where('is_deleted', 0)
|
||||
->withTrashed()
|
||||
->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,25 @@ class ProcessBankTransactionsYodlee implements ShouldQueue
|
||||
$now = now();
|
||||
|
||||
foreach ($transactions as $transaction) {
|
||||
if (BankTransaction::query()->where('transaction_id', $transaction['transaction_id'])->where('company_id', $this->company->id)->where('bank_integration_id', $this->bank_integration->id)->withTrashed()->exists()) {
|
||||
if (BankTransaction::query() //ensure we don't duplicate transactions with the same ID
|
||||
->where('transaction_id', $transaction['transaction_id'])
|
||||
->where('company_id', $this->company->id)
|
||||
->where('bank_integration_id', $this->bank_integration->id)
|
||||
->withTrashed()
|
||||
->exists()) {
|
||||
continue;
|
||||
}
|
||||
elseif (BankTransaction::query() //ensure we don't duplicate transactions that have the same amount, currency, account type, category type, date, and description
|
||||
->where('company_id', $this->company->id)
|
||||
->where('bank_integration_id', $this->bank_integration->id)
|
||||
->where('amount', $transaction['amount'])
|
||||
->where('currency_id', $transaction['currency_id'])
|
||||
->where('account_type', $transaction['account_type'])
|
||||
->where('category_type', $transaction['category_type'])
|
||||
->where('date', $transaction['date'])
|
||||
->where('description', $transaction['description'])
|
||||
->withTrashed()
|
||||
->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,21 +12,24 @@
|
||||
|
||||
namespace App\Jobs\Invoice;
|
||||
|
||||
use App\Jobs\Mail\NinjaMailer;
|
||||
use App\Jobs\Mail\NinjaMailerJob;
|
||||
use App\Jobs\Mail\NinjaMailerObject;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Mail\Admin\InvoiceOverdueObject;
|
||||
use App\Utils\Ninja;
|
||||
use App\Utils\Number;
|
||||
use App\Models\Company;
|
||||
use App\Models\Invoice;
|
||||
use App\Utils\Traits\Notifications\UserNotifies;
|
||||
use App\Libraries\MultiDB;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use App\Jobs\Mail\NinjaMailer;
|
||||
use Illuminate\Support\Carbon;
|
||||
use App\Utils\Traits\MakesDates;
|
||||
use App\Jobs\Mail\NinjaMailerJob;
|
||||
use App\Jobs\Mail\NinjaMailerObject;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Mail\Admin\InvoiceOverdueObject;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Carbon;
|
||||
use App\Utils\Ninja;
|
||||
use App\Mail\Admin\InvoiceOverdueSummaryObject;
|
||||
use App\Utils\Traits\Notifications\UserNotifies;
|
||||
|
||||
class InvoiceCheckOverdue implements ShouldQueue
|
||||
{
|
||||
@@ -35,6 +38,7 @@ class InvoiceCheckOverdue implements ShouldQueue
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
use UserNotifies;
|
||||
use MakesDates;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
@@ -113,7 +117,7 @@ class InvoiceCheckOverdue implements ShouldQueue
|
||||
// Yesterday's date in the company's timezone (Y-m-d format)
|
||||
$yesterday = $now_in_company_tz->copy()->subDay()->format('Y-m-d');
|
||||
|
||||
Invoice::query()
|
||||
$overdue_invoices = Invoice::query()
|
||||
->where('company_id', $company->id)
|
||||
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
|
||||
->where('is_deleted', false)
|
||||
@@ -140,14 +144,107 @@ class InvoiceCheckOverdue implements ShouldQueue
|
||||
});
|
||||
})
|
||||
->cursor()
|
||||
->each(function ($invoice) {
|
||||
$this->notifyOverdueInvoice($invoice);
|
||||
});
|
||||
->map(function ($invoice){
|
||||
|
||||
return [
|
||||
'id' => $invoice->id,
|
||||
'client' => $invoice->client->present()->name(),
|
||||
'number' => $invoice->number,
|
||||
'amount' => max($invoice->partial, $invoice->balance),
|
||||
'due_date' => $invoice->due_date,
|
||||
'formatted_amount' => Number::formatMoney($invoice->balance, $invoice->client),
|
||||
'formatted_due_date' => $this->translateDate($invoice->due_date, $invoice->company->date_format(), $invoice->company->locale()),
|
||||
];
|
||||
|
||||
})
|
||||
->toArray();
|
||||
|
||||
$this->sendOverdueNotifications($overdue_invoices, $company);
|
||||
|
||||
// ->each(function ($invoice) {
|
||||
// $this->notifyOverdueInvoice($invoice);
|
||||
// });
|
||||
}
|
||||
|
||||
private function sendOverdueNotifications(array $overdue_invoices, Company $company): void
|
||||
{
|
||||
|
||||
if(empty($overdue_invoices)){
|
||||
return;
|
||||
}
|
||||
|
||||
$nmo = new NinjaMailerObject();
|
||||
$nmo->company = $company;
|
||||
$nmo->settings = $company->settings;
|
||||
|
||||
/* We loop through each user and determine whether they need to be notified */
|
||||
foreach ($company->company_users as $company_user) {
|
||||
/* The User */
|
||||
$user = $company_user->user;
|
||||
|
||||
if (! $user) {
|
||||
continue;
|
||||
}
|
||||
|
||||
nlog($company_user->permissions);
|
||||
|
||||
$overdue_invoices_collection = $overdue_invoices;
|
||||
|
||||
$invoice = Invoice::withTrashed()->find($overdue_invoices[0]['id']);
|
||||
|
||||
$table_headers = [
|
||||
'client' => ctrans('texts.client'),
|
||||
'number' => ctrans('texts.invoice_number'),
|
||||
'formatted_due_date' => ctrans('texts.due_date'),
|
||||
'formatted_amount' => ctrans('texts.amount'),
|
||||
];
|
||||
|
||||
/** filter down the set if the user only has notifications for their own invoices */
|
||||
if(isset($company_user->notifications->email) && is_array($company_user->notifications->email) && in_array('invoice_late_user', $company_user->notifications->email)){
|
||||
|
||||
$overdue_invoices_collection = collect($overdue_invoices)
|
||||
->filter(function ($overdue_invoice) use ($user) {
|
||||
$invoice = Invoice::withTrashed()->find($overdue_invoice['id']);
|
||||
nlog([$invoice->user_id, $user->id, $invoice->assigned_user_id, $user->id]);
|
||||
return $invoice->user_id == $user->id || $invoice->assigned_user_id == $user->id;
|
||||
})
|
||||
->toArray();
|
||||
|
||||
if(count($overdue_invoices_collection) === 0){
|
||||
continue;
|
||||
}
|
||||
|
||||
$invoice = Invoice::withTrashed()->find(end($overdue_invoices_collection)['id']);
|
||||
|
||||
}
|
||||
|
||||
$nmo->mailable = new NinjaMailer((new InvoiceOverdueSummaryObject($overdue_invoices_collection, $table_headers, $company, $company_user->portalType()))->build());
|
||||
|
||||
/* Returns an array of notification methods */
|
||||
$methods = $this->findUserNotificationTypes(
|
||||
$invoice->invitations()->first(),
|
||||
$company_user,
|
||||
'invoice',
|
||||
['all_notifications', 'invoice_late', 'invoice_late_all', 'invoice_late_user']
|
||||
);
|
||||
|
||||
|
||||
/* If one of the methods is email then we fire the mailer */
|
||||
if (($key = array_search('mail', $methods)) !== false) {
|
||||
unset($methods[$key]);
|
||||
|
||||
$nmo->to_user = $user;
|
||||
|
||||
NinjaMailerJob::dispatch($nmo);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notifications for an overdue invoice to all relevant company users.
|
||||
*/
|
||||
/** @phpstan-ignore-next-line */
|
||||
private function notifyOverdueInvoice(Invoice $invoice): void
|
||||
{
|
||||
$nmo = new NinjaMailerObject();
|
||||
|
||||
103
app/Mail/Admin/InvoiceOverdueSummaryObject.php
Normal file
103
app/Mail/Admin/InvoiceOverdueSummaryObject.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Mail\Admin;
|
||||
|
||||
use stdClass;
|
||||
use Carbon\Carbon;
|
||||
use App\Utils\Ninja;
|
||||
use App\Utils\Number;
|
||||
use App\Models\Company;
|
||||
use App\Models\Invoice;
|
||||
use App\Libraries\MultiDB;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use App\Utils\Traits\MakesDates;
|
||||
|
||||
class InvoiceOverdueSummaryObject
|
||||
{
|
||||
use MakesDates;
|
||||
|
||||
public function __construct(public array $overdue_invoices, public array $table_headers, public Company $company, public bool $use_react_url)
|
||||
{
|
||||
}
|
||||
|
||||
public function build()
|
||||
{
|
||||
MultiDB::setDb($this->company->db);
|
||||
|
||||
App::forgetInstance('translator');
|
||||
/* Init a new copy of the translator */
|
||||
$t = app('translator');
|
||||
/* Set the locale */
|
||||
App::setLocale($this->company->getLocale());
|
||||
/* Set customized translations _NOW_ */
|
||||
$t->replace(Ninja::transformTranslations($this->company->settings));
|
||||
|
||||
$mail_obj = new stdClass();
|
||||
$mail_obj->amount = 0;
|
||||
$mail_obj->subject = $this->getSubject();
|
||||
$mail_obj->data = $this->getData();
|
||||
$mail_obj->markdown = 'email.admin.generic_table';
|
||||
$mail_obj->tag = $this->company->company_key;
|
||||
$mail_obj->text_view = 'email.admin.generic_table_text';
|
||||
|
||||
return $mail_obj;
|
||||
}
|
||||
|
||||
private function getSubject()
|
||||
{
|
||||
|
||||
$timezone = $this->company->timezone();
|
||||
$timezone_name = $timezone ? $timezone->name : 'UTC';
|
||||
|
||||
// Get the current hour in the company's timezone
|
||||
$now_in_company_tz = Carbon::now($timezone_name);
|
||||
$date = $this->translateDate($now_in_company_tz->format('Y-m-d'), $this->company->date_format(), $this->company->locale());
|
||||
|
||||
return
|
||||
ctrans(
|
||||
'texts.notification_invoice_overdue_summary_subject',
|
||||
[
|
||||
'date' => $date
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function getData()
|
||||
{
|
||||
|
||||
$invoice = Invoice::withTrashed()->find(reset($this->overdue_invoices)['id']);
|
||||
|
||||
$overdue_invoices_collection = array_map(
|
||||
fn($row) => \Illuminate\Support\Arr::except($row, ['id', 'amount', 'due_date']),
|
||||
$this->overdue_invoices
|
||||
);
|
||||
|
||||
$data = [
|
||||
'title' => $this->getSubject(),
|
||||
'content' => ctrans('texts.notification_invoice_overdue_summary'),
|
||||
'url' => $invoice->invitations->first()->getAdminLink($this->use_react_url),
|
||||
'button' => $this->use_react_url ? ctrans('texts.view_invoice') : ctrans('texts.login'),
|
||||
'signature' => $this->company->settings->email_signature,
|
||||
'logo' => $this->company->present()->logo(),
|
||||
'settings' => $this->company->settings,
|
||||
'whitelabel' => $this->company->account->isPaid() ? true : false,
|
||||
'text_body' => ctrans('texts.notification_invoice_overdue_summary'),
|
||||
'template' => $this->company->account->isPremium() ? 'email.template.admin_premium' : 'email.template.admin',
|
||||
'table' => $overdue_invoices_collection,
|
||||
'table_headers' => $this->table_headers,
|
||||
];
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,6 +351,196 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
return $this->hasMany(Client::class);
|
||||
}
|
||||
|
||||
public function activities(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Activity::class);
|
||||
}
|
||||
|
||||
public function bank_integrations(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(BankIntegration::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function bank_transaction_rules(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(BankTransactionRule::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function bank_transactions(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(BankTransaction::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function client_contacts(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(ClientContact::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function company_gateways(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(CompanyGateway::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function company_ledgers(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(CompanyLedger::class);
|
||||
}
|
||||
|
||||
public function company_tokens(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(CompanyToken::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function credit_invitations(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(CreditInvitation::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function credits(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Credit::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function designs(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Design::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function expense_categories(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(ExpenseCategory::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function expenses(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Expense::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function group_settings(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(GroupSetting::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function invoice_invitations(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(InvoiceInvitation::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function invoices(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Invoice::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function locations(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Location::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function payment_terms(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(PaymentTerm::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function payments(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Payment::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function products(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Product::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function projects(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Project::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function purchase_order_invitations(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(PurchaseOrderInvitation::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function purchase_orders(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(PurchaseOrder::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function quote_invitations(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(QuoteInvitation::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function quotes(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Quote::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function recurring_expenses(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(RecurringExpense::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function recurring_invoice_invitations(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(RecurringInvoiceInvitation::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function recurring_invoices(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(RecurringInvoice::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function recurring_quotes(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(RecurringQuote::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function recurring_quote_invitations(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(RecurringQuoteInvitation::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function schedules(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Scheduler::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function system_logs(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(SystemLog::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function tasks(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Task::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function task_statuses(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(TaskStatus::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function tax_rates(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(TaxRate::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function vendor_contacts(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(VendorContact::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function vendors(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Vendor::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function webhooks(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Webhook::class)->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a comma separated list of user permissions.
|
||||
*
|
||||
|
||||
@@ -12,17 +12,33 @@
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\DataMapper\CompanySettings;
|
||||
use App\Events\User\UserWasArchived;
|
||||
use App\Events\User\UserWasDeleted;
|
||||
use App\Events\User\UserWasRestored;
|
||||
use App\Jobs\Company\CreateCompanyToken;
|
||||
use App\Models\CompanyUser;
|
||||
use App\Models\Task;
|
||||
use App\Models\User;
|
||||
use App\Utils\Ninja;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Models\Quote;
|
||||
use App\Models\Client;
|
||||
use App\Models\Credit;
|
||||
use App\Models\Vendor;
|
||||
use App\Models\Expense;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Models\Product;
|
||||
use App\Models\Project;
|
||||
use App\Models\CompanyUser;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\PurchaseOrder;
|
||||
use App\Models\RecurringQuote;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Models\RecurringExpense;
|
||||
use App\Models\RecurringInvoice;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\DataMapper\CompanySettings;
|
||||
use App\Events\User\UserWasDeleted;
|
||||
use App\Events\User\UserWasArchived;
|
||||
use App\Events\User\UserWasRestored;
|
||||
use App\Repositories\BaseRepository;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use App\Jobs\Company\CreateCompanyToken;
|
||||
|
||||
/**
|
||||
* UserRepository.
|
||||
@@ -80,7 +96,7 @@ class UserRepository extends BaseRepository
|
||||
|
||||
$user->account_id = $account->id;//@todo we should never change the account_id if it is set at this point.
|
||||
|
||||
if (strlen($user->password) >= 1) {
|
||||
if (strlen($user->password ?? '') >= 1) {
|
||||
$user->has_password = true;
|
||||
}
|
||||
|
||||
@@ -242,4 +258,59 @@ class UserRepository extends BaseRepository
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* purge a user and all of their data across
|
||||
* all companies and accounts.
|
||||
*
|
||||
* @param User $user
|
||||
* @param User $new_owner_user
|
||||
* @return void
|
||||
*/
|
||||
public function purge(User $user, User $new_owner_user): void
|
||||
{
|
||||
|
||||
\DB::transaction(function () use ($user, $new_owner_user) {
|
||||
|
||||
// Relations to transfer user_id to new owner
|
||||
$allRelations = [
|
||||
'activities', 'bank_integrations', 'bank_transaction_rules',
|
||||
'bank_transactions', 'client_contacts', 'company_gateways',
|
||||
'company_ledgers', 'company_tokens', 'credit_invitations',
|
||||
'designs', 'expense_categories', 'group_settings',
|
||||
'invoice_invitations', 'locations', 'payment_terms',
|
||||
'quote_invitations', 'purchase_order_invitations',
|
||||
'recurring_invoice_invitations', 'recurring_quote_invitations',
|
||||
'schedules', 'system_logs', 'task_statuses', 'tax_rates',
|
||||
'vendor_contacts', 'webhooks',
|
||||
// Models that also have assigned_user_id
|
||||
'clients', 'invoices', 'credits', 'quotes', 'payments',
|
||||
'expenses', 'tasks', 'projects', 'vendors', 'products',
|
||||
'purchase_orders', 'recurring_invoices', 'recurring_expenses',
|
||||
'recurring_quotes',
|
||||
];
|
||||
|
||||
foreach ($allRelations as $relation) {
|
||||
$user->{$relation}()->update(['user_id' => $new_owner_user->id]);
|
||||
}
|
||||
|
||||
// Models with both user_id and assigned_user_id
|
||||
$modelsWithAssignedUser = [
|
||||
Client::class, Invoice::class, Credit::class, Quote::class,
|
||||
Payment::class, Expense::class, Task::class, Project::class,
|
||||
Vendor::class, Product::class, PurchaseOrder::class,
|
||||
RecurringInvoice::class, RecurringExpense::class, RecurringQuote::class,
|
||||
];
|
||||
|
||||
foreach ($modelsWithAssignedUser as $model) {
|
||||
// Null out assigned_user_id
|
||||
$model::withTrashed()
|
||||
->where('assigned_user_id', $user->id)
|
||||
->update(['assigned_user_id' => null]);
|
||||
}
|
||||
|
||||
$user->forceDelete();
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1349,9 +1349,13 @@ class Peppol extends AbstractService
|
||||
if (isset($this->invoice->e_invoice->Invoice->InvoicePeriod[0]) &&
|
||||
isset($this->invoice->e_invoice->Invoice->InvoicePeriod[0]->StartDate) &&
|
||||
isset($this->invoice->e_invoice->Invoice->InvoicePeriod[0]->EndDate)) {
|
||||
|
||||
$start_date = isset($this->invoice->e_invoice->Invoice->InvoicePeriod[0]->StartDate->date) ? $this->invoice->e_invoice->Invoice->InvoicePeriod[0]->StartDate->date :$this->invoice->e_invoice->Invoice->InvoicePeriod[0]->StartDate;
|
||||
$end_date = isset($this->invoice->e_invoice->Invoice->InvoicePeriod[0]->EndDate->date) ? $this->invoice->e_invoice->Invoice->InvoicePeriod[0]->EndDate->date : $this->invoice->e_invoice->Invoice->InvoicePeriod[0]->EndDate;
|
||||
|
||||
$ip = new \InvoiceNinja\EInvoice\Models\Peppol\PeriodType\InvoicePeriod();
|
||||
$ip->StartDate = new \DateTime($this->invoice->e_invoice->Invoice->InvoicePeriod[0]->StartDate);
|
||||
$ip->EndDate = new \DateTime($this->invoice->e_invoice->Invoice->InvoicePeriod[0]->EndDate);
|
||||
$ip->StartDate = new \DateTime($start_date);
|
||||
$ip->EndDate = new \DateTime($end_date);
|
||||
$this->p_invoice->InvoicePeriod = [$ip];
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace App\Services\EDocument\Standards\Validation;
|
||||
use App\Models\Client;
|
||||
use App\Models\Company;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\RecurringInvoice;
|
||||
|
||||
interface EntityLevelInterface
|
||||
{
|
||||
@@ -24,4 +25,6 @@ interface EntityLevelInterface
|
||||
|
||||
public function checkInvoice(Invoice $invoice): array;
|
||||
|
||||
public function checkRecurringInvoice(RecurringInvoice $recurring_invoice): array;
|
||||
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
namespace App\Services\EDocument\Standards\Validation\Peppol;
|
||||
|
||||
use App\Exceptions\PeppolValidationException;
|
||||
use XSLTProcessor;
|
||||
use App\Models\Quote;
|
||||
use App\Models\Client;
|
||||
use App\Models\Credit;
|
||||
@@ -20,11 +20,12 @@ use App\Models\Vendor;
|
||||
use App\Models\Company;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\PurchaseOrder;
|
||||
use App\Services\EDocument\Standards\Peppol;
|
||||
use App\Services\EDocument\Standards\Validation\XsltDocumentValidator;
|
||||
use App\Services\EDocument\Standards\Validation\EntityLevelInterface;
|
||||
use App\Models\RecurringInvoice;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use XSLTProcessor;
|
||||
use App\Services\EDocument\Standards\Peppol;
|
||||
use App\Exceptions\PeppolValidationException;
|
||||
use App\Services\EDocument\Standards\Validation\EntityLevelInterface;
|
||||
use App\Services\EDocument\Standards\Validation\XsltDocumentValidator;
|
||||
|
||||
class EntityLevel implements EntityLevelInterface
|
||||
{
|
||||
@@ -124,6 +125,11 @@ class EntityLevel implements EntityLevelInterface
|
||||
|
||||
}
|
||||
|
||||
public function checkRecurringInvoice(RecurringInvoice $recurring_invoice): array
|
||||
{
|
||||
return ['passes' => true];
|
||||
}
|
||||
|
||||
public function checkInvoice(Invoice $invoice): array
|
||||
{
|
||||
$this->init($invoice->client->locale());
|
||||
|
||||
@@ -19,6 +19,7 @@ use App\Models\Vendor;
|
||||
use App\Models\Company;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\PurchaseOrder;
|
||||
use App\Models\RecurringInvoice;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use App\Services\EDocument\Standards\Validation\EntityLevelInterface;
|
||||
|
||||
@@ -84,6 +85,11 @@ class EntityLevel implements EntityLevelInterface
|
||||
|
||||
}
|
||||
|
||||
public function checkRecurringInvoice(RecurringInvoice $recurring_invoice): array
|
||||
{
|
||||
return ['passes' => true];
|
||||
}
|
||||
|
||||
public function checkInvoice(Invoice $invoice): array
|
||||
{
|
||||
|
||||
|
||||
@@ -208,6 +208,8 @@ class HtmlEngine
|
||||
$data['$location.custom3'] = &$data['$location3'];
|
||||
$data['$location.custom4'] = &$data['$location4'];
|
||||
|
||||
$data['$term_days'] = ['value' => '', 'label' => ctrans('texts.payment_terms')];
|
||||
|
||||
if ($this->entity_string == 'invoice' || $this->entity_string == 'recurring_invoice') {
|
||||
|
||||
if($this->client->peppolSendingEnabled() && $this->entity->amount < 0) {
|
||||
@@ -217,6 +219,9 @@ class HtmlEngine
|
||||
$data['$entity'] = ['value' => ctrans('texts.invoice'), 'label' => ctrans('texts.invoice')];
|
||||
}
|
||||
|
||||
|
||||
$data['$term_days'] = ['value' => $this->client->getSetting('payment_terms'), 'label' => ctrans('texts.payment_terms')];
|
||||
|
||||
$data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number')];
|
||||
$data['$invoice'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number')];
|
||||
$data['$number_short'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number_short')];
|
||||
@@ -275,6 +280,10 @@ class HtmlEngine
|
||||
}
|
||||
|
||||
if ($this->entity_string == 'quote') {
|
||||
|
||||
|
||||
$data['$term_days'] = ['value' => $this->client->getSetting('valid_until'), 'label' => ctrans('texts.valid_until')];
|
||||
|
||||
$data['$entity'] = ['value' => ctrans('texts.quote'), 'label' => ctrans('texts.quote')];
|
||||
$data['$number'] = ['value' => $this->entity->number ?: '', 'label' => ctrans('texts.quote_number')];
|
||||
$data['$number_short'] = ['value' => $this->entity->number ?: '', 'label' => ctrans('texts.quote_number_short')];
|
||||
@@ -315,6 +324,10 @@ class HtmlEngine
|
||||
}
|
||||
|
||||
if ($this->entity_string == 'credit') {
|
||||
|
||||
|
||||
$data['$term_days'] = ['value' => $this->client->getSetting('payment_terms'), 'label' => ctrans('texts.payment_terms')];
|
||||
|
||||
$data['$entity'] = ['value' => ctrans('texts.credit'), 'label' => ctrans('texts.credit')];
|
||||
$data['$number'] = ['value' => $this->entity->number ?: '', 'label' => ctrans('texts.credit_number')];
|
||||
$data['$number_short'] = ['value' => $this->entity->number ?: '', 'label' => ctrans('texts.credit_number_short')];
|
||||
@@ -378,7 +391,14 @@ class HtmlEngine
|
||||
$data['$balance_due_dec'] = ['value' => sprintf("%01.2f", $this->entity->amount), 'label' => ctrans('texts.balance_due')];
|
||||
$data['$balance_due_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.balance_due')];
|
||||
$data['$amount_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.amount')];
|
||||
} else {
|
||||
}
|
||||
elseif($this->entity->status_id == 4 && $this->entity_string == 'invoice') {
|
||||
$data['$balance_due'] = ['value' => Number::formatMoney(0, $this->client) ?: ' ', 'label' => ctrans('texts.balance_due')];
|
||||
$data['$balance_due_dec'] = ['value' => sprintf("%01.2f", 0), 'label' => ctrans('texts.balance_due')];
|
||||
$data['$balance_due_raw'] = ['value' => 0, 'label' => ctrans('texts.balance_due')];
|
||||
$data['$amount_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.amount')];
|
||||
}
|
||||
else {
|
||||
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->balance, $this->client) ?: ' ', 'label' => ctrans('texts.balance_due')];
|
||||
$data['$balance_due_dec'] = ['value' => sprintf("%01.2f", $this->entity->balance), 'label' => ctrans('texts.balance_due')];
|
||||
$data['$balance_due_raw'] = ['value' => $this->entity->balance, 'label' => ctrans('texts.balance_due')];
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
"setasign/fpdi": "^2.6",
|
||||
"socialiteproviders/apple": "dev-master",
|
||||
"socialiteproviders/microsoft": "^4.1",
|
||||
"sprain/swiss-qr-bill": "^4.3",
|
||||
"sprain/swiss-qr-bill": "^5.2",
|
||||
"square/square": "30.0.0.*",
|
||||
"stripe/stripe-php": "^17",
|
||||
"symfony/brevo-mailer": "^7.1",
|
||||
|
||||
899
composer.lock
generated
899
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -145,7 +145,9 @@ return [
|
||||
'gocardless' => [
|
||||
'client_id' => env('GOCARDLESS_CLIENT_ID', null),
|
||||
'client_secret' => env('GOCARDLESS_CLIENT_SECRET', null),
|
||||
'debug' => env('APP_DEBUG',false)
|
||||
'debug' => env('APP_DEBUG',false),
|
||||
'redirect_uri' => env('GOCARDLESS_REDIRECT_URI', null),
|
||||
'environment' => env('GOCARDLESS_ENVIRONMENT', 'production'),
|
||||
],
|
||||
'quickbooks_webhook' => [
|
||||
'verifier_token' => env('QUICKBOOKS_VERIFIER_TOKEN', false),
|
||||
|
||||
@@ -5684,6 +5684,9 @@ $lang = array(
|
||||
'reject_quote_confirmation' => 'Are you sure you want to reject this quote?',
|
||||
'reason' => 'Reason',
|
||||
'enter_reason' => 'Enter a reason...',
|
||||
'notification_invoice_overdue_summary_subject' => 'Invoice Overdue Summary: :date',
|
||||
'notification_invoice_overdue_summary' => 'The following invoices are overdue:',
|
||||
'purge_user_confirmation' => 'Warning! This action will reassign all entities to the account owner and permanently delete the user across all companies and accounts. Are you sure you want to proceed?',
|
||||
);
|
||||
|
||||
return $lang;
|
||||
|
||||
80
resources/views/email/admin/generic_table.blade.php
Normal file
80
resources/views/email/admin/generic_table.blade.php
Normal file
@@ -0,0 +1,80 @@
|
||||
@component('email.template.admin', ['design' => 'light', 'settings' => $settings, 'logo' => $logo, 'url' => $url])
|
||||
<div class="center">
|
||||
@isset($greeting)
|
||||
<p>{{ $greeting }}</p>
|
||||
@endisset
|
||||
|
||||
@isset($title)
|
||||
<h1>{{ $title }}</h1>
|
||||
@endisset
|
||||
|
||||
@isset($h2)
|
||||
<h2>{{ $title }}</h2>
|
||||
@endisset
|
||||
|
||||
<div style="margin-top: 10px; margin-bottom: 30px;">
|
||||
@isset($content)
|
||||
{!! nl2br($content, true) !!}
|
||||
@endisset
|
||||
|
||||
@isset($table)
|
||||
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; font-family: Arial, Helvetica, sans-serif; font-size: 14px; color: #333333;">
|
||||
<thead>
|
||||
<tr>
|
||||
@foreach($table_headers as $key => $value)
|
||||
<th align="left" valign="middle" style="padding: 12px 16px; background-color: #f8f9fa; border-bottom: 2px solid #dee2e6; font-weight: 600; color: #495057; text-align: left;">{{ $value }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($table as $index => $row)
|
||||
<tr style="background-color: {{ $index % 2 === 0 ? '#ffffff' : '#f8f9fa' }};">
|
||||
@foreach($row as $key => $value)
|
||||
<td align="left" valign="middle" style="padding: 10px 16px; border-bottom: 1px solid #e9ecef; color: #212529;">{{ $value }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endisset
|
||||
|
||||
@isset($slot)
|
||||
{{ $slot }}
|
||||
@endisset
|
||||
</div>
|
||||
|
||||
@isset($additional_info)
|
||||
<p>{{ $additional_info }}</p>
|
||||
@endisset
|
||||
|
||||
@if($url)
|
||||
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<table align="center" cellspacing="0" cellpadding="0" style="width: 600px;">
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" >
|
||||
<tbody><tr>
|
||||
<td align="center" class="new_button" style="border-radius: 2px; background-color: {{ $settings->primary_color }} ;">
|
||||
<a href="{{ $url }}" target="_blank" class="new_button" style="text-decoration: none; border: 1px solid {{ $settings->primary_color }}; display: inline-block; border-radius: 2px; padding-top: 15px; padding-bottom: 15px; padding-left: 25px; padding-right: 25px; font-size: 20px; color: #fff">
|
||||
<singleline label="cta button">{{ ctrans($button) }}</singleline>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
|
||||
|
||||
@endif
|
||||
|
||||
@isset($signature)
|
||||
<p>{!! nl2br($signature) !!}</p>
|
||||
@endisset
|
||||
</div>
|
||||
@endcomponent
|
||||
22
resources/views/email/admin/generic_table_text.blade.php
Normal file
22
resources/views/email/admin/generic_table_text.blade.php
Normal file
@@ -0,0 +1,22 @@
|
||||
{{ $title }}
|
||||
|
||||
@isset($body)
|
||||
{{ strip_tags(str_replace("<br>", "\r\n", $body)) }}
|
||||
@endisset
|
||||
|
||||
@isset($content)
|
||||
{{ strip_tags(str_replace("<br>", "\r\n", $content)) }}
|
||||
@endisset
|
||||
|
||||
@isset($table)
|
||||
|
||||
@foreach($table as $row)
|
||||
{{ implode("\t", array_values($row)) }}
|
||||
@endforeach
|
||||
@endisset
|
||||
|
||||
@isset($whitelabel)
|
||||
@if(!$whitelabel)
|
||||
{{ ctrans('texts.ninja_email_footer', ['site' => 'https://invoiceninja.com']) }}
|
||||
@endif
|
||||
@endisset
|
||||
@@ -436,6 +436,7 @@ Route::group(['middleware' => ['throttle:api', 'token_auth', 'valid_json','local
|
||||
Route::post('/users/{user}/disconnect_mailer', [UserController::class, 'disconnectOauthMailer']);
|
||||
Route::post('/users/{user}/disconnect_oauth', [UserController::class, 'disconnectOauth']);
|
||||
Route::post('/user/{user}/reconfirm', [UserController::class, 'reconfirm']);
|
||||
Route::post('/user/{user}/purge', [UserController::class, 'purge'])->middleware('password_protected');
|
||||
|
||||
Route::resource('webhooks', WebhookController::class);
|
||||
Route::post('webhooks/bulk', [WebhookController::class, 'bulk'])->name('webhooks.bulk');
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
namespace Tests\Feature\EInvoice\RequestValidation;
|
||||
|
||||
use Tests\TestCase;
|
||||
use Tests\MockAccountData;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use App\Factory\RecurringInvoiceToInvoiceFactory;
|
||||
use App\Http\Requests\Invoice\UpdateInvoiceRequest;
|
||||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||
use Tests\MockAccountData;
|
||||
|
||||
class InvoicePeriodTest extends TestCase
|
||||
{
|
||||
@@ -85,10 +86,10 @@ class InvoicePeriodTest extends TestCase
|
||||
|
||||
$this->recurring_invoice = $this->recurring_invoice->fresh();
|
||||
|
||||
$invoice = \App\Factory\RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client);
|
||||
$invoice = RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client);
|
||||
|
||||
$this->assertEquals($invoice->e_invoice->Invoice->InvoicePeriod[0]->StartDate->date, now()->setTimezone($this->recurring_invoice->client->timezone()->name)->startOfMonth()->startOfDay()->format('Y-m-d H:i:s.u'));
|
||||
$this->assertEquals($invoice->e_invoice->Invoice->InvoicePeriod[0]->EndDate->date, now()->setTimezone($this->recurring_invoice->client->timezone()->name)->endOfMonth()->startOfDay()->format('Y-m-d H:i:s.u'));
|
||||
$this->assertEquals($invoice->e_invoice->Invoice->InvoicePeriod[0]->StartDate, now()->setTimezone($this->recurring_invoice->client->timezone()->name)->startOfMonth()->startOfDay()->format('Y-m-d'));
|
||||
$this->assertEquals($invoice->e_invoice->Invoice->InvoicePeriod[0]->EndDate, now()->setTimezone($this->recurring_invoice->client->timezone()->name)->endOfMonth()->startOfDay()->format('Y-m-d'));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -755,4 +755,260 @@ class UserTest extends TestCase
|
||||
$this->assertFalse($arr['data']['company_user']['is_owner']);
|
||||
$this->assertEquals($arr['data']['company_user']['permissions'], 'create_invoice,create_invoice');
|
||||
}
|
||||
|
||||
public function testPurgeUserTransfersEntities()
|
||||
{
|
||||
// Create account and owner user
|
||||
$account = Account::factory()->create([
|
||||
'hosted_client_count' => 1000,
|
||||
'hosted_company_count' => 1000,
|
||||
]);
|
||||
|
||||
$account->num_users = 3;
|
||||
$account->save();
|
||||
|
||||
$owner_user = User::factory()->create([
|
||||
'account_id' => $account->id,
|
||||
'email' => \Illuminate\Support\Str::random(32)."@example.com",
|
||||
]);
|
||||
|
||||
$settings = CompanySettings::defaults();
|
||||
|
||||
$company = Company::factory()->create([
|
||||
'account_id' => $account->id,
|
||||
'settings' => $settings,
|
||||
]);
|
||||
|
||||
$owner_user->companies()->attach($company->id, [
|
||||
'account_id' => $account->id,
|
||||
'is_owner' => 1,
|
||||
'is_admin' => 1,
|
||||
'is_locked' => 0,
|
||||
'permissions' => '',
|
||||
'notifications' => CompanySettings::notificationAdminDefaults(),
|
||||
'settings' => null,
|
||||
]);
|
||||
|
||||
// Create secondary user to be purged
|
||||
$secondary_user = User::factory()->create([
|
||||
'account_id' => $account->id,
|
||||
'email' => \Illuminate\Support\Str::random(32)."@example.com",
|
||||
]);
|
||||
|
||||
$secondary_user->companies()->attach($company->id, [
|
||||
'account_id' => $account->id,
|
||||
'is_owner' => 0,
|
||||
'is_admin' => 1,
|
||||
'is_locked' => 0,
|
||||
'permissions' => '',
|
||||
'notifications' => CompanySettings::notificationAdminDefaults(),
|
||||
'settings' => null,
|
||||
]);
|
||||
|
||||
// Create a client owned by secondary user
|
||||
$client = \App\Models\Client::factory()->create([
|
||||
'user_id' => $secondary_user->id,
|
||||
'company_id' => $company->id,
|
||||
'assigned_user_id' => $secondary_user->id,
|
||||
]);
|
||||
|
||||
// Create client contact
|
||||
$client_contact = \App\Models\ClientContact::factory()->create([
|
||||
'user_id' => $secondary_user->id,
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'is_primary' => true,
|
||||
]);
|
||||
|
||||
// Create invoice owned by secondary user
|
||||
$invoice = \App\Models\Invoice::factory()->create([
|
||||
'user_id' => $secondary_user->id,
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_user_id' => $secondary_user->id,
|
||||
'status_id' => \App\Models\Invoice::STATUS_DRAFT,
|
||||
]);
|
||||
$invoice = $invoice->service()->createInvitations()->markSent()->save();
|
||||
|
||||
// Create credit owned by secondary user
|
||||
$credit = \App\Models\Credit::factory()->create([
|
||||
'user_id' => $secondary_user->id,
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_user_id' => $secondary_user->id,
|
||||
'status_id' => \App\Models\Credit::STATUS_DRAFT,
|
||||
]);
|
||||
|
||||
$credit = $credit->service()->createInvitations()->markSent()->save();
|
||||
|
||||
// Create quote owned by secondary user
|
||||
$quote = \App\Models\Quote::factory()->create([
|
||||
'user_id' => $secondary_user->id,
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_user_id' => $secondary_user->id,
|
||||
'status_id' => \App\Models\Quote::STATUS_DRAFT,
|
||||
]);
|
||||
$quote = $quote->service()->createInvitations()->markSent()->save();
|
||||
|
||||
// Create recurring invoice owned by secondary user
|
||||
$recurring_invoice = \App\Models\RecurringInvoice::factory()->create([
|
||||
'user_id' => $secondary_user->id,
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_user_id' => $secondary_user->id,
|
||||
'status_id' => \App\Models\RecurringInvoice::STATUS_DRAFT,
|
||||
]);
|
||||
|
||||
$recurring_invoice = $recurring_invoice->service()->createInvitations()->start()->save();
|
||||
// Create expense owned by secondary user
|
||||
$expense = \App\Models\Expense::factory()->create([
|
||||
'user_id' => $secondary_user->id,
|
||||
'company_id' => $company->id,
|
||||
'assigned_user_id' => $secondary_user->id,
|
||||
]);
|
||||
|
||||
// Create task owned by secondary user
|
||||
$task = \App\Models\Task::factory()->create([
|
||||
'user_id' => $secondary_user->id,
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_user_id' => $secondary_user->id,
|
||||
]);
|
||||
|
||||
// Create vendor owned by secondary user
|
||||
$vendor = \App\Models\Vendor::factory()->create([
|
||||
'user_id' => $secondary_user->id,
|
||||
'company_id' => $company->id,
|
||||
'assigned_user_id' => $secondary_user->id,
|
||||
]);
|
||||
|
||||
// Create vendor contact
|
||||
$vendor_contact = \App\Models\VendorContact::factory()->create([
|
||||
'user_id' => $secondary_user->id,
|
||||
'company_id' => $company->id,
|
||||
'vendor_id' => $vendor->id,
|
||||
'is_primary' => true,
|
||||
]);
|
||||
|
||||
// Create product owned by secondary user
|
||||
$product = \App\Models\Product::factory()->create([
|
||||
'user_id' => $secondary_user->id,
|
||||
'company_id' => $company->id,
|
||||
'assigned_user_id' => $secondary_user->id,
|
||||
]);
|
||||
|
||||
// Create project owned by secondary user
|
||||
$project = \App\Models\Project::factory()->create([
|
||||
'user_id' => $secondary_user->id,
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_user_id' => $secondary_user->id,
|
||||
]);
|
||||
|
||||
// Create an entity owned by owner but assigned to secondary user
|
||||
$invoice_assigned_only = \App\Models\Invoice::factory()->create([
|
||||
'user_id' => $owner_user->id,
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_user_id' => $secondary_user->id,
|
||||
]);
|
||||
|
||||
|
||||
$invoice = $invoice->load('invitations');
|
||||
|
||||
$this->assertCount(1, $invoice->invitations);
|
||||
$this->assertCount(1, $recurring_invoice->invitations);
|
||||
// Store IDs for later assertions
|
||||
$secondary_user_id = $secondary_user->id;
|
||||
$client_id = $client->id;
|
||||
$client_contact_id = $client_contact->id;
|
||||
$invoice_id = $invoice->id;
|
||||
$invoice_invitation_id = $invoice->invitations()->first()->id;
|
||||
$credit_id = $credit->id;
|
||||
$credit_invitation_id = $credit->invitations()->first()->id;
|
||||
$quote_id = $quote->id;
|
||||
$quote_invitation_id = $quote->invitations()->first()->id;
|
||||
$recurring_invoice_id = $recurring_invoice->id;
|
||||
$expense_id = $expense->id;
|
||||
$task_id = $task->id;
|
||||
$vendor_id = $vendor->id;
|
||||
$vendor_contact_id = $vendor_contact->id;
|
||||
$product_id = $product->id;
|
||||
$project_id = $project->id;
|
||||
$invoice_assigned_only_id = $invoice_assigned_only->id;
|
||||
|
||||
// Perform the purge
|
||||
$user_repo = new UserRepository();
|
||||
$user_repo->purge($secondary_user, $owner_user);
|
||||
|
||||
// Assert secondary user is deleted
|
||||
$this->assertNull(User::find($secondary_user_id));
|
||||
|
||||
// Assert all entities are now owned by owner user
|
||||
$client = \App\Models\Client::find($client_id);
|
||||
$this->assertEquals($owner_user->id, $client->user_id);
|
||||
$this->assertNull($client->assigned_user_id);
|
||||
|
||||
// Assert client contact user_id updated
|
||||
$client_contact = \App\Models\ClientContact::find($client_contact_id);
|
||||
$this->assertEquals($owner_user->id, $client_contact->user_id);
|
||||
|
||||
$invoice = \App\Models\Invoice::find($invoice_id);
|
||||
$this->assertEquals($owner_user->id, $invoice->user_id);
|
||||
$this->assertNull($invoice->assigned_user_id);
|
||||
|
||||
// Assert invoice invitation user_id updated
|
||||
$invoice_invitation = \App\Models\InvoiceInvitation::find($invoice_invitation_id);
|
||||
$this->assertEquals($owner_user->id, $invoice_invitation->user_id);
|
||||
|
||||
$credit = \App\Models\Credit::find($credit_id);
|
||||
$this->assertEquals($owner_user->id, $credit->user_id);
|
||||
$this->assertNull($credit->assigned_user_id);
|
||||
|
||||
// Assert credit invitation user_id updated
|
||||
$credit_invitation = \App\Models\CreditInvitation::find($credit_invitation_id);
|
||||
$this->assertEquals($owner_user->id, $credit_invitation->user_id);
|
||||
|
||||
$quote = \App\Models\Quote::find($quote_id);
|
||||
$this->assertEquals($owner_user->id, $quote->user_id);
|
||||
$this->assertNull($quote->assigned_user_id);
|
||||
|
||||
// Assert quote invitation user_id updated
|
||||
$quote_invitation = \App\Models\QuoteInvitation::find($quote_invitation_id);
|
||||
$this->assertEquals($owner_user->id, $quote_invitation->user_id);
|
||||
|
||||
$recurring_invoice = \App\Models\RecurringInvoice::find($recurring_invoice_id);
|
||||
$this->assertEquals($owner_user->id, $recurring_invoice->user_id);
|
||||
$this->assertNull($recurring_invoice->assigned_user_id);
|
||||
|
||||
$expense = \App\Models\Expense::find($expense_id);
|
||||
$this->assertEquals($owner_user->id, $expense->user_id);
|
||||
$this->assertNull($expense->assigned_user_id);
|
||||
|
||||
$task = \App\Models\Task::find($task_id);
|
||||
$this->assertEquals($owner_user->id, $task->user_id);
|
||||
$this->assertNull($task->assigned_user_id);
|
||||
|
||||
$vendor = \App\Models\Vendor::find($vendor_id);
|
||||
$this->assertEquals($owner_user->id, $vendor->user_id);
|
||||
$this->assertNull($vendor->assigned_user_id);
|
||||
|
||||
// Assert vendor contact user_id updated
|
||||
$vendor_contact = \App\Models\VendorContact::find($vendor_contact_id);
|
||||
$this->assertEquals($owner_user->id, $vendor_contact->user_id);
|
||||
|
||||
$product = \App\Models\Product::find($product_id);
|
||||
$this->assertEquals($owner_user->id, $product->user_id);
|
||||
$this->assertNull($product->assigned_user_id);
|
||||
|
||||
$project = \App\Models\Project::find($project_id);
|
||||
$this->assertEquals($owner_user->id, $project->user_id);
|
||||
$this->assertNull($project->assigned_user_id);
|
||||
|
||||
// Assert entity owned by owner but assigned to secondary now has null assigned_user_id
|
||||
$invoice_assigned_only = \App\Models\Invoice::find($invoice_assigned_only_id);
|
||||
$this->assertEquals($owner_user->id, $invoice_assigned_only->user_id);
|
||||
$this->assertNull($invoice_assigned_only->assigned_user_id);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user