Merge branch 'v5-develop' of https://github.com/invoiceninja/invoiceninja into feature-inbound-email-expenses

This commit is contained in:
paulwer 2024-07-31 07:59:26 +02:00
commit ce77eff7cb
288 changed files with 314087 additions and 310496 deletions

View File

@ -63,3 +63,5 @@ APPLE_REDIRECT_URI=
NORDIGEN_SECRET_ID= NORDIGEN_SECRET_ID=
NORDIGEN_SECRET_KEY= NORDIGEN_SECRET_KEY=
OPENEXCHANGE_APP_ID=

View File

@ -38,17 +38,22 @@ jobs:
sudo php artisan cache:clear sudo php artisan cache:clear
sudo find ./vendor/bin/ -type f -exec chmod +x {} \; sudo find ./vendor/bin/ -type f -exec chmod +x {} \;
sudo find ./ -type d -exec chmod 755 {} \; sudo find ./ -type d -exec chmod 755 {} \;
- name: Set current date to variable
id: set_date
run: echo "current_date=$(date '+%Y-%m-%d')" >> $GITHUB_ENV
- name: Prepare React FrontEnd - name: Prepare React FrontEnd
run: | run: |
git clone https://${{secrets.commit_secret}}@github.com/invoiceninja/ui.git git clone https://${{secrets.commit_secret}}@github.com/invoiceninja/ui.git
cd ui cd ui
git checkout develop git checkout develop
cp .env.example .env
cp ../vite.config.ts.react ./vite.config.js cp ../vite.config.ts.react ./vite.config.js
sed -i '/"version"/c\ "version": " Latest Build - ${{ env.current_date }}",' package.json
npm i npm i
npm run build npm run build
cp -r dist/* ../public/ cp -r dist/* ../public/
mv dist/index.html ../resources/views/react/index.blade.php mv ../public/index.html ../resources/views/react/index.blade.php
- name: Prepare JS/CSS assets - name: Prepare JS/CSS assets
run: | run: |

View File

@ -1,5 +1,5 @@
<p align="center"> <p align="center">
<img src="https://raw.githubusercontent.com/hillelcoren/invoice-ninja/master/public/images/round_logo.png" alt="Sublime's custom image"/> <a href ="https://www.youtube.com/watch?v=CxGxXiotv0I" target="_blank" title="Invoice Ninja Overview Video"><img src="https://raw.githubusercontent.com/hillelcoren/invoice-ninja/master/public/images/round_logo.png" alt="Sublime's custom image"/></a>
</p> </p>
![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop) ![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop)
@ -8,25 +8,30 @@
# Invoice Ninja 5 # Invoice Ninja 5
## [Hosted](https://www.invoiceninja.com) | [Self-Hosted](https://www.invoiceninja.org) Invoice Ninja Version 5 is here! We've taken the best parts of version 4 and added the most requested features to create an invoicing application like no other. Check the [Invoice Ninja YouTube Channel](https://www.youtube.com/@appinvoiceninja) to get up to speed, or try the [Demo](https://react.invoicing.co/demo) now.
Join us on [Slack](http://slack.invoiceninja.com), [Discord](https://discord.gg/ZwEdtfCwXA), [Support Forum](https://forum.invoiceninja.com) **Choose your setup**
## Introduction - [Hosted](https://www.invoiceninja.com): Our hosted version is a Software as a Service (SaaS) solution. You're up and running in under 5 minutes, with no need to worry about hosting or server infrastructure.
- [Self-Hosted](https://www.invoiceninja.org): For those who prefer to manage their own hosting and server infrastructure. This version gives you full control and flexibility.
Version 5 of Invoice Ninja is here! All Pro and Enterprise features from the hosted app are included in the open-source code. We offer a $30 per year white-label license to remove the Invoice Ninja branding from client-facing parts of the app.
We took the best parts of version 4 and add the most requested features
to produce a invoicing application like no other.
All Pro and Enterprise features from the hosted app are included in the open code. #### Get social with us
We offer a $30 per year white-label license to remove the Invoice Ninja branding from client facing parts of the app.
* [Videos](https://www.youtube.com/@appinvoiceninja)
* [API Documentation](https://api-docs.invoicing.co/)
* [APP Documentation](https://invoiceninja.github.io/)
* [Support Forum](https://forum.invoiceninja.com) * [Support Forum](https://forum.invoiceninja.com)
* [Slack](http://slack.invoiceninja.com)
* [Discord](https://discord.gg/ZwEdtfCwXA)
* [Instagram](https://www.instagram.com/appinvoiceninja)
## Setup #### Documentation
* [Invoice Ninja - API](https://api-docs.invoicing.co/)
* [Invoice Ninja - Developer Guide](https://invoiceninja.github.io/en/developer-guide/)
* [Invoice Ninja - User Guide](https://invoiceninja.github.io/en/user-guide/)
* [Invoice Ninja - Self-Hosted Installation Guide](https://invoiceninja.github.io/en/self-host-installation/)
## Installation Options and Clients
### Mobile Apps ### Mobile Apps
* [iPhone](https://apps.apple.com/app/id1503970375?platform=iphone) * [iPhone](https://apps.apple.com/app/id1503970375?platform=iphone)
@ -39,22 +44,26 @@ We offer a $30 per year white-label license to remove the Invoice Ninja branding
* [Linux - Snap](https://snapcraft.io/invoiceninja) * [Linux - Snap](https://snapcraft.io/invoiceninja)
* [Linux - Flatpak](https://flathub.org/apps/com.invoiceninja.InvoiceNinja) * [Linux - Flatpak](https://flathub.org/apps/com.invoiceninja.InvoiceNinja)
### Installation Options ### Self-Hosted Server Installation
**Note:** The self-hosted options do support the desktop and mobile apps.
* [Server or VM](https://invoiceninja.github.io/en/self-host-installation/)
* [Docker File](https://hub.docker.com/r/invoiceninja/invoiceninja/) * [Docker File](https://hub.docker.com/r/invoiceninja/invoiceninja/)
* [Cloudron](https://cloudron.io/store/com.invoiceninja.cloudronapp.html) * [Cloudron](https://www.cloudron.io/store/com.invoiceninja.cloudronapp2.html)
* [Softaculous](https://www.softaculous.com/apps/ecommerce/Invoice_Ninja) * [Softaculous](https://www.softaculous.com/apps/ecommerce/Invoice_Ninja)
### Recommended Providers ### Recommended Providers
* [Stripe](https://stripe.com/) * [Stripe](https://stripe.com/)
* [Postmark](https://postmarkapp.com/) * [Postmark](https://postmarkapp.com/)
## Quick Hosting Setup ## [Advanced] Quick Hosting Setup
In addition to the official [Invoice Ninja - Self-Hosted Installation Guide](https://invoiceninja.github.io/en/self-host-installation/) we have a few commands for you.
```sh ```sh
git clone --single-branch --branch v5-stable https://github.com/invoiceninja/invoiceninja.git git clone --single-branch --branch v5-stable https://github.com/invoiceninja/invoiceninja.git
cp .env.example .env cp .env.example .env
composer i -o --no-dev composer i -o --no-dev
php artisan key:generate
``` ```
Please Note: Please Note:
@ -85,6 +94,7 @@ pass: password
``` ```
## Developers Guide ## Developers Guide
In addition to the official [Invoice Ninja - Developer Guide](https://invoiceninja.github.io/en/developer-guide/) we've got your back with some insights.
### App Design ### App Design

View File

@ -1 +1 @@
5.10.4 5.10.16

View File

@ -177,7 +177,6 @@ class BackupUpdate extends Command
$doc_bin = $document->getFile(); $doc_bin = $document->getFile();
} catch(\Exception $e) { } catch(\Exception $e) {
nlog("Exception:: BackupUpdate::" . $e->getMessage()); nlog("Exception:: BackupUpdate::" . $e->getMessage());
nlog($e->getMessage());
} }
if ($doc_bin) { if ($doc_bin) {

View File

@ -12,37 +12,38 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App; use App;
use App\Models\User;
use App\Utils\Ninja;
use App\Models\Quote;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Vendor;
use App\Models\Account;
use App\Models\Company;
use App\Models\Contact;
use App\Models\Expense;
use App\Models\Invoice;
use App\Models\Payment;
use App\Libraries\MultiDB;
use App\Models\CompanyUser;
use Illuminate\Support\Str;
use App\Models\CompanyToken;
use App\Models\ClientContact;
use App\Models\CompanyLedger;
use App\Models\PurchaseOrder;
use App\Models\VendorContact;
use App\Models\BankTransaction;
use App\Models\QuoteInvitation;
use Illuminate\Console\Command;
use App\Models\CreditInvitation;
use App\Models\RecurringInvoice;
use App\Models\InvoiceInvitation;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use App\Factory\ClientContactFactory; use App\Factory\ClientContactFactory;
use App\Factory\VendorContactFactory; use App\Factory\VendorContactFactory;
use App\Jobs\Company\CreateCompanyToken; use App\Jobs\Company\CreateCompanyToken;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\BankTransaction;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\CompanyLedger;
use App\Models\CompanyToken;
use App\Models\CompanyUser;
use App\Models\Contact;
use App\Models\Credit;
use App\Models\CreditInvitation;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Models\Payment;
use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Models\QuoteInvitation;
use App\Models\RecurringInvoice;
use App\Models\RecurringInvoiceInvitation; use App\Models\RecurringInvoiceInvitation;
use App\Models\User;
use App\Models\Vendor;
use App\Models\VendorContact;
use App\Utils\Ninja;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
/* /*
@ -130,6 +131,7 @@ class CheckData extends Command
$this->checkContactEmailAndSendEmailStatus(); $this->checkContactEmailAndSendEmailStatus();
$this->checkPaymentCurrency(); $this->checkPaymentCurrency();
$this->checkSubdomainsSet(); $this->checkSubdomainsSet();
$this->checkExpenseCurrency();
if (Ninja::isHosted()) { if (Ninja::isHosted()) {
$this->checkAccountStatuses(); $this->checkAccountStatuses();
@ -1158,7 +1160,21 @@ class CheckData extends Command
}); });
} }
}
public function checkExpenseCurrency()
{
Expense::with('company')
->withTrashed()
->whereNull('exchange_rate')
->orWhere('exchange_rate', 0)
->cursor()
->each(function ($expense){
$expense->exchange_rate = 1;
$expense->saveQuietly();
$this->logMessage("Fixing - exchange rate for expense :: {$expense->id}");
});
} }
} }

View File

@ -385,6 +385,9 @@ class CreateSingleAccount extends Command
}); });
$this->countryClients($company, $user);
$this->info("finished"); $this->info("finished");
} }
@ -1109,4 +1112,44 @@ class CreateSingleAccount extends Command
event(new RecurringInvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars())); event(new RecurringInvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars()));
} }
private function countryClients($company, $user)
{
Client::unguard();
Client::create([
'company_id' => $company->id,
'user_id' => $user->id,
'name' => 'Swiss Company AG',
'website' => 'https://www.testcompany.ch',
'private_notes' => 'These are some private notes about the test client.',
'balance' => 0,
'paid_to_date' => 0,
'vat_number' => '654321987',
'id_number' => 'CH9300762011623852957', // Sample Swiss IBAN
'custom_value1' => '2024-07-22 10:00:00',
'custom_value2' => 'blue',
'custom_value3' => 'sampleword',
'custom_value4' => 'test@example.com',
'address1' => '123',
'address2' => 'Test Street 45',
'city' => 'Zurich',
'state' => 'Zurich',
'postal_code' => '8001',
'country_id' => '756', // Switzerland
'shipping_address1' => '123',
'shipping_address2' => 'Test Street 45',
'shipping_city' => 'Zurich',
'shipping_state' => 'Zurich',
'shipping_postal_code' => '8001',
'shipping_country_id' => '756', // Switzerland
'settings' => ClientSettings::Defaults(),
'client_hash' => \Illuminate\Support\Str::random(32),
'routing_id' => '',
]);
}
} }

View File

@ -62,10 +62,10 @@ class SendRemindersCron extends Command
public function handle() public function handle()
{ {
Invoice::where('next_send_date', '<=', now()->toDateTimeString()) Invoice::where('next_send_date', '<=', now()->toDateTimeString())
->whereNull('deleted_at') ->whereNull('invoices.deleted_at')
->where('is_deleted', 0) ->where('invoices.is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) ->whereIn('invoices.status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('balance', '>', 0) ->where('invoices.balance', '>', 0)
->whereHas('client', function ($query) { ->whereHas('client', function ($query) {
$query->where('is_deleted', 0) $query->where('is_deleted', 0)
->where('deleted_at', null); ->where('deleted_at', null);
@ -73,6 +73,7 @@ class SendRemindersCron extends Command
->whereHas('company', function ($query) { ->whereHas('company', function ($query) {
$query->where('is_disabled', 0); $query->where('is_disabled', 0);
}) })
->with('invitations')->cursor()->each(function ($invoice) { ->with('invitations')->cursor()->each(function ($invoice) {
if ($invoice->isPayable()) { if ($invoice->isPayable()) {
$reminder_template = $invoice->calculateTemplate('invoice'); $reminder_template = $invoice->calculateTemplate('invoice');

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\DataMapper\Tax\PL;
use App\DataMapper\Tax\DE\Rule as DERule;
class Rule extends DERule
{
/** @var string $seller_region */
public string $seller_region = 'EU';
/** @var bool $consumer_tax_exempt */
public bool $consumer_tax_exempt = false;
/** @var bool $business_tax_exempt */
public bool $business_tax_exempt = false;
/** @var bool $eu_business_tax_exempt */
public bool $eu_business_tax_exempt = true;
/** @var bool $foreign_business_tax_exempt */
public bool $foreign_business_tax_exempt = false;
/** @var bool $foreign_consumer_tax_exempt */
public bool $foreign_consumer_tax_exempt = false;
/** @var float $tax_rate */
public float $tax_rate = 0;
/** @var float $reduced_tax_rate */
public float $reduced_tax_rate = 0;
public string $tax_name1 = 'VAT';
}

