Merge branch 'v5-develop' into v5-stable

This commit is contained in:
David Bomba 2024-12-08 16:41:14 +11:00
commit 69a9225957
238 changed files with 292221 additions and 284794 deletions

View File

@ -16,10 +16,11 @@ jobs:
extensions: mysql, mysqlnd, sqlite3, bcmath, gd, curl, zip, openssl, mbstring, xml
- name: Checkout code
uses: actions/checkout@v1
uses: actions/checkout@v4
with:
ref: v5-develop
fetch-depth: 1
- name: Copy .env file
run: |
cp .env.example .env

View File

@ -1 +1 @@
5.10.53
5.10.62

View File

@ -0,0 +1,44 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use App\DataMapper\EInvoice\TaxEntity;
use App\DataMapper\Referral\ReferralEarning;
class AsReferralEarningCollection implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
{
if (!$value || (is_string($value) && $value == "null")) {
return [];
}
$items = json_decode($value, true);
return array_map(fn ($item) => new ReferralEarning($item), $items);
}
public function set($model, string $key, $value, array $attributes)
{
if (!$value) {
return '[]';
}
if ($value instanceof ReferralEarning) {
$value = [$value];
}
return json_encode(array_map(fn ($entity) => get_object_vars($entity), $value));
}
}

View File

@ -88,7 +88,8 @@ class SendRemindersCron extends Command
});
if ($invoice->invitations->count() > 0) {
event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(), $reminder_template));
// event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(), $reminder_template));
$invoice->entityEmailEvent($invoice->invitations->first(), $reminder_template);
}
}
$invoice->service()->setReminder()->save();

View File

@ -79,7 +79,7 @@ class Kernel extends ConsoleKernel
$schedule->job(new UpdateExchangeRates())->dailyAt('23:30')->withoutOverlapping()->name('exchange-rate-job')->onOneServer();
/* Runs cleanup code for subscriptions */
$schedule->job(new SubscriptionCron())->dailyAt('00:01')->withoutOverlapping()->name('subscription-job')->onOneServer();
$schedule->job(new SubscriptionCron())->hourlyAt(1)->withoutOverlapping()->name('subscription-job')->onOneServer();
/* Sends recurring expenses*/
$schedule->job(new RecurringExpensesCron())->dailyAt('00:10')->withoutOverlapping()->name('recurring-expense-job')->onOneServer();

View File

@ -0,0 +1,65 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\Referral;
class ReferralEarning
{
/** @var string $version */
public string $version = 'alpha';
public string $referral_start_date = ''; // The date this referral was registered.
public string $qualifies_after = ''; // The date the payout qualifies after (5 months / 1 year)
public string $period_ending = ''; // The Date this set relates to. ie 2024-07-31 = July 2024
public string $account_key = '';
public string $payout_status = 'pending'; //pending //qualified //paidout //invalid
public float $gross_amount = 0;
public float $commission_amount = 0;
public string $notes = '';
/**
* __construct
*
* @param mixed $entity
*/
public function __construct(mixed $entity = null)
{
if (!$entity) {
$this->init();
return;
}
$entityArray = is_object($entity) ? get_object_vars($entity) : $entity;
foreach ($entityArray as $key => $value) {
$this->{$key} = $value;
}
$this->migrate();
}
public function init(): self
{
return $this;
}
private function migrate(): self
{
return $this;
}
}

View File

@ -111,7 +111,35 @@ class BaseRule implements RuleInterface
];
/** EU TAXES */
/** Supported E Delivery Countries */
public array $peppol_business_countries = [
'AT',
'BE',
'DK',
'EE',
'FI',
'DE',
'IS',
'IT',
'LT',
'LU',
'NL',
'NO',
'PL',
'SE',
];
public array $peppol_government_countries = [
'FR',
'GR',
'PT',
'RO',
'SI',
'ES',
'GB',
];
/** Supported E Delivery Countries */
public string $tax_name1 = '';
public float $tax_rate1 = 0;

View File

@ -95,31 +95,35 @@ class TaxModel
}
//@pending Flutter AP upgrade - deploying this breaks the AP.
// if($this->version == 'gamma') {
if($this->version == 'gamma') {
// $this->regions->EU->subregions->IS = new \stdClass();
// $this->regions->EU->subregions->IS->tax_rate = 24;
// $this->regions->EU->subregions->IS->tax_name = 'VSK';
// $this->regions->EU->subregions->IS->reduced_tax_rate = 11;
// $this->regions->EU->subregions->IS->apply_tax = false;
$this->regions->EU->subregions->IS = new \stdClass();
$this->regions->EU->subregions->IS->tax_rate = 24;
$this->regions->EU->subregions->IS->tax_name = 'VSK';
$this->regions->EU->subregions->IS->reduced_tax_rate = 11;
$this->regions->EU->subregions->IS->apply_tax = false;
$this->regions->EU->subregions->IS->vat_number = '';
// $this->regions->EU->subregions->LI = new \stdClass();
// $this->regions->EU->subregions->LI->tax_rate = 8.1;
// $this->regions->EU->subregions->LI->tax_name = 'MWST';
// $this->regions->EU->subregions->LI->reduced_tax_rate = 2.6;
// $this->regions->EU->subregions->LI->apply_tax = false;
$this->regions->EU->subregions->LI = new \stdClass();
$this->regions->EU->subregions->LI->tax_rate = 8.1;
$this->regions->EU->subregions->LI->tax_name = 'MWST';
$this->regions->EU->subregions->LI->reduced_tax_rate = 2.6;
$this->regions->EU->subregions->LI->apply_tax = false;
$this->regions->EU->subregions->LI->vat_number = '';
// $this->regions->EU->subregions->NO = new \stdClass();
// $this->regions->EU->subregions->NO->tax_rate = 25;
// $this->regions->EU->subregions->NO->tax_name = 'MVA';
// $this->regions->EU->subregions->NO->reduced_tax_rate = 12;
// $this->regions->EU->subregions->NO->apply_tax = false;
$this->regions->EU->subregions->NO = new \stdClass();
$this->regions->EU->subregions->NO->tax_rate = 25;
$this->regions->EU->subregions->NO->tax_name = 'MVA';
$this->regions->EU->subregions->NO->reduced_tax_rate = 12;
$this->regions->EU->subregions->NO->apply_tax = false;
$this->regions->EU->subregions->NO->vat_number = '';
// $this->ukRegion();
$this->ukRegion();
$this->stubVatNumbersOnSubregions();
// $this->version = 'delta';
$this->version = 'delta';
// }
}
return $this;
}
@ -138,7 +142,6 @@ class TaxModel
$this->usRegion()
->euRegion()
->auRegion();
// ->ukRegion();
return $this->regions;
@ -160,6 +163,7 @@ class TaxModel
$this->regions->UK->subregions->GB->tax_name = 'VAT';
$this->regions->UK->subregions->GB->reduced_tax_rate = 5;
$this->regions->UK->subregions->GB->apply_tax = false;
$this->regions->UK->subregions->GB->vat_number = '';
// Northern Ireland (special case due to NI Protocol)
$this->regions->UK->subregions->{'GB-NIR'} = new \stdClass();
@ -167,6 +171,7 @@ class TaxModel
$this->regions->UK->subregions->{'GB-NIR'}->tax_name = 'VAT';
$this->regions->UK->subregions->{'GB-NIR'}->reduced_tax_rate = 5;
$this->regions->UK->subregions->{'GB-NIR'}->apply_tax = false;
$this->regions->UK->subregions->{'GB-NIR'}->vat_number = '';
// Isle of Man (follows UK VAT rules)
$this->regions->UK->subregions->{'IM'} = new \stdClass();
@ -174,11 +179,113 @@ class TaxModel
$this->regions->UK->subregions->{'IM'}->tax_name = 'VAT';
$this->regions->UK->subregions->{'IM'}->reduced_tax_rate = 5;
$this->regions->UK->subregions->{'IM'}->apply_tax = false;
$this->regions->UK->subregions->{'IM'}->vat_number = '';
return $this;
}
public function stubVatNumbersOnSubregions(): self
{
// US Subregions
$this->regions->US->subregions->AL->vat_number = '';
$this->regions->US->subregions->AK->vat_number = '';
$this->regions->US->subregions->AZ->vat_number = '';
$this->regions->US->subregions->AR->vat_number = '';
$this->regions->US->subregions->CA->vat_number = '';
$this->regions->US->subregions->CO->vat_number = '';
$this->regions->US->subregions->CT->vat_number = '';
$this->regions->US->subregions->DE->vat_number = '';
$this->regions->US->subregions->FL->vat_number = '';
$this->regions->US->subregions->GA->vat_number = '';
$this->regions->US->subregions->HI->vat_number = '';
$this->regions->US->subregions->ID->vat_number = '';
$this->regions->US->subregions->IL->vat_number = '';
$this->regions->US->subregions->IN->vat_number = '';
$this->regions->US->subregions->IA->vat_number = '';
$this->regions->US->subregions->KS->vat_number = '';
$this->regions->US->subregions->KY->vat_number = '';
$this->regions->US->subregions->LA->vat_number = '';
$this->regions->US->subregions->ME->vat_number = '';
$this->regions->US->subregions->MD->vat_number = '';
$this->regions->US->subregions->MA->vat_number = '';
$this->regions->US->subregions->MI->vat_number = '';
$this->regions->US->subregions->MN->vat_number = '';
$this->regions->US->subregions->MS->vat_number = '';
$this->regions->US->subregions->MO->vat_number = '';
$this->regions->US->subregions->MT->vat_number = '';
$this->regions->US->subregions->NE->vat_number = '';
$this->regions->US->subregions->NV->vat_number = '';
$this->regions->US->subregions->NH->vat_number = '';
$this->regions->US->subregions->NJ->vat_number = '';
$this->regions->US->subregions->NM->vat_number = '';
$this->regions->US->subregions->NY->vat_number = '';
$this->regions->US->subregions->NC->vat_number = '';
$this->regions->US->subregions->ND->vat_number = '';
$this->regions->US->subregions->OH->vat_number = '';
$this->regions->US->subregions->OK->vat_number = '';
$this->regions->US->subregions->OR->vat_number = '';
$this->regions->US->subregions->PA->vat_number = '';
$this->regions->US->subregions->RI->vat_number = '';
$this->regions->US->subregions->SC->vat_number = '';
$this->regions->US->subregions->SD->vat_number = '';
$this->regions->US->subregions->TN->vat_number = '';
$this->regions->US->subregions->TX->vat_number = '';
$this->regions->US->subregions->UT->vat_number = '';
$this->regions->US->subregions->VT->vat_number = '';
$this->regions->US->subregions->VA->vat_number = '';
$this->regions->US->subregions->WA->vat_number = '';
$this->regions->US->subregions->WV->vat_number = '';
$this->regions->US->subregions->WI->vat_number = '';
$this->regions->US->subregions->WY->vat_number = '';
// EU Subregions
$this->regions->EU->subregions->AT->vat_number = '';
$this->regions->EU->subregions->BE->vat_number = '';
$this->regions->EU->subregions->BG->vat_number = '';
$this->regions->EU->subregions->CY->vat_number = '';
$this->regions->EU->subregions->CZ->vat_number = '';
$this->regions->EU->subregions->DE->vat_number = '';
$this->regions->EU->subregions->DK->vat_number = '';
$this->regions->EU->subregions->EE->vat_number = '';
$this->regions->EU->subregions->ES->vat_number = '';
$this->regions->EU->subregions->{'ES-CE'}->vat_number = '';
$this->regions->EU->subregions->{'ES-ML'}->vat_number = '';
$this->regions->EU->subregions->{'ES-CN'}->vat_number = '';
$this->regions->EU->subregions->FI->vat_number = '';
$this->regions->EU->subregions->FR->vat_number = '';
$this->regions->EU->subregions->GR->vat_number = '';
$this->regions->EU->subregions->HR->vat_number = '';
$this->regions->EU->subregions->HU->vat_number = '';
$this->regions->EU->subregions->IE->vat_number = '';
$this->regions->EU->subregions->IS->vat_number = '';
$this->regions->EU->subregions->IT->vat_number = '';
$this->regions->EU->subregions->LI->vat_number = '';
$this->regions->EU->subregions->LT->vat_number = '';
$this->regions->EU->subregions->LU->vat_number = '';
$this->regions->EU->subregions->LV->vat_number = '';
$this->regions->EU->subregions->MT->vat_number = '';
$this->regions->EU->subregions->NO->vat_number = '';
$this->regions->EU->subregions->NL->vat_number = '';
$this->regions->EU->subregions->PL->vat_number = '';
$this->regions->EU->subregions->PT->vat_number = '';
$this->regions->EU->subregions->RO->vat_number = '';
$this->regions->EU->subregions->SE->vat_number = '';
$this->regions->EU->subregions->SI->vat_number = '';
$this->regions->EU->subregions->SK->vat_number = '';
// UK Subregions
$this->regions->UK->subregions->GB->vat_number = '';
$this->regions->UK->subregions->{'GB-NIR'}->vat_number = '';
$this->regions->UK->subregions->IM->vat_number = '';
// AU Subregions
$this->regions->AU->subregions->AU->vat_number = '';
return $this;
}
/**
* Builds the model for Australian Taxes
*

View File

@ -0,0 +1,52 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Events\Socket;
use App\Models\User;
use League\Fractal\Manager;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use App\Utils\Traits\Invoice\Broadcasting\DefaultResourceBroadcast;
/**
* Class DownloadAvailable.
*/
class DownloadAvailable implements ShouldBroadcast
{
use SerializesModels;
use InteractsWithSockets;
public function __construct(public string $url, public string $message, public User $user)
{
}
public function broadcastOn()
{
return [
new PrivateChannel("user-{$this->user->account->key}-{$this->user->id}"),
];
}
public function broadcastWith(): array
{
ctrans('texts.document_download_subject');
return [
'message' => $this->message,
'url' => $this->url,
];
}
}

