Merge remote-tracking branch 'upstream/v5-develop' into 1403-gocardless

This commit is contained in:
Benjamin Beganović 2024-09-23 16:00:24 +02:00
commit c7ba9cb908
1415 changed files with 505123 additions and 423915 deletions

View File

@ -24,3 +24,4 @@ PHANTOMJS_PDF_GENERATION=false
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
PDF_GENERATOR=snappdf

View File

@ -24,6 +24,7 @@ QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MAIL_MAILER=smtp
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
@ -66,3 +67,5 @@ NORDIGEN_SECRET_KEY=
GOCARDLESS_CLIENT_ID=
GOCARDLESS_CLIENT_SECRET=
OPENEXCHANGE_APP_ID=

View File

@ -8,7 +8,7 @@ assignees: ''
---
<!-- Before posting please check our "Troubleshooting" category in the docs:
https://invoiceninja.github.io/docs/self-host-troubleshooting/ -->
https://invoiceninja.github.io/en/self-host-troubleshooting/ -->
## Setup
- Version: <!-- i.e. v4.5.25 / v5.0.30 -->

View File

@ -18,7 +18,7 @@ jobs:
phpunit-versions: ['latest']
ci_node_total: [ 8 ]
ci_node_index: [ 0, 1, 2, 3, 4, 5, 6, 7]
laravel: [10.*]
laravel: [11.*]
dependency-version: [prefer-stable]
env:
@ -103,7 +103,6 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ matrix.php }}-composer-
- name: Install composer dependencies
run: |
composer config -g github-oauth.github.com ${{ secrets.GITHUB_TOKEN }}

View File

@ -38,16 +38,22 @@ jobs:
sudo php artisan cache:clear
sudo find ./vendor/bin/ -type f -exec chmod +x {} \;
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
run: |
git clone https://${{secrets.commit_secret}}@github.com/invoiceninja/ui.git
cd ui
git checkout develop
cp .env.example .env
cp ../vite.config.ts.react ./vite.config.js
sed -i '/"version"/c\ "version": " Latest Build - ${{ env.current_date }}",' package.json
npm i
npm run build
cp -r dist/* ../public/
cp dist/index.html ../resources/views/react/index.blade.php
mv ../public/index.html ../resources/views/react/index.blade.php
- name: Prepare JS/CSS assets
run: |
@ -60,11 +66,12 @@ jobs:
sudo rm -rf node_modules
sudo rm -rf .git
sudo rm .env
sudo rm -rf ui
- name: Build project
run: |
shopt -s dotglob
tar --exclude='public/storage' --exclude='./htaccess' --exclude='invoiceninja.zip' -zcvf /home/runner/work/invoiceninja/react-invoiceninja.tar *
tar --exclude='public/storage' --exclude='./htaccess' --exclude='invoiceninja.zip' -zcvf /home/runner/work/invoiceninja/invoiceninja.tar *
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
@ -72,4 +79,4 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: |
/home/runner/work/invoiceninja/react-invoiceninja.tar
/home/runner/work/invoiceninja/invoiceninja.tar

View File

@ -1,86 +0,0 @@
on:
release:
types: [released]
name: Upload Release Asset
jobs:
build:
name: Upload Release Asset
runs-on: ubuntu-latest
steps:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
extensions: mysql, mysqlnd, sqlite3, bcmath, gd, curl, zip, openssl, mbstring, xml
- name: Checkout code
uses: actions/checkout@v1
with:
ref: v5-develop
- name: Copy .env file
run: |
cp .env.example .env
- name: Install composer dependencies
run: |
composer config -g github-oauth.github.com ${{ secrets.GITHUB_TOKEN }}
composer install --no-dev
- name: Prepare Laravel Application
run: |
cp .env.example .env
php artisan key:generate --force
php artisan optimize
php artisan storage:link --force
sudo php artisan cache:clear
sudo find ./vendor/bin/ -type f -exec chmod +x {} \;
sudo find ./ -type d -exec chmod 755 {} \;
- name: Prepare React FrontEnd
run: |
git clone https://${{secrets.commit_secret}}@github.com/invoiceninja/ui.git
cd ui
git checkout develop
npm i
npm run build
mkdir -p ../public/react/${{ github.event.release.tag_name }}/
cp -r dist/react/* ../public/react/${{ github.event.release.tag_name }}/
cp -r dist/react/* ../public/react/
cp dist/index.html ../resources/views/react/index.blade.php
mkdir -p ../public/tinymce_6.4.2/tinymce/js/
cp -r node_modules/tinymce ../public/tinymce_6.4.2/tinymce/js/
cd ..
rm -rf ui
php artisan ninja:react
- name: Prepare JS/CSS assets
run: |
npm i
npm run production
- name: Cleanup Builds
run: |
sudo rm -rf bootstrap/cache/*
sudo rm -rf node_modules
sudo rm -rf .git
sudo rm .env
- name: Build project
run: |
zip -r /home/runner/work/invoiceninja/invoiceninja.zip .* -x "../*"
shopt -s dotglob
tar --exclude='public/storage' --exclude='./htaccess' --exclude='invoiceninja.zip' -zcvf /home/runner/work/invoiceninja/invoiceninja.tar *
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: |
/home/runner/work/invoiceninja/invoiceninja.tar
/home/runner/work/invoiceninja/invoiceninja.zip

View File

@ -1,5 +1,5 @@
<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>
![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop)
@ -8,25 +8,30 @@
# 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!
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-source code. We offer a $30 per year white-label license to remove the Invoice Ninja branding from client-facing parts of the app.
All Pro and Enterprise features from the hosted app are included in the open code.
We offer a $30 per year white-label license to remove the Invoice Ninja branding from client facing parts of the app.
#### Get social with us
* [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)
* [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
* [iPhone](https://apps.apple.com/app/id1503970375?platform=iphone)
@ -39,22 +44,28 @@ We offer a $30 per year white-label license to remove the Invoice Ninja branding
* [Linux - Snap](https://snapcraft.io/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/)
* [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)
* [Elestio](https://elest.io/open-source/invoiceninja)
* [YunoHost](https://apps.yunohost.org/app/invoiceninja5)
### Recommended Providers
* [Stripe](https://stripe.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
git clone --single-branch --branch v5-stable https://github.com/invoiceninja/invoiceninja.git
cp .env.example .env
composer i -o --no-dev
php artisan key:generate
```
Please Note:
@ -85,6 +96,7 @@ pass: password
```
## 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

View File

@ -1 +1 @@
5.9.1
5.10.29

View File

@ -0,0 +1,50 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Casts;
use App\DataMapper\QuickbooksSettings;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class QuickbooksSettingsCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
{
$data = json_decode($value, true);
if(!is_array($data))
return null;
$qb = new QuickbooksSettings();
$qb->accessTokenKey = $data['accessTokenKey'];
$qb->refresh_token = $data['refresh_token'];
$qb->realmID = $data['realmID'];
$qb->accessTokenExpiresAt = $data['accessTokenExpiresAt'];
$qb->refreshTokenExpiresAt = $data['refreshTokenExpiresAt'];
$qb->settings = $data['settings'] ?? [];
return $qb;
}
public function set($model, string $key, $value, array $attributes)
{
return [
$key => json_encode([
'accessTokenKey' => $value->accessTokenKey,
'refresh_token' => $value->refresh_token,
'realmID' => $value->realmID,
'accessTokenExpiresAt' => $value->accessTokenExpiresAt,
'refreshTokenExpiresAt' => $value->refreshTokenExpiresAt,
'settings' => $value->settings,
])
];
}
}

View File

@ -176,7 +176,7 @@ class BackupUpdate extends Command
try {
$doc_bin = $document->getFile();
} catch(\Exception $e) {
nlog($e->getMessage());
nlog("Exception:: BackupUpdate::" . $e->getMessage());
}
if ($doc_bin) {
@ -184,8 +184,6 @@ class BackupUpdate extends Command
$document->disk = $this->option('disk');
$document->saveQuietly();
nlog("Documents - Moving {$document->url} to {$this->option('disk')}");
}
});
@ -199,8 +197,6 @@ class BackupUpdate extends Command
if ($backup_bin) {
Storage::disk($this->option('disk'))->put($backup->filename, $backup_bin);
nlog("Backups - Moving {$backup->filename} to {$this->option('disk')}");
}
});
}

View File

@ -12,37 +12,38 @@
namespace App\Console\Commands;
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\VendorContactFactory;
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\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;
/*
@ -130,6 +131,7 @@ class CheckData extends Command
$this->checkContactEmailAndSendEmailStatus();
$this->checkPaymentCurrency();
$this->checkSubdomainsSet();
$this->checkExpenseCurrency();
if (Ninja::isHosted()) {
$this->checkAccountStatuses();
@ -478,10 +480,9 @@ class CheckData extends Command
} else {
$this->logMessage("No contact present, so cannot add invitation for {$entity_key} - {$entity->id}");
try{
try {
$entity->service()->createInvitations()->save();
}
catch(\Exception $e){
} catch(\Exception $e) {
}
@ -949,12 +950,12 @@ class CheckData extends Command
});
Company::whereDoesntHave('company_users', function ($query){
$query->where('is_owner', 1);
Company::whereDoesntHave('company_users', function ($query) {
$query->where('is_owner', 1);
})
->cursor()
->when(Ninja::isHosted())
->each(function ($c){
->each(function ($c) {
$this->logMessage("Orphan Account # {$c->account_id}");
@ -963,7 +964,7 @@ class CheckData extends Command
CompanyUser::whereDoesntHave('tokens')
->cursor()
->when(Ninja::isHosted())
->each(function ($cu){
->each(function ($cu) {
$this->logMessage("Missing tokens for Company User # {$cu->id}");
@ -1159,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

@ -56,8 +56,6 @@ class CreateAccount extends Command
{
$this->info(date('r').' Create Single Account...');
$this->warmCache();
$this->createAccount();
}
@ -121,28 +119,6 @@ class CreateAccount extends Command
(new CreateCompanyTaskStatuses($company, $user))->handle();
(new VersionCheck())->handle();
$this->warmCache();
}
private function warmCache()
{
/* Warm up the cache !*/
$cached_tables = config('ninja.cached_tables');
foreach ($cached_tables as $name => $class) {
if ($name == 'payment_terms') {
$orderBy = 'num_days';
} elseif ($name == 'fonts') {
$orderBy = 'sort_order';
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) {
$orderBy = 'name';
} else {
$orderBy = 'id';
}
$tableData = $class::orderBy($orderBy)->get();
if ($tableData->count()) {
Cache::forever($name, $tableData);
}
}
}
}

View File

@ -97,10 +97,6 @@ class CreateSingleAccount extends Command
$this->count = 5;
$this->gateway = $this->argument('gateway');
$this->info('Warming up cache');
$this->warmCache();
$this->createSmallAccount();
@ -389,6 +385,9 @@ class CreateSingleAccount extends Command
});
$this->countryClients($company, $user);
$this->info("finished");
}
@ -774,32 +773,6 @@ class CreateSingleAccount extends Command
return $line_items;
}
private function warmCache()
{
/* Warm up the cache !*/
$cached_tables = config('ninja.cached_tables');
foreach ($cached_tables as $name => $class) {
// check that the table exists in case the migration is pending
if (! Schema::hasTable((new $class())->getTable())) {
continue;
}
if ($name == 'payment_terms') {
$orderBy = 'num_days';
} elseif ($name == 'fonts') {
$orderBy = 'sort_order';
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) {
$orderBy = 'name';
} else {
$orderBy = 'id';
}
$tableData = $class::orderBy($orderBy)->get();
if ($tableData->count()) {
Cache::forever($name, $tableData);
}
}
}
private function createGateways($company, $user)
{
if (config('ninja.testvars.stripe') && ($this->gateway == 'all' || $this->gateway == 'stripe')) {
@ -1139,4 +1112,44 @@ class CreateSingleAccount extends Command
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

@ -86,8 +86,6 @@ class CreateTestData extends Command
$this->info('Warming up cache');
$this->warmCache();
$this->createSmallAccount();
$this->createMediumAccount();
$this->createLargeAccount();
@ -673,31 +671,4 @@ class CreateTestData extends Command
return $line_items;
}
private function warmCache()
{
/* Warm up the cache !*/
$cached_tables = config('ninja.cached_tables');
foreach ($cached_tables as $name => $class) {
if (! Cache::has($name)) {
// check that the table exists in case the migration is pending
if (! Schema::hasTable((new $class())->getTable())) {
continue;
}
if ($name == 'payment_terms') {
$orderBy = 'num_days';
} elseif ($name == 'fonts') {
$orderBy = 'sort_order';
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) {
$orderBy = 'name';
} else {
$orderBy = 'id';
}
$tableData = $class::orderBy($orderBy)->get();
if ($tableData->count()) {
Cache::forever($name, $tableData);
}
}
}
}
}

View File

@ -84,15 +84,13 @@ class DemoMode extends Command
$this->invoice_repo = new InvoiceRepository();
$cached_tables = config('ninja.cached_tables');
$this->info('Migrating');
Artisan::call('migrate:fresh --force');
$this->info('Seeding');
Artisan::call('db:seed --force');
$this->buildCache(true);
Artisan::call('db:seed --force');
Artisan::call('cache:clear');
$this->info('Seeding Random Data');
$this->createSmallAccount();
@ -623,31 +621,4 @@ class DemoMode extends Command
return $line_items;
}
private function warmCache()
{
/* Warm up the cache !*/
$cached_tables = config('ninja.cached_tables');
foreach ($cached_tables as $name => $class) {
if (! Cache::has($name)) {
// check that the table exists in case the migration is pending
if (! Schema::hasTable((new $class())->getTable())) {
continue;
}
if ($name == 'payment_terms') {
$orderBy = 'num_days';
} elseif ($name == 'fonts') {
$orderBy = 'sort_order';
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) {
$orderBy = 'name';
} else {
$orderBy = 'id';
}
$tableData = $class::orderBy($orderBy)->get();
if ($tableData->count()) {
Cache::forever($name, $tableData);
}
}
}
}
}