View File

@ -474,6 +474,12 @@ class TaxModel
$this->regions->EU->subregions->NL->reduced_tax_rate = 9; $this->regions->EU->subregions->NL->reduced_tax_rate = 9;
$this->regions->EU->subregions->NL->apply_tax = false; $this->regions->EU->subregions->NL->apply_tax = false;
$this->regions->EU->subregions->PL = new \stdClass();
$this->regions->EU->subregions->PL->tax_rate = 23;
$this->regions->EU->subregions->PL->tax_name = 'VAT';
$this->regions->EU->subregions->PL->reduced_tax_rate = 8;
$this->regions->EU->subregions->PL->apply_tax = false;
$this->regions->EU->subregions->PT = new \stdClass(); $this->regions->EU->subregions->PT = new \stdClass();
$this->regions->EU->subregions->PT->tax_rate = 23; $this->regions->EU->subregions->PT->tax_rate = 23;
$this->regions->EU->subregions->PT->tax_name = 'IVA'; $this->regions->EU->subregions->PT->tax_name = 'IVA';

View File

@ -197,6 +197,10 @@ region:
vat: 21 vat: 21
reduced_vat: 9 reduced_vat: 9
apply_tax: false apply_tax: false
PL:
vat: 23
reduced_vat: 8
apply_tax: false
PT: PT:
vat: 23 vat: 23
reduced_vat: 6 reduced_vat: 6

View File

@ -13,15 +13,21 @@ namespace App\Events\Client;
use App\Models\Client; use App\Models\Client;
use App\Models\Company; use App\Models\Company;
use League\Fractal\Manager;
use League\Fractal\Resource\Item;
use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets; use App\Transformers\ArraySerializer;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use App\Transformers\ClientTransformer;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
/** /**
* Class ClientWasArchived. * Class ClientWasArchived.
*/ */
class ClientWasArchived class ClientWasArchived implements ShouldBroadcast
{ {
use Dispatchable; use Dispatchable;
use InteractsWithSockets; use InteractsWithSockets;
@ -50,13 +56,34 @@ class ClientWasArchived
$this->event_vars = $event_vars; $this->event_vars = $event_vars;
} }
// /** public function broadcastWith()
// * Get the channels the event should broadcast on. {
// *
// * @return Channel|array $manager = new Manager();
// */ $manager->setSerializer(new ArraySerializer());
$class = sprintf('App\\Transformers\\%sTransformer', class_basename($this->client));
$transformer = new $class();
$resource = new Item($this->client, $transformer, $this->client->getEntityType());
$data = $manager->createData($resource)->toArray();
return $data;
}
/**
* Get the channels the event should broadcast on.
*
* @return Channel|array
*/
public function broadcastOn() public function broadcastOn()
{ {
return [];
return [
new PrivateChannel("company-{$this->company->company_key}"),
];
} }
} }

View File

@ -18,6 +18,7 @@ use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
use Illuminate\Database\Eloquent\RelationNotFoundException; use Illuminate\Database\Eloquent\RelationNotFoundException;
use Illuminate\Encryption\MissingAppKeyException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Exceptions\ThrottleRequestsException; use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -94,18 +95,8 @@ class Handler extends ExceptionHandler
*/ */
public function report(Throwable $exception) public function report(Throwable $exception)
{ {
if (! Schema::hasTable('accounts')) {
info('account table not found');
return;
}
if (Ninja::isHosted()) { if (Ninja::isHosted()) {
// if($exception instanceof ThrottleRequestsException && class_exists(\Modules\Admin\Events\ThrottledExceptionRaised::class)) {
// $uri = urldecode(request()->getRequestUri());
// event(new \Modules\Admin\Events\ThrottledExceptionRaised(auth()->user()?->account?->key, $uri, request()->ip()));
// }
Integration::configureScope(function (Scope $scope): void { Integration::configureScope(function (Scope $scope): void {
$name = 'hosted@invoiceninja.com'; $name = 'hosted@invoiceninja.com';
@ -154,6 +145,10 @@ class Handler extends ExceptionHandler
} }
parent::report($exception); parent::report($exception);
if (Ninja::isSelfHost() && $exception instanceof MissingAppKeyException) {
info('To setup the app run: cp .env.example .env');
}
} }
private function validException($exception) private function validException($exception)

View File

@ -57,6 +57,7 @@ class ActivityExport extends BaseExport
$report = $query->cursor() $report = $query->cursor()
->map(function ($resource) { ->map(function ($resource) {
/** @var \App\Models\Activity $resource */
$row = $this->buildActivityRow($resource); $row = $this->buildActivityRow($resource);
return $this->processMetaData($row, $resource); return $this->processMetaData($row, $resource);
})->toArray(); })->toArray();
@ -128,6 +129,9 @@ class ActivityExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($entity) { ->each(function ($entity) {
/** @var \App\Models\Activity $entity */
$this->buildRow($entity); $this->buildRow($entity);
}); });

View File

@ -172,6 +172,7 @@ class BaseExport
'tax_rate3' => 'invoice.tax_rate3', 'tax_rate3' => 'invoice.tax_rate3',
'recurring_invoice' => 'invoice.recurring_id', 'recurring_invoice' => 'invoice.recurring_id',
'auto_bill' => 'invoice.auto_bill_enabled', 'auto_bill' => 'invoice.auto_bill_enabled',
'project' => 'invoice.project',
]; ];
protected array $recurring_invoice_report_keys = [ protected array $recurring_invoice_report_keys = [
@ -449,6 +450,7 @@ class BaseExport
'status' => 'task.status_id', 'status' => 'task.status_id',
'project' => 'task.project_id', 'project' => 'task.project_id',
'billable' => 'task.billable', 'billable' => 'task.billable',
'item_notes' => 'task.item_notes',
]; ];
protected array $forced_client_fields = [ protected array $forced_client_fields = [
@ -1038,6 +1040,10 @@ class BaseExport
$recurring_filters = []; $recurring_filters = [];
if($this->company->getSetting('report_include_drafts')){
$recurring_filters[] = RecurringInvoice::STATUS_DRAFT;
}
if (in_array('active', $status_parameters)) { if (in_array('active', $status_parameters)) {
$recurring_filters[] = RecurringInvoice::STATUS_ACTIVE; $recurring_filters[] = RecurringInvoice::STATUS_ACTIVE;
} }

View File

@ -102,6 +102,8 @@ class ClientExport extends BaseExport
$report = $query->cursor() $report = $query->cursor()
->map(function ($client) { ->map(function ($client) {
/** @var \App\Models\Client $client */
$row = $this->buildRow($client); $row = $this->buildRow($client);
return $this->processMetaData($row, $client); return $this->processMetaData($row, $client);
})->toArray(); })->toArray();
@ -154,6 +156,8 @@ class ClientExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($client) { ->each(function ($client) {
/** @var \App\Models\Client $client */
$this->csv->insertOne($this->buildRow($client)); $this->csv->insertOne($this->buildRow($client));
}); });

View File

@ -82,6 +82,7 @@ class ContactExport extends BaseExport
$this->csv->insertOne($this->buildHeader()); $this->csv->insertOne($this->buildHeader());
$query->cursor()->each(function ($contact) { $query->cursor()->each(function ($contact) {
/** @var \App\Models\ClientContact $contact */
$this->csv->insertOne($this->buildRow($contact)); $this->csv->insertOne($this->buildRow($contact));
}); });
@ -101,6 +102,7 @@ class ContactExport extends BaseExport
$report = $query->cursor() $report = $query->cursor()
->map(function ($contact) { ->map(function ($contact) {
/** @var \App\Models\ClientContact $contact */
$row = $this->buildRow($contact); $row = $this->buildRow($contact);
return $this->processMetaData($row, $contact); return $this->processMetaData($row, $contact);
})->toArray(); })->toArray();

View File

@ -52,6 +52,8 @@ class CreditExport extends BaseExport
$report = $query->cursor() $report = $query->cursor()
->map(function ($credit) { ->map(function ($credit) {
/** @var \App\Models\Credit $credit */
$row = $this->buildRow($credit); $row = $this->buildRow($credit);
return $this->processMetaData($row, $credit); return $this->processMetaData($row, $credit);
})->toArray(); })->toArray();
@ -139,6 +141,7 @@ class CreditExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($credit) { ->each(function ($credit) {
/** @var \App\Models\Credit $credit */
$this->csv->insertOne($this->buildRow($credit)); $this->csv->insertOne($this->buildRow($credit));
}); });

View File

@ -54,6 +54,8 @@ class DocumentExport extends BaseExport
$report = $query->cursor() $report = $query->cursor()
->map(function ($document) { ->map(function ($document) {
/** @var \App\Models\Document $document */
$row = $this->buildRow($document); $row = $this->buildRow($document);
return $this->processMetaData($row, $document); return $this->processMetaData($row, $document);
})->toArray(); })->toArray();
@ -99,6 +101,7 @@ class DocumentExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($entity) { ->each(function ($entity) {
/** @var mixed $entity */
$this->csv->insertOne($this->buildRow($entity)); $this->csv->insertOne($this->buildRow($entity));
}); });

View File

@ -52,6 +52,8 @@ class ExpenseExport extends BaseExport
$report = $query->cursor() $report = $query->cursor()
->map(function ($resource) { ->map(function ($resource) {
/** @var \App\Models\Expense $resource */
$row = $this->buildRow($resource); $row = $this->buildRow($resource);
return $this->processMetaData($row, $resource); return $this->processMetaData($row, $resource);
})->toArray(); })->toArray();
@ -132,6 +134,8 @@ class ExpenseExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($expense) { ->each(function ($expense) {
/** @var \App\Models\Expense $expense */
$this->csv->insertOne($this->buildRow($expense)); $this->csv->insertOne($this->buildRow($expense));
}); });

View File