View File

@ -218,6 +218,14 @@ class ExpenseFilters extends QueryFilters
->whereColumn('clients.id', 'expenses.client_id'), $sort_col[1]);
}
if ($sort_col[0] == 'project' && in_array($sort_col[1], ['asc', 'desc'])) {
return $this->builder
->orderByRaw('ISNULL(project_id), project_id '. $sort_col[1])
->orderBy(\App\Models\Project::select('name')
->whereColumn('projects.id', 'expenses.project_id'), $sort_col[1]);
}
if ($sort_col[0] == 'vendor_id' && in_array($sort_col[1], ['asc', 'desc'])) {
return $this->builder
->orderByRaw('ISNULL(vendor_id), vendor_id '. $sort_col[1])

View File

@ -103,11 +103,11 @@ class EpcQrGenerator
private function validateFields()
{
if (Ninja::isSelfHost() && isset($this->company?->custom_fields?->company2)) {
nlog('The BIC field is not present and _may_ be a required fields for EPC QR codes');
// nlog('The BIC field is not present and _may_ be a required fields for EPC QR codes');
}
if (Ninja::isSelfHost() && isset($this->company?->custom_fields?->company1)) {
nlog('The IBAN field is required');
// nlog('The IBAN field is required');
}
}

View File

@ -242,11 +242,11 @@ class InvoiceItemSum
private function setDiscount()
{
if ($this->invoice->is_amount_discount) {
$this->setLineTotal($this->getLineTotal() - $this->formatValue($this->item->discount, $this->currency->precision));
$this->setLineTotal(round($this->getLineTotal() - $this->formatValue($this->item->discount, $this->currency->precision),2));
} else {
$discount = ($this->item->line_total * ($this->item->discount / 100));
$this->setLineTotal($this->formatValue(($this->getLineTotal() - $discount), $this->currency->precision));
$this->setLineTotal(round($this->formatValue(($this->getLineTotal() - $discount), $this->currency->precision),2));
}
$this->item->is_amount_discount = $this->invoice->is_amount_discount;
@ -488,6 +488,8 @@ class InvoiceItemSum
$amount = $this->item->line_total;
}
// $amount = round($amount,2);
$item_tax_rate1_total = $this->calcAmountLineTax($this->item->tax_rate1, $amount);
$item_tax += $item_tax_rate1_total;

View File

@ -177,9 +177,9 @@ class InvoiceItemSumInclusive
private function setDiscount()
{
if ($this->invoice->is_amount_discount) {
$this->setLineTotal($this->getLineTotal() - $this->formatValue($this->item->discount, $this->currency->precision));
$this->setLineTotal(round($this->getLineTotal() - $this->formatValue($this->item->discount, $this->currency->precision),2));
} else {
$this->setLineTotal($this->getLineTotal() - $this->formatValue(($this->item->line_total * ($this->item->discount / 100)), $this->currency->precision));
$this->setLineTotal(round($this->getLineTotal() - $this->formatValue(($this->item->line_total * ($this->item->discount / 100)), $this->currency->precision),2));
}
$this->item->is_amount_discount = $this->invoice->is_amount_discount;

View File

@ -130,22 +130,10 @@ class ActivityController extends BaseController
$backup = $activity->backup;
$html_backup = '';
/* Refactor 20-10-2021
*
* We have moved the backups out of the database and into object storage.
* In order to handle edge cases, we still check for the database backup
* in case the file no longer exists
*/
$file = $backup->getFile();
if ($backup && $backup->filename && Storage::disk(config('filesystems.default'))->exists($backup->filename)) { //disk
if (Ninja::isHosted()) {
$html_backup = file_get_contents(Storage::disk(config('filesystems.default'))->url($backup->filename));
} else {
$html_backup = file_get_contents(Storage::disk(config('filesystems.default'))->path($backup->filename));
}
} else { //failed
if(!$file)
return response()->json(['message' => ctrans('texts.no_backup_exists'), 'errors' => new stdClass()], 404);
}
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
$pdf = (new Phantom())->convertHtmlToPdf($html_backup);

View File

@ -13,6 +13,7 @@ namespace App\Http\Controllers\Auth;
use App\Events\Contact\ContactLoggedIn;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\Contact\ContactLoginRequest;
use App\Http\ViewComposers\PortalComposer;
use App\Libraries\MultiDB;
use App\Models\Account;
@ -36,54 +37,64 @@ class ContactLoginController extends Controller
$this->middleware('guest:contact', ['except' => ['logout']]);
}
private function resolveCompany($request, $company_key)
{
if($company_key && MultiDB::findAndSetDbByCompanyKey($company_key))
return Company::where('company_key', $company_key)->first();
$domain_name = $request->getHost();
if (strpos($domain_name, config('ninja.app_domain')) !== false) {
$subdomain = explode('.', $domain_name)[0];
$query = ['subdomain' => $subdomain];
if($company = MultiDB::findAndSetDbByDomain($query))
return $company;
}
$query = [
'portal_domain' => $request->getSchemeAndHttpHost(),
'portal_mode' => 'domain',
];
if ($company = MultiDB::findAndSetDbByDomain($query)) {
return $company;
}
if(Ninja::isSelfHost())
return Company::first();
return false;
}
public function showLoginForm(Request $request, $company_key = false)
{
$company = false;
$account = false;
$intended = $request->query('intended') ?: false;
$request->session()->invalidate();
$request->session()->regenerateToken();
if ($request->query('intended')) {
$request->session()->put('url.intended', $request->query('intended'));
if ($intended) {
$request->session()->put('url.intended', $intended);
}
if ($request->session()->has('company_key')) {
MultiDB::findAndSetDbByCompanyKey($request->session()->get('company_key'));
$company = Company::where('company_key', $request->session()->get('company_key'))->first();
} elseif ($request->has('company_key')) {
MultiDB::findAndSetDbByCompanyKey($request->input('company_key'));
$company = Company::where('company_key', $request->input('company_key'))->first();
} elseif ($company_key) {
MultiDB::findAndSetDbByCompanyKey($company_key);
$company = Company::where('company_key', $company_key)->first();
}
$company = $this->resolveCompany($request, $company_key);
/** @var ?\App\Models\Company $company **/
if ($company) {
$account = $company->account;
} elseif (! $company && strpos($request->getHost(), config('ninja.app_domain')) !== false) {
$subdomain = explode('.', $request->getHost())[0];
MultiDB::findAndSetDbByDomain(['subdomain' => $subdomain]);
$company = Company::where('subdomain', $subdomain)->first();
} elseif (Ninja::isHosted()) {
MultiDB::findAndSetDbByDomain(['portal_domain' => $request->getSchemeAndHttpHost()]);
$company = Company::where('portal_domain', $request->getSchemeAndHttpHost())->first();
} elseif (Ninja::isSelfHost()) {
/** @var \App\Models\Account $account **/
$account = Account::first();
$company = $account->default_company;
} else {
$company = null;
}
if (! $account) {
$account_id = $request->get('account_id');
$account = Account::find($account_id);
else {
abort(404, "We could not find this site, if you think this is an error, please contact the administrator.");
}
return $this->render('auth.login', ['account' => $account, 'company' => $company]);
}
public function login(Request $request)
public function login(ContactLoginRequest $request)
{
Auth::shouldUse('contact');
@ -171,6 +182,7 @@ class ContactLoginController extends Controller
{
Auth::guard('contact')->logout();
request()->session()->invalidate();
request()->session()->regenerateToken();
return redirect('/client/login');
}

View File

@ -44,11 +44,34 @@ class ContactRegisterController extends Controller
$t = app('translator');
$t->replace(Ninja::transformTranslations($company->settings));
return render('auth.register', ['register_company' => $company, 'account' => $company->account, 'submitsForm' => false]);
$domain_name = request()->getHost();
$show_turnstile = false;
if (config('ninja.cloudflare.turnstile.site_key') && strpos($domain_name, config('ninja.app_domain')) !== false) {
$show_turnstile = true;
}
$data = [
'formed_disabled' => $company->account->isFreeHostedClient(),
'register_company' => $company,
'account' => $company->account,
'submitsForm' => false,
'show_turnstile' => $show_turnstile
];
return render('auth.register', $data);
}
public function register(RegisterRequest $request)
{
$company = $request->company();
if (! $company->client_can_register || $company->account->isFreeHostedClient()) {
abort(403, 'This page is restricted');
}
$request->merge(['company' => $request->company()]);
$service = new ClientRegisterService(

View File

@ -174,7 +174,7 @@ class PaymentController extends Controller
$payment_hash->payment_id = $payment->id;
$payment_hash->save();
}
$payment->type_id = PaymentType::CREDIT;
$payment = $payment->service()->applyCredits($payment_hash)->save();
/** @var \Illuminate\Database\Eloquent\Collection<\App\Models\Invoice> $invoices */

View File

@ -570,6 +570,7 @@ class DesignController extends BaseController
case 'invoice':
$company->invoices()
->withTrashed()
->when($settings_level == 'company', function ($query) {
$query->where(function ($query) {
$query->whereDoesntHave('client.group_settings')
@ -592,18 +593,19 @@ class DesignController extends BaseController
$query->where('client_id', $client_id);
})
->update(['design_id' => $design_id]);
})->update(['design_id' => $design_id]);
// Recurring Invoice Designs are set using the global company level.
if ($settings_level == 'company') {
$company->recurring_invoices()->update(['design_id' => $design_id]);
$company->recurring_invoices()->withTrashed()->update(['design_id' => $design_id]);
}
break;
case 'quote':
$company->quotes()
->withTrashed()
->when($settings_level == 'company', function ($query) {
$query->where(function ($query) {
$query->whereDoesntHave('client.group_settings')
@ -633,6 +635,7 @@ class DesignController extends BaseController
case 'credit':
$company->credits()
->withTrashed()
->when($settings_level == 'company', function ($query) {
$query->where(function ($query) {
$query->whereDoesntHave('client.group_settings')
@ -661,10 +664,10 @@ class DesignController extends BaseController
break;
case 'purchase_order':
$company->purchase_orders()->update(['design_id' => $design_id]);
$company->purchase_orders()->withTrashed()->update(['design_id' => $design_id]);
break;
case 'recurring_invoice':
$company->recurring_invoices()->update(['design_id' => $design_id]);
$company->recurring_invoices()->withTrashed()->update(['design_id' => $design_id]);
break;
default:

View File

@ -64,7 +64,11 @@ class EInvoiceController extends BaseController
{
$einvoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice();
foreach ($request->input('payment_means', []) as $payment_means) {
$payment_means_array = $request->input('payment_means', []);
$einvoice->PaymentMeans = [];
foreach ($payment_means_array as $payment_means) {
$pm = new PaymentMeans();
$pmc = new PaymentMeansCode();
@ -102,9 +106,12 @@ class EInvoiceController extends BaseController
$pm->InstructionNote = $payment_means['information'];
}
// nlog($pm);
$einvoice->PaymentMeans[] = $pm;
}
// nlog($einvoice);
$stub = new \stdClass();
$stub->Invoice = $einvoice;
@ -137,7 +144,6 @@ class EInvoiceController extends BaseController
'account_key' => $company->account->key,
]);
if ($response->status() == 422) {
return response()->json(['message' => $response->json('message')], 422);
}
@ -146,8 +152,13 @@ class EInvoiceController extends BaseController
return response()->json(['message' => $response->json('message')], 400);
}
$account = $company->account;
$account->e_invoice_quota = (int) $response->body();
$account->save();
return response()->json([
'quota' => $response->body(),
'quota' => $account->e_invoice_quota,
]);
}
}

View File

@ -18,8 +18,10 @@ use App\Services\EDocument\Gateway\Storecove\Storecove;
use App\Http\Requests\EInvoice\Peppol\DisconnectRequest;
use App\Http\Requests\EInvoice\Peppol\AddTaxIdentifierRequest;
use App\Http\Requests\EInvoice\Peppol\RemoveTaxIdentifierRequest;
use App\Http\Requests\EInvoice\Peppol\RetrySendRequest;
use App\Http\Requests\EInvoice\Peppol\ShowEntityRequest;
use App\Http\Requests\EInvoice\Peppol\UpdateEntityRequest;
use App\Services\EDocument\Jobs\SendEDocument;
class EInvoicePeppolController extends BaseController
{
@ -59,7 +61,7 @@ class EInvoicePeppolController extends BaseController
->setup($request->validated());
if (data_get($response, 'status') === 'error') {
return response()->json(data_get($response, 'errors', 'message'), status: $response['code']);
return response()->json(data_get($response, 'message'), status: $response['code']);
}
$company->legal_entity_id = $response['legal_entity_id'];
@ -113,7 +115,7 @@ class EInvoicePeppolController extends BaseController
->updateLegalEntity($request->validated());
if (data_get($response, 'status') === 'error') {
return response()->json(data_get($response, 'errors', 'message'), status: $response['code']);
return response()->json(data_get($response, 'message'), status: $response['code']);
}
$tax_data = $company->tax_data;
@ -147,7 +149,7 @@ class EInvoicePeppolController extends BaseController
->disconnect();
if (data_get($response, 'status') === 'error') {
return response()->json(data_get($response, 'errors', 'message'), status: $response['code']);
return response()->json(data_get($response, 'message'), status: $response['code']);
}
$company->legal_entity_id = null;
@ -198,7 +200,7 @@ class EInvoicePeppolController extends BaseController
->addAdditionalTaxIdentifier($request->validated());
if (data_get($response, 'status') === 'error') {
return response()->json(data_get($response, 'errors', 'message'), status: $response['code']);
return response()->json(data_get($response, 'message'), status: $response['code']);
}
if ($country == 'GB') {
@ -225,7 +227,7 @@ class EInvoicePeppolController extends BaseController
->removeAdditionalTaxIdentifier($request->validated());
if (data_get($response, 'status') === 'error') {
return response()->json(data_get($response, 'errors', 'message'), status: $response['code']);
return response()->json(data_get($response, 'message'), status: $response['code']);
}
if (is_bool($response)) {
@ -251,6 +253,14 @@ class EInvoicePeppolController extends BaseController
return response()->json([]);
}
public function retrySend(RetrySendRequest $request)
{
SendEDocument::dispatch($request->entity, $request->entity_id, auth()->user()->company()->db);
return response()->json(['message' => 'trying....'], 200);
}
private function unsetVatNumbers(mixed $taxData): mixed
{
if (isset($taxData->regions->EU->subregions)) {

View File

@ -102,7 +102,7 @@ class EmailController extends BaseController
$this->entity_transformer = InvoiceTransformer::class;
if ($entity_obj->invitations->count() >= 1) {
$entity_obj->entityEmailEvent($entity_obj->invitations->first(), 'invoice', $template);
$entity_obj->entityEmailEvent($entity_obj->invitations->first(), $template, $template);
$entity_obj->sendEvent(Webhook::EVENT_SENT_INVOICE, "client");
}
}
@ -112,9 +112,8 @@ class EmailController extends BaseController
$this->entity_transformer = QuoteTransformer::class;
if ($entity_obj->invitations->count() >= 1) {
event(new QuoteWasEmailed($entity_obj->invitations->first(), $entity_obj->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), 'quote'));
$entity_obj->entityEmailEvent($entity_obj->invitations->first(), $template);
$entity_obj->sendEvent(Webhook::EVENT_SENT_QUOTE, "client");
}
}

View File

@ -65,7 +65,7 @@ class PostMarkController extends BaseController
public function webhook(Request $request)
{
if ($request->header('X-API-SECURITY') && $request->header('X-API-SECURITY') == config('services.postmark.token')) {
ProcessPostmarkWebhook::dispatch($request->all())->delay(rand(6, 14));
ProcessPostmarkWebhook::dispatch($request->all())->delay(15);
return response()->json(['message' => 'Success'], 200);
}

View File

@ -27,6 +27,19 @@ class SearchController extends Controller
private array $invoices = [];
private array $quotes = [];
private array $expenses = [];
private array $credits = [];
private array $recurring_invoices = [];
private array $vendors = [];
private array $vendor_contacts = [];
private array $purchase_orders = [];
public function __invoke(GenericSearchRequest $request)
{
@ -42,6 +55,7 @@ class SearchController extends Controller
$user = auth()->user();
$this->clientMap($user);
$this->invoiceMap($user);
return response()->json([
@ -64,6 +78,7 @@ class SearchController extends Controller
$params = [
'index' => 'clients,invoices,client_contacts',
// 'index' => 'clients,invoices,client_contacts,quotes,expenses,credits,recurring_invoices,vendors,vendor_contacts,purchase_orders',
'body' => [
'query' => [
'bool' => [
@ -93,6 +108,14 @@ class SearchController extends Controller
'clients' => $this->clients,
'client_contacts' => $this->client_contacts,
'invoices' => $this->invoices,
'quotes' => $this->quotes,
'expenses' => $this->expenses,
'credits' => $this->credits,
'recurring_invoices' => $this->recurring_invoices,
'vendors' => $this->vendors,
'vendor_contacts' => $this->vendor_contacts,
'purchase_orders' => $this->purchase_orders,
'settings' => $this->settingsMap(),
], 200);
@ -133,7 +156,7 @@ class SearchController extends Controller
break;
case 'client_contacts':
if ($result['_source']['__soft_deleted']) { // do not return deleted contacts
if ($result['_source']['__soft_deleted']) {
break;
}
@ -146,7 +169,7 @@ class SearchController extends Controller
break;
case 'quotes':
if ($result['_source']['__soft_deleted']) { // do not return deleted contacts
if ($result['_source']['__soft_deleted']) {
break;
}
@ -158,6 +181,97 @@ class SearchController extends Controller
];
break;
case 'expenses':
if ($result['_source']['__soft_deleted']) {
break;
}
$this->expenses[] = [
'name' => $result['_source']['name'],
'type' => '/expense',
'id' => $result['_source']['hashed_id'],
'path' => "/expenses/{$result['_source']['hashed_id']}"
];
break;
case 'credits':
if ($result['_source']['__soft_deleted']) {
break;
}
$this->credits[] = [
'name' => $result['_source']['name'],
'type' => '/credit',
'id' => $result['_source']['hashed_id'],
'path' => "/credits/{$result['_source']['hashed_id']}"
];
break;
case 'recurring_invoices':
if ($result['_source']['__soft_deleted']) {
break;
}
$this->recurring_invoices[] = [
'name' => $result['_source']['name'],
'type' => '/recurring_invoice',
'id' => $result['_source']['hashed_id'],
'path' => "/recurring_invoices/{$result['_source']['hashed_id']}"
];
break;
case 'vendors':
if ($result['_source']['__soft_deleted']) {
break;
}
$this->vendors[] = [
'name' => $result['_source']['name'],
'type' => '/vendor',
'id' => $result['_source']['hashed_id'],
'path' => "/vendors/{$result['_source']['hashed_id']}"
];
break;
case 'vendor_contacts':
if ($result['_source']['__soft_deleted']) {
break;
}
$this->vendor_contacts[] = [
'name' => $result['_source']['name'],
'type' => '/client',
'id' => $result['_source']['hashed_id'],
'path' => "/clients/{$result['_source']['hashed_id']}"
];
break;
case 'purchase_orders':
if ($result['_source']['__soft_deleted']) {
break;
}
$this->purchase_orders[] = [
'name' => $result['_source']['name'],
'type' => '/purchase_order',
'id' => $result['_source']['hashed_id'],
'path' => "/purchase_orders/{$result['_source']['hashed_id']}"
];
break;
}
}
}

View File

@ -58,7 +58,7 @@ class SetDomainNameDb
MultiDB::setDb('db-ninja-01');
nlog('SetDomainNameDb:: I could not set the DB - defaulting to DB1');
$request->session()->invalidate();
//abort(400, 'Domain not found');
$request->session()->regenerateToken();
}
}
} else {
@ -76,6 +76,7 @@ class SetDomainNameDb
MultiDB::setDb('db-ninja-01');
nlog('SetDomainNameDb:: I could not set the DB - defaulting to DB1');
$request->session()->invalidate();
$request->session()->regenerateToken();
}
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests\ClientPortal\Contact;
use Illuminate\Foundation\Http\FormRequest;
class ContactLoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'email' => 'required',
'password' => 'required',
];
}
}

View File

@ -11,12 +11,13 @@
namespace App\Http\Requests\ClientPortal;
use App\Libraries\MultiDB;
use App\Utils\Ninja;
use App\Models\Account;
use App\Models\Company;
use App\Utils\Ninja;
use Illuminate\Foundation\Http\FormRequest;
use App\Libraries\MultiDB;
use Illuminate\Validation\Rule;
use Illuminate\Foundation\Http\FormRequest;
use App\Http\ValidationRules\Turnstile\Turnstile;
class RegisterRequest extends FormRequest
{
@ -59,6 +60,8 @@ class RegisterRequest extends FormRequest
$rules['terms'] = ['required'];
}
$rules['cf-turnstile-response'] = ['sometimes', new Turnstile];
return $rules;
}

View File

@ -81,6 +81,11 @@ class StoreCreditRequest extends Request
$rules['exchange_rate'] = 'bail|sometimes|numeric';
$rules['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge1'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge2'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge3'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge4'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['date'] = 'bail|sometimes|date:Y-m-d';
if ($this->invoice_id) {

View File

@ -84,6 +84,11 @@ class UpdateCreditRequest extends Request
$rules['exchange_rate'] = 'bail|sometimes|numeric';
$rules['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge1'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge2'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge3'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge4'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
return $rules;
}

View File

@ -0,0 +1,67 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\EInvoice\Peppol;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use App\Http\Requests\Request;
class RetrySendRequest extends Request
{
private string $entity_plural = 'invoices';
public function authorize(): bool
{
/** @var \App\Models\User $user */
$user = auth()->user();
if (config('ninja.app_env') == 'local') {
return true;
}
return $user->account->isPaid() && $user->isAdmin() && $user->company()->legal_entity_id != null;
}
/**
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
/** @var \App\Models\User $user **/
$user = auth()->user();
return [
'entity' => ['bail','required','in:App\Models\Invoice,App\Models\Quote,App\Models\Credit,App\Models\PurchaseOrder'],
'entity_id' => ['bail', 'required', Rule::exists($this->entity_plural, 'id')->where('company_id', $user->company()->id)],
];
}
public function prepareForValidation()
{
$input = $this->all();
if (array_key_exists('entity_id', $input)) {
$input['entity_id'] = $this->decodePrimaryKey($input['entity_id']);
}
if (isset($input['entity']) && in_array($input['entity'], ['invoice','quote','credit','purchase_order'])) {
$this->entity_plural = Str::plural($input['entity']);
$input['entity'] = "App\Models\\".ucfirst(Str::camel($input['entity']));
}
$this->replace($input);
}
}

View File

@ -53,14 +53,14 @@ class UpdateEntityRequest extends FormRequest
$this->replace($input);
}
public function after(): array
{
return [
function (Validator $validator) {
if ($this->input('acts_as_sender') === false && $this->input('acts_as_receiver') === false) {
$validator->errors()->add('acts_as_receiver', ctrans('texts.acts_as_must_be_true'));
}
}
];
}
// public function after(): array
// {
// return [
// function (Validator $validator) {
// if ($this->input('acts_as_sender') === false && $this->input('acts_as_receiver') === false) {
// $validator->errors()->add('acts_as_receiver', ctrans('texts.acts_as_must_be_true'));
// }
// }
// ];
// }
}

View File

@ -44,7 +44,7 @@ class BulkInvoiceRequest extends Request
throw new DuplicatePaymentException('Action still processing, please wait. ', 429);
}
$delay = $this->input('action', 'delete') == 'delete' ? (ceil(count($this->input('ids', 4)))) : 1;
$delay = $this->input('action', 'delete') == 'delete' ? (ceil(count($this->input('ids', 2)))) : 1;
\Illuminate\Support\Facades\Cache::put(($this->ip()."|".$this->input('action', 0)."|".$user->company()->company_key), true, $delay);
}

View File

@ -83,6 +83,10 @@ class StoreInvoiceRequest extends Request
$rules['partial'] = 'bail|sometimes|nullable|numeric|gte:0';
$rules['partial_due_date'] = ['bail', 'sometimes', 'nullable', 'exclude_if:partial,0', 'date', 'before:due_date', 'after_or_equal:date'];
$rules['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge1'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge2'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge3'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge4'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
return $rules;
}

View File

@ -83,6 +83,12 @@ class UpdateInvoiceRequest extends Request
$rules['partial'] = 'bail|sometimes|nullable|numeric';
$rules['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge1'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge2'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge3'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge4'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['date'] = 'bail|sometimes|date:Y-m-d';
$rules['partial_due_date'] = ['bail', 'sometimes', 'nullable', 'exclude_if:partial,0', 'date', 'before:due_date', 'after_or_equal:date'];

View File

@ -145,6 +145,10 @@ class StorePaymentRequest extends Request
$input['idempotency_key'] = substr(time()."{$input['date']}{$input['amount']}{$credits_total}{$this->client_id}{$user->company()->company_key}", 0, 64);
}
if (array_key_exists('exchange_rate', $input) && $input['exchange_rate'] === null) {
unset($input['exchange_rate']);
}
$this->replace($input);
}

View File

@ -77,6 +77,11 @@ class StorePurchaseOrderRequest extends Request
$rules['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge1'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge2'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge3'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge4'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
return $rules;
}

View File

@ -76,6 +76,11 @@ class UpdatePurchaseOrderRequest extends Request
$rules['status_id'] = 'sometimes|integer|in:1,2,3,4,5';
$rules['exchange_rate'] = 'bail|sometimes|numeric';
$rules['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge1'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge2'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge3'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge4'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
return $rules;
}

View File

@ -84,6 +84,11 @@ class StoreQuoteRequest extends Request
$rules['partial_due_date'] = ['bail', 'sometimes', 'nullable', 'exclude_if:partial,0', 'date', 'before:due_date', 'after_or_equal:date'];
$rules['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge1'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge2'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge3'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge4'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
return $rules;
}

View File

@ -72,6 +72,11 @@ class UpdateQuoteRequest extends Request
$rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', 'after_or_equal:date', Rule::requiredIf(fn () => strlen($this->partial_due_date) > 1), 'date'];
$rules['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge1'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge2'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge3'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['custom_surcharge4'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
return $rules;
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\ValidationRules\Turnstile;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Http;
class Turnstile implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$response = Http::asForm()->post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
'secret' => config('ninja.cloudflare.turnstile.secret'),
'response' => $value,
'remoteip' => request()->ip(),
]);
$data = $response->json();
if($data['success']){
}
else {
$fail("Captcha failed");
}
}
}