View File

@ -62,7 +62,6 @@ class HostedMigrations extends Command
*/
public function handle()
{
$this->buildCache();
if (! MultiDB::userFindAndSetDb($this->option('email'))) {
$this->info('Could not find a user with that email address');

View File

@ -75,8 +75,6 @@ class ImportMigrations extends Command
{
$this->faker = Factory::create();
$this->buildCache();
$path = $this->option('path') ?? public_path('storage/migrations/import');
$directory = new DirectoryIterator($path);

View File

@ -75,7 +75,7 @@ class MobileLocalization extends Command
private function flutterResources()
{
$languages = cache('languages');
$languages = app('languages');
$resources = $this->getResources();
foreach ($languages as $language) {

View File

@ -54,13 +54,6 @@ class PostUpdate extends Command
info('finished migrating');
$output = [];
// exec('vendor/bin/composer install --no-dev -o', $output);
info(print_r($output, 1));
info('finished running composer install ');
try {
// Artisan::call('optimize');
Artisan::call('config:clear');
@ -86,8 +79,7 @@ class PostUpdate extends Command
info('queue restarted');
$this->buildCache(true);
Artisan::call('cache:clear');
VersionCheck::dispatch();
info('Sent for version check');

View File

@ -50,32 +50,32 @@ class ReactBuilder extends Command
$includes = '';
$includes = '';
$directoryIterator = false;
$directoryIterator = false;
try {
$directoryIterator = new \RecursiveDirectoryIterator(public_path('react/v'.config('ninja.app_version').'/'), \RecursiveDirectoryIterator::SKIP_DOTS);
} catch (\Exception $e) {
$this->error('React files not found');
return;
}
try {
$directoryIterator = new \RecursiveDirectoryIterator(public_path('react/v'.config('ninja.app_version').'/'), \RecursiveDirectoryIterator::SKIP_DOTS);
} catch (\Exception $e) {
$this->error('React files not found');
return;
}
foreach (new \RecursiveIteratorIterator($directoryIterator) as $file) {
if ($file->getExtension() == 'js') {
if (str_contains($file->getFileName(), 'index-')) {
$includes .= '<script type="module" crossorigin src="/react/v'.config('ninja.app_version').'/'.$file->getFileName().'"></script>'."\n";
} else {
$includes .= '<link rel="modulepreload" href="/react/v'.config('ninja.app_version').'/'.$file->getFileName().'">'."\n";
foreach (new \RecursiveIteratorIterator($directoryIterator) as $file) {
if ($file->getExtension() == 'js') {
if (str_contains($file->getFileName(), 'index-')) {
$includes .= '<script type="module" crossorigin src="/react/v'.config('ninja.app_version').'/'.$file->getFileName().'"></script>'."\n";
} else {
$includes .= '<link rel="modulepreload" href="/react/v'.config('ninja.app_version').'/'.$file->getFileName().'">'."\n";
}
}
if (str_contains($file->getFileName(), '.css')) {
$includes .= '<link rel="stylesheet" href="/react/v'.config('ninja.app_version').'/'.$file->getFileName().'">'."\n";
}
}
if (str_contains($file->getFileName(), '.css')) {
$includes .= '<link rel="stylesheet" href="/react/v'.config('ninja.app_version').'/'.$file->getFileName().'">'."\n";
}
}
file_put_contents(resource_path('views/react/head.blade.php'), $includes);
file_put_contents(resource_path('views/react/head.blade.php'), $includes);
}

View File

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

View File

@ -76,6 +76,7 @@ class TranslationsExport extends Command
'sv',
'th',
'tr_TR',
'vi',
'zh_TW',
];

View File

@ -15,7 +15,6 @@ use App\Jobs\Cron\AutoBillCron;
use App\Jobs\Cron\RecurringExpensesCron;
use App\Jobs\Cron\RecurringInvoicesCron;
use App\Jobs\Cron\SubscriptionCron;
use App\Jobs\Cron\UpdateCalculatedFields;
use App\Jobs\Invoice\InvoiceCheckLateWebhook;
use App\Jobs\Ninja\AdjustEmailQuota;
use App\Jobs\Ninja\BankTransactionSync;
@ -27,11 +26,13 @@ use App\Jobs\Ninja\TaskScheduler;
use App\Jobs\Quote\QuoteCheckExpired;
use App\Jobs\Subscription\CleanStaleInvoiceOrder;
use App\Jobs\Util\DiskCleanup;
use App\Jobs\Util\QuoteReminderJob;
use App\Jobs\Util\ReminderJob;
use App\Jobs\Util\SchedulerCheck;
use App\Jobs\Util\UpdateExchangeRates;
use App\Jobs\Util\VersionCheck;
use App\Models\Account;
use App\PaymentDrivers\Rotessa\Jobs\TransactionReport;
use App\Utils\Ninja;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@ -55,17 +56,20 @@ class Kernel extends ConsoleKernel
/* Send reminders */
$schedule->job(new ReminderJob())->hourly()->withoutOverlapping()->name('reminder-job')->onOneServer();
/* Send quote reminders */
$schedule->job(new QuoteReminderJob())->hourly()->withoutOverlapping()->name('quote-reminder-job')->onOneServer();
/* Sends recurring invoices*/
$schedule->job(new RecurringInvoicesCron())->hourly()->withoutOverlapping()->name('recurring-invoice-job')->onOneServer();
/* Checks for scheduled tasks */
$schedule->job(new TaskScheduler())->hourlyAt(10)->withoutOverlapping()->name('task-scheduler-job')->onOneServer();
/* Stale Invoice Cleanup*/
$schedule->job(new CleanStaleInvoiceOrder())->hourlyAt(30)->withoutOverlapping()->name('stale-invoice-job')->onOneServer();
/* Checks Rotessa Transactions */
$schedule->job(new TransactionReport())->dailyAt('01:48')->withoutOverlapping()->name('rotessa-transaction-report')->onOneServer();
/* Stale Invoice Cleanup*/
$schedule->job(new UpdateCalculatedFields())->hourlyAt(40)->withoutOverlapping()->name('update-calculated-fields-job')->onOneServer();
$schedule->job(new CleanStaleInvoiceOrder())->hourlyAt(30)->withoutOverlapping()->name('stale-invoice-job')->onOneServer();
/* Checks for large companies and marked them as is_large */
$schedule->job(new CompanySizeCheck())->dailyAt('23:20')->withoutOverlapping()->name('company-size-job')->onOneServer();

View File

@ -54,8 +54,8 @@ class AccountPlatform extends GenericMixedMetric
public function __construct($string_metric5, $string_metric6, $string_metric7)
{
$this->string_metric5 = $string_metric5;
$this->string_metric6 = $string_metric6;
$this->string_metric5 = mb_convert_encoding($string_metric5, 'UTF-8');
$this->string_metric6 = mb_convert_encoding($string_metric6, 'UTF-8');
$this->string_metric7 = $string_metric7;
}
}

View File

@ -66,12 +66,14 @@ class DbQuery extends GenericMixedMetric
public $double_metric2 = 1;
public function __construct($string_metric5, $string_metric6, $int_metric1, $double_metric2, $string_metric7)
public function __construct($string_metric5, $string_metric6, $int_metric1, $double_metric2, $string_metric7, $string_metric8, $string_metric9)
{
$this->int_metric1 = $int_metric1;
$this->string_metric5 = $string_metric5;
$this->string_metric6 = $string_metric6;
$this->double_metric2 = $double_metric2;
$this->string_metric7 = $string_metric7;
$this->string_metric8 = mb_convert_encoding($string_metric8, "UTF-8");
$this->string_metric9 = mb_convert_encoding($string_metric9, "UTF-8");
}
}

View File

@ -0,0 +1,83 @@
<?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\Analytics;
use Turbo124\Beacon\ExampleMetric\GenericMixedMetric;
class LegalEntityCreated extends GenericMixedMetric
{
/**
* The type of Sample.
*
* Monotonically incrementing counter
*
* - counter
*
* @var string
*/
public $type = 'mixed_metric';
/**
* The name of the counter.
* @var string
*/
public $name = 'einvoice.legal_entity.created';
/**
* The datetime of the counter measurement.
*
* date("Y-m-d H:i:s")
*
*/
public $datetime;
/**
* The Class failure name
* set to 0.
*
* @var string
*/
public $string_metric5 = 'stub';
/**
* The exception string
* set to 0.
*
* @var string
*/
public $string_metric6 = 'stub';
/**
* The counter
* set to 1.
*
*/
public $int_metric1 = 1;
/**
* Company Key
* @var string
*/
public $string_metric7 = '';
/**
* Subject
* @var string
*/
public $string_metric8 = '';
public function __construct($string_metric7 = '', $string_metric8 = '')
{
$this->string_metric7 = $string_metric7;
$this->string_metric8 = $string_metric8;
}
}

View File

@ -0,0 +1,61 @@
<?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\Analytics;
use Turbo124\Beacon\ExampleMetric\GenericMixedMetric;
class LoginMeta extends GenericMixedMetric
{
/**
* The type of Sample.
*
* Monotonically incrementing counter
*
* - counter
*
* @var string
*/
public $type = 'mixed_metric';
/**
* The name of the counter.
* @var string
*/
public $name = 'login.meta';
/**
* The datetime of the counter measurement.
*
* date("Y-m-d H:i:s")
*
*/
public $datetime;
/**
* The Class failure name
* set to 0.
*
* @var string
*/
public $string_metric5 = 'email';
public $string_metric6 = 'ip';
public $string_metric7 = 'result';
public $int_metric1 = 1;
public function __construct($string_metric5, $string_metric6, $string_metric7)
{
$this->string_metric7 = $string_metric7;
$this->string_metric6 = $string_metric6;
$this->string_metric5 = $string_metric5;
}
}

View File

@ -16,13 +16,6 @@ namespace App\DataMapper;
*/
class BaseSettings
{
// //@deprecated
// public function __construct($obj)
// {
// // foreach ($obj as $key => $value) {
// // $obj->{$key} = $value;
// // }
// }
public static function setCasts($obj, $casts)
{

View File

@ -29,7 +29,7 @@ class CompanySettings extends BaseSettings
public $besr_id = ''; //@implemented
public $lock_invoices = 'off'; //off,when_sent,when_paid //@implemented
public $lock_invoices = 'off'; //off,when_sent,when_paid,end_of_month //@implemented
public $enable_client_portal_tasks = false; //@ben to implement
@ -507,7 +507,29 @@ class CompanySettings extends BaseSettings
public int $task_round_to_nearest = 1;
/** quote reminders */
public $email_quote_template_reminder1 = '';
public $email_quote_subject_reminder1 = '';
public $enable_quote_reminder1 = false;
public $quote_num_days_reminder1 = 0;
public $quote_schedule_reminder1 = ''; //before_valid_until_date,after_valid_until_date,after_quote_date
public $quote_late_fee_amount1 = 0;
public $quote_late_fee_percent1 = 0;
public string $payment_flow = 'default'; //smooth
public string $email_subject_payment_failed = '';
public string $email_template_payment_failed = '';
public static $casts = [
'payment_flow' => 'string',
'enable_quote_reminder1' => 'bool',
'quote_num_days_reminder1' => 'int',
'quote_schedule_reminder1' => 'string',
'quote_late_fee_amount1' => 'float',
'quote_late_fee_percent1' => 'float',
'email_quote_template_reminder1' => 'string',
'email_quote_subject_reminder1' => 'string',
'task_round_up' => 'bool',
'task_round_to_nearest' => 'int',
'e_quote_type' => 'string',
@ -750,6 +772,8 @@ class CompanySettings extends BaseSettings
'portal_custom_js' => 'string',
'client_portal_enable_uploads' => 'bool',
'purchase_order_number_counter' => 'integer',
'email_template_payment_failed' => 'string',
'email_subject_payment_failed' => 'string',
];
public static $free_plan_casts = [
@ -962,6 +986,7 @@ class CompanySettings extends BaseSettings
'$invoice.due_date',
'$invoice.total',
'$invoice.balance_due',
'$invoice.project',
],
'quote_details' => [
'$quote.number',
@ -969,6 +994,7 @@ class CompanySettings extends BaseSettings
'$quote.date',
'$quote.valid_until',
'$quote.total',
'$quote.project',
],
'credit_details' => [
'$credit.number',

View File

@ -30,6 +30,7 @@ class EmailTemplateDefaults
'email_template_custom2',
'email_template_custom3',
'email_template_purchase_order',
'email_template_payment_failed'
];
public static function getDefaultTemplate($template, $locale)
@ -39,6 +40,8 @@ class EmailTemplateDefaults
switch ($template) {
/* Template */
case 'email_template_payment_failed':
return self::emailPaymentFailedTemplate();
case 'email_template_invoice':
return self::emailInvoiceTemplate();
case 'email_template_quote':
@ -73,6 +76,9 @@ class EmailTemplateDefaults
case 'email_subject_invoice':
return self::emailInvoiceSubject();
case 'email_subject_payment_failed':
return self::emailPaymentFailedSubject();
case 'email_subject_quote':
return self::emailQuoteSubject();
@ -115,12 +121,40 @@ class EmailTemplateDefaults
case 'email_vendor_notification_body':
return self::emailVendorNotificationBody();
case 'email_quote_template_reminder1':
return self::emailQuoteReminder1Body();
case 'email_quote_subject_reminder1':
return self::emailQuoteReminder1Subject();
default:
return self::emailInvoiceTemplate();
}
}
public static function emailPaymentFailedSubject()
{
return ctrans('texts.notification_invoice_payment_failed_subject', ['invoice' => '$number']);
}
public static function emailPaymentFailedTemplate()
{
return '<p>$client<br><br>'.ctrans('texts.client_payment_failure_body', ['invoice' => '$number', 'amount' => '$amount']).'</p><div>$payment_error</div><br><div>$view_button</div>';
}
public static function emailQuoteReminder1Subject()
{
return ctrans('texts.quote_reminder_subject', ['quote' => '$number', 'company' => '$company.name']);
}
public static function emailQuoteReminder1Body()
{
return '<p>$client<br><br>'.self::transformText('quote_reminder_message').'</p><div>$view_button</div>';
}
public static function emailVendorNotificationSubject()
{
return self::transformText('vendor_notification_subject');
@ -143,14 +177,14 @@ class EmailTemplateDefaults
public static function emailInvoiceTemplate()
{
$invoice_message = '<p>$client<br><br>'.self::transformText('invoice_message').'</p><div class="center">$view_button</div>';
$invoice_message = '<p>$client<br><br>'.self::transformText('invoice_message').'</p><div>$view_button</div>';
return $invoice_message;
}
public static function emailInvoiceReminderTemplate()
{
$invoice_message = '<p>$client<br><br>'.self::transformText('reminder_message').'</p><div class="center">$view_button</div>';
$invoice_message = '<p>$client<br><br>'.self::transformText('reminder_message').'</p><div>$view_button</div>';
return $invoice_message;
}
@ -162,7 +196,7 @@ class EmailTemplateDefaults
public static function emailQuoteTemplate()
{
$quote_message = '<p>$client<br><br>'.self::transformText('quote_message').'</p><div class="center">$view_button</div>';
$quote_message = '<p>$client<br><br>'.self::transformText('quote_message').'</p><div>$view_button</div>';
return $quote_message;
}
@ -179,28 +213,28 @@ class EmailTemplateDefaults
public static function emailPurchaseOrderTemplate()
{
$purchase_order_message = '<p>$vendor<br><br>'.self::transformText('purchase_order_message').'</p><div class="center">$view_button</div>';
$purchase_order_message = '<p>$vendor<br><br>'.self::transformText('purchase_order_message').'</p><div>$view_button</div>';
return $purchase_order_message;
}
public static function emailPaymentTemplate()
{
$payment_message = '<p>$client<br><br>'.self::transformText('payment_message').'<br><br>$invoices</p><div class="center">$view_button</div>';
$payment_message = '<p>$client<br><br>'.self::transformText('payment_message').'<br><br>$invoices</p><div>$view_button</div>';
return $payment_message;
}
public static function emailCreditTemplate()
{
$credit_message = '<p>$client<br><br>'.self::transformText('credit_message').'</p><div class="center">$view_button</div>';
$credit_message = '<p>$client<br><br>'.self::transformText('credit_message').'</p><div>$view_button</div>';
return $credit_message;
}
public static function emailPaymentPartialTemplate()
{
$payment_message = '<p>$client<br><br>'.self::transformText('payment_message').'<br><br>$invoices</p><div class="center">$view_button</div>';
$payment_message = '<p>$client<br><br>'.self::transformText('payment_message').'<br><br>$invoices</p><div>$view_button</div>';
return $payment_message;
}

View File

@ -0,0 +1,63 @@
<?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;
use Illuminate\Contracts\Database\Eloquent\Castable;
use App\Casts\QuickbooksSettingsCast;
/**
* QuickbooksSettings.
*/
class QuickbooksSettings implements Castable
{
public string $accessTokenKey;
public string $refresh_token;
public string $realmID;
public int $accessTokenExpiresAt;
public int $refreshTokenExpiresAt;
public string $baseURL;
/**
* entity client,invoice,quote,purchase_order,vendor,payment
* sync true/false
* update_record true/false
* direction push/pull/birectional
* */
public array $settings = [
'client' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
'vendor' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
'invoice' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
'sales' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
'quote' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
'purchase_order' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
'product' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
'payment' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
'vendor' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
'expense' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
];
/**
* Get the name of the caster class to use when casting from / to this cast target.
*
* @param array<string, mixed> $arguments
*/
public static function castUsing(array $arguments): string
{
return QuickbooksSettingsCast::class;
}
}

View File

@ -15,7 +15,6 @@ use App\DataMapper\InvoiceItem;
class PayPalBalanceAffecting
{
private array $key_map = [
'Date' => 'date',
'Time' => 'time',
@ -106,16 +105,35 @@ class PayPalBalanceAffecting
public $creditTransactionalFee;
public $originalInvoiceId;
public function __construct(private array $import_row){}
public function __construct(private array $import_row)
{
}
public function run(): self
{
$this->cleanUp();
foreach($this->import_row as $key => $value) {
$prop = $this->key_map[$key] ?? false;
if($prop)
if($prop) {
echo "Setting {$prop} to {$value}".PHP_EOL;
$this->{$prop} = $value;
}
}
return $this;
}
private function cleanUp(): self
{
foreach($this->key_map as $value) {
echo "Setting {$value} to null".PHP_EOL;
$this->{$value} = null;
}
return $this;
@ -137,7 +155,7 @@ class PayPalBalanceAffecting
public function getInvoice(): array
{
$item = new InvoiceItem;
$item = new InvoiceItem();
$item->cost = $this->gross ?? 0;
$item->product_key = $this->itemId ?? '';
$item->notes = $this->subject ?? $this->itemDetails;
@ -145,10 +163,11 @@ class PayPalBalanceAffecting
return [
'number' => trim($this->invoiceNumber ?? $this->transactionId),
'date' => str_replace('/','-', $this->date ?? ''),
'date' => str_replace('/', '-', $this->date ?? ''),
'line_items' => [$item],
'name' => $this->name ?? '',
'email' => $this->fromEmailAddress ?? '',
'transaction_reference' => $this->transactionId ?? '',
];
}
@ -156,12 +175,10 @@ class PayPalBalanceAffecting
{
$name_parts = explode(" ", $this->name ?? '');
if(count($name_parts) == 2)
{
if(count($name_parts) == 2) {
$contact['first_name'] = $name_parts[0];
$contact['last_name'] = $name_parts[1];
}
else {
} else {
$contact['first_name'] = $this->name ?? '';
}
@ -185,13 +202,15 @@ class PayPalBalanceAffecting
private function returnShippingAddress(): array
{
if(strlen($this->shippingAddress ?? '') <3)
if(strlen($this->shippingAddress ?? '') < 3) {
return [];
}
$ship_parts = explode(",", $this->shippingAddress);
if(count($ship_parts) != 7)
if(count($ship_parts) != 7) {
return [];
}
return [
'shipping_address1' => $ship_parts[2],

View File

@ -17,6 +17,7 @@ use App\Models\Invoice;
use App\Models\Product;
use App\DataProviders\USStates;
use App\DataMapper\Tax\ZipTax\Response;
use App\Models\RecurringInvoice;
class BaseRule implements RuleInterface
{
@ -47,6 +48,9 @@ class BaseRule implements RuleInterface
'DK', // Denmark
'EE', // Estonia
'ES', // Spain
'ES-CN', // Canary Islands
'ES-CE', // Ceuta
'ES-ML', // Melilla
'FI', // Finland
'FR', // France
'GR', // Greece
@ -77,6 +81,9 @@ class BaseRule implements RuleInterface
'DK' => 'EU', // Denmark
'EE' => 'EU', // Estonia
'ES' => 'EU', // Spain
'ES-CN' => 'EU', // Canary Islands
'ES-CE' => 'EU', // Ceuta
'ES-ML' => 'EU', // Melilla
'FI' => 'EU', // Finland
'FR' => 'EU', // France
'GR' => 'EU', // Greece
@ -132,7 +139,7 @@ class BaseRule implements RuleInterface
public function shouldCalcTax(): bool
{
return $this->should_calc_tax;
return $this->should_calc_tax && $this->checkIfInvoiceLocked();
}
/**
* Initializes the tax rule for the entity.
@ -215,11 +222,12 @@ class BaseRule implements RuleInterface
$this->invoice->tax_data = $tax_data;
if(\DB::transactionLevel() == 0) {
if(\DB::transactionLevel() == 0 && isset($this->invoice->id)) {
try {
$this->invoice->saveQuietly();
} catch(\Exception $e) {
nlog("Exception:: BaseRule::" . $e->getMessage());
}
}
@ -261,7 +269,7 @@ class BaseRule implements RuleInterface
return $this->client->state;
}
return USStates::getState(strlen($this->client->postal_code) > 1 ? $this->client->postal_code : $this->client->shipping_postal_code);
return USStates::getState(strlen($this->client->postal_code ?? '') > 1 ? $this->client->postal_code : $this->client->shipping_postal_code);
} catch (\Exception $e) {
return 'CA';
@ -399,4 +407,40 @@ class BaseRule implements RuleInterface
return ! in_array($iso_3166_2, array_merge($this->eu_country_codes, array_keys($this->region_codes)));
}
private function checkIfInvoiceLocked(): bool
{
$lock_invoices = $this->client->getSetting('lock_invoices');
if($this->invoice instanceof RecurringInvoice) {
return true;
}
switch ($lock_invoices) {
case 'off':
return true;
case 'when_sent':
if ($this->invoice->status_id == Invoice::STATUS_SENT) {
return false;
}
return true;
case 'when_paid':
if ($this->invoice->status_id == Invoice::STATUS_PAID) {
return false;
}
return true;
//if now is greater than the end of month the invoice was dated - do not modify
case 'end_of_month':
if(\Carbon\Carbon::parse($this->invoice->date)->setTimezone($this->invoice->company->timezone()->name)->endOfMonth()->lte(now())) {
return false;
}
return true;
default:
return true;
}
}
}

View File

@ -43,6 +43,8 @@ class Rule extends BaseRule implements RuleInterface
public float $reduced_tax_rate = 0;
public string $tax_name1 = 'MwSt.';
private string $tax_name;
/**
* Initializes the rules and builds any required data.
*
@ -50,6 +52,7 @@ class Rule extends BaseRule implements RuleInterface
*/
public function init(): self
{
$this->tax_name = $this->tax_name1;
$this->calculateRates();
return $this;
@ -91,6 +94,7 @@ class Rule extends BaseRule implements RuleInterface
*/
public function reverseTax($item): self
{
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = 0;
return $this;
@ -103,6 +107,8 @@ class Rule extends BaseRule implements RuleInterface
*/
public function taxReduced($item): self
{
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = $this->reduced_tax_rate;
return $this;
@ -115,6 +121,8 @@ class Rule extends BaseRule implements RuleInterface
*/
public function zeroRated($item): self
{
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = 0;
return $this;
@ -142,6 +150,7 @@ class Rule extends BaseRule implements RuleInterface
public function taxDigital($item): self
{
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = $this->tax_rate;
return $this;
@ -155,6 +164,7 @@ class Rule extends BaseRule implements RuleInterface
public function taxService($item): self
{
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = $this->tax_rate;
return $this;
@ -168,6 +178,7 @@ class Rule extends BaseRule implements RuleInterface
public function taxShipping($item): self
{
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = $this->tax_rate;
return $this;
@ -181,6 +192,7 @@ class Rule extends BaseRule implements RuleInterface
public function taxPhysical($item): self
{
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = $this->tax_rate;
return $this;
@ -207,6 +219,14 @@ class Rule extends BaseRule implements RuleInterface
*/
public function override($item): self
{
$this->tax_rate1 = $item->tax_rate1;
$this->tax_name1 = $item->tax_name1;
$this->tax_rate2 = $item->tax_rate2;
$this->tax_name2 = $item->tax_name2;
$this->tax_rate3 = $item->tax_rate3;
$this->tax_name3 = $item->tax_name3;
return $this;
}
@ -221,8 +241,7 @@ class Rule extends BaseRule implements RuleInterface
// nlog("tax exempt");
$this->tax_rate = 0;
$this->reduced_tax_rate = 0;
} elseif($this->client_subregion != $this->client->company->tax_data->seller_subregion && in_array($this->client_subregion, $this->eu_country_codes) && $this->client->vat_number && $this->eu_business_tax_exempt) {
// elseif($this->client_subregion != $this->client->company->tax_data->seller_subregion && in_array($this->client_subregion, $this->eu_country_codes) && $this->client->has_valid_vat_number && $this->eu_business_tax_exempt)
} elseif($this->client_subregion != $this->client->company->tax_data->seller_subregion && in_array($this->client_subregion, $this->eu_country_codes) && $this->client->vat_number && $this->client->has_valid_vat_number && $this->eu_business_tax_exempt) {
// nlog("euro zone and tax exempt");
$this->tax_rate = 0;
$this->reduced_tax_rate = 0;
@ -232,8 +251,8 @@ class Rule extends BaseRule implements RuleInterface
$this->reduced_tax_rate = 0;
} elseif(!in_array($this->client_subregion, $this->eu_country_codes)) {
$this->defaultForeign();
} elseif(in_array($this->client_subregion, $this->eu_country_codes) && !$this->client->vat_number) { //eu country / no valid vat
if(($this->client->company->tax_data->seller_subregion != $this->client_subregion) && $this->client->company->tax_data->regions->EU->has_sales_above_threshold) {
} elseif(in_array($this->client_subregion, $this->eu_country_codes) && ((strlen($this->client->vat_number ?? '') == 1) || !$this->client->has_valid_vat_number)) { //eu country / no valid vat
if($this->client->company->tax_data->seller_subregion != $this->client_subregion) {
// nlog("eu zone with sales above threshold");
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->tax_rate ?? 0;
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->reduced_tax_rate ?? 0;

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\ES_CE;
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 = 'IGIC';
}

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\ES_CN;
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 = 'IGIC';
}

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\ES_ML;
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 = 'IGIC';
}

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

@ -24,6 +24,7 @@ class TaxData
public function __construct(public Response $origin)
{
// @phpstan-ignore-next-line
foreach($origin as $key => $value) {
$this->{$key} = $value;
}

View File

@ -17,7 +17,7 @@ class TaxModel
public string $seller_subregion = 'CA';
/** @var string $version */
public string $version = 'alpha';
public string $version = 'gamma';
/** @var object $regions */
public object $regions;
@ -28,15 +28,63 @@ class TaxModel
* @param TaxModel $model
* @return void
*/
public function __construct(public ?TaxModel $model = null)
public function __construct(public mixed $model = null)
{
if(!$this->model) {
if(!$model) {
$this->regions = $this->init();
} else {
$this->regions = $model;
//@phpstan-ignore-next-line
foreach($model as $key => $value) {
$this->{$key} = $value;
}
}
$this->migrate();
}
public function migrate(): self
{
if($this->version == 'alpha') {
$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->version = 'beta';
}
if($this->version == 'beta') {
//CEUTA
$this->regions->EU->subregions->{'ES-CE'} = new \stdClass();
$this->regions->EU->subregions->{'ES-CE'}->tax_rate = 4;
$this->regions->EU->subregions->{'ES-CE'}->tax_name = 'IGIC';
$this->regions->EU->subregions->{'ES-CE'}->reduced_tax_rate = 4;
$this->regions->EU->subregions->{'ES-CE'}->apply_tax = false;
//MELILLA ML 4
$this->regions->EU->subregions->{'ES-ML'} = new \stdClass();
$this->regions->EU->subregions->{'ES-ML'}->tax_rate = 4;
$this->regions->EU->subregions->{'ES-ML'}->tax_name = 'IGIC';
$this->regions->EU->subregions->{'ES-ML'}->reduced_tax_rate = 4;
$this->regions->EU->subregions->{'ES-ML'}->apply_tax = false;
//CANARIAS CN 7/3
$this->regions->EU->subregions->{'ES-CN'} = new \stdClass();
$this->regions->EU->subregions->{'ES-CN'}->tax_rate = 7;
$this->regions->EU->subregions->{'ES-CN'}->tax_name = 'IGIC';
$this->regions->EU->subregions->{'ES-CN'}->reduced_tax_rate = 4;
$this->regions->EU->subregions->{'ES-CN'}->apply_tax = false;
$this->version = 'gamma';
}
return $this;
}
/**
@ -397,6 +445,25 @@ class TaxModel
$this->regions->EU->subregions->ES->reduced_tax_rate = 10;
$this->regions->EU->subregions->ES->apply_tax = false;
$this->regions->EU->subregions->{'ES-CE'} = new \stdClass();
$this->regions->EU->subregions->{'ES-CE'}->tax_rate = 4;
$this->regions->EU->subregions->{'ES-CE'}->tax_name = 'IGIC';
$this->regions->EU->subregions->{'ES-CE'}->reduced_tax_rate = 4;
$this->regions->EU->subregions->{'ES-CE'}->apply_tax = false;
$this->regions->EU->subregions->{'ES-ML'} = new \stdClass();
$this->regions->EU->subregions->{'ES-ML'}->tax_rate = 4;
$this->regions->EU->subregions->{'ES-ML'}->tax_name = 'IGIC';
$this->regions->EU->subregions->{'ES-ML'}->reduced_tax_rate = 4;
$this->regions->EU->subregions->{'ES-ML'}->apply_tax = false;
$this->regions->EU->subregions->{'ES-CN'} = new \stdClass();
$this->regions->EU->subregions->{'ES-CN'}->tax_rate = 7;
$this->regions->EU->subregions->{'ES-CN'}->tax_name = 'IGIC';
$this->regions->EU->subregions->{'ES-CN'}->reduced_tax_rate = 3;
$this->regions->EU->subregions->{'ES-CN'}->apply_tax = false;
$this->regions->EU->subregions->FI = new \stdClass();
$this->regions->EU->subregions->FI->tax_rate = 24;
$this->regions->EU->subregions->FI->tax_name = 'ALV';
@ -474,6 +541,12 @@ class TaxModel
$this->regions->EU->subregions->NL->reduced_tax_rate = 9;
$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->tax_rate = 23;
$this->regions->EU->subregions->PT->tax_name = 'IVA';

View File

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

View File

@ -0,0 +1,68 @@
<?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\DataProviders;
final class CAProvinces
{
/**
* The provinces and territories of Canada
*
* @var array
*/
protected static $provinces = [
'AB' => 'Alberta',
'BC' => 'British Columbia',
'MB' => 'Manitoba',
'NB' => 'New Brunswick',
'NL' => 'Newfoundland And Labrador',
'NS' => 'Nova Scotia',
'ON' => 'Ontario',
'PE' => 'Prince Edward Island',
'QC' => 'Quebec',
'SK' => 'Saskatchewan',
'NT' => 'Northwest Territories',
'NU' => 'Nunavut',
'YT' => 'Yukon'
];
/**
* Get the name of the province or territory for a given abbreviation.
*
* @param string $abbreviation
* @return string
*/
public static function getName($abbreviation)
{
return self::$provinces[$abbreviation];
}
/**
* Get all provinces and territories.
*
* @return array
*/
public static function get()
{
return self::$provinces;
}
/**
* Get the abbreviation for a given province or territory name.
*
* @param string $name
* @return string
*/
public static function getAbbreviation($name)
{
return array_search(ucwords($name), self::$provinces);
}
}

View File

@ -1,24 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataProviders;
/**
* Class FACT1.
*/
class FACT1
{
public function build()
{
$i = new \InvoiceNinja\EInvoice\Models\FACT1\Invoice();
}
}

View File

@ -34006,7 +34006,7 @@ class USStates
'WA', 'WA', 'WA', 'WA', 'WA', 'WA', 'WA', 'AK', 'AK', 'AK', 'AK', 'AK'
];
$prefix = substr($zip, 0, 3);
$prefix = substr(($zip ?? ''), 0, 3);
$index = intval($prefix);
/* converts prefix to integer */
return $zip_by_state[$index] == "--" ? false : $zip_by_state[$index];

22
app/Enum/HttpVerb.php Normal file
View File

@ -0,0 +1,22 @@
<?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\Enum;
enum HttpVerb: string
{
case POST = 'post';
case PUT = 'put';
case GET = 'get';
case PATCH = 'patch';
case DELETE = 'delete';
}

View File

@ -13,15 +13,21 @@ namespace App\Events\Client;
use App\Models\Client;
use App\Models\Company;
use League\Fractal\Manager;
use League\Fractal\Resource\Item;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use App\Transformers\ArraySerializer;
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 implements ShouldBroadcast
{
use Dispatchable;
use InteractsWithSockets;
@ -50,13 +56,34 @@ class ClientWasArchived
$this->event_vars = $event_vars;
}
// /**
// * Get the channels the event should broadcast on.
// *
// * @return Channel|array
// */
public function broadcastWith()
{
$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()
{
return [];
return [
new PrivateChannel("company-{$this->company->company_key}"),
];
}
}

View File

@ -0,0 +1,35 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Events\Invoice;
use App\Models\Company;
use App\Models\Invoice;
use Illuminate\Queue\SerializesModels;
/**
* Class InvoiceAutoBillFailed.
*/
class InvoiceAutoBillFailed
{
use SerializesModels;
/**
* Create a new event instance.
*
* @param Invoice $invoice
* @param Company $company
* @param array $event_vars
*/
public function __construct(public Invoice $invoice, public Company $company, public array $event_vars, public ?string $notes)
{
}
}

View File

@ -0,0 +1,35 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Events\Invoice;
use App\Models\Company;
use App\Models\Invoice;
use Illuminate\Queue\SerializesModels;
/**
* Class InvoiceAutoBillSuccess.
*/
class InvoiceAutoBillSuccess
{
use SerializesModels;
/**
* Create a new event instance.
*
* @param Invoice $invoice
* @param Company $company
* @param array $event_vars
*/
public function __construct(public Invoice $invoice, public Company $company, public array $event_vars)
{
}
}

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -14,14 +15,16 @@ namespace App\Events\Invoice;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\Payment;
use App\Utils\Traits\Invoice\Broadcasting\DefaultInvoiceBroadcast;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;
/**
* Class InvoiceWasPaid.
*/
class InvoiceWasPaid
class InvoiceWasPaid implements ShouldBroadcast
{
use SerializesModels;
use SerializesModels, DefaultInvoiceBroadcast;
/**
* @var Invoice

View File

@ -0,0 +1,28 @@
<?php
/**
* Quote Ninja (https://Quoteninja.com).
*
* @link https://github.com/Quoteninja/Quoteninja source repository
*
* @copyright Copyright (c) 2024. Quote Ninja LLC (https://Quoteninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Events\Quote;
use App\Models\Company;
use App\Models\QuoteInvitation;
use Illuminate\Queue\SerializesModels;
/**
* Class QuoteReminderWasEmailed.
*/
class QuoteReminderWasEmailed
{
use SerializesModels;
public function __construct(public QuoteInvitation $invitation, public Company $company, public array $event_vars, public string $template)
{
}
}

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\Exceptions;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class DuplicatePaymentException extends Exception
{
/**
* Report the exception.
*
* @return void
*/
public function report()
{
//
}
/**
* Render the exception into an HTTP response.
*
* @param Request $request
* @return JsonResponse
*/
public function render($request)
{
return response()->json([
'message' => 'Duplicate request',
], 400);
}
}

View File

@ -18,6 +18,7 @@ use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
use Illuminate\Database\Eloquent\RelationNotFoundException;
use Illuminate\Encryption\MissingAppKeyException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Http\Request;
@ -94,25 +95,16 @@ class Handler extends ExceptionHandler
*/
public function report(Throwable $exception)
{
if (! Schema::hasTable('accounts')) {
info('account table not found');
return;
}
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 {
$name = 'hosted@invoiceninja.com';
if (auth()->guard('contact') && auth()->guard('contact')->user()) {
if (auth()->guard('contact') && auth()->guard('contact')->user()) { // @phpstan-ignore-line
$name = 'Contact = '.auth()->guard('contact')->user()->email;
$key = auth()->guard('contact')->user()->company->account->key;
} elseif (auth()->guard('user') && auth()->guard('user')->user()) {
} elseif (auth()->guard('user') && auth()->guard('user')->user()) { // @phpstan-ignore-line
$name = 'Admin = '.auth()->guard('user')->user()->email;
$key = auth()->user()->account->key;
} else {
@ -131,13 +123,14 @@ class Handler extends ExceptionHandler
}
} elseif (app()->bound('sentry')) {
Integration::configureScope(function (Scope $scope): void {
if (auth()->guard('contact') && auth()->guard('contact')->user() && auth()->guard('contact')->user()->company->account->report_errors) {
if (auth()->guard('contact') && auth()->guard('contact')->user() && auth()->guard('contact')->user()->company->account->report_errors) {// @phpstan-ignore-line
$scope->setUser([
'id' => auth()->guard('contact')->user()->company->account->key,
'email' => 'anonymous@example.com',
'name' => 'Anonymous User',
]);
} elseif (auth()->guard('user') && auth()->guard('user')->user() && auth()->user()->companyIsSet() && auth()->user()->company()->account->report_errors) {
} elseif (auth()->guard('user') && auth()->guard('user')->user() && auth()->user()->companyIsSet() && auth()->user()->company()->account->report_errors) {// @phpstan-ignore-line
$scope->setUser([
'id' => auth()->user()->account->key,
'email' => 'anonymous@example.com',
@ -152,6 +145,10 @@ class Handler extends ExceptionHandler
}
parent::report($exception);
if (Ninja::isSelfHost() && $exception instanceof MissingAppKeyException) {
info('To setup the app run: cp .env.example .env');
}
}
private function validException($exception)

View File

@ -0,0 +1,34 @@
<?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\Exceptions;
use Exception;
class PeppolValidationException extends Exception
{
protected string $field = '';
public function __construct($message, $field, $code = 0, Exception $previous = null)
{
// Store the custom data
$this->field = $field;
// Ensure that everything is assigned properly by calling the parent constructor
parent::__construct($message, $code, $previous);
}
public function getInvalidField()
{
return $this->field;
}
}

View File

@ -25,8 +25,6 @@ use League\Csv\Writer;
class ActivityExport extends BaseExport
{
private $entity_transformer;
public string $date_key = 'created_at';
private string $date_format = 'YYYY-MM-DD';
@ -43,7 +41,7 @@ class ActivityExport extends BaseExport
{
$this->company = $company;
$this->input = $input;
$this->entity_transformer = new ActivityTransformer();
}
public function returnJson()
@ -59,6 +57,7 @@ class ActivityExport extends BaseExport
$report = $query->cursor()
->map(function ($resource) {
/** @var \App\Models\Activity $resource */
$row = $this->buildActivityRow($resource);
return $this->processMetaData($row, $resource);
})->toArray();
@ -102,7 +101,10 @@ class ActivityExport extends BaseExport
$t = app('translator');
$t->replace(Ninja::transformTranslations($this->company->settings));
$this->date_format = DateFormat::find($this->company->settings->date_format_id)->format;
/** @var \App\Models\DateFormat $df */
$df = DateFormat::query()->find($this->company->settings->date_format_id);
$this->date_format = $df->format;
if (count($this->input['report_keys']) == 0) {
$this->input['report_keys'] = array_values($this->entity_keys);
@ -111,7 +113,7 @@ class ActivityExport extends BaseExport
$query = Activity::query()
->where('company_id', $this->company->id);
$query = $this->addDateRange($query);
$query = $this->addDateRange($query, 'activities');
return $query;
}
@ -130,6 +132,9 @@ class ActivityExport extends BaseExport
$query->cursor()
->each(function ($entity) {
/** @var \App\Models\Activity $entity */
$this->buildRow($entity);
});
@ -143,10 +148,10 @@ class ActivityExport extends BaseExport
}
private function decorateAdvancedFields(Task $task, array $entity): array
{
return $entity;
}
// private function decorateAdvancedFields(Task $task, array $entity): array
// {
// return $entity;
// }
public function processMetaData(array $row, $resource): array

View File

@ -172,6 +172,7 @@ class BaseExport
'tax_rate3' => 'invoice.tax_rate3',
'recurring_invoice' => 'invoice.recurring_id',
'auto_bill' => 'invoice.auto_bill_enabled',
'project' => 'invoice.project',
];
protected array $recurring_invoice_report_keys = [
@ -449,6 +450,8 @@ class BaseExport
'status' => 'task.status_id',
'project' => 'task.project_id',
'billable' => 'task.billable',
'item_notes' => 'task.item_notes',
'time_log' => 'task.time_log',
];
protected array $forced_client_fields = [
@ -842,7 +845,7 @@ class BaseExport
/**
* Apply Product Filters
*
* @param Builder $query
* @param \Illuminate\Database\Eloquent\Builder $query
*
* @return Builder
*/
@ -867,7 +870,7 @@ class BaseExport
/**
* Add Client Filter
*
* @param Builder $query
* @param \Illuminate\Database\Eloquent\Builder $query
* @param mixed $clients
*
* @return Builder
@ -890,7 +893,7 @@ class BaseExport
/**
* Add Vendor Filter
*
* @param Builder $query
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $vendors
*
* @return Builder
@ -914,7 +917,7 @@ class BaseExport
/**
* AddProjectFilter
*
* @param Builder $query
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $projects
*
* @return Builder
@ -938,7 +941,7 @@ class BaseExport
/**
* Add Category Filter
*
* @param Builder $query
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $expense_categories
*
* @return Builder
@ -963,7 +966,7 @@ class BaseExport
/**
* Add Payment Status Filters
*
* @param Builder $query
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $status
*
* @return Builder
@ -971,9 +974,10 @@ class BaseExport
protected function addPaymentStatusFilters(Builder $query, string $status): Builder
{
/** @var array $status_parameters */
$status_parameters = explode(',', $status);
if(in_array('all', $status_parameters) || count($status_parameters) == 0) {
if((count($status_parameters) == 0) || in_array('all', $status_parameters)) {
return $query;
}
@ -1020,22 +1024,27 @@ class BaseExport
/**
* Add RecurringInvoice Status Filter
*
* @param Builder $query
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $status
*
* @return Builder
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function addRecurringInvoiceStatusFilter(Builder $query, string $status): Builder
{
/** @var array $status_parameters */
$status_parameters = explode(',', $status);
if (in_array('all', $status_parameters) || count($status_parameters) == 0){
if (in_array('all', $status_parameters) || count($status_parameters) == 0) {
return $query;
}
$recurring_filters = [];
if($this->company->getSetting('report_include_drafts')) {
$recurring_filters[] = RecurringInvoice::STATUS_DRAFT;
}
if (in_array('active', $status_parameters)) {
$recurring_filters[] = RecurringInvoice::STATUS_ACTIVE;
}
@ -1058,7 +1067,7 @@ class BaseExport
/**
* Add QuoteStatus Filter
*
* @param Builder $query
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $status
*
* @return Builder
@ -1124,7 +1133,7 @@ class BaseExport
/**
* Add PurchaseOrder Status Filter
*
* @param Builder $query
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $status
*
* @return Builder
@ -1132,6 +1141,7 @@ class BaseExport
protected function addPurchaseOrderStatusFilter(Builder $query, string $status): Builder
{
/** @var array $status_parameters */
$status_parameters = explode(',', $status);
if (in_array('all', $status_parameters) || count($status_parameters) == 0) {
@ -1173,13 +1183,14 @@ class BaseExport
/**
* Add Invoice Status Filter
*
* @param Builder $query
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $status
* @return Builder
*/
protected function addInvoiceStatusFilter(Builder $query, string $status): Builder
{
/** @var array $status_parameters */
$status_parameters = explode(',', $status);
if(in_array('all', $status_parameters) || count($status_parameters) == 0) {
@ -1238,16 +1249,17 @@ class BaseExport
/**
* Add Date Range
*
* @param Builder $query
* @param \Illuminate\Database\Eloquent\Builder $query
* @param ?string $table_name
* @return Builder
*/
protected function addDateRange(Builder $query): Builder
protected function addDateRange(Builder $query, ?string $table_name = null): Builder
{
$query = $this->applyProductFilters($query);
$date_range = $this->input['date_range'];
if (array_key_exists('date_key', $this->input) && strlen($this->input['date_key']) > 1) {
if (array_key_exists('date_key', $this->input) && strlen($this->input['date_key'] ?? '') > 1 && ($table_name && $this->columnExists($table_name, $this->input['date_key']))) {
$this->date_key = $this->input['date_key'];
}
@ -1605,4 +1617,17 @@ class BaseExport
}
}
/**
* Tests that the column exists
* on the table prior to adding it to
* the query builder
*
* @param string $table
* @param string $column
* @return bool
*/
public function columnExists($table, $column): bool
{
return \Illuminate\Support\Facades\Schema::hasColumn($table, $column);
}
}

View File

@ -102,6 +102,8 @@ class ClientExport extends BaseExport
$report = $query->cursor()
->map(function ($client) {
/** @var \App\Models\Client $client */
$row = $this->buildRow($client);
return $this->processMetaData($row, $client);
})->toArray();
@ -127,10 +129,11 @@ class ClientExport extends BaseExport
->withTrashed()
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false)
if(!$this->input['include_deleted'] ?? false) {
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);
$query = $this->addDateRange($query, ' clients');
if($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query);
@ -153,6 +156,8 @@ class ClientExport extends BaseExport
$query->cursor()
->each(function ($client) {
/** @var \App\Models\Client $client */
$this->csv->insertOne($this->buildRow($client));
});
@ -242,16 +247,16 @@ class ClientExport extends BaseExport
return $entity;
}
private function calculateStatus($client)
{
if ($client->is_deleted) {
return ctrans('texts.deleted');
}
// private function calculateStatus($client)
// {
// if ($client->is_deleted) {
// return ctrans('texts.deleted');
// }
if ($client->deleted_at) {
return ctrans('texts.archived');
}
// if ($client->deleted_at) {
// return ctrans('texts.archived');
// }
return ctrans('texts.active');
}
// return ctrans('texts.active');
// }
}

View File

@ -59,11 +59,11 @@ class ContactExport extends BaseExport
$query = ClientContact::query()
->where('company_id', $this->company->id)
->whereHas('client', function ($q){
->whereHas('client', function ($q) {
$q->where('is_deleted', false);
});
$query = $this->addDateRange($query);
$query = $this->addDateRange($query, 'client_contacts');
return $query;
@ -82,6 +82,7 @@ class ContactExport extends BaseExport
$this->csv->insertOne($this->buildHeader());
$query->cursor()->each(function ($contact) {
/** @var \App\Models\ClientContact $contact */
$this->csv->insertOne($this->buildRow($contact));
});
@ -101,6 +102,7 @@ class ContactExport extends BaseExport
$report = $query->cursor()
->map(function ($contact) {
/** @var \App\Models\ClientContact $contact */
$row = $this->buildRow($contact);
return $this->processMetaData($row, $contact);
})->toArray();
@ -155,7 +157,7 @@ class ContactExport extends BaseExport
}
if (in_array('client.user_id', $this->input['report_keys'])) {
$entity['client.user_id'] = $client->user ? $client->user->present()->name() : '';
$entity['client.user_id'] = $client->user ? $client->user->present()->name() : '';// @phpstan-ignore-line
}
if (in_array('client.assigned_user_id', $this->input['report_keys'])) {

View File

@ -52,6 +52,8 @@ class CreditExport extends BaseExport
$report = $query->cursor()
->map(function ($credit) {
/** @var \App\Models\Credit $credit */
$row = $this->buildRow($credit);
return $this->processMetaData($row, $credit);
})->toArray();
@ -102,13 +104,13 @@ class CreditExport extends BaseExport
$query = Credit::query()
->withTrashed()
->with('client')
->whereHas('client', function ($q){
->whereHas('client', function ($q) {
$q->where('is_deleted', false);
})
->where('company_id', $this->company->id)
->where('is_deleted', $this->input['include_deleted'] ?? false);
$query = $this->addDateRange($query);
$query = $this->addDateRange($query, 'credits');
$clients = &$this->input['client_id'];
@ -139,6 +141,7 @@ class CreditExport extends BaseExport
$query->cursor()
->each(function ($credit) {
/** @var \App\Models\Credit $credit */
$this->csv->insertOne($this->buildRow($credit));
});
@ -241,7 +244,7 @@ class CreditExport extends BaseExport
}
if (in_array('credit.user_id', $this->input['report_keys'])) {
$entity['credit.user_id'] = $credit->user ? $credit->user->present()->name() : '';
$entity['credit.user_id'] = $credit->user ? $credit->user->present()->name() : ''; //@phpstan-ignore-line
}
return $entity;

View File

@ -54,6 +54,8 @@ class DocumentExport extends BaseExport
$report = $query->cursor()
->map(function ($document) {
/** @var \App\Models\Document $document */
$row = $this->buildRow($document);
return $this->processMetaData($row, $document);
})->toArray();
@ -76,7 +78,7 @@ class DocumentExport extends BaseExport
$query = Document::query()->where('company_id', $this->company->id);
$query = $this->addDateRange($query);
$query = $this->addDateRange($query, 'documents');
if($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query);
@ -99,6 +101,7 @@ class DocumentExport extends BaseExport
$query->cursor()
->each(function ($entity) {
/** @var mixed $entity */
$this->csv->insertOne($this->buildRow($entity));
});

View File

@ -52,6 +52,8 @@ class ExpenseExport extends BaseExport
$report = $query->cursor()
->map(function ($resource) {
/** @var \App\Models\Expense $resource */
$row = $this->buildRow($resource);
return $this->processMetaData($row, $resource);
})->toArray();
@ -85,11 +87,11 @@ class ExpenseExport extends BaseExport
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
if(!$this->input['include_deleted'] ?? false) { // @phpstan-ignore-line
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);
$query = $this->addDateRange($query, 'expenses');
if($this->input['status'] ?? false) {
$query = $this->addExpenseStatusFilter($query, $this->input['status']);
@ -132,6 +134,8 @@ class ExpenseExport extends BaseExport
$query->cursor()
->each(function ($expense) {
/** @var \App\Models\Expense $expense */
$this->csv->insertOne($this->buildRow($expense));
});
@ -220,17 +224,17 @@ class ExpenseExport extends BaseExport
// $entity['expense.client'] = $expense->client ? $expense->client->present()->name() : '';
// }
// if (in_array('expense.invoice_id', $this->input['report_keys'])) {
// $entity['expense.invoice_id'] = $expense->invoice ? $expense->invoice->number : '';
// }
if (in_array('expense.invoice_id', $this->input['report_keys'])) {
$entity['expense.invoice_id'] = $expense->invoice ? $expense->invoice->number : '';
}
// if (in_array('expense.category', $this->input['report_keys'])) {
// $entity['expense.category'] = $expense->category ? $expense->category->name : '';
// }
// if (in_array('expense.vendor_id', $this->input['report_keys'])) {
// $entity['expense.vendor'] = $expense->vendor ? $expense->vendor->name : '';
// }
if (in_array('expense.vendor_id', $this->input['report_keys'])) {
$entity['expense.vendor'] = $expense->vendor ? $expense->vendor->name : '';
}
// if (in_array('expense.payment_type_id', $this->input['report_keys'])) {
// $entity['expense.payment_type_id'] = $expense->payment_type ? $expense->payment_type->name : '';
@ -259,10 +263,16 @@ class ExpenseExport extends BaseExport
{
$precision = $expense->currency->precision ?? 2;
$entity['expense.net_amount'] = round($expense->amount, $precision);
if($expense->calculate_tax_by_amount) {
$total_tax_amount = round($expense->tax_amount1 + $expense->tax_amount2 + $expense->tax_amount3, $precision);
if($expense->uses_inclusive_taxes) {
$entity['expense.net_amount'] = round($expense->amount, $precision) - $total_tax_amount;
} else {
$entity['expense.net_amount'] = round($expense->amount, $precision);
}
} else {
if($expense->uses_inclusive_taxes) {

View File

@ -57,17 +57,17 @@ class InvoiceExport extends BaseExport
$query = Invoice::query()
->withTrashed()
->with('client')
->whereHas('client', function ($q){
->whereHas('client', function ($q) {
$q->where('is_deleted', false);
})
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
if(!$this->input['include_deleted'] ?? false) {// @phpstan-ignore-line
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);
$query = $this->addDateRange($query, 'invoices');
$clients = &$this->input['client_id'];
@ -99,6 +99,8 @@ class InvoiceExport extends BaseExport
$report = $query->cursor()
->map(function ($resource) {
/** @var \App\Models\Invoice $resource */
$row = $this->buildRow($resource);
return $this->processMetaData($row, $resource);
})->toArray();
@ -119,6 +121,8 @@ class InvoiceExport extends BaseExport
$query->cursor()
->each(function ($invoice) {
/** @var \App\Models\Invoice $invoice */
$this->csv->insertOne($this->buildRow($invoice));
});
@ -149,9 +153,9 @@ class InvoiceExport extends BaseExport
private function decorateAdvancedFields(Invoice $invoice, array $entity): array
{
// if (in_array('invoice.status', $this->input['report_keys'])) {
// $entity['invoice.status'] = $invoice->stringStatus($invoice->status_id);
// }
if (in_array('invoice.project', $this->input['report_keys'])) {
$entity['invoice.project'] = $invoice->project ? $invoice->project->name : '';
}
if (in_array('invoice.recurring_id', $this->input['report_keys'])) {
$entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? '';
@ -166,7 +170,8 @@ class InvoiceExport extends BaseExport
}
if (in_array('invoice.user_id', $this->input['report_keys'])) {
$entity['invoice.user_id'] = $invoice->user ? $invoice->user->present()->name() : '';
$entity['invoice.user_id'] = $invoice->user ? $invoice->user->present()->name() : ''; // @phpstan-ignore-line
}
return $entity;

View File

@ -70,16 +70,16 @@ class InvoiceItemExport extends BaseExport
$query = Invoice::query()
->withTrashed()
->with('client')
->whereHas('client', function ($q){
->whereHas('client', function ($q) {
$q->where('is_deleted', false);
})
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
if(!$this->input['include_deleted'] ?? false) {// @phpstan-ignore-line
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);
$query = $this->addDateRange($query, 'invoices');
$clients = &$this->input['client_id'];
@ -113,6 +113,8 @@ class InvoiceItemExport extends BaseExport
$query->cursor()
->each(function ($resource) {
/** @var \App\Models\Invoice $resource */
$this->iterateItems($resource);
foreach($this->storage_array as $row) {
@ -141,6 +143,8 @@ class InvoiceItemExport extends BaseExport
$query->cursor()
->each(function ($invoice) {
/** @var \App\Models\Invoice $invoice */
$this->iterateItems($invoice);
});
@ -225,10 +229,6 @@ class InvoiceItemExport extends BaseExport
// $entity['currency'] = $invoice->client->currency() ? $invoice->client->currency()->code : $invoice->company->currency()->code;
// }
// if(array_key_exists('type', $entity)) {
// $entity['type'] = $invoice->typeIdString($entity['type']);
// }
// if(array_key_exists('tax_category', $entity)) {
// $entity['tax_category'] = $invoice->taxTypeString($entity['tax_category']);
// }
@ -258,7 +258,11 @@ class InvoiceItemExport extends BaseExport
}
if (in_array('invoice.user_id', $this->input['report_keys'])) {
$entity['invoice.user_id'] = $invoice->user ? $invoice->user->present()->name() : '';
$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;

View File

@ -56,13 +56,13 @@ class PaymentExport extends BaseExport
$query = Payment::query()
->withTrashed()
->whereHas('client', function ($q){
->whereHas('client', function ($q) {
$q->where('is_deleted', false);
})
->where('company_id', $this->company->id)
->where('is_deleted', 0);
$query = $this->addDateRange($query);
$query = $this->addDateRange($query, 'payments');
$clients = &$this->input['client_id'];
@ -92,6 +92,8 @@ class PaymentExport extends BaseExport
$report = $query->cursor()
->map(function ($resource) {
/** @var \App\Models\Payment $resource */
$row = $this->buildRow($resource);
return $this->processMetaData($row, $resource);
})->toArray();
@ -112,6 +114,8 @@ class PaymentExport extends BaseExport
$query->cursor()
->each(function ($entity) {
/** @var \App\Models\Payment $entity */
$this->csv->insertOne($this->buildRow($entity));
});

View File

@ -51,6 +51,8 @@ class ProductExport extends BaseExport
$report = $query->cursor()
->map(function ($resource) {
/** @var \App\Models\Product $resource */
$row = $this->buildRow($resource);
return $this->processMetaData($row, $resource);
})->toArray();
@ -75,12 +77,11 @@ class ProductExport extends BaseExport
->withTrashed()
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
if(!$this->input['include_deleted'] ?? false) { //@phpstan-ignore-line
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);
$query = $this->addDateRange($query, 'products');
if($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query);
@ -104,6 +105,8 @@ class ProductExport extends BaseExport
$query->cursor()
->each(function ($entity) {
/** @var \App\Models\Product $entity */
$this->csv->insertOne($this->buildRow($entity));
});
@ -133,16 +136,16 @@ class ProductExport extends BaseExport
// return $this->decorateAdvancedFields($product, $entity);
}
private function decorateAdvancedFields(Product $product, array $entity): array
{
if (in_array('vendor_id', $this->input['report_keys'])) {
$entity['vendor'] = $product->vendor()->exists() ? $product->vendor->name : '';
}
// private function decorateAdvancedFields(Product $product, array $entity): array
// {
// if (in_array('vendor_id', $this->input['report_keys'])) {
// $entity['vendor'] = $product->vendor()->exists() ? $product->vendor->name : '';
// }
// if (array_key_exists('project_id', $this->input['report_keys'])) {
// $entity['project'] = $product->project()->exists() ? $product->project->name : '';
// }
// // if (array_key_exists('project_id', $this->input['report_keys'])) {
// // $entity['project'] = $product->project()->exists() ? $product->project->name : '';
// // }
return $entity;
}
// return $entity;
// }
}

View File

@ -25,6 +25,7 @@ class ProductSalesExport extends BaseExport
{
public string $date_key = 'created_at';
/** @var Collection<\App\Models\Product> $products*/
protected Collection $products;
public Writer $csv;
@ -65,11 +66,11 @@ class ProductSalesExport extends BaseExport
'custom_value4' => 'custom_value4',
];
private array $decorate_keys = [
'client',
'currency',
'date',
];
// private array $decorate_keys = [
// 'client',
// 'currency',
// 'date',
// ];
public function __construct(Company $company, array $input)
{
@ -86,9 +87,9 @@ class ProductSalesExport extends BaseExport
if ($product_keys && !empty($this->input['product_key'])) {
$keys = explode(",", $product_keys);
$query->where(function ($q) use ($keys){
$query->where(function ($q) use ($keys) {
foreach($keys as $key) {
foreach($keys as $key) {
$q->orWhereJsonContains('line_items', ['product_key' => $key]);
}
@ -121,14 +122,14 @@ class ProductSalesExport extends BaseExport
//insert the header
$query = Invoice::query()
->withTrashed()
->whereHas('client', function ($q){
->whereHas('client', function ($q) {
$q->where('is_deleted', false);
})
->where('company_id', $this->company->id)
->where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL, Invoice::STATUS_PAID]);
$query = $this->addDateRange($query);
$query = $this->addDateRange($query, 'invoices');
$query = $this->filterByClients($query);
@ -138,30 +139,29 @@ class ProductSalesExport extends BaseExport
$product_keys = &$this->input['product_key'];
if($product_keys){
if($product_keys) {
$product_keys = explode(",", $product_keys);
}
$query->cursor()
->each(function ($invoice) use($product_keys) {
->each(function ($invoice) use ($product_keys) {
foreach ($invoice->line_items as $item) {
if($product_keys)
{
if(in_array($item->product_key, $product_keys))
$this->csv->insertOne($this->buildRow($invoice, $item));
}
else {
$this->csv->insertOne($this->buildRow($invoice, $item));
}
if($product_keys) {
if(in_array($item->product_key, $product_keys)) {
$this->csv->insertOne($this->buildRow($invoice, $item));
}
} else {
$this->csv->insertOne($this->buildRow($invoice, $item));
}
}
});
$grouped = $this->sales->groupBy('product_key')->map(function ($key, $value) use($product_keys){
$grouped = $this->sales->groupBy('product_key')->map(function ($key, $value) use ($product_keys) {
if($product_keys && !in_array($value, $product_keys)){
if($product_keys && !in_array($value, $product_keys)) {
return false;
}
@ -185,7 +185,8 @@ class ProductSalesExport extends BaseExport
})->reject(function ($value) {
return $value === false;
});;
});
;
$this->csv->insertOne([]);
$this->csv->insertOne([]);
@ -327,10 +328,10 @@ class ProductSalesExport extends BaseExport
* getProduct
*
* @param string $product_key
* @return Product
* @return ?\Illuminate\Database\Eloquent\Model
*/
private function getProduct(string $product_key): ?Product
{
return $this->products->firstWhere('product_key', $product_key);
}
// private function getProduct(string $product_key)
// {
// return $this->products->firstWhere('product_key', $product_key);
// }
}

View File

@ -58,22 +58,23 @@ class PurchaseOrderExport extends BaseExport
$query = PurchaseOrder::query()
->withTrashed()
->with('vendor')
->whereHas('vendor', function ($q){
->whereHas('vendor', function ($q) {
$q->where('is_deleted', false);
})
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
if(!$this->input['include_deleted'] ?? false) { // @phpstan-ignore-line
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);
$query = $this->addDateRange($query, 'purchase_orders');
$clients = &$this->input['client_id'];
if($clients)
if($clients) {
$query = $this->addClientFilter($query, $clients);
}
$query = $this->addPurchaseOrderStatusFilter($query, $this->input['status'] ?? '');
@ -97,6 +98,8 @@ class PurchaseOrderExport extends BaseExport
$report = $query->cursor()
->map(function ($resource) {
/** @var \App\Models\PurchaseOrder $resource */
$row = $this->buildRow($resource);
return $this->processMetaData($row, $resource);
})->toArray();
@ -118,6 +121,8 @@ class PurchaseOrderExport extends BaseExport
$query->cursor()
->each(function ($purchase_order) {
/** @var \App\Models\PurchaseOrder $purchase_order */
$this->csv->insertOne($this->buildRow($purchase_order));
});
@ -166,7 +171,8 @@ class PurchaseOrderExport extends BaseExport
}
if (in_array('purchase_order.user_id', $this->input['report_keys'])) {
$entity['purchase_order.user_id'] = $purchase_order->user ? $purchase_order->user->present()->name() : '';
$entity['purchase_order.user_id'] = $purchase_order->user ? $purchase_order->user->present()->name() : ''; // @phpstan-ignore-line
}
if (in_array('purchase_order.assigned_user_id', $this->input['report_keys'])) {

View File

@ -62,16 +62,16 @@ class PurchaseOrderItemExport extends BaseExport
$query = PurchaseOrder::query()
->withTrashed()
->whereHas('vendor', function ($q){
->whereHas('vendor', function ($q) {
$q->where('is_deleted', false);
})
->with('vendor')->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
if(!$this->input['include_deleted'] ?? false) {
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);
$query = $this->addDateRange($query, 'purchase_orders');
$clients = &$this->input['client_id'];
@ -101,6 +101,8 @@ class PurchaseOrderItemExport extends BaseExport
$query->cursor()
->each(function ($resource) {
/** @var \App\Models\PurchaseOrder $resource */
$this->iterateItems($resource);
foreach($this->storage_array as $row) {
@ -127,6 +129,8 @@ class PurchaseOrderItemExport extends BaseExport
$query->cursor()
->each(function ($purchase_order) {
/** @var \App\Models\PurchaseOrder $purchase_order */
$this->iterateItems($purchase_order);
});
@ -209,10 +213,6 @@ class PurchaseOrderItemExport extends BaseExport
// $entity['currency'] = $purchase_order->vendor->currency() ? $purchase_order->vendor->currency()->code : $purchase_order->company->currency()->code;
// }
// if(array_key_exists('type', $entity)) {
// $entity['type'] = $purchase_order->typeIdString($entity['type']);
// }
// if(array_key_exists('tax_category', $entity)) {
// $entity['tax_category'] = $purchase_order->taxTypeString($entity['tax_category']);
// }

View File

@ -31,11 +31,11 @@ class QuoteExport extends BaseExport
private Decorator $decorator;
private array $decorate_keys = [
'client',
'currency',
'invoice',
];
// private array $decorate_keys = [
// 'client',
// 'currency',
// 'invoice',
// ];
public function __construct(Company $company, array $input)
{
@ -64,16 +64,16 @@ class QuoteExport extends BaseExport
$query = Quote::query()
->withTrashed()
->with('client')
->whereHas('client', function ($q){
->whereHas('client', function ($q) {
$q->where('is_deleted', false);
})
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
if(!$this->input['include_deleted'] ?? false) {
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);
$query = $this->addDateRange($query, 'quotes');
$clients = &$this->input['client_id'];
@ -103,6 +103,8 @@ class QuoteExport extends BaseExport
$report = $query->cursor()
->map(function ($resource) {
/** @var \App\Models\Quote $resource */
$row = $this->buildRow($resource);
return $this->processMetaData($row, $resource);
})->toArray();
@ -125,6 +127,8 @@ class QuoteExport extends BaseExport
$query->cursor()
->each(function ($quote) {
/** @var \App\Models\Quote $quote */
$this->csv->insertOne($this->buildRow($quote));
});

View File

@ -65,16 +65,16 @@ class QuoteItemExport extends BaseExport
$query = Quote::query()
->withTrashed()
->whereHas('client', function ($q){
->whereHas('client', function ($q) {
$q->where('is_deleted', false);
})
->with('client')->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
if(!$this->input['include_deleted'] ?? false) {
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);
$query = $this->addDateRange($query, 'quotes');
$clients = &$this->input['client_id'];
@ -104,6 +104,8 @@ class QuoteItemExport extends BaseExport
$query->cursor()
->each(function ($resource) {
/** @var \App\Models\Quote $resource */
$this->iterateItems($resource);
foreach($this->storage_array as $row) {
@ -134,6 +136,8 @@ class QuoteItemExport extends BaseExport
$query->cursor()
->each(function ($quote) {
/** @var \App\Models\Quote $quote */
$this->iterateItems($quote);
});

View File

@ -56,16 +56,16 @@ class RecurringInvoiceExport extends BaseExport
$query = RecurringInvoice::query()
->withTrashed()
->with('client')
->whereHas('client', function ($q){
->whereHas('client', function ($q) {
$q->where('is_deleted', false);
})
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
if(!$this->input['include_deleted'] ?? false) {
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);
$query = $this->addDateRange($query, 'recurring_invoices');
$clients = &$this->input['client_id'];
@ -93,6 +93,8 @@ class RecurringInvoiceExport extends BaseExport
$query->cursor()
->each(function ($invoice) {
/** @var \App\Models\RecurringInvoice $invoice */
$this->csv->insertOne($this->buildRow($invoice));
});
@ -112,6 +114,8 @@ class RecurringInvoiceExport extends BaseExport
$report = $query->cursor()
->map(function ($resource) {
/** @var \App\Models\RecurringInvoice $resource */
$row = $this->buildRow($resource);
return $this->processMetaData($row, $resource);
})->toArray();

View File

@ -29,9 +29,9 @@ class TaskExport extends BaseExport
{
private $entity_transformer;
public string $date_key = 'created_at';
public string $date_key = 'calculated_start_date';
private string $date_format = 'YYYY-MM-DD';
private string $date_format = 'Y-m-d';
public Writer $csv;
@ -70,21 +70,23 @@ class TaskExport extends BaseExport
->withTrashed()
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
if(!$this->input['include_deleted'] ?? false) {
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);
$query = $this->addDateRange($query, 'tasks');
$clients = &$this->input['client_id'];
if($clients)
if($clients) {
$query = $this->addClientFilter($query, $clients);
}
$document_attachments = &$this->input['document_email_attachment'];
if($document_attachments)
if($document_attachments) {
$this->queueDocuments($query);
}
return $query;
@ -104,6 +106,8 @@ class TaskExport extends BaseExport
$query->cursor()
->each(function ($entity) {
/** @var \App\Models\Task $entity*/
$this->buildRow($entity);
});
@ -126,6 +130,7 @@ class TaskExport extends BaseExport
$query->cursor()
->each(function ($resource) {
/** @var \App\Models\Task $resource*/
$this->buildRow($resource);
foreach($this->storage_array as $row) {
@ -151,7 +156,7 @@ class TaskExport extends BaseExport
$entity[$key] = $transformed_entity[$parts[1]];
} elseif (array_key_exists($key, $transformed_entity)) {
$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', 'task.time_log'])) {
$entity[$key] = '';
} else {
$entity[$key] = $this->decorator->transform($key, $task);
@ -159,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;
} else {
$this->iterateLogs($task, $entity);
@ -170,31 +175,25 @@ class TaskExport extends BaseExport
private function iterateLogs(Task $task, array $entity)
{
$timezone = Timezone::find($task->company->settings->timezone_id);
$timezone_name = 'US/Eastern';
$timezone_name = 'America/New_York';
if ($timezone) {
$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 = DateFormat::find($task->company->settings->date_format_id);
if ($date_format) {
$date_format_default = $date_format->format;
}
$date_format_default = $this->date_format;
foreach ($logs as $key => $item) {
if (in_array('task.start_date', $this->input['report_keys']) || in_array('start_date', $this->input['report_keys'])) {
$carbon_object = Carbon::createFromTimeStamp($item[0])->setTimezone($timezone_name);
$carbon_object = Carbon::createFromTimeStamp((int)$item[0])->setTimezone($timezone_name);
$entity['task.start_date'] = $carbon_object->format($date_format_default);
$entity['task.start_time'] = $carbon_object->format('H:i:s');
}
if ((in_array('task.end_date', $this->input['report_keys']) || in_array('end_date', $this->input['report_keys'])) && $item[1] > 0) {
$carbon_object = Carbon::createFromTimeStamp($item[1])->setTimezone($timezone_name);
$carbon_object = Carbon::createFromTimeStamp((int)$item[1])->setTimezone($timezone_name);
$entity['task.end_date'] = $carbon_object->format($date_format_default);
$entity['task.end_time'] = $carbon_object->format('H:i:s');
}
@ -208,6 +207,17 @@ class TaskExport extends BaseExport
$seconds = $task->calcDuration();
$entity['task.duration'] = $seconds;
$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.time_log'] = (isset($item[1]) && $item[1] != 0) ? $item[1] - $item[0] : ctrans('texts.is_running');
}
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);
@ -220,6 +230,8 @@ class TaskExport extends BaseExport
$entity['task.end_time'] = '';
$entity['task.duration'] = '';
$entity['task.duration_words'] = '';
$entity['task.billable'] = '';
$entity['task.item_notes'] = '';
}
@ -234,7 +246,7 @@ class TaskExport extends BaseExport
*/
protected function addTaskStatusFilter(Builder $query, string $status): Builder
{
/** @var array $status_parameters */
$status_parameters = explode(',', $status);
if (in_array('all', $status_parameters) || count($status_parameters) == 0) {

View File

@ -64,11 +64,11 @@ class VendorExport extends BaseExport
->withTrashed()
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
if(!$this->input['include_deleted'] ?? false) {
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);
$query = $this->addDateRange($query, 'vendors');
if($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query);
@ -90,6 +90,8 @@ class VendorExport extends BaseExport
$report = $query->cursor()
->map(function ($resource) {
/** @var \App\Models\Vendor $resource */
$row = $this->buildRow($resource);
return $this->processMetaData($row, $resource);
})->toArray();
@ -107,6 +109,8 @@ class VendorExport extends BaseExport
$query->cursor()
->each(function ($vendor) {
/** @var \App\Models\Vendor $vendor */
$this->csv->insertOne($this->buildRow($vendor));
});
@ -171,16 +175,16 @@ class VendorExport extends BaseExport
return $entity;
}
private function calculateStatus($vendor)
{
if ($vendor->is_deleted) {
return ctrans('texts.deleted');
}
// private function calculateStatus($vendor)
// {
// if ($vendor->is_deleted) {
// return ctrans('texts.deleted');
// }
if ($vendor->deleted_at) {
return ctrans('texts.archived');
}
// if ($vendor->deleted_at) {
// return ctrans('texts.archived');
// }
return ctrans('texts.active');
}
// return ctrans('texts.active');
// }
}

View File

@ -92,6 +92,7 @@ class InvoiceDecorator extends Decorator implements DecoratorInterface
{
return $invoice->recurring_invoice ? $invoice->recurring_invoice->number : '';
}
public function auto_bill_enabled(Invoice $invoice)
{
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
{
//@todo - we do not handle iterating through the timelog here.
public function transform(string $key, mixed $entity): mixed
{
$task = false;
@ -42,13 +43,13 @@ class TaskDecorator extends Decorator implements DecoratorInterface
{
$timezone = Timezone::find($task->company->settings->timezone_id);
$timezone_name = 'US/Eastern';
$timezone_name = 'America/New_York';
if ($timezone) {
$timezone_name = $timezone->name;
}
$logs = json_decode($task->time_log, 1);
$logs = json_decode($task->time_log, true);
$date_format_default = 'Y-m-d';
@ -60,7 +61,7 @@ class TaskDecorator extends Decorator implements DecoratorInterface
if(is_array($logs)) {
$item = $logs[0];
return Carbon::createFromTimeStamp($item[0])->setTimezone($timezone_name)->format($date_format_default);
return Carbon::createFromTimeStamp((int)$item[0])->setTimezone($timezone_name)->format($date_format_default);
}
return '';
@ -71,13 +72,13 @@ class TaskDecorator extends Decorator implements DecoratorInterface
{
$timezone = Timezone::find($task->company->settings->timezone_id);
$timezone_name = 'US/Eastern';
$timezone_name = 'America/New_York';
if ($timezone) {
$timezone_name = $timezone->name;
}
$logs = json_decode($task->time_log, 1);
$logs = json_decode($task->time_log, true);
$date_format_default = 'Y-m-d';
@ -89,12 +90,32 @@ class TaskDecorator extends Decorator implements DecoratorInterface
if(is_array($logs)) {
$item = $logs[1];
return Carbon::createFromTimeStamp($item[1])->setTimezone($timezone_name)->format($date_format_default);
return Carbon::createFromTimeStamp((int)$item[1])->setTimezone($timezone_name)->format($date_format_default);
}
return '';
}
/**
* billable
*
* @todo
*/
public function billable(Task $task)
{
return '';
}
/**
* items_notes
* @todo
*/
public function items_notes(Task $task)
{
return '';
}
public function duration(Task $task)
{
return $task->calcDuration();

View File

@ -23,14 +23,13 @@ class BankIntegrationFactory
$bank_integration->company_id = $company_id;
$bank_integration->provider_name = '';
$bank_integration->bank_account_id = '';
$bank_integration->bank_account_name = '';
$bank_integration->bank_account_number = '';
$bank_integration->bank_account_status = '';
$bank_integration->bank_account_type = '';
$bank_integration->balance = 0;
$bank_integration->currency = '';
$bank_integration->auto_sync = 1;
$bank_integration->auto_sync = true;
return $bank_integration;
}

View File

@ -29,7 +29,7 @@ class ClientFactory
$client->balance = 0;
$client->paid_to_date = 0;
$client->country_id = null;
$client->is_deleted = 0;
$client->is_deleted = false;
$client->client_hash = Str::random(40);
$client->settings = ClientSettings::defaults();
$client->classification = '';

View File

@ -33,7 +33,7 @@ class CloneQuoteToProjectFactory
$project->custom_value2 = '';
$project->custom_value3 = '';
$project->custom_value4 = '';
$project->is_deleted = 0;
$project->is_deleted = false;
return $project;
}

View File

@ -48,7 +48,7 @@ class CompanyFactory
$company->markdown_email_enabled = true;
$company->markdown_enabled = false;
$company->tax_data = new TaxModel();
$company->first_month_of_year = 1;
$company->first_month_of_year = '1';
$company->smtp_encryption = 'tls';
$company->smtp_host = '';
$company->smtp_local_domain = '';

View File

@ -16,7 +16,7 @@ use App\Models\Credit;
class CreditFactory
{
public static function create(int $company_id, int $user_id, object $settings = null, Client $client = null): Credit
public static function create(int $company_id, int $user_id): Credit
{
$credit = new Credit();
$credit->status_id = Credit::STATUS_DRAFT;

View File

@ -76,6 +76,26 @@ class InvoiceItemFactory
$data[] = $item;
}
$item = self::create();
$item->quantity = $faker->numberBetween(1, 10);
$item->cost = $faker->randomFloat(2, 1, 1000);
$item->line_total = $item->quantity * $item->cost;
$item->is_amount_discount = true;
$item->discount = $faker->numberBetween(1, 10);
$item->notes = str_replace(['"',"'"], ['',""], $faker->realText(20));
$item->product_key = $faker->word();
// $item->custom_value1 = $faker->realText(10);
// $item->custom_value2 = $faker->realText(10);
// $item->custom_value3 = $faker->realText(10);
// $item->custom_value4 = $faker->realText(10);
$item->tax_name1 = 'GST';
$item->tax_rate1 = 10.00;
$item->type_id = '2';
$data[] = $item;
return $data;
}

View File

@ -82,11 +82,15 @@ class RecurringExpenseToExpenseFactory
} else {
$locale = $recurring_expense->company->locale();
$date_formats = Cache::get('date_formats');
//@deprecated
// $date_formats = Cache::get('date_formats');
$date_format = $date_formats->filter(function ($item) use ($recurring_expense) {
/** @var \Illuminate\Support\Collection<\App\Models\DateFormat> */
$date_formats = app('date_formats');
$date_format = $date_formats->first(function ($item) use ($recurring_expense) {
return $item->id == $recurring_expense->company->settings->date_format_id;
})->first()->format;
})->format;
}
Carbon::setLocale($locale);
@ -144,45 +148,45 @@ class RecurringExpenseToExpenseFactory
continue;
}
if (Str::contains($match, '|')) {
$parts = explode('|', $match); // [ '[MONTH', 'MONTH+2]' ]
// if (Str::contains($match, '|')) {
$parts = explode('|', $match); // [ '[MONTH', 'MONTH+2]' ]
$left = substr($parts[0], 1); // 'MONTH'
$right = substr($parts[1], 0, -1); // MONTH+2
$left = substr($parts[0], 1); // 'MONTH'
$right = substr($parts[1], 0, -1); // MONTH+2
// If left side is not part of replacements, skip.
if (! array_key_exists($left, $replacements['ranges'])) {
continue;
}
$_left = Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F Y');
$_right = '';
// If right side doesn't have any calculations, replace with raw ranges keyword.
if (! Str::contains($right, ['-', '+', '/', '*'])) {
$_right = Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F Y');
}
// If right side contains one of math operations, calculate.
if (Str::contains($right, ['+'])) {
$operation = preg_match_all('/(?!^-)[+*\/-](\s?-)?/', $right, $_matches);
$_operation = array_shift($_matches)[0]; // + -
$_value = explode($_operation, $right); // [MONTHYEAR, 4]
$_right = Carbon::createFromDate(now()->year, now()->month)->addMonths($_value[1])->translatedFormat('F Y');
}
$replacement = sprintf('%s to %s', $_left, $_right);
$value = preg_replace(
sprintf('/%s/', preg_quote($match)),
$replacement,
$value,
1
);
// If left side is not part of replacements, skip.
if (! array_key_exists($left, $replacements['ranges'])) {
continue;
}
$_left = Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F Y');
$_right = '';
// If right side doesn't have any calculations, replace with raw ranges keyword.
if (! Str::contains($right, ['-', '+', '/', '*'])) {
$_right = Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F Y');
}
// If right side contains one of math operations, calculate.
if (Str::contains($right, ['+'])) {
$operation = preg_match_all('/(?!^-)[+*\/-](\s?-)?/', $right, $_matches);
$_operation = array_shift($_matches)[0]; // + -
$_value = explode($_operation, $right); // [MONTHYEAR, 4]
$_right = Carbon::createFromDate(now()->year, now()->month)->addMonths($_value[1])->translatedFormat('F Y'); //@phpstan-ignore-line
}
$replacement = sprintf('%s to %s', $_left, $_right);
$value = preg_replace(
sprintf('/%s/', preg_quote($match)),
$replacement,
$value,
1
);
// }
}
// Second case with more common calculations.

View File

@ -23,7 +23,7 @@ class SubscriptionFactory
$billing_subscription->company_id = $company_id;
$billing_subscription->user_id = $user_id;
$billing_subscription->steps = collect(Purchase::defaultSteps())
->map(fn($step) => StepService::mapClassNameToString($step))
->map(fn ($step) => StepService::mapClassNameToString($step))
->implode(',');
return $billing_subscription;

View File

@ -20,7 +20,7 @@ class TaxRateFactory
$tax_rate = new TaxRate();
$tax_rate->name = '';
$tax_rate->rate = '';
$tax_rate->rate = 0;
$tax_rate->company_id = $company_id;
$tax_rate->user_id = $user_id;

View File

@ -26,7 +26,7 @@ class VendorFactory
$vendor->private_notes = '';
$vendor->public_notes = '';
$vendor->country_id = 4;
$vendor->is_deleted = 0;
$vendor->is_deleted = false;
$vendor->vendor_hash = Str::random(40);
// $vendor->classification = '';

View File

@ -68,7 +68,7 @@ class BankTransactionFilters extends QueryFilters
*/
public function client_status(string $value = ''): Builder
{
if (strlen($value) == 0) {
if (strlen($value ?? '') == 0) {
return $this->builder;
}
@ -108,13 +108,47 @@ class BankTransactionFilters extends QueryFilters
}
if (count($debit_or_withdrawal_array) >= 1) {
$query->orWhereIn('base_type', $debit_or_withdrawal_array);
$query->whereIn('base_type', $debit_or_withdrawal_array);
}
});
return $this->builder;
}
public function active_banks(string $value = ''): Builder
{
if (strlen($value) == 0 || $value != 'true') {
return $this->builder;
}
return $this->builder->whereHas('bank_integration', function ($query){
$query->where('is_deleted', 0)->whereNull('deleted_at');
});
}
/**
* Filters the list based on Bank Accounts.
*
* @param string $ids Comma Separated List of bank account ids
* @return Builder
*/
public function bank_integration_ids(string $ids = ''): Builder
{
if(strlen($ids) == 0) {
return $this->builder;
}
$ids = $this->transformKeys(explode(",", $ids));
$this->builder->where(function ($query) use ($ids) {
$query->whereIn('bank_integration_id', $ids);
});
return $this->builder;
}
/**
* Sorts the list based on $sort.
*
@ -132,11 +166,13 @@ class BankTransactionFilters extends QueryFilters
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
if ($sort_col[0] == 'deposit') {
return $this->builder->where('base_type', 'CREDIT')->orderBy('amount', $dir);
return $this->builder->orderByRaw("(CASE WHEN base_type = 'CREDIT' THEN amount END) $dir")->orderBy('amount', $dir);
// return $this->builder->where('base_type', 'CREDIT')->orderBy('amount', $dir);
}
if ($sort_col[0] == 'withdrawal') {
return $this->builder->where('base_type', 'DEBIT')->orderBy('amount', $dir);
return $this->builder->orderByRaw("(CASE WHEN base_type = 'DEBIT' THEN amount END) $dir")->orderBy('amount', $dir);
// return $this->builder->where('base_type', 'DEBIT')->orderBy('amount', $dir);
}
if ($sort_col[0] == 'status') {

View File

@ -41,7 +41,7 @@ class ClientFilters extends QueryFilters
*/
public function balance(string $balance = ''): Builder
{
if (strlen($balance) == 0) {
if (strlen($balance) == 0 || count(explode(":", $balance)) < 2) {
return $this->builder;
}
@ -160,8 +160,9 @@ class ClientFilters extends QueryFilters
return $this->builder;
}
if($sort_col[0] == 'documents')
if($sort_col[0] == 'documents') {
return $this->builder;
}
if ($sort_col[0] == 'display_name') {
$sort_col[0] = 'name';

View File

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

View File

@ -59,8 +59,9 @@ class DesignFilters extends QueryFilters
public function entities(string $entities = ''): Builder
{
if(stripos($entities, 'statement') !== false)
if(stripos($entities, 'statement') !== false) {
$entities = 'client';
}
if (strlen($entities) == 0 || str_contains($entities, ',')) {
return $this->builder;

View File

@ -62,9 +62,9 @@ class DocumentFilters extends QueryFilters
\App\Models\RecurringInvoice::class,
\App\Models\Project::class,
], function ($q2) use ($client_id) {
$q2->where('client_id', $this->decodePrimaryKey($client_id));
})->orWhereHasMorph('documentable', [\App\Models\Client::class], function ($q3) use ($client_id) {
$q3->where('id', $this->decodePrimaryKey($client_id));
$q2->where('client_id', $this->decodePrimaryKey($client_id));
})->orWhereHasMorph('documentable', [\App\Models\Client::class], function ($q3) use ($client_id) {
$q3->where('id', $this->decodePrimaryKey($client_id));
});
});
@ -74,8 +74,7 @@ class DocumentFilters extends QueryFilters
{
$types = explode(',', $types);
foreach ($types as $type)
{
foreach ($types as $type) {
match($type) {
'private' => $this->builder->where('is_public', 0),
'public' => $this->builder->where('is_public', 1),

View File

@ -44,6 +44,9 @@ class ExpenseFilters extends QueryFilters
})
->orWhereHas('vendor', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%');
})
->orWhereHas('client', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%');
});
});
}
@ -76,7 +79,7 @@ class ExpenseFilters extends QueryFilters
$this->builder->where(function ($query) use ($status_parameters) {
if (in_array('logged', $status_parameters)) {
$query->orWhere(function ($query) {
$query->where('amount', '>', 0)
$query->where('amount', '>=', 0)
->whereNull('invoice_id')
->whereNull('payment_date')
->where('should_be_invoiced', false);
@ -96,6 +99,12 @@ class ExpenseFilters extends QueryFilters
});
}
if (in_array('uninvoiced', $status_parameters)) {
$query->orWhere(function ($query) {
$query->whereNull('invoice_id');
});
}
if (in_array('paid', $status_parameters)) {
$query->orWhere(function ($query) {
$query->whereNotNull('payment_date');
@ -108,8 +117,8 @@ class ExpenseFilters extends QueryFilters
});
}
if(in_array('uncategorized', $status_parameters)){
$query->orWhere(function ($query){
if(in_array('uncategorized', $status_parameters)) {
$query->orWhere(function ($query) {
$query->whereNull('category_id');
});
}
@ -155,6 +164,19 @@ class ExpenseFilters extends QueryFilters
return $this->builder;
}
public function categories(string $categories = ''): Builder
{
$categories_exploded = explode(",", $categories);
if(empty($categories) || count(array_filter($categories_exploded)) == 0) {
return $this->builder;
}
$categories_keys = $this->transformKeys($categories_exploded);
return $this->builder->whereIn('category_id', $categories_keys);
}
public function number(string $number = ''): Builder
{
if (strlen($number) == 0) {
@ -202,6 +224,11 @@ class ExpenseFilters extends QueryFilters
->whereColumn('expense_categories.id', 'expenses.category_id'), $sort_col[1]);
}
if ($sort_col[0] == 'payment_date' && in_array($sort_col[1], ['asc', 'desc'])) {
return $this->builder
->orderByRaw('ISNULL(payment_date), payment_date '. $sort_col[1]);
}
if($sort_col[0] == 'number') {
return $this->builder->orderByRaw("REGEXP_REPLACE(number,'[^0-9]+','')+0 " . $dir);
}

View File

@ -124,7 +124,15 @@ class InvoiceFilters extends QueryFilters
$q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%');
});
})
->orWhereRaw("
JSON_UNQUOTE(JSON_EXTRACT(
JSON_ARRAY(
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')),
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].product_key'))
), '$[*]')
) LIKE ?", ['%'.$filter.'%']);
// ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
});
}
@ -228,10 +236,9 @@ class InvoiceFilters extends QueryFilters
$date = Carbon::createFromTimestamp((int)$date);
} else {
try{
try {
$date = Carbon::parse($date);
}
catch(\Exception $e){
} catch(\Exception $e) {
return $this->builder;
}
}
@ -259,59 +266,6 @@ class InvoiceFilters extends QueryFilters
return $this->builder->where('due_date', '>=', $date);
}
/**
* Filter by date range
*
* @param string $date_range
* @return Builder
*/
public function date_range(string $date_range = ''): Builder
{
$parts = explode(",", $date_range);
if (count($parts) != 2) {
return $this->builder;
}
try {
$start_date = Carbon::parse($parts[0]);
$end_date = Carbon::parse($parts[1]);
return $this->builder->whereBetween('date', [$start_date, $end_date]);
} catch(\Exception $e) {
return $this->builder;
}
return $this->builder;
}
/**
* Filter by due date range
*
* @param string $date_range
* @return Builder
*/
public function due_date_range(string $date_range = ''): Builder
{
$parts = explode(",", $date_range);
if (count($parts) != 2) {
return $this->builder;
}
try {
$start_date = Carbon::parse($parts[0]);
$end_date = Carbon::parse($parts[1]);
return $this->builder->whereBetween('due_date', [$start_date, $end_date]);
} catch(\Exception $e) {
return $this->builder;
}
return $this->builder;
}
/**
* Sorts the list based on $sort.
*
@ -322,7 +276,7 @@ class InvoiceFilters extends QueryFilters
{
$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;
}
@ -339,10 +293,10 @@ class InvoiceFilters extends QueryFilters
// return $this->builder->orderByRaw('CAST(number AS UNSIGNED), number ' . $dir);
// return $this->builder->orderByRaw("number REGEXP '^[A-Za-z]+$',CAST(number as SIGNED INTEGER),CAST(REPLACE(number,'-','')AS SIGNED INTEGER) ,number");
// return $this->builder->orderByRaw('ABS(number) ' . $dir);
return $this->builder->orderByRaw("REGEXP_REPLACE(number,'[^0-9]+','')+0 " . $dir);
return $this->builder->orderByRaw("REGEXP_REPLACE(invoices.number,'[^0-9]+','')+0 " . $dir);
}
return $this->builder->orderBy($sort_col[0], $dir);
return $this->builder->orderBy("{$this->builder->getQuery()->from}.".$sort_col[0], $dir);
}
/**

View File

@ -204,7 +204,6 @@ class PaymentFilters extends QueryFilters
return $this->builder;
}
return $this->builder;
}
/**

View File

@ -60,7 +60,7 @@ class ProjectFilters extends QueryFilters
{
$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], \Illuminate\Support\Facades\Schema::getColumnListing('projects'))) {
return $this->builder;
}

View File

@ -96,7 +96,14 @@ class PurchaseOrderFilters extends QueryFilters
->orWhere('custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('vendor', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%');
});
})
->orWhereRaw("
JSON_UNQUOTE(JSON_EXTRACT(
JSON_ARRAY(
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')),
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].product_key'))
), '$[*]')
) LIKE ?", ['%'.$filter.'%']);
});
}

View File

@ -331,4 +331,61 @@ abstract class QueryFilters
->orderByRaw("{$this->with_property} = ? DESC", [$value])
->company();
}
/**
* Filter by date range
*
* @param string $date_range
* @return Builder
*/
public function date_range(string $date_range = ''): Builder
{
$parts = explode(",", $date_range);
if (count($parts) != 2 || !in_array('date', \Illuminate\Support\Facades\Schema::getColumnListing($this->builder->getModel()->getTable()))) {
return $this->builder;
}
try {
$start_date = Carbon::parse($parts[0]);
$end_date = Carbon::parse($parts[1]);
return $this->builder->whereBetween('date', [$start_date, $end_date]);
} catch(\Exception $e) {
return $this->builder;
}
}
/**
* Filter by due date range
*
* @param string $date_range
* @return Builder
*/
public function due_date_range(string $date_range = ''): Builder
{
$parts = explode(",", $date_range);
if (count($parts) != 2 || !in_array('due_date', \Illuminate\Support\Facades\Schema::getColumnListing($this->builder->getModel()->getTable()))) {
return $this->builder;
}
try {
$start_date = Carbon::parse($parts[0]);
$end_date = Carbon::parse($parts[1]);
return $this->builder->whereBetween('due_date', [$start_date, $end_date]);
} catch(\Exception $e) {
return $this->builder;
}
}
}

View File

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

View File

@ -48,7 +48,15 @@ class RecurringInvoiceFilters extends QueryFilters
$q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%');
});
})
->orWhereRaw("
JSON_UNQUOTE(JSON_EXTRACT(
JSON_ARRAY(
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')),
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].product_key'))
), '$[*]')
) LIKE ?", ['%'.$filter.'%']);
//->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
});
}
@ -133,7 +141,11 @@ class RecurringInvoiceFilters extends QueryFilters
return $this->builder->orderByRaw("REGEXP_REPLACE(number,'[^0-9]+','')+0 " . $dir);
}
if($sort_col[0] == 'next_send_datetime'){
if($sort_col[0] == 'status_id') {
return $this->builder->orderBy('status_id', $dir)->orderBy('last_sent_date', $dir);
}
if($sort_col[0] == 'next_send_datetime') {
$sort_col[0] = 'next_send_date';
}
@ -162,9 +174,10 @@ class RecurringInvoiceFilters extends QueryFilters
return $this->builder;
}
/** @var array $key_parameters */
$key_parameters = explode(',', $value);
if (count($key_parameters)) {
if (count($key_parameters) > 0) {
return $this->builder->where(function ($query) use ($key_parameters) {
foreach ($key_parameters as $key) {
$query->orWhereJsonContains('line_items', ['product_key' => $key]);
@ -183,6 +196,7 @@ class RecurringInvoiceFilters extends QueryFilters
*/
public function next_send_between(string $range = ''): Builder
{
/** @var array $parts */
$parts = explode('|', $range);
if (!isset($parts[0]) || !isset($parts[1])) {

View File

@ -26,7 +26,6 @@ class TaskFilters extends QueryFilters
*
* @param string $filter
* @return Builder
* @deprecated
*/
public function filter(string $filter = ''): Builder
{
@ -86,10 +85,14 @@ class TaskFilters extends QueryFilters
$this->builder->whereNull('invoice_id');
}
if (in_array('is_running', $status_parameters)) {
$this->builder->where('is_running', true);
}
return $this->builder;
}
public function project_tasks($project): Builder
public function project_tasks(string $project = ''): Builder
{
if (strlen($project) == 0) {
return $this->builder;
@ -176,6 +179,7 @@ class TaskFilters extends QueryFilters
return $this->builder;
}
/** @var array $status_parameters */
$status_parameters = explode(',', $value);
if(count($status_parameters) >= 1) {

View File

@ -46,7 +46,7 @@ class WebhookFilters extends QueryFilters
{
$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], \Illuminate\Support\Facades\Schema::getColumnListing('webhooks'))) {
return $this->builder;
}

View File

@ -24,9 +24,10 @@ use App\Services\Email\Email;
use App\Models\BankIntegration;
use App\Services\Email\EmailObject;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Mail\Mailables\Address;
use App\Helpers\Bank\Nordigen\Transformer\AccountTransformer;
use App\Helpers\Bank\Nordigen\Transformer\TransactionTransformer;
use Illuminate\Mail\Mailables\Address;
class Nordigen
{
@ -149,6 +150,11 @@ class Nordigen
public function disabledAccountEmail(BankIntegration $bank_integration): void
{
$cache_key = "email_quota:{$bank_integration->company->company_key}:{$bank_integration->id}";
if(Cache::has($cache_key)) {
return;
}
App::setLocale($bank_integration->company->getLocale());
@ -164,6 +170,7 @@ class Nordigen
Email::dispatch($mo, $bank_integration->company);
Cache::put($cache_key, true, 60 * 60 * 24);
}

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