@ -99,6 +99,8 @@ class InvoiceExport extends BaseExport
$report = $query->cursor() $report = $query->cursor()
->map(function ($resource) { ->map(function ($resource) {
/** @var \App\Models\Invoice $resource */
$row = $this->buildRow($resource); $row = $this->buildRow($resource);
return $this->processMetaData($row, $resource); return $this->processMetaData($row, $resource);
})->toArray(); })->toArray();
@ -119,6 +121,8 @@ class InvoiceExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($invoice) { ->each(function ($invoice) {
/** @var \App\Models\Invoice $invoice */
$this->csv->insertOne($this->buildRow($invoice)); $this->csv->insertOne($this->buildRow($invoice));
}); });
@ -149,9 +153,9 @@ class InvoiceExport extends BaseExport
private function decorateAdvancedFields(Invoice $invoice, array $entity): array private function decorateAdvancedFields(Invoice $invoice, array $entity): array
{ {
// if (in_array('invoice.status', $this->input['report_keys'])) { if (in_array('invoice.project', $this->input['report_keys'])) {
// $entity['invoice.status'] = $invoice->stringStatus($invoice->status_id); $entity['invoice.project'] = $invoice->project ? $invoice->project->name : '';
// } }
if (in_array('invoice.recurring_id', $this->input['report_keys'])) { if (in_array('invoice.recurring_id', $this->input['report_keys'])) {
$entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? ''; $entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? '';

View File

@ -113,6 +113,8 @@ class InvoiceItemExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($resource) { ->each(function ($resource) {
/** @var \App\Models\Invoice $resource */
$this->iterateItems($resource); $this->iterateItems($resource);
foreach($this->storage_array as $row) { foreach($this->storage_array as $row) {
@ -141,6 +143,8 @@ class InvoiceItemExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($invoice) { ->each(function ($invoice) {
/** @var \App\Models\Invoice $invoice */
$this->iterateItems($invoice); $this->iterateItems($invoice);
}); });
@ -261,6 +265,10 @@ class InvoiceItemExport extends BaseExport
$entity['invoice.user_id'] = $invoice->user ? $invoice->user->present()->name() : '';// @phpstan-ignore-line $entity['invoice.user_id'] = $invoice->user ? $invoice->user->present()->name() : '';// @phpstan-ignore-line
} }
if (in_array('invoice.project', $this->input['report_keys'])) {
$entity['invoice.project'] = $invoice->project ? $invoice->project->name : '';// @phpstan-ignore-line
}
return $entity; return $entity;
} }

View File

@ -92,6 +92,8 @@ class PaymentExport extends BaseExport
$report = $query->cursor() $report = $query->cursor()
->map(function ($resource) { ->map(function ($resource) {
/** @var \App\Models\Payment $resource */
$row = $this->buildRow($resource); $row = $this->buildRow($resource);
return $this->processMetaData($row, $resource); return $this->processMetaData($row, $resource);
})->toArray(); })->toArray();
@ -112,6 +114,8 @@ class PaymentExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($entity) { ->each(function ($entity) {
/** @var \App\Models\Payment $entity */
$this->csv->insertOne($this->buildRow($entity)); $this->csv->insertOne($this->buildRow($entity));
}); });

View File

@ -51,6 +51,8 @@ class ProductExport extends BaseExport
$report = $query->cursor() $report = $query->cursor()
->map(function ($resource) { ->map(function ($resource) {
/** @var \App\Models\Product $resource */
$row = $this->buildRow($resource); $row = $this->buildRow($resource);
return $this->processMetaData($row, $resource); return $this->processMetaData($row, $resource);
})->toArray(); })->toArray();
@ -103,7 +105,9 @@ class ProductExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($entity) { ->each(function ($entity) {
$this->csv->insertOne($this->buildRow($entity));
/** @var \App\Models\Product $entity */
$this->csv->insertOne($this->buildRow($entity));
}); });
return $this->csv->toString(); return $this->csv->toString();

View File

@ -98,6 +98,8 @@ class PurchaseOrderExport extends BaseExport
$report = $query->cursor() $report = $query->cursor()
->map(function ($resource) { ->map(function ($resource) {
/** @var \App\Models\PurchaseOrder $resource */
$row = $this->buildRow($resource); $row = $this->buildRow($resource);
return $this->processMetaData($row, $resource); return $this->processMetaData($row, $resource);
})->toArray(); })->toArray();
@ -119,7 +121,9 @@ class PurchaseOrderExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($purchase_order) { ->each(function ($purchase_order) {
$this->csv->insertOne($this->buildRow($purchase_order));
/** @var \App\Models\PurchaseOrder $purchase_order */
$this->csv->insertOne($this->buildRow($purchase_order));
}); });
return $this->csv->toString(); return $this->csv->toString();

View File

@ -101,13 +101,15 @@ class PurchaseOrderItemExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($resource) { ->each(function ($resource) {
$this->iterateItems($resource);
/** @var \App\Models\PurchaseOrder $resource */
$this->iterateItems($resource);
foreach($this->storage_array as $row) { foreach($this->storage_array as $row) {
$this->storage_item_array[] = $this->processItemMetaData($row, $resource); $this->storage_item_array[] = $this->processItemMetaData($row, $resource);
} }
$this->storage_array = []; $this->storage_array = [];
}); });
@ -127,7 +129,9 @@ class PurchaseOrderItemExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($purchase_order) { ->each(function ($purchase_order) {
$this->iterateItems($purchase_order);
/** @var \App\Models\PurchaseOrder $purchase_order */
$this->iterateItems($purchase_order);
}); });
$this->csv->insertAll($this->storage_array); $this->csv->insertAll($this->storage_array);

View File

@ -103,6 +103,8 @@ class QuoteExport extends BaseExport
$report = $query->cursor() $report = $query->cursor()
->map(function ($resource) { ->map(function ($resource) {
/** @var \App\Models\Quote $resource */
$row = $this->buildRow($resource); $row = $this->buildRow($resource);
return $this->processMetaData($row, $resource); return $this->processMetaData($row, $resource);
})->toArray(); })->toArray();
@ -125,6 +127,8 @@ class QuoteExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($quote) { ->each(function ($quote) {
/** @var \App\Models\Quote $quote */
$this->csv->insertOne($this->buildRow($quote)); $this->csv->insertOne($this->buildRow($quote));
}); });

View File

@ -104,6 +104,8 @@ class QuoteItemExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($resource) { ->each(function ($resource) {
/** @var \App\Models\Quote $resource */
$this->iterateItems($resource); $this->iterateItems($resource);
foreach($this->storage_array as $row) { foreach($this->storage_array as $row) {
@ -134,6 +136,8 @@ class QuoteItemExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($quote) { ->each(function ($quote) {
/** @var \App\Models\Quote $quote */
$this->iterateItems($quote); $this->iterateItems($quote);
}); });

View File

@ -93,6 +93,8 @@ class RecurringInvoiceExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($invoice) { ->each(function ($invoice) {
/** @var \App\Models\RecurringInvoice $invoice */
$this->csv->insertOne($this->buildRow($invoice)); $this->csv->insertOne($this->buildRow($invoice));
}); });
@ -112,6 +114,8 @@ class RecurringInvoiceExport extends BaseExport
$report = $query->cursor() $report = $query->cursor()
->map(function ($resource) { ->map(function ($resource) {
/** @var \App\Models\RecurringInvoice $resource */
$row = $this->buildRow($resource); $row = $this->buildRow($resource);
return $this->processMetaData($row, $resource); return $this->processMetaData($row, $resource);
})->toArray(); })->toArray();

View File

@ -106,7 +106,9 @@ class TaskExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($entity) { ->each(function ($entity) {
$this->buildRow($entity);
/** @var \App\Models\Task $entity*/
$this->buildRow($entity);
}); });
$this->csv->insertAll($this->storage_array); $this->csv->insertAll($this->storage_array);
@ -128,6 +130,7 @@ class TaskExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($resource) { ->each(function ($resource) {
/** @var \App\Models\Task $resource*/
$this->buildRow($resource); $this->buildRow($resource);
foreach($this->storage_array as $row) { foreach($this->storage_array as $row) {
@ -153,7 +156,7 @@ class TaskExport extends BaseExport
$entity[$key] = $transformed_entity[$parts[1]]; $entity[$key] = $transformed_entity[$parts[1]];
} elseif (array_key_exists($key, $transformed_entity)) { } elseif (array_key_exists($key, $transformed_entity)) {
$entity[$key] = $transformed_entity[$key]; $entity[$key] = $transformed_entity[$key];
} elseif (in_array($key, ['task.start_date', 'task.end_date', 'task.duration'])) { } elseif (in_array($key, ['task.start_date', 'task.end_date', 'task.duration', 'task.billable', 'task.item_notes'])) {
$entity[$key] = ''; $entity[$key] = '';
} else { } else {
$entity[$key] = $this->decorator->transform($key, $task); $entity[$key] = $this->decorator->transform($key, $task);
@ -161,7 +164,7 @@ class TaskExport extends BaseExport
} }
if (is_null($task->time_log) || (is_array(json_decode($task->time_log, 1)) && count(json_decode($task->time_log, 1)) == 0)) { if (is_null($task->time_log) || (is_array(json_decode($task->time_log, true)) && count(json_decode($task->time_log, true)) == 0)) {
$this->storage_array[] = $entity; $this->storage_array[] = $entity;
} else { } else {
$this->iterateLogs($task, $entity); $this->iterateLogs($task, $entity);
@ -172,13 +175,13 @@ class TaskExport extends BaseExport
private function iterateLogs(Task $task, array $entity) private function iterateLogs(Task $task, array $entity)
{ {
$timezone = Timezone::find($task->company->settings->timezone_id); $timezone = Timezone::find($task->company->settings->timezone_id);
$timezone_name = 'US/Eastern'; $timezone_name = 'America/New_York';
if ($timezone) { if ($timezone) {
$timezone_name = $timezone->name; $timezone_name = $timezone->name;
} }
$logs = json_decode($task->time_log, 1); $logs = json_decode($task->time_log, true);
$date_format_default = $this->date_format; $date_format_default = $this->date_format;
@ -206,6 +209,14 @@ class TaskExport extends BaseExport
$entity['task.duration_words'] = $seconds > 86400 ? CarbonInterval::seconds($seconds)->locale($this->company->locale())->cascade()->forHumans() : now()->startOfDay()->addSeconds($seconds)->format('H:i:s'); $entity['task.duration_words'] = $seconds > 86400 ? CarbonInterval::seconds($seconds)->locale($this->company->locale())->cascade()->forHumans() : now()->startOfDay()->addSeconds($seconds)->format('H:i:s');
} }
if (in_array('task.billable', $this->input['report_keys']) || in_array('billable', $this->input['report_keys'])) {
$entity['task.billable'] = isset($item[3]) && $item[3] == 'true' ? ctrans('texts.yes') : ctrans('texts.no');
}
if (in_array('task.item_notes', $this->input['report_keys']) || in_array('item_notes', $this->input['report_keys'])) {
$entity['task.item_notes'] = isset($item[2]) ? (string)$item[2] : '';
}
$entity = $this->decorateAdvancedFields($task, $entity); $entity = $this->decorateAdvancedFields($task, $entity);
$this->storage_array[] = $entity; $this->storage_array[] = $entity;
@ -216,6 +227,8 @@ class TaskExport extends BaseExport
$entity['task.end_time'] = ''; $entity['task.end_time'] = '';
$entity['task.duration'] = ''; $entity['task.duration'] = '';
$entity['task.duration_words'] = ''; $entity['task.duration_words'] = '';
$entity['task.billable'] = '';
$entity['task.item_notes'] = '';
} }

View File

@ -90,6 +90,8 @@ class VendorExport extends BaseExport
$report = $query->cursor() $report = $query->cursor()
->map(function ($resource) { ->map(function ($resource) {
/** @var \App\Models\Vendor $resource */
$row = $this->buildRow($resource); $row = $this->buildRow($resource);
return $this->processMetaData($row, $resource); return $this->processMetaData($row, $resource);
})->toArray(); })->toArray();
@ -107,7 +109,9 @@ class VendorExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($vendor) { ->each(function ($vendor) {
$this->csv->insertOne($this->buildRow($vendor));
/** @var \App\Models\Vendor $vendor */
$this->csv->insertOne($this->buildRow($vendor));
}); });
return $this->csv->toString(); return $this->csv->toString();

View File

@ -92,6 +92,7 @@ class InvoiceDecorator extends Decorator implements DecoratorInterface
{ {
return $invoice->recurring_invoice ? $invoice->recurring_invoice->number : ''; return $invoice->recurring_invoice ? $invoice->recurring_invoice->number : '';
} }
public function auto_bill_enabled(Invoice $invoice) public function auto_bill_enabled(Invoice $invoice)
{ {
return $invoice->auto_bill_enabled ? ctrans('texts.yes') : ctrans('texts.no'); return $invoice->auto_bill_enabled ? ctrans('texts.yes') : ctrans('texts.no');

View File

@ -18,6 +18,7 @@ use Carbon\Carbon;
class TaskDecorator extends Decorator implements DecoratorInterface class TaskDecorator extends Decorator implements DecoratorInterface
{ {
//@todo - we do not handle iterating through the timelog here.
public function transform(string $key, mixed $entity): mixed public function transform(string $key, mixed $entity): mixed
{ {
$task = false; $task = false;
@ -42,13 +43,13 @@ class TaskDecorator extends Decorator implements DecoratorInterface
{ {
$timezone = Timezone::find($task->company->settings->timezone_id); $timezone = Timezone::find($task->company->settings->timezone_id);
$timezone_name = 'US/Eastern'; $timezone_name = 'America/New_York';
if ($timezone) { if ($timezone) {
$timezone_name = $timezone->name; $timezone_name = $timezone->name;
} }
$logs = json_decode($task->time_log, 1); $logs = json_decode($task->time_log, true);
$date_format_default = 'Y-m-d'; $date_format_default = 'Y-m-d';
@ -71,13 +72,13 @@ class TaskDecorator extends Decorator implements DecoratorInterface
{ {
$timezone = Timezone::find($task->company->settings->timezone_id); $timezone = Timezone::find($task->company->settings->timezone_id);
$timezone_name = 'US/Eastern'; $timezone_name = 'America/New_York';
if ($timezone) { if ($timezone) {
$timezone_name = $timezone->name; $timezone_name = $timezone->name;
} }
$logs = json_decode($task->time_log, 1); $logs = json_decode($task->time_log, true);
$date_format_default = 'Y-m-d'; $date_format_default = 'Y-m-d';
@ -95,6 +96,26 @@ class TaskDecorator extends Decorator implements DecoratorInterface
return ''; return '';
} }
/**
* billable
*
* @todo
*/
public function billable(Task $task)
{
return '';
}
/**
* items_notes
* @todo
*/
public function items_notes(Task $task)
{
return '';
}
public function duration(Task $task) public function duration(Task $task)
{ {
return $task->calcDuration(); return $task->calcDuration();

View File

@ -175,7 +175,7 @@ class RecurringExpenseToExpenseFactory
$_value = explode($_operation, $right); // [MONTHYEAR, 4] $_value = explode($_operation, $right); // [MONTHYEAR, 4]
$_right = Carbon::createFromDate(now()->year, now()->month)->addMonths($_value[1])->translatedFormat('F Y'); $_right = Carbon::createFromDate(now()->year, now()->month)->addMonths($_value[1])->translatedFormat('F Y'); //@phpstan-ignore-line
} }
$replacement = sprintf('%s to %s', $_left, $_right); $replacement = sprintf('%s to %s', $_left, $_right);

View File

@ -97,7 +97,8 @@ class CreditFilters extends QueryFilters
$q->where('first_name', 'like', '%'.$filter.'%') $q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%') ->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%'); ->orWhere('email', 'like', '%'.$filter.'%');
}); })
->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
}); });
} }

View File

@ -124,7 +124,8 @@ class InvoiceFilters extends QueryFilters
$q->where('first_name', 'like', '%'.$filter.'%') $q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%') ->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%'); ->orWhere('email', 'like', '%'.$filter.'%');
}); })
->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
}); });
} }
@ -152,22 +153,22 @@ class InvoiceFilters extends QueryFilters
{ {
return $this->builder->where(function ($query) { return $this->builder->where(function ($query) {
$query->whereIn('invoices.status_id', [Invoice::STATUS_PARTIAL, Invoice::STATUS_SENT]) $query->whereIn('status_id', [Invoice::STATUS_PARTIAL, Invoice::STATUS_SENT])
->where('invoices.is_deleted', 0) ->where('is_deleted', 0)
->where('invoices.balance', '>', 0) ->where('balance', '>', 0)
->orWhere(function ($query) { ->where(function ($query) {
$query->whereNull('invoices.due_date') $query->whereNull('due_date')
->orWhere(function ($q) { ->orWhere(function ($q) {
$q->where('invoices.due_date', '>=', now()->startOfDay()->subSecond())->where('invoices.partial', 0); $q->where('due_date', '>=', now()->startOfDay()->subSecond())->where('partial', 0);
}) })
->orWhere(function ($q) { ->orWhere(function ($q) {
$q->where('invoices.partial_due_date', '>=', now()->startOfDay()->subSecond())->where('invoices.partial', '>', 0); $q->where('partial_due_date', '>=', now()->startOfDay()->subSecond())->where('partial', '>', 0);
}); });
}) })
->orderByRaw('ISNULL(invoices.due_date), invoices.due_date ' . 'desc') ->orderByRaw('ISNULL(due_date), due_date ' . 'desc')
->orderByRaw('ISNULL(invoices.partial_due_date), invoices.partial_due_date ' . 'desc'); ->orderByRaw('ISNULL(partial_due_date), partial_due_date ' . 'desc');
}); });
} }
@ -320,7 +321,7 @@ class InvoiceFilters extends QueryFilters
{ {
$sort_col = explode('|', $sort); $sort_col = explode('|', $sort);
if (!is_array($sort_col) || count($sort_col) != 2) { if (!is_array($sort_col) || count($sort_col) != 2 || in_array($sort_col[0], ['documents'])) {
return $this->builder; return $this->builder;
} }

View File

@ -45,7 +45,8 @@ class QuoteFilters extends QueryFilters
$q->where('first_name', 'like', '%'.$filter.'%') $q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%') ->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%'); ->orWhere('email', 'like', '%'.$filter.'%');
}); })
->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
}); });
} }