View File

@ -34,6 +34,9 @@ class ClientTransformer extends BaseTransformer
throw new ImportException('Client already exists');
}
if(!is_array($data))
throw new ImportException('Empty row, or invalid data encountered.');
$settings = ClientSettings::defaults();
$settings->currency_id = (string) $this->getCurrencyByCode($data);

View File

@ -14,12 +14,14 @@ namespace App\Import\Transformer\Csv;
use App\Import\ImportException;
use App\Import\Transformer\BaseTransformer;
use App\Models\Invoice;
use App\Utils\Traits\CleanLineItems;
/**
* Class InvoiceTransformer.
*/
class InvoiceTransformer extends BaseTransformer
{
use CleanLineItems;
/**
* @param $data
*
@ -224,7 +226,7 @@ class InvoiceTransformer extends BaseTransformer
];
}
$transformed['line_items'] = $line_items;
$transformed['line_items'] = $this->cleanItems($line_items);
return $transformed;
}

View File

@ -14,12 +14,15 @@ namespace App\Import\Transformer\Csv;
use App\Import\ImportException;
use App\Import\Transformer\BaseTransformer;
use App\Models\Quote;
use App\Utils\Traits\CleanLineItems;
/**
* Class QuoteTransformer.
*/
class QuoteTransformer extends BaseTransformer
{
use CleanLineItems;
/**
* @param $data
*
@ -120,7 +123,7 @@ class QuoteTransformer extends BaseTransformer
$this->getString($quote_data, 'quote.status')
))
] ?? Quote::STATUS_SENT,
'archived' => $status === 'archived',
// 'archived' => $status === 'archived',
];
/* If we can't find the client, then lets try and create a client */
@ -221,7 +224,7 @@ class QuoteTransformer extends BaseTransformer
'type_id' => '1', //$this->getQuoteTypeId( $record, 'item.type_id' ),
];
}
$transformed['line_items'] = $line_items;
$transformed['line_items'] = $this->cleanItems($line_items);
return $transformed;
}