View File

@ -48,7 +48,8 @@ class RecurringInvoiceFilters extends QueryFilters
$q->where('first_name', 'like', '%'.$filter.'%') $q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%') ->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%'); ->orWhere('email', 'like', '%'.$filter.'%');
}); })
->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
}); });
} }

View File

@ -171,7 +171,7 @@ class TransactionTransformer implements BankRevenueInterface
private function formatDate(string $input) private function formatDate(string $input)
{ {
$timezone = Timezone::find($this->company->settings->timezone_id); $timezone = Timezone::find($this->company->settings->timezone_id);
$timezone_name = 'US/Eastern'; $timezone_name = 'America/New_York';
if ($timezone) { if ($timezone) {
$timezone_name = $timezone->name; $timezone_name = $timezone->name;

View File

@ -27,7 +27,7 @@ function nlog($output, $context = []): void
} }
if (gettype($output) == 'object') { if (gettype($output) == 'object') {
$output = print_r($output, 1); $output = print_r($output, true);
} }
// $trace = debug_backtrace(); // $trace = debug_backtrace();
@ -53,7 +53,7 @@ function nrlog($output, $context = []): void
} }
if (gettype($output) == 'object') { if (gettype($output) == 'object') {
$output = print_r($output, 1); $output = print_r($output, true);
} }
// $trace = debug_backtrace(); // $trace = debug_backtrace();

View File

@ -411,7 +411,7 @@ class InvoiceItemSumInclusive
$this->rule = new $class(); $this->rule = new $class();
if($this->rule->regionWithNoTaxCoverage($this->client->country->iso_3166_2)) { if($this->rule->regionWithNoTaxCoverage($this->client->country->iso_3166_2 ?? false)) {
return $this; return $this;
} }

View File

@ -107,16 +107,16 @@ class InvoiceSumInclusive
private function calculateCustomValues() private function calculateCustomValues()
{ {
$this->total_taxes += $this->multiInclusiveTax($this->invoice->custom_surcharge1, $this->invoice->custom_surcharge_tax1); // $this->total_taxes += $this->multiInclusiveTax($this->invoice->custom_surcharge1, $this->invoice->custom_surcharge_tax1);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge1); $this->total_custom_values += $this->valuer($this->invoice->custom_surcharge1);
$this->total_taxes += $this->multiInclusiveTax($this->invoice->custom_surcharge2, $this->invoice->custom_surcharge_tax2); // $this->total_taxes += $this->multiInclusiveTax($this->invoice->custom_surcharge2, $this->invoice->custom_surcharge_tax2);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge2); $this->total_custom_values += $this->valuer($this->invoice->custom_surcharge2);
$this->total_taxes += $this->multiInclusiveTax($this->invoice->custom_surcharge3, $this->invoice->custom_surcharge_tax3); // $this->total_taxes += $this->multiInclusiveTax($this->invoice->custom_surcharge3, $this->invoice->custom_surcharge_tax3);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge3); $this->total_custom_values += $this->valuer($this->invoice->custom_surcharge3);
$this->total_taxes += $this->multiInclusiveTax($this->invoice->custom_surcharge4, $this->invoice->custom_surcharge_tax4); // $this->total_taxes += $this->multiInclusiveTax($this->invoice->custom_surcharge4, $this->invoice->custom_surcharge_tax4);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge4); $this->total_custom_values += $this->valuer($this->invoice->custom_surcharge4);
$this->total += $this->total_custom_values; $this->total += $this->total_custom_values;
@ -137,21 +137,21 @@ class InvoiceSumInclusive
} }
//Handles cases where the surcharge is not taxed //Handles cases where the surcharge is not taxed
// if(is_numeric($this->invoice->custom_surcharge1) && $this->invoice->custom_surcharge1 > 0 && !$this->invoice->custom_surcharge_tax1) { if(is_numeric($this->invoice->custom_surcharge1) && $this->invoice->custom_surcharge1 > 0 && $this->invoice->custom_surcharge_tax1) {
// $amount += $this->invoice->custom_surcharge1; $amount += $this->invoice->custom_surcharge1;
// } }
// if(is_numeric($this->invoice->custom_surcharge2) && $this->invoice->custom_surcharge2 > 0 && !$this->invoice->custom_surcharge_tax2) { if(is_numeric($this->invoice->custom_surcharge2) && $this->invoice->custom_surcharge2 > 0 && $this->invoice->custom_surcharge_tax2) {
// $amount += $this->invoice->custom_surcharge2; $amount += $this->invoice->custom_surcharge2;
// } }
// if(is_numeric($this->invoice->custom_surcharge3) && $this->invoice->custom_surcharge3 > 0 && !$this->invoice->custom_surcharge_tax3) { if(is_numeric($this->invoice->custom_surcharge3) && $this->invoice->custom_surcharge3 > 0 && $this->invoice->custom_surcharge_tax3) {
// $amount += $this->invoice->custom_surcharge3; $amount += $this->invoice->custom_surcharge3;
// } }
// if(is_numeric($this->invoice->custom_surcharge4) && $this->invoice->custom_surcharge4 > 0 && !$this->invoice->custom_surcharge_tax4) { if(is_numeric($this->invoice->custom_surcharge4) && $this->invoice->custom_surcharge4 > 0 && $this->invoice->custom_surcharge_tax4) {
// $amount += $this->invoice->custom_surcharge4; $amount += $this->invoice->custom_surcharge4;
// } }
if (is_string($this->invoice->tax_name1) && strlen($this->invoice->tax_name1) > 1) { if (is_string($this->invoice->tax_name1) && strlen($this->invoice->tax_name1) > 1) {
$tax = $this->calcInclusiveLineTax($this->invoice->tax_rate1, $amount); $tax = $this->calcInclusiveLineTax($this->invoice->tax_rate1, $amount);

View File

@ -31,7 +31,7 @@ class GmailTransport extends AbstractTransport
protected function doSend(SentMessage $message): void protected function doSend(SentMessage $message): void
{ {
nlog("In Do Send"); nlog("In Do Send");
$message = MessageConverter::toEmail($message->getOriginalMessage()); $message = MessageConverter::toEmail($message->getOriginalMessage()); //@phpstan-ignore-line
/** @phpstan-ignore-next-line **/ /** @phpstan-ignore-next-line **/
$token = $message->getHeaders()->get('gmailtoken')->getValue(); // @phpstan-ignore-line $token = $message->getHeaders()->get('gmailtoken')->getValue(); // @phpstan-ignore-line

View File

@ -25,7 +25,8 @@ class Office365MailTransport extends AbstractTransport
protected function doSend(SentMessage $message): void protected function doSend(SentMessage $message): void
{ {
$symfony_message = MessageConverter::toEmail($message->getOriginalMessage()); $symfony_message = MessageConverter::toEmail($message->getOriginalMessage()); //@phpstan-ignore-line
$graph = new Graph(); $graph = new Graph();

View File

@ -115,13 +115,8 @@ class AccountController extends BaseController
public function update(UpdateAccountRequest $request, Account $account) public function update(UpdateAccountRequest $request, Account $account)
{ {
$fi = new \FilesystemIterator(public_path('react'), \FilesystemIterator::SKIP_DOTS);
if (iterator_count($fi) < 30) { $account->set_react_as_default_ap = $request->input('set_react_as_default_ap');
return response()->json(['message' => 'React App Not Installed, Please install the React app before attempting to switch.'], 400);
}
$account->fill($request->all());
$account->save(); $account->save();
$this->entity_type = Account::class; $this->entity_type = Account::class;

View File

@ -376,6 +376,7 @@ class LoginController extends BaseController
/** @var \App\Models\User $user */ /** @var \App\Models\User $user */
$user = auth()->user(); $user = auth()->user();
/** @var Builder $cu */
$cu = CompanyUser::query()->where('user_id', $user->id); $cu = CompanyUser::query()->where('user_id', $user->id);
if ($cu->count() == 0) { if ($cu->count() == 0) {
@ -398,18 +399,15 @@ class LoginController extends BaseController
$truth->setCompany($set_company); $truth->setCompany($set_company);
//21-03-2024 //21-03-2024
$cu->each(function ($cu) { $cu->each(function ($cu) {
if(CompanyToken::where('company_id', $cu->company_id)->where('user_id', $cu->user_id)->where('is_system', true)->doesntExist()) { /** @var \App\Models\CompanyUser $cu */
if(CompanyToken::query()->where('company_id', $cu->company_id)->where('user_id', $cu->user_id)->where('is_system', true)->doesntExist()) {
(new CreateCompanyToken($cu->company, $cu->user, request()->server('HTTP_USER_AGENT')))->handle(); (new CreateCompanyToken($cu->company, $cu->user, request()->server('HTTP_USER_AGENT')))->handle();
} }
}); });
// $user->account->companies->each(function ($company) use ($user) {
// if ($company->tokens()->where('user_id',$user->id)->where('is_system', true)->count() == 0) {
// (new CreateCompanyToken($company, $user, request()->server('HTTP_USER_AGENT')))->handle();
// }
// });
$truth->setCompanyToken(CompanyToken::where('user_id', $user->id)->where('company_id', $set_company->id)->where('is_system', true)->first()); $truth->setCompanyToken(CompanyToken::where('user_id', $user->id)->where('company_id', $set_company->id)->where('is_system', true)->first());
return CompanyUser::query()->where('user_id', $user->id); return CompanyUser::query()->where('user_id', $user->id);

View File

@ -97,7 +97,7 @@ class YodleeController extends BaseController
} }
$company->account->bank_integrations->where("integration_type", BankIntegration::INTEGRATION_TYPE_YODLEE)->where('auto_sync', true)->each(function ($bank_integration) use ($company) { // TODO: filter to yodlee only $company->account->bank_integrations->where("integration_type", BankIntegration::INTEGRATION_TYPE_YODLEE)->where('auto_sync', true)->each(function ($bank_integration) use ($company) { // TODO: filter to yodlee only
ProcessBankTransactionsYodlee::dispatch($company->account->id, $bank_integration); ProcessBankTransactionsYodlee::dispatch($company->account->bank_integration_account_id, $bank_integration);
}); });
} }

View File

@ -197,6 +197,7 @@ class BankIntegrationController extends BaseController
/** @var \App\Models\User $user */ /** @var \App\Models\User $user */
$user = auth()->user(); $user = auth()->user();
/** @var \App\Models\Account $user_account */
$user_account = $user->account; $user_account = $user->account;
$this->refreshAccountsYodlee($user); $this->refreshAccountsYodlee($user);
@ -210,12 +211,14 @@ class BankIntegrationController extends BaseController
// Processing transactions for each bank account // Processing transactions for each bank account
if (Ninja::isHosted() && $user->account->bank_integration_account_id) { if (Ninja::isHosted() && $user->account->bank_integration_account_id) {
$user_account->bank_integrations->where("integration_type", BankIntegration::INTEGRATION_TYPE_YODLEE)->each(function ($bank_integration) use ($user_account) { $user_account->bank_integrations->where("integration_type", BankIntegration::INTEGRATION_TYPE_YODLEE)->each(function ($bank_integration) use ($user_account) {
ProcessBankTransactionsYodlee::dispatch($user_account->id, $bank_integration); /** @var \App\Models\BankIntegration $bank_integration */
ProcessBankTransactionsYodlee::dispatch($user_account->bank_integration_account_id, $bank_integration);
}); });
} }
if (config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key') && (Ninja::isSelfHost() || (Ninja::isHosted() && $user_account->isEnterprisePaidClient()))) { if (config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key') && (Ninja::isSelfHost() || (Ninja::isHosted() && $user_account->isEnterprisePaidClient()))) {
$user_account->bank_integrations->where("integration_type", BankIntegration::INTEGRATION_TYPE_NORDIGEN)->each(function ($bank_integration) { $user_account->bank_integrations->where("integration_type", BankIntegration::INTEGRATION_TYPE_NORDIGEN)->each(function ($bank_integration) {
/** @var \App\Models\BankIntegration $bank_integration */
ProcessBankTransactionsNordigen::dispatch($bank_integration); ProcessBankTransactionsNordigen::dispatch($bank_integration);
}); });
} }
@ -345,7 +348,7 @@ class BankIntegrationController extends BaseController
if (Ninja::isHosted() && $account->isPaid() && $account->plan == 'enterprise') { if (Ninja::isHosted() && $account->isPaid() && $account->plan == 'enterprise') {
$account->bank_integrations()->where('integration_type', BankIntegration::INTEGRATION_TYPE_YODLEE)->where('auto_sync', true)->cursor()->each(function ($bank_integration) use ($account) { $account->bank_integrations()->where('integration_type', BankIntegration::INTEGRATION_TYPE_YODLEE)->where('auto_sync', true)->cursor()->each(function ($bank_integration) use ($account) {
(new ProcessBankTransactionsYodlee($account->id, $bank_integration))->handle(); (new ProcessBankTransactionsYodlee($account->bank_integration_account_id, $bank_integration))->handle();
}); });
} }

View File

@ -35,6 +35,7 @@ use League\Fractal\Resource\Item;
use App\Models\BankTransactionRule; use App\Models\BankTransactionRule;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use App\Transformers\ArraySerializer; use App\Transformers\ArraySerializer;
use Illuminate\Support\Facades\Schema as DbSchema;
use App\Transformers\EntityTransformer; use App\Transformers\EntityTransformer;
use League\Fractal\Resource\Collection; use League\Fractal\Resource\Collection;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@ -653,7 +654,7 @@ class BaseController extends Controller
/** /**
* Passes back the miniloaded data response * Passes back the miniloaded data response
* *
* @param Builder $query * @param mixed $query
* *
*/ */
protected function timeConstrainedResponse($query) protected function timeConstrainedResponse($query)
@ -894,11 +895,7 @@ class BaseController extends Controller
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator)); $resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
} }
// else { return $this->response($this->manager->createData($resource)->toArray()); //@phpstan-ignore-line
// $resource = new Collection($query, $transformer, $this->entity_type);
// }
return $this->response($this->manager->createData($resource)->toArray());
} }
/** /**
@ -1106,7 +1103,7 @@ class BaseController extends Controller
public function flutterRoute() public function flutterRoute()
{ {
if ((bool) $this->checkAppSetup() !== false && $account = Account::first()) { if ((bool) $this->checkAppSetup() !== false && DbSchema::hasTable('accounts') && $account = Account::first()) {
/** @var \App\Models\Account $account */ /** @var \App\Models\Account $account */

View File

@ -66,7 +66,7 @@ class ChartController extends BaseController
return response()->json($cs->chart_summary($request->input('start_date'), $request->input('end_date')), 200); return response()->json($cs->chart_summary($request->input('start_date'), $request->input('end_date')), 200);
} }
public function calculatedField(ShowCalculatedFieldRequest $request) public function calculatedFields(ShowCalculatedFieldRequest $request)
{ {
/** @var \App\Models\User auth()->user() */ /** @var \App\Models\User auth()->user() */

View File

@ -300,7 +300,9 @@ class InvitationController extends Controller
'signature' => false, 'signature' => false,
'contact_first_name' => $invitation->contact->first_name ?? '', 'contact_first_name' => $invitation->contact->first_name ?? '',
'contact_last_name' => $invitation->contact->last_name ?? '', 'contact_last_name' => $invitation->contact->last_name ?? '',
'contact_email' => $invitation->contact->email ?? '' 'contact_email' => $invitation->contact->email ?? '',
'client_city' => $invitation->client->city ?? '',
'client_postal_code' => $invitation->client->postal_code ?? '',
]; ];
$request->replace($data); $request->replace($data);

View File

@ -108,11 +108,11 @@ class PaymentController extends Controller
*/ */
public function process(Request $request) public function process(Request $request)
{ {
$request->validate([ // $request->validate([
'contact_first_name' => ['required'], // 'contact_first_name' => ['required'],
'contact_last_name' => ['required'], // 'contact_last_name' => ['required'],
'contact_email' => ['required', 'email'], // 'contact_email' => ['required', 'email'],
]); // ]);
return (new InstantPayment($request))->run(); return (new InstantPayment($request))->run();
} }
@ -209,7 +209,7 @@ class PaymentController extends Controller
if (property_exists($payment_hash->data, 'billing_context')) { if (property_exists($payment_hash->data, 'billing_context')) {
$billing_subscription = \App\Models\Subscription::find($this->decodePrimaryKey($payment_hash->data->billing_context->subscription_id)); $billing_subscription = \App\Models\Subscription::find($this->decodePrimaryKey($payment_hash->data->billing_context->subscription_id));
/** @var \App\Models\Subscription $billing_subscription */
return (new SubscriptionService($billing_subscription))->completePurchase($payment_hash); return (new SubscriptionService($billing_subscription))->completePurchase($payment_hash);
} }

View File

@ -183,7 +183,7 @@ class DocumentController extends BaseController
} }
if ($action == 'download') { if ($action == 'download') {
ZipDocuments::dispatch($documents->pluck('id'), $user->company(), auth()->user()); ZipDocuments::dispatch($documents->pluck('id'), $user->company(), auth()->user()); //@phpstan-ignore-line
return response()->json(['message' => ctrans('texts.sent_message')], 200); return response()->json(['message' => ctrans('texts.sent_message')], 200);
} }

View File

@ -85,7 +85,7 @@ class ImportController extends Controller
$contents = $this->convertEncoding($contents); $contents = $this->convertEncoding($contents);
// Store the csv in cache with an expiry of 10 minutes // Store the csv in cache with an expiry of 10 minutes
Cache::put($hash.'-'.$entityType, base64_encode($contents), 600); Cache::put($hash.'-'.$entityType, base64_encode($contents), 1200);
// Parse CSV // Parse CSV
$csv_array = $this->getCsvData($contents); $csv_array = $this->getCsvData($contents);

View File

@ -267,7 +267,7 @@ class MigrationController extends BaseController
if ($request->companies) { if ($request->companies) {
//handle Laravel 5.5 UniHTTP //handle Laravel 5.5 UniHTTP
$companies = json_decode($request->companies, 1); $companies = json_decode($request->companies, true);
} else { } else {
//handle Laravel 6 Guzzle //handle Laravel 6 Guzzle
$companies = []; $companies = [];
@ -275,7 +275,7 @@ class MigrationController extends BaseController
foreach ($request->all() as $input) { foreach ($request->all() as $input) {
if ($input instanceof UploadedFile) { if ($input instanceof UploadedFile) {
} else { } else {
$companies[] = json_decode($input, 1); $companies[] = json_decode($input, true);
} }
} }
} }

View File

@ -24,8 +24,9 @@ class PaymentNotificationWebhookController extends Controller
public function __invoke(PaymentNotificationWebhookRequest $request, string $company_key, string $company_gateway_id, string $client_hash) public function __invoke(PaymentNotificationWebhookRequest $request, string $company_key, string $company_gateway_id, string $client_hash)
{ {
/** @var \App\Models\CompanyGateway $company_gateway */ /** @var \App\Models\CompanyGateway $company_gateway */
$company_gateway = CompanyGateway::find($this->decodePrimaryKey($company_gateway_id)); $company_gateway = CompanyGateway::find($this->decodePrimaryKey($company_gateway_id));
/** @var \App\Models\Client $client */
$client = Client::find($this->decodePrimaryKey($client_hash)); $client = Client::find($this->decodePrimaryKey($client_hash));
return $company_gateway return $company_gateway

View File

@ -81,4 +81,34 @@ class PingController extends BaseController
return response()->json(SystemHealth::check(), 200); return response()->json(SystemHealth::check(), 200);
} }
/**
* Get the last error from storage/logs/laravel.log
*
* @return Response| \Illuminate\Http\JsonResponse
*
* @OA\Get(
* path="/api/v1/last_error",
* operationId="getLastError",
* tags={"last_error"},
* summary="Get the last error from storage/logs/laravel.log",
* description="Get the last error from storage/logs/laravel.log",
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Response(
* response=200,
* description="The last error from the logs",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* )
* )
*/
public function lastError()
{
if (Ninja::isNinja() || ! auth()->user()->isAdmin()) {
return response()->json(['message' => ctrans('texts.route_not_available'), 'errors' => []], 403);
}
return response()->json(['last_error' => SystemHealth::lastError()], 200);
}
} }

View File

@ -125,7 +125,7 @@ class PreviewController extends BaseController
$response = Response::make($pdf, 200); $response = Response::make($pdf, 200);
$response->header('Content-Type', 'application/pdf'); $response->header('Content-Type', 'application/pdf');
$response->header('Server-Timing', microtime(true) - $start); $response->header('Server-Timing', (string) (microtime(true) - $start));
return $response; return $response;
} }
@ -288,7 +288,7 @@ class PreviewController extends BaseController
/** @var \App\Models\Company $company */ /** @var \App\Models\Company $company */
$company = $user->company(); $company = $user->company();
$design_object = json_decode(json_encode(request()->input('design')), 1); $design_object = json_decode(json_encode(request()->input('design')), true);
$ts = (new TemplateService()); $ts = (new TemplateService());

View File

@ -81,14 +81,20 @@ class SearchController extends Controller
$invoices = Invoice::query() $invoices = Invoice::query()
->company() ->company()
->with('client') ->with('client')
->where('is_deleted', 0) ->where('invoices.is_deleted', 0)
->whereHas('client', function ($q) { // ->whereHas('client', function ($q) {
$q->where('is_deleted', 0); // $q->where('is_deleted', 0);
}) // })
->leftJoin('clients', function ($join) {
$join->on('invoices.client_id', '=', 'clients.id')
->where('clients.is_deleted', 0);
})
->when(!$user->hasPermission('view_all') || !$user->hasPermission('view_invoice'), function ($query) use ($user) { ->when(!$user->hasPermission('view_all') || !$user->hasPermission('view_invoice'), function ($query) use ($user) {
$query->where('user_id', $user->id); $query->where('invoices.user_id', $user->id);
}) })
->orderBy('id', 'desc') ->orderBy('invoices.id', 'desc')
->take(3000) ->take(3000)
->get(); ->get();

View File

@ -181,6 +181,9 @@ class SelfUpdateController extends BaseController
public function checkVersion() public function checkVersion()
{ {
if(Ninja::isHosted())
return '5.10.SaaS';
return trim(file_get_contents(config('ninja.version_url'))); return trim(file_get_contents(config('ninja.version_url')));
} }