View File

@ -15,12 +15,15 @@ use App\Import\ImportException;
use App\Import\Transformer\BaseTransformer;
use App\Models\Invoice;
use App\Models\RecurringInvoice;
use App\Utils\Traits\CleanLineItems;
/**
* Class RecurringInvoiceTransformer.
*/
class RecurringInvoiceTransformer extends BaseTransformer
{
use CleanLineItems;
/**
* @param $data
*
@ -187,7 +190,7 @@ class RecurringInvoiceTransformer extends BaseTransformer
];
}
$transformed['line_items'] = $line_items;
$transformed['line_items'] = $this->cleanItems($line_items);
return $transformed;
}

View File

@ -130,6 +130,9 @@ class CreateAccount
NinjaMailerJob::dispatch($nmo, true);
(new \Modules\Admin\Jobs\Account\NinjaUser([], $sp035a66))->handle();
// if($sp794f3f->referral_code && Ninja::isHosted()) //2024-11-29 - pausing on this.
// \Modules\Admin\Jobs\Account\NewReferredAccount::dispatch($sp794f3f->key);
}
VersionCheck::dispatch();

View File

@ -51,15 +51,17 @@ class AutoBill implements ShouldQueue
if ($this->db) {
MultiDB::setDb($this->db);
}
$invoice = false;
try {
nlog("autobill {$this->invoice_id}");
$invoice = Invoice::withTrashed()->find($this->invoice_id);
$invoice->service()->autoBill();
if($invoice)
$invoice->service()->autoBill();
} catch (\Exception $e) {
nlog("Failed to capture payment for {$this->invoice_id} ->".$e->getMessage());
@ -67,7 +69,8 @@ class AutoBill implements ShouldQueue
if ($this->send_email_on_failure && $invoice) {
$invoice->invitations->each(function ($invitation) use ($invoice) {
if ($invitation->contact && ! $invitation->contact->trashed() && strlen($invitation->contact->email) >= 1 && $invoice->client->getSetting('auto_email_invoice')) {
if ($invitation->contact && !$invitation->contact->trashed() && strlen($invitation->contact->email) >= 1 && $invoice->client->getSetting('auto_email_invoice')) {
try {
EmailEntity::dispatch($invitation->withoutRelations(), $invoice->company->db)->delay(rand(1, 2));
@ -77,7 +80,7 @@ class AutoBill implements ShouldQueue
nlog($e->getMessage());
}
nlog("Firing email for invoice {$invoice->number}");
nlog("Firing email for invoice {$invoice->number} which failed to capture payment");
}
});

View File

@ -59,17 +59,19 @@ class CopyDocs implements ShouldQueue
$new_hash = \Illuminate\Support\Str::random(32) . "." . $extension;
$relative_path = "{$this->entity->company->company_key}/documents/{$new_hash}";
Storage::disk($document->disk)->put(
"{$this->entity->company->company_key}/documents/{$new_hash}",
$relative_path,
$file,
);
$instance = Storage::disk($document->disk)->path("{$this->entity->company->company_key}/documents/{$new_hash}");
// $instance = Storage::disk($document->disk)->path("{$this->entity->company->company_key}/documents/{$new_hash}");
$new_doc = new Document();
$new_doc->user_id = $this->entity->user_id;
$new_doc->company_id = $this->entity->company_id;
$new_doc->url = $instance;
$new_doc->url = $relative_path;
$new_doc->name = $document->name;
$new_doc->type = $extension;
$new_doc->disk = $document->disk;
@ -84,4 +86,4 @@ class CopyDocs implements ShouldQueue
});
}
}
}

View File

@ -11,6 +11,7 @@
namespace App\Jobs\Invoice;
use App\Models\Company;
use App\Models\Invoice;
use CleverIt\UBL\Invoice\Address;
use CleverIt\UBL\Invoice\Contact;
@ -125,7 +126,7 @@ class CreateUbl implements ShouldQueue
if ($company->country_id) {
$country = new Country();
$country->setIdentificationCode($company->country->iso_3166_2);
$country->setIdentificationCode(($company instanceof Company) ? $company->country()->iso_3166_2 : $company->country->iso_3166_2);
$address->setCountry($country);
}

View File

@ -11,20 +11,22 @@
namespace App\Jobs\Invoice;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Jobs\Util\UnlinkFile;
use App\Libraries\MultiDB;
use App\Mail\DownloadInvoices;
use App\Models\User;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\User;
use App\Libraries\MultiDB;
use App\Jobs\Util\UnlinkFile;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use App\Mail\DownloadInvoices;
use App\Jobs\Mail\NinjaMailerJob;
use Illuminate\Support\Facades\App;
use App\Jobs\Mail\NinjaMailerObject;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
use App\Events\Socket\DownloadAvailable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class ZipInvoices implements ShouldQueue
{
@ -55,11 +57,10 @@ class ZipInvoices implements ShouldQueue
public function handle(): void
{
MultiDB::setDb($this->company->db);
App::setLocale($this->company->locale());
$settings = $this->company->settings;
nlog(count($this->invoices));
$this->invoices = Invoice::withTrashed()
->where('company_id', $this->company->id)
->whereIn('id', $this->invoices)
@ -99,9 +100,10 @@ class ZipInvoices implements ShouldQueue
}
Storage::put($path.$file_name, $zipFile->outputAsString());
$storage_url = Storage::url($path.$file_name);
$nmo = new NinjaMailerObject();
$nmo->mailable = new DownloadInvoices(Storage::url($path.$file_name), $this->company);
$nmo->mailable = new DownloadInvoices($storage_url, $this->company);
$nmo->to_user = $this->user;
$nmo->settings = $settings;
$nmo->company = $this->company;
@ -110,6 +112,11 @@ class ZipInvoices implements ShouldQueue
UnlinkFile::dispatch(config('filesystems.default'), $path.$file_name)->delay(now()->addHours(1));
$message = count($this->invoices). " ". ctrans('texts.invoices');
$message = ctrans('texts.download_ready', ['message' => $message]);
broadcast(new DownloadAvailable($storage_url, $message, $this->user));
} catch (\PhpZip\Exception\ZipException $e) {
nlog('could not make zip => '.$e->getMessage());
} finally {

View File

@ -82,11 +82,13 @@ class PaymentFailedMailer implements ShouldQueue
$amount = 0;
$invoice = false;
$invitation = false;
if ($this->payment_hash) {
$amount = $this->payment_hash?->amount_with_fee() ?: 0;
$invoice = Invoice::query()->whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->withTrashed()->first();
$invoice = Invoice::query()->with('invitations')->whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->withTrashed()->first();
$invitation = $invoice->invitations->first();
}
//iterate through company_users
@ -97,6 +99,8 @@ class PaymentFailedMailer implements ShouldQueue
if (($key = array_search('mail', $methods)) !== false) {
unset($methods[$key]);
$invitation = $invoice->invitations->first();
$mail_obj = (new PaymentFailureObject($this->client, $this->error, $this->company, $amount, $this->payment_hash, $company_user->portalType()))->build();
$nmo = new NinjaMailerObject();
@ -121,6 +125,10 @@ class PaymentFailedMailer implements ShouldQueue
$nmo->company = $this->company;
$nmo->to_user = $contact;
$nmo->settings = $settings;
if ($invitation) {
$nmo->invitation = $invitation->withoutRelations();
}
NinjaMailerJob::dispatch($nmo);
}

View File

@ -364,7 +364,7 @@ class ProcessMailgunWebhook implements ShouldQueue
$bounce = new EmailBounce(
$this->request['event-data']['tags'][0],
$this->request['event-data']['envelope']['sender'] ?? $this->request['event-data']['envelope']['from'],
$this->request['event-data']['message']['headers']['from'] ?? $this->request['event-data']['message']['headers']['to'],
$this->message_id
);
@ -374,11 +374,11 @@ class ProcessMailgunWebhook implements ShouldQueue
$event = [
'bounce_id' => $this->request['event-data']['id'],
'recipient' => $this->request['event-data']['recipient'] ?? '',
'recipient' => $this->request['event-data']['message']['headers']['to'] ?? '',
'status' => $this->request['event-data']['event'] ?? '',
'delivery_message' => $this->request['event-data']['delivery-status']['description'] ?? $this->request['event-data']['delivery-status']['message'] ?? '',
'server' => $this->request['event-data']['delivery-status']['mx-host'] ?? '',
'server_ip' => $this->request['event-data']['envelope']['sending-ip'] ?? '',
'delivery_message' => $this->request['event-data']['delivery-status']['message'] ?? $this->request['event-data']['delivery-status']['bounce-code'] ?? '',
'server' => $this->request['event-data']['delivery-status']['message'] ?? '',
'server_ip' => '',
'date' => \Carbon\Carbon::parse($this->request['event-data']['timestamp'])->format('Y-m-d H:i:s') ?? '',
];

View File

@ -219,7 +219,8 @@ class SendReminders implements ShouldQueue
nlog('firing email');
EmailEntity::dispatch($invitation->withoutRelations(), $invitation->company->db, $template)->delay(10);
event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(), $template));
// event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(), $template));
$invoice->entityEmailEvent($invoice->invitations->first(), $template);
$invoice->sendEvent(Webhook::EVENT_REMIND_INVOICE, "client");
}
});

View File

@ -62,7 +62,7 @@ class TaskScheduler implements ShouldQueue
try {
//@var \App\Models\Schedule $scheduler
$scheduler->service()->runTask();
} catch (\Exception $e) {
} catch (\Throwable $e) {
nlog("Exception:: TaskScheduler:: Doing job :: {$scheduler->id} :: {$scheduler->name}" . $e->getMessage());
}
@ -88,7 +88,7 @@ class TaskScheduler implements ShouldQueue
try {
/** @var \App\Models\Scheduler $scheduler */
$scheduler->service()->runTask();
} catch (\Exception $e) {
} catch (\Throwable $e) {
nlog("Exception:: TaskScheduler:: #{$scheduler->id}::" . $e->getMessage());
nlog($e->getMessage());
}