View File

@ -43,7 +43,7 @@ class SetupController extends Controller
public function index() public function index()
{ {
$check = SystemHealth::check(false); $check = SystemHealth::check(false, false);
if ($check['system_health'] == true && $check['simple_db_check'] && Schema::hasTable('accounts') && $account = Account::first()) { if ($check['system_health'] == true && $check['simple_db_check'] && Schema::hasTable('accounts') && $account = Account::first()) {
return redirect('/'); return redirect('/');
@ -59,7 +59,7 @@ class SetupController extends Controller
public function doSetup(StoreSetupRequest $request) public function doSetup(StoreSetupRequest $request)
{ {
try { try {
$check = SystemHealth::check(false); $check = SystemHealth::check(false, false);
} catch (Exception $e) { } catch (Exception $e) {
nlog(['message' => $e->getMessage(), 'action' => 'SetupController::doSetup()']); nlog(['message' => $e->getMessage(), 'action' => 'SetupController::doSetup()']);
@ -145,6 +145,7 @@ class SetupController extends Controller
Artisan::call('config:clear'); Artisan::call('config:clear');
Artisan::call('key:generate', ['--force' => true]);
Artisan::call('migrate', ['--force' => true]); Artisan::call('migrate', ['--force' => true]);
Artisan::call('db:seed', ['--force' => true]); Artisan::call('db:seed', ['--force' => true]);

View File

@ -11,13 +11,14 @@
namespace App\Http\Requests\Client; namespace App\Http\Requests\Client;
use App\DataMapper\CompanySettings;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Http\ValidationRules\ValidClientGroupSettingsRule;
use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Cache;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use App\DataMapper\CompanySettings;
use Illuminate\Support\Facades\Cache;
use App\Utils\Traits\ChecksEntityStatus;
use App\Http\ValidationRules\EInvoice\ValidClientScheme;
use App\Http\ValidationRules\ValidClientGroupSettingsRule;
class UpdateClientRequest extends Request class UpdateClientRequest extends Request
{ {
@ -66,6 +67,8 @@ class UpdateClientRequest extends Request
$rules['id_number'] = ['sometimes', 'bail', 'nullable', Rule::unique('clients')->where('company_id', $user->company()->id)->ignore($this->client->id)]; $rules['id_number'] = ['sometimes', 'bail', 'nullable', Rule::unique('clients')->where('company_id', $user->company()->id)->ignore($this->client->id)];
$rules['number'] = ['sometimes', 'bail', Rule::unique('clients')->where('company_id', $user->company()->id)->ignore($this->client->id)]; $rules['number'] = ['sometimes', 'bail', Rule::unique('clients')->where('company_id', $user->company()->id)->ignore($this->client->id)];
$rules['e_invoice'] = ['sometimes','nullable', new ValidClientScheme()];
$rules['settings'] = new ValidClientGroupSettingsRule(); $rules['settings'] = new ValidClientGroupSettingsRule();
$rules['contacts'] = 'array'; $rules['contacts'] = 'array';
$rules['contacts.*.email'] = 'bail|nullable|distinct|sometimes|email'; $rules['contacts.*.email'] = 'bail|nullable|distinct|sometimes|email';

View File

@ -29,7 +29,7 @@ class CreatePaymentMethodRequest extends FormRequest
$available_methods = []; $available_methods = [];
collect($client->service()->getPaymentMethods(-1)) collect($client->service()->getPaymentMethods(-1))
->filter(function ($method) use (&$available_methods) { ->filter(function ($method) use (&$available_methods) { //@phpstan-ignore-line
$available_methods[] = $method['gateway_type_id']; $available_methods[] = $method['gateway_type_id'];
}); });

View File

@ -14,8 +14,9 @@ namespace App\Http\Requests\Company;
use App\DataMapper\CompanySettings; use App\DataMapper\CompanySettings;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Http\ValidationRules\Company\ValidExpenseMailbox; use App\Http\ValidationRules\Company\ValidExpenseMailbox;
use App\Http\ValidationRules\Company\ValidSubdomain;
use App\Http\ValidationRules\ValidSettingsRule; use App\Http\ValidationRules\ValidSettingsRule;
use App\Http\ValidationRules\EInvoice\ValidCompanyScheme;
use App\Http\ValidationRules\Company\ValidSubdomain;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
@ -65,7 +66,7 @@ class UpdateCompanyRequest extends Request
$rules['smtp_local_domain'] = 'sometimes|string|nullable'; $rules['smtp_local_domain'] = 'sometimes|string|nullable';
// $rules['smtp_verify_peer'] = 'sometimes|string'; // $rules['smtp_verify_peer'] = 'sometimes|string';
// $rules['e_invoice'] = ['sometimes','nullable', new ValidScheme()]; $rules['e_invoice'] = ['sometimes', 'nullable', new ValidCompanyScheme()];
if (isset($input['portal_mode']) && ($input['portal_mode'] == 'domain' || $input['portal_mode'] == 'iframe')) { if (isset($input['portal_mode']) && ($input['portal_mode'] == 'domain' || $input['portal_mode'] == 'iframe')) {
$rules['portal_domain'] = 'bail|nullable|sometimes|url'; $rules['portal_domain'] = 'bail|nullable|sometimes|url';

View File

@ -48,7 +48,7 @@ class StoreCompanyGatewayRequest extends Request
{ {
$input = $this->all(); $input = $this->all();
if ($gateway = Gateway::where('key', $input['gateway_key'])->first()) { if ($gateway = Gateway::query()->where('key', $input['gateway_key'])->first()) {
$default_gateway_fields = json_decode($gateway->fields); $default_gateway_fields = json_decode($gateway->fields);
/*Force gateway properties */ /*Force gateway properties */

View File

@ -48,7 +48,7 @@ class UpdateCompanyGatewayRequest extends Request
/*Force gateway properties */ /*Force gateway properties */
if (isset($input['config']) && is_object(json_decode($input['config'])) && array_key_exists('gateway_key', $input)) { if (isset($input['config']) && is_object(json_decode($input['config'])) && array_key_exists('gateway_key', $input)) {
$gateway = Gateway::where('key', $input['gateway_key'])->first(); $gateway = Gateway::query()->where('key', $input['gateway_key'])->first();
$default_gateway_fields = json_decode($gateway->fields); $default_gateway_fields = json_decode($gateway->fields);
foreach (json_decode($input['config']) as $key => $value) { foreach (json_decode($input['config']) as $key => $value) {

View File

@ -63,7 +63,7 @@ class SendEmailRequest extends Request
$user = auth()->user(); $user = auth()->user();
return [ return [
'template' => 'bail|required|in:'.implode(',', $this->templates), 'template' => 'bail|required|string|in:'.implode(',', $this->templates),
'entity' => 'bail|required|in:App\Models\Invoice,App\Models\Quote,App\Models\Credit,App\Models\RecurringInvoice,App\Models\PurchaseOrder,App\Models\Payment', 'entity' => 'bail|required|in:App\Models\Invoice,App\Models\Quote,App\Models\Credit,App\Models\RecurringInvoice,App\Models\PurchaseOrder,App\Models\Payment',
'entity_id' => ['bail', 'required', Rule::exists($this->entity_plural, 'id')->where('company_id', $user->company()->id)], 'entity_id' => ['bail', 'required', Rule::exists($this->entity_plural, 'id')->where('company_id', $user->company()->id)],
'cc_email.*' => 'bail|sometimes|email', 'cc_email.*' => 'bail|sometimes|email',
@ -94,7 +94,7 @@ class SendEmailRequest extends Request
$this->entity_plural = Str::plural($input['entity']) ?? ''; $this->entity_plural = Str::plural($input['entity']) ?? '';
if (isset($input['entity'])) { if (isset($input['entity']) && in_array($input['entity'], ['invoice','quote','credit','recurring_invoice','purchase_order','payment'])) {
$input['entity'] = "App\Models\\".ucfirst(Str::camel($input['entity'])); $input['entity'] = "App\Models\\".ucfirst(Str::camel($input['entity']));
} }

View File

@ -56,7 +56,7 @@ class UpdateExpenseRequest extends Request
$rules['invoice_id'] = 'bail|sometimes|nullable|exists:invoices,id,company_id,'.$user->company()->id; $rules['invoice_id'] = 'bail|sometimes|nullable|exists:invoices,id,company_id,'.$user->company()->id;
$rules['documents'] = 'bail|sometimes|array'; $rules['documents'] = 'bail|sometimes|array';
$rules['amount'] = ['sometimes', 'bail', 'nullable', 'numeric', 'max:99999999999999']; $rules['amount'] = ['sometimes', 'bail', 'nullable', 'numeric', 'max:99999999999999'];
return $this->globalRules($rules); return $this->globalRules($rules);
} }

View File

@ -78,7 +78,9 @@ class StoreInvoiceRequest extends Request
$rules['tax_name3'] = 'bail|sometimes|string|nullable'; $rules['tax_name3'] = 'bail|sometimes|string|nullable';
$rules['exchange_rate'] = 'bail|sometimes|numeric'; $rules['exchange_rate'] = 'bail|sometimes|numeric';
$rules['partial'] = 'bail|sometimes|nullable|numeric|gte:0'; $rules['partial'] = 'bail|sometimes|nullable|numeric|gte:0';
$rules['partial_due_date'] = ['bail', 'sometimes', 'exclude_if:partial,0', Rule::requiredIf(fn () => $this->partial > 0), 'date']; $rules['partial_due_date'] = ['bail', 'sometimes', 'nullable', 'exclude_if:partial,0', 'date', 'before:due_date', 'after_or_equal:date'];
$rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date ?? '') > 1), 'date'];
$rules['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999']; $rules['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999'];
// $rules['amount'] = ['sometimes', 'bail', 'max:99999999999999']; // $rules['amount'] = ['sometimes', 'bail', 'max:99999999999999'];

View File

@ -82,8 +82,8 @@ class UpdateInvoiceRequest extends Request
$rules['date'] = 'bail|sometimes|date:Y-m-d'; $rules['date'] = 'bail|sometimes|date:Y-m-d';
// $rules['partial_due_date'] = ['bail', 'sometimes', 'exclude_if:partial,0', Rule::requiredIf(fn () => $this->partial > 0), 'date', 'before:due_date']; $rules['partial_due_date'] = ['bail', 'sometimes', 'nullable', 'exclude_if:partial,0', 'date', 'before:due_date', 'after_or_equal:date'];
// $rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date) > 1), 'date']; $rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', 'after_or_equal:date', Rule::requiredIf(fn () => strlen($this->partial_due_date) > 1), 'date'];
return $rules; return $rules;
} }

View File

@ -46,7 +46,7 @@ class StorePaymentRequest extends Request
$rules = [ $rules = [
'client_id' => ['bail','required',Rule::exists('clients', 'id')->where('company_id', $user->company()->id)->where('is_deleted', 0)], 'client_id' => ['bail','required',Rule::exists('clients', 'id')->where('company_id', $user->company()->id)->where('is_deleted', 0)],
'invoices' => ['bail','sometimes', 'nullable', 'array', new ValidPayableInvoicesRule()], 'invoices' => ['bail', 'sometimes', 'nullable', 'array', new ValidPayableInvoicesRule()],
'invoices.*.amount' => ['bail','required'], 'invoices.*.amount' => ['bail','required'],
'invoices.*.invoice_id' => ['bail','required','distinct', new ValidInvoicesRules($this->all()),Rule::exists('invoices', 'id')->where('company_id', $user->company()->id)->where('client_id', $this->client_id)], 'invoices.*.invoice_id' => ['bail','required','distinct', new ValidInvoicesRules($this->all()),Rule::exists('invoices', 'id')->where('company_id', $user->company()->id)->where('client_id', $this->client_id)],
'credits.*.credit_id' => ['bail','required','distinct', new ValidCreditsRules($this->all()),Rule::exists('credits', 'id')->where('company_id', $user->company()->id)->where('client_id', $this->client_id)], 'credits.*.credit_id' => ['bail','required','distinct', new ValidCreditsRules($this->all()),Rule::exists('credits', 'id')->where('company_id', $user->company()->id)->where('client_id', $this->client_id)],

View File

@ -80,8 +80,8 @@ class StoreProjectRequest extends Request
$input['budgeted_hours'] = 0; $input['budgeted_hours'] = 0;
} }
$input['task_rate'] = isset($input['task_rate']) ? $input['task_rate'] : 0; $input['task_rate'] = (isset($input['task_rate']) && floatval($input['task_rate']) >= 0) ? $input['task_rate'] : 0;
$this->replace($input); $this->replace($input);
} }

View File

@ -45,7 +45,8 @@ class UpdateProjectRequest extends Request
$rules['number'] = Rule::unique('projects')->where('company_id', $user->company()->id)->ignore($this->project->id); $rules['number'] = Rule::unique('projects')->where('company_id', $user->company()->id)->ignore($this->project->id);
} }
$rules['budgeted_hours'] = 'sometimes|numeric'; $rules['budgeted_hours'] = 'sometimes|bail|numeric';
$rules['task_rate'] = 'sometimes|bail|numeric';
if ($this->file('documents') && is_array($this->file('documents'))) { if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->fileValidation(); $rules['documents.*'] = $this->fileValidation();

View File

@ -66,8 +66,8 @@ class StoreQuoteRequest extends Request
$rules['exchange_rate'] = 'bail|sometimes|numeric'; $rules['exchange_rate'] = 'bail|sometimes|numeric';
$rules['line_items'] = 'array'; $rules['line_items'] = 'array';
$rules['date'] = 'bail|sometimes|date:Y-m-d'; $rules['date'] = 'bail|sometimes|date:Y-m-d';
$rules['partial_due_date'] = ['bail', 'sometimes', 'exclude_if:partial,0', Rule::requiredIf(fn () => $this->partial > 0), 'date', 'before:due_date', 'after_or_equal:date']; $rules['partial_due_date'] = ['bail', 'sometimes', 'nullable', 'exclude_if:partial,0', 'date', 'before:due_date', 'after_or_equal:date'];
$rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date) > 1), 'date']; $rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date ?? '') > 1), 'date'];
$rules['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999']; $rules['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999'];
return $rules; return $rules;

View File

@ -65,7 +65,7 @@ class UpdateQuoteRequest extends Request
$rules['date'] = 'bail|sometimes|date:Y-m-d'; $rules['date'] = 'bail|sometimes|date:Y-m-d';
$rules['partial_due_date'] = ['bail', 'sometimes', 'exclude_if:partial,0', Rule::requiredIf(fn () => $this->partial > 0), 'date', 'before:due_date']; $rules['partial_due_date'] = ['bail', 'sometimes', 'nullable', 'exclude_if:partial,0', 'date', 'before:due_date', 'after_or_equal:date'];
$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['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['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999'];

View File

@ -34,7 +34,10 @@ class BulkRecurringExpenseRequest extends Request
return false; return false;
} }
return auth()->user()->can(auth()->user()->isAdmin(), RecurringExpense::class); /** @var \App\Models\User $user */
$user = auth()->user();
return $user->can('edit', RecurringExpense::class);
} }
/** /**

View File

@ -69,7 +69,7 @@ class StoreTaskRequest extends Request
foreach ($values as $k) { foreach ($values as $k) {
if (!is_int($k[0]) || !is_int($k[1])) { if (!is_int($k[0]) || !is_int($k[1])) {
return $fail('The '.$attribute.' - '.print_r($k, 1).' is invalid. Unix timestamps only.'); return $fail('The '.$attribute.' - '.print_r($k, true).' is invalid. Unix timestamps only.');
} }
} }

View File

@ -75,7 +75,7 @@ class UpdateTaskRequest extends Request
foreach ($values as $k) { foreach ($values as $k) {
if (!is_int($k[0]) || !is_int($k[1])) { if (!is_int($k[0]) || !is_int($k[1])) {
return $fail('The '.$attribute.' - '.print_r($k, 1).' is invalid. Unix timestamps only.'); return $fail('The '.$attribute.' - '.print_r($k, true).' is invalid. Unix timestamps only.');
} }
} }

View File

@ -34,7 +34,11 @@ class BulkVendorRequest extends Request
return false; return false;
} }
return auth()->user()->can(auth()->user()->isAdmin(), Vendor::class); /** @var \App\Models\User $user */
$user = auth()->user();
return $user->can('edit', Vendor::class);
} }
/** /**

View File

@ -19,8 +19,9 @@ use Illuminate\Contracts\Validation\ValidationRule;
*/ */
class BlackListRule implements ValidationRule class BlackListRule implements ValidationRule
{ {
/** Bad domains +/- dispoable email domains */ /** Bad domains +/- disposable email domains */
private array $blacklist = [ private array $blacklist = [
'padvn.com',
'anonaddy.me', 'anonaddy.me',
'nqmo.com', 'nqmo.com',
'wireconnected.com', 'wireconnected.com',

View File

@ -11,6 +11,7 @@
namespace App\Http\ValidationRules\EInvoice; namespace App\Http\ValidationRules\EInvoice;
use App\Services\EDocument\Standards\Validation\Peppol\ClientLevel;
use Closure; use Closure;
use InvoiceNinja\EInvoice\EInvoice; use InvoiceNinja\EInvoice\EInvoice;
use Illuminate\Validation\Validator; use Illuminate\Validation\Validator;
@ -19,11 +20,10 @@ use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Contracts\Validation\ValidatorAwareRule; use Illuminate\Contracts\Validation\ValidatorAwareRule;
/** /**
* Class BlackListRule. * Class ValidClientScheme.
*/ */
class ValidScheme implements ValidationRule, ValidatorAwareRule class ValidClientScheme implements ValidationRule, ValidatorAwareRule
{ {
/** /**
* The validator instance. * The validator instance.
* *
@ -34,27 +34,28 @@ class ValidScheme implements ValidationRule, ValidatorAwareRule
public function validate(string $attribute, mixed $value, Closure $fail): void public function validate(string $attribute, mixed $value, Closure $fail): void
{ {
$r = new EInvoice(); if(isset($value['Invoice']))
$errors = $r->validateRequest($value['Invoice'], Invoice::class); {
$r = new EInvoice();
foreach ($errors as $key => $msg) { $errors = $r->validateRequest($value['Invoice'], ClientLevel::class);
$this->validator->errors()->add( foreach ($errors as $key => $msg) {
"e_invoice.{$key}",
"{$key} - {$msg}"
);
$this->validator->errors()->add(
"e_invoice.{$key}",
"{$key} - {$msg}"
);
}
} }
} }
/** /**
* Set the current validator. * Set the current validator.
*/ */
public function setValidator(Validator $validator): static public function setValidator(Validator $validator): static
{ {
$this->validator = $validator; $this->validator = $validator;
return $this; return $this;
} }

View File

@ -0,0 +1,66 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*1`
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\ValidationRules\EInvoice;
use App\Services\EDocument\Standards\Validation\Peppol\CompanyLevel;
use Closure;
use InvoiceNinja\EInvoice\EInvoice;
use Illuminate\Validation\Validator;
use InvoiceNinja\EInvoice\Models\Peppol\Invoice;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Contracts\Validation\ValidatorAwareRule;
/**
* Class ValidScheme.
*/
class ValidCompanyScheme implements ValidationRule, ValidatorAwareRule
{
/**
* The validator instance.
*
* @var Validator
*/
protected $validator;
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if(isset($value['Invoice']))
{
$r = new EInvoice();
$errors = $r->validateRequest($value['Invoice'], CompanyLevel::class);
foreach ($errors as $key => $msg) {
$this->validator->errors()->add(
"e_invoice.{$key}",
"{$key} - {$msg}"
);
}
}
}
/**
* Set the current validator.
*/
public function setValidator(Validator $validator): static
{
$this->validator = $validator;
return $this;
}
}

View File

@ -85,11 +85,12 @@ class ValidInvoicesRules implements Rule
//catch here nothing to do - we need this to prevent the last elseif triggering //catch here nothing to do - we need this to prevent the last elseif triggering
} elseif ($inv->status_id == Invoice::STATUS_DRAFT && floatval($invoice['amount']) > floatval($inv->amount)) { } elseif ($inv->status_id == Invoice::STATUS_DRAFT && floatval($invoice['amount']) > floatval($inv->amount)) {
$this->error_msg = 'Amount cannot be greater than invoice balance'; $this->error_msg = 'Amount cannot be greater than invoice balance';
return false; return false;
} elseif (floatval($invoice['amount']) > floatval($inv->balance)) { } elseif (floatval($invoice['amount']) > floatval($inv->balance)) {
$this->error_msg = ctrans('texts.amount_greater_than_balance_v5'); $this->error_msg = ctrans('texts.amount_greater_than_balance_v5');
return false;
} elseif($inv->is_deleted){
$this->error_msg = 'One or more invoices in this request have since been deleted';
return false; return false;
} }
} }

View File

@ -35,7 +35,7 @@ class ValidPayableInvoicesRule implements Rule
$invoices = []; $invoices = [];
if (is_array($value)) { if (is_array($value)) {
$invoices = Invoice::query()->whereIn('id', array_column($value, 'invoice_id'))->company()->get(); $invoices = Invoice::query()->withTrashed()->whereIn('id', array_column($value, 'invoice_id'))->company()->get();
} }
foreach ($invoices as $invoice) { foreach ($invoices as $invoice) {

View File

@ -31,7 +31,10 @@ class ProductMap
12 => 'product.custom_value2', 12 => 'product.custom_value2',
13 => 'product.custom_value3', 13 => 'product.custom_value3',
14 => 'product.custom_value4', 14 => 'product.custom_value4',
15 => 'product.image_url' 15 => 'product.image_url',
16 => 'product.in_stock_quantity',
17 => 'product.tax_category',
18 => 'product.max_quantity',
]; ];
} }
@ -54,6 +57,9 @@ class ProductMap
13 => 'texts.custom_value', 13 => 'texts.custom_value',
14 => 'texts.custom_value', 14 => 'texts.custom_value',
15 => 'texts.image_url', 15 => 'texts.image_url',
16 => 'texts.in_stock_quantity',
17 => 'texts.tax_category',
18 => 'texts.max_quantity',
]; ];
} }
} }

View File

@ -98,7 +98,7 @@ class BaseImport
} }
/** @var string $base64_encoded_csv */ /** @var string $base64_encoded_csv */
$base64_encoded_csv = Cache::pull($this->hash.'-'.$entity_type); $base64_encoded_csv = Cache::get($this->hash.'-'.$entity_type);
if (empty($base64_encoded_csv)) { if (empty($base64_encoded_csv)) {
return null; return null;
@ -473,6 +473,8 @@ class BaseImport
$tasks = $this->groupTasks($tasks, $task_number_key); $tasks = $this->groupTasks($tasks, $task_number_key);
nlog($tasks);
foreach ($tasks as $raw_task) { foreach ($tasks as $raw_task) {
$task_data = []; $task_data = [];
@ -702,16 +704,16 @@ class BaseImport
->save(); ->save();
} }
if ($invoice->status_id === Invoice::STATUS_DRAFT) { if ($invoice->status_id == Invoice::STATUS_DRAFT) {
} elseif ($invoice->status_id === Invoice::STATUS_SENT) { return $invoice;
$invoice = $invoice }
->service()
->markSent() $invoice = $invoice
->save(); ->service()
} elseif ( ->markSent()
$invoice->status_id <= Invoice::STATUS_SENT && ->save();
$invoice->amount > 0
) { if ($invoice->status_id <= Invoice::STATUS_SENT && $invoice->amount > 0) {
if ($invoice->balance <= 0) { if ($invoice->balance <= 0) {
$invoice->status_id = Invoice::STATUS_PAID; $invoice->status_id = Invoice::STATUS_PAID;
$invoice->save(); $invoice->save();

View File

@ -172,7 +172,7 @@ class Wave extends BaseImport implements ImportInterface
{ {
$entity_type = 'expense'; $entity_type = 'expense';
$data = $this->getCsvData($entity_type); $data = $this->getCsvData('invoice');
if (!$data) { if (!$data) {
$this->entity_count['expense'] = 0; $this->entity_count['expense'] = 0;
@ -244,14 +244,17 @@ class Wave extends BaseImport implements ImportInterface
if (empty($expense_data['vendor_id'])) { if (empty($expense_data['vendor_id'])) {
$vendor_data['user_id'] = $this->getUserIDForRecord($expense_data); $vendor_data['user_id'] = $this->getUserIDForRecord($expense_data);
$vendor_repository->save( if(isset($raw_expense['Vendor Name']) || isset($raw_expense['Vendor']))
['name' => $raw_expense['Vendor Name']], {
$vendor = VendorFactory::create( $vendor_repository->save(
$this->company->id, ['name' => isset($raw_expense['Vendor Name']) ? $raw_expense['Vendor Name'] : isset($raw_expense['Vendor'])],
$vendor_data['user_id'] $vendor = VendorFactory::create(
) $this->company->id,
); $vendor_data['user_id']
$expense_data['vendor_id'] = $vendor->id; )
);
$expense_data['vendor_id'] = $vendor->id;
}
} }
$validator = Validator::make( $validator = Validator::make(

View File

@ -42,7 +42,7 @@ class ExpenseTransformer extends BaseTransformer
'client_id' => isset($data['expense.client']) 'client_id' => isset($data['expense.client'])
? $this->getClientId($data['expense.client']) ? $this->getClientId($data['expense.client'])
: null, : null,
'date' => strlen($this->getString($data, 'expense.date') > 1) ? $this->parseDate($data['expense.date']) : now()->format('Y-m-d'), 'date' => strlen($this->getString($data, 'expense.date')) > 1 ? $this->parseDate($data['expense.date']) : now()->format('Y-m-d'),
'public_notes' => $this->getString($data, 'expense.public_notes'), 'public_notes' => $this->getString($data, 'expense.public_notes'),
'private_notes' => $this->getString($data, 'expense.private_notes'), 'private_notes' => $this->getString($data, 'expense.private_notes'),
'category_id' => isset($data['expense.category']) 'category_id' => isset($data['expense.category'])

View File

@ -43,6 +43,7 @@ class ProductTransformer extends BaseTransformer
'custom_value3' => $this->getString($data, 'product.custom_value3'), 'custom_value3' => $this->getString($data, 'product.custom_value3'),
'custom_value4' => $this->getString($data, 'product.custom_value4'), 'custom_value4' => $this->getString($data, 'product.custom_value4'),
'product_image' => $this->getString($data, 'product.image_url'), 'product_image' => $this->getString($data, 'product.image_url'),
'in_stock_quantity' => $this->getFloat($data, 'product.in_stock_quantity'),
]; ];
} }
} }

View File

@ -46,6 +46,7 @@ class TaskTransformer extends BaseTransformer
'company_id' => $this->company->id, 'company_id' => $this->company->id,
'number' => $this->getString($task_data, 'task.number'), 'number' => $this->getString($task_data, 'task.number'),
'user_id' => $this->getString($task_data, 'task.user_id'), 'user_id' => $this->getString($task_data, 'task.user_id'),
'rate' => $this->getFloat($task_data, 'task.rate'),
'client_id' => $clientId, 'client_id' => $clientId,
'project_id' => $this->getProjectId($projectId, $clientId), 'project_id' => $this->getProjectId($projectId, $clientId),
'description' => $this->getString($task_data, 'task.description'), 'description' => $this->getString($task_data, 'task.description'),
@ -87,8 +88,7 @@ class TaskTransformer extends BaseTransformer
$is_billable = true; $is_billable = true;
} }
if(isset($item['task.start_date']) && if(isset($item['task.start_date'])) {
isset($item['task.end_date'])) {
$start_date = $this->resolveStartDate($item); $start_date = $this->resolveStartDate($item);
$end_date = $this->resolveEndDate($item); $end_date = $this->resolveEndDate($item);
} elseif(isset($item['task.duration'])) { } elseif(isset($item['task.duration'])) {
@ -136,7 +136,7 @@ class TaskTransformer extends BaseTransformer
private function resolveEndDate($item) private function resolveEndDate($item)
{ {
$stub_end_date = $item['task.end_date']; $stub_end_date = isset($item['task.end_date']) ? $item['task.end_date'] : $item['task.start_date'];
$stub_end_date .= isset($item['task.end_time']) ? " ".$item['task.end_time'] : ''; $stub_end_date .= isset($item['task.end_time']) ? " ".$item['task.end_time'] : '';
try { try {

View File

@ -36,18 +36,26 @@ class ExpenseTransformer extends BaseTransformer
$total_tax += floatval($record['Sales Tax Amount']); $total_tax += floatval($record['Sales Tax Amount']);
} }
$tax_rate = round(($total_tax / $amount) * 100, 3); $tax_rate = $total_tax > 0 ? round(($total_tax / $amount) * 100, 3) : 0;
if(isset($data['Notes / Memo']) && strlen($data['Notes / Memo']) > 1)
$public_notes = $data['Notes / Memo'];
elseif (isset($data['Transaction Description']) && strlen($data['Transaction Description']) > 1)
$public_notes = $data['Transaction Description'];
else
$public_notes = '';
$transformed = [ $transformed = [
'company_id' => $this->company->id, 'company_id' => $this->company->id,
'vendor_id' => $this->getVendorIdOrCreate($this->getString($data, 'Vendor')), 'vendor_id' => $this->getVendorIdOrCreate($this->getString($data, 'Vendor')),
'number' => $this->getString($data, 'Bill Number'), 'number' => $this->getString($data, 'Bill Number'),
'public_notes' => $this->getString($data, 'Notes / Memo'), 'public_notes' => $public_notes,
'date' => $this->parseDate($data['Transaction Date Added']) ?: now()->format('Y-m-d'), //27-01-2022 'date' => $this->parseDate($data['Transaction Date Added']) ?: now()->format('Y-m-d'), //27-01-2022
'currency_id' => $this->company->settings->currency_id, 'currency_id' => $this->company->settings->currency_id,
'category_id' => $this->getOrCreateExpenseCategry($data['Account Name']), 'category_id' => $this->getOrCreateExpenseCategry($data['Account Name']),
'amount' => $amount, 'amount' => $amount,
'tax_name1' => $data['Sales Tax Name'], 'tax_name1' => isset($data['Sales Tax Name']) ? $data['Sales Tax Name'] : '',
'tax_rate1' => $tax_rate, 'tax_rate1' => $tax_rate,
]; ];

View File

@ -448,6 +448,6 @@ class MatchBankTransactions implements ShouldQueue
public function middleware() public function middleware()
{ {
return [new WithoutOverlapping($this->company_id)]; return [new WithoutOverlapping($this->company->account->bank_integration_account_id)];
} }
} }

View File

@ -35,10 +35,6 @@ class ProcessBankTransactionsYodlee implements ShouldQueue
use Queueable; use Queueable;
use SerializesModels; use SerializesModels;
private string $bank_integration_account_id;
private BankIntegration $bank_integration;
private ?string $from_date; private ?string $from_date;
private bool $stop_loop = true; private bool $stop_loop = true;
@ -50,10 +46,8 @@ class ProcessBankTransactionsYodlee implements ShouldQueue
/** /**
* Create a new job instance. * Create a new job instance.
*/ */
public function __construct(string $bank_integration_account_id, BankIntegration $bank_integration) public function __construct(private string $bank_integration_account_id, private BankIntegration $bank_integration)
{ {
$this->bank_integration_account_id = $bank_integration_account_id;
$this->bank_integration = $bank_integration;
$this->from_date = $bank_integration->from_date; $this->from_date = $bank_integration->from_date;
$this->company = $this->bank_integration->company; $this->company = $this->bank_integration->company;
} }

View File

@ -93,8 +93,7 @@ class ProcessBrevoWebhook implements ShouldQueue
{ {
MultiDB::findAndSetDbByCompanyKey($this->request['tags'][0]); MultiDB::findAndSetDbByCompanyKey($this->request['tags'][0]);
/** @phpstan-ignore-next-line */ $this->company = Company::query()->where('company_key', $this->request['tags'][0])->first();
$this->company = Company::where('company_key', $this->request['tags'][0])->first();
$this->invitation = $this->discoverInvitation($this->request['message-id']); $this->invitation = $this->discoverInvitation($this->request['message-id']);

View File

@ -58,7 +58,7 @@ class CheckVat implements ShouldQueue
public function middleware() public function middleware()
{ {
return [new WithoutOverlapping($this->client->id)]; return [new WithoutOverlapping($this->client->client_hash)];
} }
} }

View File

@ -108,7 +108,8 @@ class CompanyExport implements ShouldQueue
$this->export_data['users'] = $this->company->users()->withTrashed()->cursor()->map(function ($user) { $this->export_data['users'] = $this->company->users()->withTrashed()->cursor()->map(function ($user) {
$user->account_id = $this->encodePrimaryKey($user->account_id); /** @var \App\Models\User $user */
$user->account_id = $this->encodePrimaryKey($user->account_id); //@phpstan-ignore-line
return $user; return $user;
})->all(); })->all();

View File

@ -94,7 +94,7 @@ class CompanyTaxRate implements ShouldQueue
public function middleware() public function middleware()
{ {
return [new WithoutOverlapping($this->company->id)]; return [new WithoutOverlapping($this->company->company_key)];
} }
public function failed($e) public function failed($e)

View File

@ -65,7 +65,7 @@ class AutoBillCron
$auto_bill_partial_invoices->chunk(400, function ($invoices) { $auto_bill_partial_invoices->chunk(400, function ($invoices) {
foreach ($invoices as $invoice) { foreach ($invoices as $invoice) {
AutoBill::dispatch($invoice->id, false); AutoBill::dispatch($invoice->id, null);
} }
sleep(2); sleep(2);
@ -87,7 +87,7 @@ class AutoBillCron
$auto_bill_invoices->chunk(400, function ($invoices) { $auto_bill_invoices->chunk(400, function ($invoices) {
foreach ($invoices as $invoice) { foreach ($invoices as $invoice) {
AutoBill::dispatch($invoice->id, false); AutoBill::dispatch($invoice->id, null);
} }
sleep(2); sleep(2);

View File

@ -11,16 +11,17 @@
namespace App\Jobs\Cron; namespace App\Jobs\Cron;
use App\Events\Expense\ExpenseWasCreated; use App\Utils\Ninja;
use App\Factory\RecurringExpenseToExpenseFactory;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use Illuminate\Support\Carbon;
use App\Models\RecurringExpense; use App\Models\RecurringExpense;
use App\Models\RecurringInvoice; use App\Models\RecurringInvoice;
use App\Utils\Ninja;
use App\Utils\Traits\GeneratesCounter;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use App\Utils\Traits\GeneratesCounter;
use App\Events\Expense\ExpenseWasCreated;
use Illuminate\Foundation\Bus\Dispatchable;
use App\Factory\RecurringExpenseToExpenseFactory;
use App\Libraries\Currency\Conversion\CurrencyApi;
class RecurringExpensesCron class RecurringExpensesCron
{ {
@ -109,6 +110,15 @@ class RecurringExpensesCron
$expense->payment_date = now()->format('Y-m-d'); $expense->payment_date = now()->format('Y-m-d');
} }
if ((int)$expense->company->settings->currency_id != $expense->currency_id) {
$exchange_rate = new CurrencyApi();
$expense->exchange_rate = $exchange_rate->exchangeRate($expense->currency_id, (int)$expense->company->settings->currency_id, Carbon::parse($expense->date));
}
else {
$expense->exchange_rate = 1;
}
$expense->number = $this->getNextExpenseNumber($expense); $expense->number = $this->getNextExpenseNumber($expense);
$expense->saveQuietly(); $expense->saveQuietly();

View File

@ -48,12 +48,12 @@ class RecurringInvoicesCron
Auth::logout(); Auth::logout();
if (! config('ninja.db.multi_db_enabled')) { if (! config('ninja.db.multi_db_enabled')) {
$recurring_invoices = RecurringInvoice::query()->where('status_id', RecurringInvoice::STATUS_ACTIVE) $recurring_invoices = RecurringInvoice::query()->where('recurring_invoices.status_id', RecurringInvoice::STATUS_ACTIVE)
->where('is_deleted', false) ->where('recurring_invoices.is_deleted', false)
->where('remaining_cycles', '!=', '0') ->where('recurring_invoices.remaining_cycles', '!=', '0')
->whereNotNull('next_send_date') ->whereNotNull('recurring_invoices.next_send_date')
->whereNull('deleted_at') ->whereNull('recurring_invoices.deleted_at')
->where('next_send_date', '<=', now()->toDateTimeString()) ->where('recurring_invoices.next_send_date', '<=', now()->toDateTimeString())
->whereHas('client', function ($query) { ->whereHas('client', function ($query) {
$query->where('is_deleted', 0) $query->where('is_deleted', 0)
->where('deleted_at', null); ->where('deleted_at', null);
@ -87,18 +87,27 @@ class RecurringInvoicesCron
foreach (MultiDB::$dbs as $db) { foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db); MultiDB::setDB($db);
$recurring_invoices = RecurringInvoice::query()->where('status_id', RecurringInvoice::STATUS_ACTIVE) $recurring_invoices = RecurringInvoice::query()->where('recurring_invoices.status_id', RecurringInvoice::STATUS_ACTIVE)
->where('is_deleted', false) ->where('recurring_invoices.is_deleted', false)
->where('remaining_cycles', '!=', '0') ->where('recurring_invoices.remaining_cycles', '!=', '0')
->whereNull('deleted_at') ->whereNull('recurring_invoices.deleted_at')
->whereNotNull('next_send_date') ->whereNotNull('recurring_invoices.next_send_date')
->where('next_send_date', '<=', now()->toDateTimeString()) ->where('recurring_invoices.next_send_date', '<=', now()->toDateTimeString())
->whereHas('client', function ($query) { // ->whereHas('client', function ($query) {
$query->where('is_deleted', 0) // $query->where('is_deleted', 0)
->where('deleted_at', null); // ->where('deleted_at', null);
// })
// ->whereHas('company', function ($query) {
// $query->where('is_disabled', 0);
// })
->leftJoin('clients', function ($join) {
$join->on('recurring_invoices.client_id', '=', 'clients.id')
->where('clients.is_deleted', 0)
->whereNull('clients.deleted_at');
}) })
->whereHas('company', function ($query) { ->leftJoin('companies', function ($join) {
$query->where('is_disabled', 0); $join->on('recurring_invoices.company_id', '=', 'companies.id')
->where('companies.is_disabled', 0);
}) })
->with('company') ->with('company')
->cursor(); ->cursor();

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