View File

@ -155,7 +155,7 @@ class SendRecurring implements ShouldQueue
$invoice->invitations->each(function ($invitation) use ($invoice) {
if ($invitation->contact && ! $invitation->contact->trashed() && strlen($invitation->contact->email) >= 1 && $invoice->client->getSetting('auto_email_invoice')) {
try {
EmailEntity::dispatch($invitation->withoutRelations(), $invoice->company->db)->delay(rand(1, 2));
EmailEntity::dispatch($invitation->withoutRelations(), $invoice->company->db, 'invoice')->delay(rand(1, 2));
} catch (\Exception $e) {
nlog($e->getMessage());
}

View File

@ -20,6 +20,9 @@ use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Services\Report\ARDetailReport;
use App\Services\Report\ARSummaryReport;
use App\Services\Report\ClientBalanceReport;
use App\Services\Report\ClientSalesReport;
use App\Services\Report\TaxSummaryReport;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -61,10 +64,10 @@ class SendToAdmin implements ShouldQueue
$files = [];
$files[] = ['file' => $csv, 'file_name' => "{$this->file_name}", 'mime' => 'text/csv'];
// if(in_array(get_class($export), [ARDetailReport::class, ARSummaryReport::class])) {
// $pdf = base64_encode($export->getPdf());
// $files[] = ['file' => $pdf, 'file_name' => str_replace(".csv", ".pdf", $this->file_name), 'mime' => 'application/pdf'];
// }
if(in_array(get_class($export), [ARDetailReport::class, ARSummaryReport::class, ClientBalanceReport::class, ClientSalesReport::class, TaxSummaryReport::class])) {
$pdf = base64_encode($export->getPdf());
$files[] = ['file' => $pdf, 'file_name' => str_replace(".csv", ".pdf", $this->file_name), 'mime' => 'application/pdf'];
}
$user = $this->company->owner();

View File

@ -130,12 +130,8 @@ class QuoteReminderJob implements ShouldQueue
nrlog("#{$quote->number} => reminder template = {$reminder_template}");
$quote->service()->touchReminder($reminder_template)->save();
//20-04-2022 fixes for endless reminders - generic template naming was wrong
$enabled_reminder = 'enable_quote_'.$reminder_template;
// if ($reminder_template == 'endless_reminder') {
// $enabled_reminder = 'enable_reminder_endless';
// }
if (in_array($reminder_template, ['reminder1', 'reminder2', 'reminder3', 'reminder_endless', 'endless_reminder']) &&
$quote->client->getSetting($enabled_reminder) &&
$quote->client->getSetting('send_reminders') &&

View File

@ -126,9 +126,11 @@ class ReminderJob implements ShouldQueue
$reminder_template = $invoice->calculateTemplate('invoice');
nrlog("#{$invoice->number} => reminder template = {$reminder_template}");
$invoice->service()->touchReminder($reminder_template)->save();
$fees = $this->calcLateFee($invoice, $reminder_template);
if ($invoice->isLocked()) {
nlog("invoice is locked - adding fee to new invoice");
return $this->addFeeToNewInvoice($invoice, $reminder_template, $fees);
}
@ -285,7 +287,7 @@ class ReminderJob implements ShouldQueue
*/
private function setLateFee($invoice, $amount, $percent): Invoice
{
$temp_invoice_balance = $invoice->balance;
if ($amount <= 0 && $percent <= 0) {

View File

@ -53,9 +53,6 @@ class InvoiceEmailedNotification implements ShouldQueue
/* The User */
$user = $company_user->user;
/* This is only here to handle the alternate message channels - ie Slack */
// $notification = new EntitySentNotification($event->invitation, 'invoice');
/* Returns an array of notification methods */
$methods = $this->findUserNotificationTypes($event->invitation, $company_user, 'invoice', ['all_notifications', 'invoice_sent', 'invoice_sent_all', 'invoice_sent_user']);
@ -76,11 +73,6 @@ class InvoiceEmailedNotification implements ShouldQueue
$first_notification_sent = false;
}
/* Override the methods in the Notification Class */
// $notification->method = $methods;
// Notify on the alternate channels
// $user->notify($notification);
}
}
}

View File

@ -23,7 +23,7 @@ use App\Models\RecurringInvoiceInvitation;
use Illuminate\Contracts\Queue\ShouldQueue;
use Symfony\Component\Mime\MessageConverter;
class MailSentListener implements ShouldQueue
class MailSentListener
{
/**
* Create the event listener.

View File

@ -22,6 +22,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
class QuoteEmailedNotification implements ShouldQueue
{
use UserNotifies;
public $delay = 5;
public function __construct()
@ -38,8 +39,6 @@ class QuoteEmailedNotification implements ShouldQueue
{
MultiDB::setDb($event->company->db);
// $first_notification_sent = true;
$quote = $event->invitation->quote->fresh();
$quote->last_sent_date = now();
$quote->saveQuietly();

View File

@ -51,6 +51,7 @@ class QuoteReminderEmailActivity implements ShouldQueue
$fields->user_id = $user_id;
$fields->quote_id = $event->invitation->quote_id;
$fields->company_id = $event->invitation->company_id;
$fields->account_id = $event->invitation->company->account_id;
$fields->client_contact_id = $event->invitation->client_contact_id;
$fields->client_id = $event->invitation->quote->client_id;
$fields->activity_type_id = $reminder;

View File

@ -63,6 +63,22 @@ class RegisterOrLogin extends Component
$this->state['initial_completed'] = true;
if(!$this->subscription()->registration_required){
$service = new ClientRegisterService(
company: $this->subscription()->company,
additional: $this->additional_fields,
);
$client = $service->createClient([]);
$contact = $service->createClientContact(['email' => $this->email], $client);
auth()->guard('contact')->loginUsingId($contact->id, true);
$this->dispatch('purchase.next');
return;
}
if ($this->state['otp']) {
return $this->withOtp();
}
@ -112,7 +128,6 @@ class RegisterOrLogin extends Component
if ($contact === null) {
$this->registerForm();
return;
}
@ -262,11 +277,10 @@ class RegisterOrLogin extends Component
{
if (auth()->guard('contact')->check()) {
// $this->dispatch('purchase.context', property: 'contact', value: auth()->guard('contact')->user());
$this->dispatch('purchase.next');
return;
}
}
public function render()

View File

@ -26,6 +26,16 @@ class Cart extends Component
public string $subscription_id;
public function mount()
{
\Illuminate\Support\Facades\App::forgetInstance('translator');
$t = app('translator');
$t->replace(\App\Utils\Ninja::transformTranslations($this->subscription()->company->settings));
\Illuminate\Support\Facades\App::setLocale($this->subscription()->company->locale());
}
#[Computed()]
public function subscription()
{

View File

@ -12,18 +12,20 @@
namespace App\Livewire\BillingPortal;
use App\Utils\Ninja;
use Livewire\Component;
use App\Libraries\MultiDB;
use App\Livewire\BillingPortal\Authentication\Login;
use App\Livewire\BillingPortal\Authentication\Register;
use App\Livewire\BillingPortal\Authentication\RegisterOrLogin;
use App\Livewire\BillingPortal\Cart\Cart;
use App\Livewire\BillingPortal\Payments\Methods;
use Illuminate\Support\Str;
use Livewire\Attributes\On;
use App\Models\Subscription;
use App\Utils\Traits\MakesHash;
use Livewire\Attributes\Computed;
use Livewire\Attributes\On;
use Livewire\Component;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\App;
use App\Livewire\BillingPortal\Cart\Cart;
use App\Livewire\BillingPortal\Payments\Methods;
use App\Livewire\BillingPortal\Authentication\Login;
use App\Livewire\BillingPortal\Authentication\Register;
use App\Livewire\BillingPortal\Authentication\RegisterOrLogin;
class Purchase extends Component
{
@ -115,7 +117,6 @@ class Purchase extends Component
return "summary-{$this->id}";
}
#[Computed()]
public function subscription()
{
@ -166,7 +167,8 @@ class Purchase extends Component
->handleContext('hash', $this->hash)
->handleContext('quantity', 1)
->handleContext('request_data', $this->request_data)
->handleContext('campaign', $this->campaign);
->handleContext('campaign', $this->campaign)
->handleContext('subcription_id', $this->subscription_id);
}
public function render()

View File

@ -13,12 +13,16 @@
namespace App\Livewire\BillingPortal;
use App\Models\CompanyGateway;
use App\Models\Subscription;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Cache;
use Livewire\Attributes\On;
use Livewire\Component;
class RFF extends Component
{
use MakesHash;
public array $context;
public string $contact_first_name;
@ -70,6 +74,7 @@ class RFF extends Component
$gateway = CompanyGateway::find($this->context['form']['company_gateway_id']);
$countries = Cache::get('countries');
if ($gateway === null) {
return view('billing-portal.v3.rff-basic');
}

View File

@ -12,13 +12,15 @@
namespace App\Livewire\BillingPortal;
use App\Models\RecurringInvoice;
use App\Models\Subscription;
use App\Utils\Ninja;
use App\Utils\Number;
use App\Utils\Traits\MakesHash;
use Livewire\Attributes\Computed;
use Livewire\Attributes\On;
use Livewire\Component;
use Livewire\Attributes\On;
use App\Models\Subscription;
use App\Utils\Traits\MakesHash;
use App\Models\RecurringInvoice;
use Livewire\Attributes\Computed;
use Illuminate\Support\Facades\App;
class Summary extends Component
{
@ -38,6 +40,11 @@ class Summary extends Component
{
$subscription = Subscription::find($this->decodePrimaryKey($this->subscription_id));
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($subscription->company->settings));
App::setLocale($subscription->company->locale());
$bundle = $this->context['bundle'] ?? [
'recurring_products' => [],
'optional_recurring_products' => [],
@ -190,7 +197,7 @@ class Summary extends Component
foreach ($this->context['bundle']['recurring_products'] as $key => $item) {
$products[] = [
'product_key' => $item['product']['product_key'],
'notes' => strip_tags(\Illuminate\Support\Str::markdown($item['product']['notes'])),
'notes' => strip_tags(\Illuminate\Support\Str::markdown($item['product']['notes'] ?? '')),
'quantity' => $item['quantity'],
'total_raw' => $item['product']['price'] * $item['quantity'],
'total' => Number::formatMoney($item['product']['price'] * $item['quantity'], $this->subscription()->company) . ' / ' . RecurringInvoice::frequencyForKey($this->subscription()->frequency_id),
@ -200,7 +207,7 @@ class Summary extends Component
foreach ($this->context['bundle']['optional_recurring_products'] as $key => $item) {
$products[] = [
'product_key' => $item['product']['product_key'],
'notes' => strip_tags(\Illuminate\Support\Str::markdown($item['product']['notes'])),
'notes' => strip_tags(\Illuminate\Support\Str::markdown($item['product']['notes'] ?? '')),
'quantity' => $item['quantity'],
'total_raw' => $item['product']['price'] * $item['quantity'],
'total' => Number::formatMoney($item['product']['price'] * $item['quantity'], $this->subscription()->company) . ' / ' . RecurringInvoice::frequencyForKey($this->subscription()->frequency_id),
@ -210,7 +217,7 @@ class Summary extends Component
foreach ($this->context['bundle']['one_time_products'] as $key => $item) {
$products[] = [
'product_key' => $item['product']['product_key'],
'notes' => strip_tags(\Illuminate\Support\Str::markdown($item['product']['notes'])),
'notes' => strip_tags(\Illuminate\Support\Str::markdown($item['product']['notes'] ?? '')),
'quantity' => $item['quantity'],
'total_raw' => $item['product']['price'] * $item['quantity'],
'total' => Number::formatMoney($item['product']['price'] * $item['quantity'], $this->subscription()->company),
@ -220,7 +227,7 @@ class Summary extends Component
foreach ($this->context['bundle']['optional_one_time_products'] as $key => $item) {
$products[] = [
'product_key' => $item['product']['product_key'],
'notes' => strip_tags(\Illuminate\Support\Str::markdown($item['product']['notes'])),
'notes' => strip_tags(\Illuminate\Support\Str::markdown($item['product']['notes'] ?? '')),
'quantity' => $item['quantity'],
'total_raw' => $item['product']['price'] * $item['quantity'],
'total' => Number::formatMoney($item['product']['price'] * $item['quantity'], $this->subscription()->company),
@ -235,17 +242,6 @@ class Summary extends Component
#[On('summary.refresh')]
public function refresh()
{
// nlog("am i refreshing here?");
// $this->oneTimePurchasesTotal = $this->oneTimePurchasesTotal();
// $this->recurringPurchasesTotal = $this->recurringPurchasesTotal();
// $this->discount = $this->discount();
// nlog($this->oneTimePurchasesTotal);
// nlog($this->recurringPurchasesTotal);
// nlog($this->discount);
}

View File

@ -48,7 +48,13 @@ class CreditsTable extends Component
$query->whereDate('due_date', '>=', now())
->orWhereNull('due_date');
})
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
// ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->when($this->sort_field == 'number', function ($q){
$q->orderByRaw("REGEXP_REPLACE(number,'[^0-9]+','')+0 " . ($this->sort_asc ? 'desc' : 'asc'));
})
->when($this->sort_field != 'number', function ($q){
$q->orderBy($this->sort_field, ($this->sort_asc ? 'desc' : 'asc'));
})
->withTrashed()
->paginate($this->per_page);

View File

@ -238,16 +238,18 @@ class InvoicePay extends Component
$this->setContext('db', $this->db); // $this->context['db'] = $this->db;
$this->setContext('invitation_id', $this->invitation_id);
$this->invoices = Invoice::find($this->transformKeys($this->invoices));
$invoices = Invoice::withTrashed()
->whereIn('id', $this->transformKeys($this->invoices))
->where('is_deleted', 0)
->get()
->filter(function ($i) {
$i = $i->service()
->markSent()
->removeUnpaidGatewayFees()
->save();
$invoices = $this->invoices->filter(function ($i) {
$i = $i->service()
->markSent()
->removeUnpaidGatewayFees()
->save();
return $i->isPayable();
});
return $i->isPayable();
});
//under-over / payment
@ -260,7 +262,7 @@ class InvoicePay extends Component
$this->setContext('variables', $this->variables); // $this->context['variables'] = $this->variables;
$this->setContext('invoices', $invoices); // $this->context['invoices'] = $invoices;
$this->setContext('settings', $settings); // $this->context['settings'] = $settings;
$this->setContext('invitation', $invite); // $this->context['invitation'] = $invite;
// $this->setContext('invitation', $invite->withoutRelations()); // $this->context['invitation'] = $invite;
$payable_invoices = $invoices->map(function ($i) {
/** @var \App\Models\Invoice $i */
@ -271,7 +273,8 @@ class InvoicePay extends Component
'formatted_currency' => Number::formatMoney($i->partial > 0 ? $i->partial : $i->balance, $i->client),
'number' => $i->number,
'date' => $i->translateDate($i->date, $i->client->date_format(), $i->client->locale()),
'due_date' => $i->translateDate($i->due_date, $i->client->date_format(), $i->client->locale())
'due_date' => $i->translateDate($i->due_date, $i->client->date_format(), $i->client->locale()),
'terms' => $i->terms,
];
})->toArray();

View File

@ -12,6 +12,7 @@
namespace App\Livewire\Flow2;
use App\Models\InvoiceInvitation;
use App\Utils\Number;
use Livewire\Component;
use Livewire\Attributes\On;
@ -61,15 +62,15 @@ class InvoiceSummary extends Component
public function downloadDocument($invoice_hashed_id)
{
$contact = $this->getContext()['contact'] ?? auth()->guard('contact')->user();
$_invoices = $this->getContext()['invoices'];
$i = $_invoices->first(function ($i) use ($invoice_hashed_id) {
return $i->hashed_id == $invoice_hashed_id;
});
$invitation_id = $this->getContext()['invitation_id'];
$file_name = $i->numberFormatter().'.pdf';
$db = $this->getContext()['db'];
$invite = \App\Models\InvoiceInvitation::on($db)->withTrashed()->find($invitation_id);
$file = (new \App\Jobs\Entity\CreateRawPdf($i->invitations()->where('client_contact_id', $contact->id)->first()))->handle();
$file_name = $invite->invoice->numberFormatter().'.pdf';
$file = (new \App\Jobs\Entity\CreateRawPdf($invite))->handle();
$headers = ['Content-Type' => 'application/pdf'];

View File

@ -61,7 +61,9 @@ class PaymentMethod extends Component
MultiDB::setDb($this->getContext()['db']);
$this->methods = $this->getContext()['invitation']->contact->client->service()->getPaymentMethods($this->amount);
$contact = $this->getContext()['contact'] ?? auth()->guard('contact')->user();
$this->methods = $contact->client->service()->getPaymentMethods($this->amount);
if (count($this->methods) == 1) {
$this->dispatch('singlePaymentMethodFound', company_gateway_id: $this->methods[0]['company_gateway_id'], gateway_type_id: $this->methods[0]['gateway_type_id'], amount: $this->amount);

View File

@ -30,7 +30,14 @@ class Terms extends Component
#[Computed()]
public function invoice()
{
return $this->getContext()['invoices']->first();
$invitation_id = $this->getContext()['invitation_id'];
$db = $this->getContext()['db'];
$invite = \App\Models\InvoiceInvitation::on($db)->withTrashed()->find($invitation_id);
return $invite->invoice;
}
public function render()

View File

@ -32,9 +32,10 @@ class UnderOverPayment extends Component
public function mount()
{
$contact = $this->getContext()['contact'] ?? auth()->guard('contact')->user();
$this->invoice_amount = array_sum(array_column($this->getContext()['payable_invoices'], 'amount'));
$this->currency = $this->getContext()['invitation']->contact->client->currency();
$this->currency = $contact->client->currency();
$this->payableInvoices = $this->getContext()['payable_invoices'];
}
@ -44,9 +45,11 @@ class UnderOverPayment extends Component
$settings = $this->getContext()['settings'];
$contact = $this->getContext()['contact'] ?? auth()->guard('contact')->user();
foreach ($payableInvoices as $key => $invoice) {
$payableInvoices[$key]['amount'] = Number::parseFloat($invoice['formatted_amount']);
$payableInvoices[$key]['formatted_currency'] = Number::FormatMoney($payableInvoices[$key]['amount'], $this->getContext()['invitation']->contact->client);
$payableInvoices[$key]['formatted_currency'] = Number::FormatMoney($payableInvoices[$key]['amount'], $contact->client);
}
$input_amount = collect($payableInvoices)->sum('amount');

View File

@ -51,7 +51,13 @@ class InvoicesTable extends Component
->where('is_deleted', false)
->where('is_proforma', false)
->with('client.gateway_tokens', 'client.contacts')
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc');
->when($this->sort_field == 'number', function ($q){
$q->orderByRaw("REGEXP_REPLACE(number,'[^0-9]+','')+0 " . ($this->sort_asc ? 'desc' : 'asc'));
})
->when($this->sort_field != 'number', function ($q){
$q->orderBy($this->sort_field, ($this->sort_asc ? 'desc' : 'asc'));
});
// ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc');
if (in_array('paid', $this->status)) {
$local_status[] = Invoice::STATUS_PAID;

View File

@ -42,7 +42,13 @@ class PaymentsTable extends Component
->where('company_id', auth()->guard('contact')->user()->company_id)
->where('client_id', auth()->guard('contact')->user()->client_id)
->whereIn('status_id', [Payment::STATUS_FAILED, Payment::STATUS_COMPLETED, Payment::STATUS_PENDING, Payment::STATUS_REFUNDED, Payment::STATUS_PARTIALLY_REFUNDED])
->orderBy($this->sort_field, $this->sort_asc ? 'desc' : 'asc')
// ->orderBy($this->sort_field, $this->sort_asc ? 'desc' : 'asc')
->when($this->sort_field == 'number', function ($q){
$q->orderByRaw("REGEXP_REPLACE(number,'[^0-9]+','')+0 " . ($this->sort_asc ? 'desc' : 'asc'));
})
->when($this->sort_field != 'number', function ($q){
$q->orderBy($this->sort_field, ($this->sort_asc ? 'desc' : 'asc'));
})
->withTrashed()
->paginate($this->per_page);

View File

@ -53,7 +53,14 @@ class QuotesTable extends Component
{
$query = Quote::query()
->with('client.contacts', 'company')
->orderBy($this->sort, $this->sort_asc ? 'asc' : 'desc');
// ->orderBy($this->sort, $this->sort_asc ? 'asc' : 'desc');
->when($this->sort == 'number', function ($q){
$q->orderByRaw("REGEXP_REPLACE(number,'[^0-9]+','')+0 " . ($this->sort_asc ? 'desc' : 'asc'));
})
->when($this->sort != 'number', function ($q){
$q->orderBy($this->sort, ($this->sort_asc ? 'desc' : 'asc'));
});
if (count($this->status) > 0) {
/* Special filter for expired*/

View File

@ -108,7 +108,7 @@ class EntitySentObject
private function setTemplate()
{
switch ($this->template) {
case 'invoice':
$this->template_subject = 'texts.notification_invoice_sent_subject';
@ -127,6 +127,7 @@ class EntitySentObject
$this->template_body = 'texts.notification_invoice_sent';
break;
case 'reminder_endless':
case 'endless_reminder':
$this->template_subject = 'texts.notification_invoice_reminder_endless_sent_subject';
$this->template_body = 'texts.notification_invoice_sent';
break;
@ -134,6 +135,10 @@ class EntitySentObject
$this->template_subject = 'texts.notification_quote_sent_subject';
$this->template_body = 'texts.notification_quote_sent';
break;
case 'email_quote_template_reminder1':
$this->template_subject = 'texts.notification_quote_reminder1_sent_subject';
$this->template_body = 'texts.notification_quote_sent';
break;
case 'credit':
$this->template_subject = 'texts.notification_credit_sent_subject';
$this->template_body = 'texts.notification_credit_sent';

View File

@ -87,7 +87,6 @@ use Laracasts\Presenter\PresentableTrait;
* @method static \Illuminate\Database\Eloquent\Builder|Account query()
* @method static \Illuminate\Database\Eloquent\Builder|BaseModel scope()
* @method static \Illuminate\Database\Eloquent\Builder|Account first()
* @method static \Illuminate\Database\Eloquent\Builder|Account with()
* @method static \Illuminate\Database\Eloquent\Builder|Account count()
* @method static \Illuminate\Database\Eloquent\Builder|Account where($query)
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\BankIntegration> $bank_integrations

View File

@ -11,6 +11,7 @@
namespace App\Models;
use App\Utils\Ninja;
use Illuminate\Support\Facades\Storage;
/**
@ -53,7 +54,15 @@ class Backup extends BaseModel
{
return $this->belongsTo(Activity::class);
}
/**
* storeRemotely
*
* @param string $html
* @param Client | Vendor $client_or_vendor
*
* @return void
*/
public function storeRemotely(?string $html, Client | Vendor $client_or_vendor)
{
if (empty($html)) {
@ -64,12 +73,40 @@ class Backup extends BaseModel
$filename = now()->format('Y_m_d').'_'.md5(time()).'.html'; //@phpstan-ignore-line
$file_path = $path.$filename;
Storage::disk(config('filesystems.default'))->put($file_path, $html);
$disk = Ninja::isHosted() ? config('filesystems.backup') : config('filesystems.default');
Storage::disk($disk)->put($file_path, $html);
$this->filename = $file_path;
$this->disk = $disk;
$this->save();
}
/**
* getFile
*
* pulls backup file from storage
*
* @return mixed
*/
public function getFile()
{
if(!$this->filename)
return null;
$disk = Ninja::isHosted() ? $this->disk : config('filesystems.default');
return Storage::disk($disk)->get($this->filename);
}
/**
* deleteFile
*
* removes backup file from storage
*
* @return void
*/
public function deleteFile()
{
nlog('deleting => '.$this->filename);
@ -78,8 +115,10 @@ class Backup extends BaseModel
return;
}
$disk = Ninja::isHosted() ? $this->disk : config('filesystems.default');
try {
Storage::disk(config('filesystems.default'))->delete($this->filename);
Storage::disk($disk)->delete($this->filename);
} catch (\Exception $e) {
nlog('BACKUPEXCEPTION deleting backup file with error '.$e->getMessage());
}

View File

@ -14,13 +14,14 @@ namespace App\Models;
use Illuminate\Support\Str;
use Illuminate\Support\Carbon;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesDates;
use App\Jobs\Entity\CreateRawPdf;
use App\Jobs\Util\WebhookHandler;
use App\Models\Traits\Excludable;
use App\Services\EDocument\Jobes\SendEDocument;
use App\Services\PdfMaker\PdfMerge;
use Illuminate\Database\Eloquent\Model;
use App\Utils\Traits\UserSessionAttributes;
use App\Services\EDocument\Jobes\SendEDocument;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
@ -80,6 +81,7 @@ class BaseModel extends Model
use UserSessionAttributes;
use HasFactory;
use Excludable;
use MakesDates;
public int $max_attachment_size = 3000000;

View File

@ -15,7 +15,6 @@ use Laravel\Scout\Searchable;
use App\DataMapper\ClientSync;
use App\Utils\Traits\AppSetup;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesDates;
use App\DataMapper\FeesAndLimits;
use App\Models\Traits\Excludable;
use App\DataMapper\ClientSettings;
@ -120,7 +119,6 @@ class Client extends BaseModel implements HasLocalePreference
{
use PresentableTrait;
use MakesHash;
use MakesDates;
use SoftDeletes;
use Filterable;
use GeneratesCounter;
@ -251,6 +249,7 @@ class Client extends BaseModel implements HasLocalePreference
}
return [
'id' => $this->id,
'name' => $name,
'is_deleted' => $this->is_deleted,
'hashed_id' => $this->hashed_id,
@ -286,6 +285,11 @@ class Client extends BaseModel implements HasLocalePreference
return $this->hashed_id;
}
// public function getScoutKeyName()
// {
// return 'hashed_id';
// }
public function getEntityType()
{
return self::class;
@ -1029,7 +1033,35 @@ class Client extends BaseModel implements HasLocalePreference
*/
public function peppolSendingEnabled(): bool
{
return $this->getSetting('e_invoice_type') == 'PEPPOL' && $this->company->peppolSendingEnabled();
return $this->getSetting('e_invoice_type') == 'PEPPOL' && $this->company->peppolSendingEnabled() && is_null($this->checkDeliveryNetwork());
}
/**
* checkDeliveryNetwork
*
* Checks whether the client country is supported
* for sending over the PEPPOL network.
*
* @return string|null
*/
public function checkDeliveryNetwork(): ?string
{
if(!isset($this->country->iso_3166_2))
return "Client has no country set!";
$br = new \App\DataMapper\Tax\BaseRule();
$government_countries = array_merge($br->peppol_business_countries, $br->peppol_government_countries);
if(in_array($this->country->iso_3166_2, $government_countries) && $this->classification == 'government'){
return null;
}
if(in_array($this->country->iso_3166_2, $br->peppol_business_countries))
return null;
return "Country {$this->country->full_name} ( {$this->country->iso_3166_2} ) is not supported by the PEPPOL network for e-delivery.";
}
}

View File

@ -171,6 +171,7 @@ class ClientContact extends Authenticatable implements HasLocalePreference
public function toSearchableArray()
{
return [
'id' => $this->id,
'name' => $this->present()->search_display(),
'hashed_id' => $this->client->hashed_id,
'email' => $this->email,

View File

@ -11,7 +11,6 @@
namespace App\Models;
use App\Utils\Traits\MakesDates;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
@ -42,7 +41,6 @@ use Illuminate\Database\Eloquent\SoftDeletes;
*/
class ClientGatewayToken extends BaseModel
{
use MakesDates;
use SoftDeletes;
protected $casts = [

View File

@ -129,7 +129,6 @@ class CompanyGateway extends BaseModel
// const TYPE_AUTHORIZE = 305;
// const TYPE_CUSTOM = 306;
// const TYPE_BRAINTREE = 307;
// const TYPE_WEPAY = 309;
// const TYPE_PAYFAST = 310;
// const TYPE_PAYTRACE = 311;
// const TYPE_MOLLIE = 312;

View File

@ -11,18 +11,20 @@
namespace App\Models;
use App\Utils\Number;
use Laravel\Scout\Searchable;
use Illuminate\Support\Carbon;
use App\Utils\Traits\MakesHash;
use App\Helpers\Invoice\InvoiceSum;
use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Models\Presenters\CreditPresenter;
use Illuminate\Support\Facades\App;
use App\Utils\Traits\MakesReminders;
use App\Services\Credit\CreditService;
use App\Services\Ledger\LedgerService;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesInvoiceValues;
use App\Utils\Traits\MakesReminders;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Laracasts\Presenter\PresentableTrait;
use App\Models\Presenters\CreditPresenter;
use App\Helpers\Invoice\InvoiceSumInclusive;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* App\Models\Credit
@ -128,12 +130,12 @@ class Credit extends BaseModel
{
use MakesHash;
use Filterable;
use MakesDates;
use SoftDeletes;
use PresentableTrait;
use MakesInvoiceValues;
use MakesReminders;
use Searchable;
protected $presenter = CreditPresenter::class;
protected $fillable = [
@ -194,6 +196,35 @@ class Credit extends BaseModel
public const STATUS_APPLIED = 4;
public function toSearchableArray()
{
$locale = $this->company->locale();
App::setLocale($locale);
return [
'id' => $this->id,
'name' => ctrans('texts.credit') . " " . $this->number . " | " . $this->client->present()->name() . ' | ' . Number::formatMoney($this->amount, $this->company) . ' | ' . $this->translateDate($this->date, $this->company->date_format(), $locale),
'hashed_id' => $this->hashed_id,
'number' => $this->number,
'is_deleted' => $this->is_deleted,
'amount' => (float) $this->amount,
'balance' => (float) $this->balance,
'due_date' => $this->due_date,
'date' => $this->date,
'custom_value1' => (string)$this->custom_value1,
'custom_value2' => (string)$this->custom_value2,
'custom_value3' => (string)$this->custom_value3,
'custom_value4' => (string)$this->custom_value4,
'company_key' => $this->company->company_key,
'po_number' => (string)$this->po_number,
];
}
public function getScoutKey()
{
return $this->hashed_id;
}
public function getEntityType()
{
return self::class;

View File

@ -12,7 +12,6 @@
namespace App\Models;
use App\Utils\Traits\Inviteable;
use App\Utils\Traits\MakesDates;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
@ -76,7 +75,6 @@ use Illuminate\Support\Carbon;
*/
class CreditInvitation extends BaseModel
{
use MakesDates;
use SoftDeletes;
use Inviteable;

View File

@ -0,0 +1,73 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Models;
use App\Models\License;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
/**
* App\Models\EInvoicingLog
*
* @package App\Models
* @property int $id
* @property string $tenant_id (sent|received)
* @property string $direction
* @property int $legal_entity_id
* @property string|null $license_key The license key string
* @property string|null $notes
* @property int $counter
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property \Carbon\Carbon $deleted_at
* @property-read \App\Models\License $license
* @mixin \Eloquent
*
*/
class EInvoicingLog extends Model
{
use SoftDeletes;
protected $fillable = [
'tenant_id',
'direction',
'legal_entity_id',
'license_key',
'notes',
'counter',
];
protected $casts = [
'created_at' => 'date',
'updated_at' => 'date',
'deleted_at' => 'date',
];
/**
* license
*
*/
public function license(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(License::class, 'license_key', 'license_key');
}
public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Company::class, 'tenant_id', 'id');
}
}

View File

@ -41,6 +41,6 @@ class EInvoicingToken extends Model
*/
public function license_relation(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(License::class, 'license_key', 'license_key');
return $this->belongsTo(License::class, 'license', 'license_key');
}
}

View File

@ -11,6 +11,9 @@
namespace App\Models;
use App\Utils\Number;
use Laravel\Scout\Searchable;
use Illuminate\Support\Facades\App;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
@ -95,7 +98,8 @@ class Expense extends BaseModel
{
use SoftDeletes;
use Filterable;
use Searchable;
protected $fillable = [
'client_id',
'assigned_user_id',
@ -161,6 +165,33 @@ class Expense extends BaseModel
protected $touches = [];
public function toSearchableArray()
{
$locale = $this->company->locale();
App::setLocale($locale);
return [
'id' => $this->id,
'name' => ctrans('texts.expense') . " " . $this->number . ' | ' . Number::formatMoney($this->amount, $this->company) . ' | ' . $this->translateDate($this->date, $this->company->date_format(), $locale),
'hashed_id' => $this->hashed_id,
'number' => $this->number,
'is_deleted' => $this->is_deleted,
'amount' => (float) $this->amount,
'date' => $this->date,
'custom_value1' => (string)$this->custom_value1,
'custom_value2' => (string)$this->custom_value2,
'custom_value3' => (string)$this->custom_value3,
'custom_value4' => (string)$this->custom_value4,
'company_key' => $this->company->company_key,
];
}
public function getScoutKey()
{
return $this->hashed_id;
}
public function getEntityType()
{
return self::class;

View File

@ -108,7 +108,7 @@ class Gateway extends StaticModel
} elseif ($this->id == 63) {
$link = 'https://rotessa.com';
} elseif ($this->id == 64) {
$link = 'https://blockonomics.co';
$link = 'https://help.blockonomics.co/a/solutions/articles/33000291849';
}
return $link;

View File

@ -15,7 +15,6 @@ use App\Utils\Ninja;
use Laravel\Scout\Searchable;
use Illuminate\Support\Carbon;
use App\DataMapper\InvoiceSync;
use App\Utils\Traits\MakesDates;
use App\Helpers\Invoice\InvoiceSum;
use Illuminate\Support\Facades\App;
use App\Utils\Traits\MakesReminders;
@ -143,7 +142,6 @@ class Invoice extends BaseModel
use SoftDeletes;
use Filterable;
use NumberFormatter;
use MakesDates;
use PresentableTrait;
use MakesInvoiceValues;
use MakesReminders;
@ -249,6 +247,7 @@ class Invoice extends BaseModel
App::setLocale($locale);
return [
'id' => $this->id,
'name' => ctrans('texts.invoice') . " " . $this->number . " | " . $this->client->present()->name() . ' | ' . Number::formatMoney($this->amount, $this->company) . ' | ' . $this->translateDate($this->date, $this->company->date_format(), $locale),
'hashed_id' => $this->hashed_id,
'number' => $this->number,
@ -644,10 +643,10 @@ class Invoice extends BaseModel
public function entityEmailEvent($invitation, $reminder_template, $template = '')
{
switch ($reminder_template) {
case 'invoice':
event(new InvoiceWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $template));
event(new InvoiceWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $reminder_template));
break;
case 'reminder1':
event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $reminder_template));
@ -665,7 +664,7 @@ class Invoice extends BaseModel
case 'custom1':
case 'custom2':
case 'custom3':
event(new InvoiceWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $template));
event(new InvoiceWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $reminder_template));
break;
default:
// code...

View File

@ -12,7 +12,6 @@
namespace App\Models;
use App\Utils\Traits\Inviteable;
use App\Utils\Traits\MakesDates;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
@ -77,7 +76,6 @@ use Illuminate\Support\Carbon;
*/
class InvoiceInvitation extends BaseModel
{
use MakesDates;
use SoftDeletes;
use Inviteable;

View File

@ -165,6 +165,16 @@ class License extends StaticModel
}
public function countEntities(): int
{
if (!is_array($this->entities)) {
return 0;
}
return count($this->entities);
}
public function findEntity(string $key, mixed $value): ?TaxEntity
{

View File

@ -18,7 +18,6 @@ use App\Services\Payment\PaymentService;
use App\Utils\Ninja;
use App\Utils\Number;
use App\Utils\Traits\Inviteable;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\Payment\Refundable;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -97,7 +96,6 @@ class Payment extends BaseModel
{
use MakesHash;
use Filterable;
use MakesDates;
use SoftDeletes;
use Refundable;
use Inviteable;

View File

@ -73,7 +73,7 @@ class UserPresenter extends EntityPresenter
return 'No First Name Available';
}
return $this->entity->first_name ?? 'First Name';
return $this->entity->first_name ?? ' ';
}

View File

@ -39,4 +39,9 @@ class VendorContactPresenter extends EntityPresenter
{
return $this->entity->last_name ?: '';
}
public function search_display()
{
return strlen($this->entity->email ?? '') > 2 ? $this->name().' <'.$this->entity->email.'>' : $this->name();
}
}

View File

@ -2,6 +2,7 @@
namespace App\Models;
use App\Services\Project\ProjectService;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Laracasts\Presenter\PresentableTrait;
@ -143,6 +144,15 @@ class Project extends BaseModel
return $this->hasMany(Quote::class);
}
/**
* Service entry points.
*
* @return ProjectService
*/
public function service(): ProjectService
{
return new ProjectService($this);
}
public function translate_entity()
{

View File

@ -11,12 +11,14 @@
namespace App\Models;
use App\Helpers\Invoice\InvoiceSum;
use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Services\PurchaseOrder\PurchaseOrderService;
use App\Utils\Traits\MakesDates;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Utils\Number;
use Laravel\Scout\Searchable;
use Illuminate\Support\Carbon;
use App\Helpers\Invoice\InvoiceSum;
use Illuminate\Support\Facades\App;
use App\Helpers\Invoice\InvoiceSumInclusive;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Services\PurchaseOrder\PurchaseOrderService;
/**
* App\Models\PurchaseOrder
@ -117,8 +119,8 @@ class PurchaseOrder extends BaseModel
{
use Filterable;
use SoftDeletes;
use MakesDates;
use Searchable;
protected $hidden = [
'id',
'private_notes',
@ -197,6 +199,36 @@ class PurchaseOrder extends BaseModel
public const STATUS_RECEIVED = 4;
public const STATUS_CANCELLED = 5;
public function toSearchableArray()
{
$locale = $this->company->locale();
App::setLocale($locale);
return [
'id' => $this->id,
'name' => ctrans('texts.purchase_order') . " " . $this->number . " | " . $this->vendor->present()->name() . ' | ' . Number::formatMoney($this->amount, $this->company) . ' | ' . $this->translateDate($this->date, $this->company->date_format(), $locale),
'hashed_id' => $this->hashed_id,
'number' => $this->number,
'is_deleted' => $this->is_deleted,
'amount' => (float) $this->amount,
'balance' => (float) $this->balance,
'due_date' => $this->due_date,
'date' => $this->date,
'custom_value1' => (string)$this->custom_value1,
'custom_value2' => (string)$this->custom_value2,
'custom_value3' => (string)$this->custom_value3,
'custom_value4' => (string)$this->custom_value4,
'company_key' => $this->company->company_key,
'po_number' => (string)$this->po_number,
];
}
public function getScoutKey()
{
return $this->hashed_id;
}
public static function stringStatus(int $status)
{
switch ($status) {
@ -214,7 +246,6 @@ class PurchaseOrder extends BaseModel
}
}
public static function badgeForStatus(int $status)
{
switch ($status) {

View File

@ -13,7 +13,6 @@ namespace App\Models;
use App\Utils\Ninja;
use App\Utils\Traits\Inviteable;
use App\Utils\Traits\MakesDates;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
@ -76,7 +75,6 @@ use Illuminate\Support\Str;
*/
class PurchaseOrderInvitation extends BaseModel
{
use MakesDates;
use SoftDeletes;
use Inviteable;

View File

@ -11,17 +11,22 @@
namespace App\Models;
use App\Helpers\Invoice\InvoiceSum;
use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Models\Presenters\QuotePresenter;
use App\Services\Quote\QuoteService;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesInvoiceValues;
use App\Utils\Traits\MakesReminders;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Utils\Ninja;
use App\Utils\Number;
use Laravel\Scout\Searchable;
use Illuminate\Support\Carbon;
use App\Utils\Traits\MakesHash;
use App\Helpers\Invoice\InvoiceSum;
use Illuminate\Support\Facades\App;
use App\Services\Quote\QuoteService;
use App\Utils\Traits\MakesReminders;
use App\Events\Quote\QuoteWasEmailed;
use App\Utils\Traits\MakesInvoiceValues;
use App\Models\Presenters\QuotePresenter;
use Laracasts\Presenter\PresentableTrait;
use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Events\Quote\QuoteReminderWasEmailed;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* App\Models\Quote
@ -113,12 +118,12 @@ use Laracasts\Presenter\PresentableTrait;
class Quote extends BaseModel
{
use MakesHash;
use MakesDates;
use Filterable;
use SoftDeletes;
use MakesReminders;
use PresentableTrait;
use MakesInvoiceValues;
use Searchable;
protected $presenter = QuotePresenter::class;
@ -187,6 +192,35 @@ class Quote extends BaseModel
public const STATUS_EXPIRED = -1;
public function toSearchableArray()
{
$locale = $this->company->locale();
App::setLocale($locale);
return [
'id' => $this->id,
'name' => ctrans('texts.quote') . " " . $this->number . " | " . $this->client->present()->name() . ' | ' . Number::formatMoney($this->amount, $this->company) . ' | ' . $this->translateDate($this->date, $this->company->date_format(), $locale),
'hashed_id' => $this->hashed_id,
'number' => $this->number,
'is_deleted' => $this->is_deleted,
'amount' => (float) $this->amount,
'balance' => (float) $this->balance,
'due_date' => $this->due_date,
'date' => $this->date,
'custom_value1' => (string)$this->custom_value1,
'custom_value2' => (string)$this->custom_value2,
'custom_value3' => (string)$this->custom_value3,
'custom_value4' => (string)$this->custom_value4,
'company_key' => $this->company->company_key,
'po_number' => (string)$this->po_number,
];
}
public function getScoutKey()
{
return $this->hashed_id;
}
public function getEntityType()
{
return self::class;
@ -197,16 +231,6 @@ class Quote extends BaseModel
return $this->dateMutator($value);
}
// public function getDueDateAttribute($value)
// {
// return $value ? $this->dateMutator($value) : null;
// }
// public function getPartialDueDateAttribute($value)
// {
// return $this->dateMutator($value);
// }
public function getStatusIdAttribute($value)
{
if ($this->due_date && ! $this->is_deleted && $value == self::STATUS_SENT && Carbon::parse($this->due_date)->lte(now()->startOfDay())) {
@ -442,5 +466,34 @@ class Quote extends BaseModel
return true;
}
/**
* entityEmailEvent
*
* Translates the email type into an activity + notification
* that matches.
*/
public function entityEmailEvent($invitation, $reminder_template)
{
switch ($reminder_template) {
case 'quote':
event(new QuoteWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $reminder_template));
break;
case 'email_quote_template_reminder1':
event(new QuoteReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $reminder_template));
break;
case 'custom1':
case 'custom2':
case 'custom3':
event(new QuoteWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $reminder_template));
break;
default:
// code...
break;
}
}
}

View File

@ -12,7 +12,6 @@
namespace App\Models;
use App\Utils\Traits\Inviteable;
use App\Utils\Traits\MakesDates;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
@ -57,7 +56,6 @@ use Illuminate\Support\Carbon;
*/
class QuoteInvitation extends BaseModel
{
use MakesDates;
use Inviteable;
use SoftDeletes;

View File

@ -11,16 +11,18 @@
namespace App\Models;
use App\Helpers\Invoice\InvoiceSum;
use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Models\Presenters\RecurringInvoicePresenter;
use App\Services\Recurring\RecurringService;
use App\Utils\Traits\MakesDates;
use App\Utils\Number;
use Laravel\Scout\Searchable;
use Illuminate\Support\Carbon;
use App\Utils\Traits\MakesHash;
use App\Helpers\Invoice\InvoiceSum;
use Illuminate\Support\Facades\App;
use Laracasts\Presenter\PresentableTrait;
use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Services\Recurring\RecurringService;
use App\Utils\Traits\Recurring\HasRecurrence;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Laracasts\Presenter\PresentableTrait;
use App\Models\Presenters\RecurringInvoicePresenter;
/**
* Class for Recurring Invoices.
@ -129,10 +131,10 @@ class RecurringInvoice extends BaseModel
use MakesHash;
use SoftDeletes;
use Filterable;
use MakesDates;
use HasRecurrence;
use PresentableTrait;
use Searchable;
protected $presenter = RecurringInvoicePresenter::class;
/**
@ -261,6 +263,35 @@ class RecurringInvoice extends BaseModel
'footer',
];
public function toSearchableArray()
{
$locale = $this->company->locale();
App::setLocale($locale);
return [
'id' => $this->id,
'name' => ctrans('texts.recurring_invoice') . " " . $this->number . " | " . $this->client->present()->name() . ' | ' . Number::formatMoney($this->amount, $this->company) . ' | ' . $this->translateDate($this->date, $this->company->date_format(), $locale),
'hashed_id' => $this->hashed_id,
'number' => $this->number,
'is_deleted' => $this->is_deleted,
'amount' => (float) $this->amount,
'balance' => (float) $this->balance,
'due_date' => $this->due_date,
'date' => $this->date,
'custom_value1' => (string)$this->custom_value1,
'custom_value2' => (string)$this->custom_value2,
'custom_value3' => (string)$this->custom_value3,
'custom_value4' => (string)$this->custom_value4,
'company_key' => $this->company->company_key,
'po_number' => (string)$this->po_number,
];
}
public function getScoutKey()
{
return $this->hashed_id;
}
public function getEntityType()
{
return self::class;
@ -508,6 +539,40 @@ class RecurringInvoice extends BaseModel
}
}
public function nextDateByFrequencyNoOffset(Carbon $carbon)
{
switch ($this->frequency_id) {
case self::FREQUENCY_DAILY:
return $carbon->startOfDay()->addDay();
case self::FREQUENCY_WEEKLY:
return $carbon->startOfDay()->addWeek();
case self::FREQUENCY_TWO_WEEKS:
return $carbon->startOfDay()->addWeeks(2);
case self::FREQUENCY_FOUR_WEEKS:
return $carbon->startOfDay()->addWeeks(4);
case self::FREQUENCY_MONTHLY:
return $carbon->startOfDay()->addMonthNoOverflow();
case self::FREQUENCY_TWO_MONTHS:
return $carbon->startOfDay()->addMonthsNoOverflow(2);
case self::FREQUENCY_THREE_MONTHS:
return $carbon->startOfDay()->addMonthsNoOverflow(3);
case self::FREQUENCY_FOUR_MONTHS:
return $carbon->startOfDay()->addMonthsNoOverflow(4);
case self::FREQUENCY_SIX_MONTHS:
return $carbon->addMonthsNoOverflow(6);
case self::FREQUENCY_ANNUALLY:
return $carbon->startOfDay()->addYear();
case self::FREQUENCY_TWO_YEARS:
return $carbon->startOfDay()->addYears(2);
case self::FREQUENCY_THREE_YEARS:
return $carbon->startOfDay()->addYears(3);
default:
return null;
}
}
public function remainingCycles(): int
{
if ($this->remaining_cycles == 0) {
@ -642,26 +707,26 @@ class RecurringInvoice extends BaseModel
return $data;
}
$next_send_date = Carbon::parse($this->next_send_date_client)->copy();
$next_send_date = Carbon::parse($this->next_send_date_client)->startOfDay()->copy();
for ($x = 0; $x < $iterations; $x++) {
// we don't add the days... we calc the day of the month!!
$next_due_date = $this->calculateDueDate($next_send_date->copy()->format('Y-m-d'));
$next_due_date_string = $next_due_date ? $next_due_date->format('Y-m-d') : '';
$next_send_date = Carbon::parse($next_send_date);
// $next_send_date = Carbon::parse($next_send_date);
$data[] = [
'send_date' => $next_send_date->format('Y-m-d'),
'due_date' => $next_due_date_string,
];
/* Fixes the timeshift in case the offset is negative which cause a infinite loop due to UTC +0*/
if ($this->client->timezone_offset() < 0) {
$next_send_date = $this->nextDateByFrequency($next_send_date->addDay()->format('Y-m-d'));
} else {
$next_send_date = $this->nextDateByFrequency($next_send_date->format('Y-m-d'));
}
// /* Fixes the timeshift in case the offset is negative which cause a infinite loop due to UTC +0*/
// if ($this->client->timezone_offset() < 0) {
// $next_send_date = $this->nextSendDateClient($next_send_date->addDay()->format('Y-m-d'));
// } else {
$next_send_date = $this->nextDateByFrequencyNoOffset($next_send_date);
// }
}
return $data;

Some files were not shown because too many files have changed in this diff Show More