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

@ -23,4 +23,5 @@ API_SECRET=superdoopersecrethere
PHANTOMJS_PDF_GENERATION=false PHANTOMJS_PDF_GENERATION=false
CACHE_DRIVER=redis CACHE_DRIVER=redis
QUEUE_CONNECTION=redis QUEUE_CONNECTION=redis
SESSION_DRIVER=redis SESSION_DRIVER=redis
PDF_GENERATOR=snappdf

View File

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

View File

@ -8,7 +8,7 @@ assignees: ''
--- ---
<!-- Before posting please check our "Troubleshooting" category in the docs: <!-- 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 ## Setup
- Version: <!-- i.e. v4.5.25 / v5.0.30 --> - Version: <!-- i.e. v4.5.25 / v5.0.30 -->

View File

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

View File

@ -38,16 +38,22 @@ jobs:
sudo php artisan cache:clear sudo php artisan cache:clear
sudo find ./vendor/bin/ -type f -exec chmod +x {} \; sudo find ./vendor/bin/ -type f -exec chmod +x {} \;
sudo find ./ -type d -exec chmod 755 {} \; sudo find ./ -type d -exec chmod 755 {} \;
- name: Set current date to variable
id: set_date
run: echo "current_date=$(date '+%Y-%m-%d')" >> $GITHUB_ENV
- name: Prepare React FrontEnd - name: Prepare React FrontEnd
run: | run: |
git clone https://${{secrets.commit_secret}}@github.com/invoiceninja/ui.git git clone https://${{secrets.commit_secret}}@github.com/invoiceninja/ui.git
cd ui cd ui
git checkout develop git checkout develop
cp .env.example .env
cp ../vite.config.ts.react ./vite.config.js
sed -i '/"version"/c\ "version": " Latest Build - ${{ env.current_date }}",' package.json
npm i npm i
npm run build npm run build
cp -r dist/* ../public/ cp -r dist/* ../public/
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 - name: Prepare JS/CSS assets
run: | run: |
@ -60,11 +66,12 @@ jobs:
sudo rm -rf node_modules sudo rm -rf node_modules
sudo rm -rf .git sudo rm -rf .git
sudo rm .env sudo rm .env
sudo rm -rf ui
- name: Build project - name: Build project
run: | run: |
shopt -s dotglob 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 - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
@ -72,4 +79,4 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
files: | 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"> <p align="center">
<img src="https://raw.githubusercontent.com/hillelcoren/invoice-ninja/master/public/images/round_logo.png" alt="Sublime's custom image"/> <a href ="https://www.youtube.com/watch?v=CxGxXiotv0I" target="_blank" title="Invoice Ninja Overview Video"><img src="https://raw.githubusercontent.com/hillelcoren/invoice-ninja/master/public/images/round_logo.png" alt="Sublime's custom image"/></a>
</p> </p>
![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop) ![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop)
@ -8,25 +8,30 @@
# Invoice Ninja 5 # Invoice Ninja 5
## [Hosted](https://www.invoiceninja.com) | [Self-Hosted](https://www.invoiceninja.org) Invoice Ninja Version 5 is here! We've taken the best parts of version 4 and added the most requested features to create an invoicing application like no other. Check the [Invoice Ninja YouTube Channel](https://www.youtube.com/@appinvoiceninja) to get up to speed, or try the [Demo](https://react.invoicing.co/demo) now.
Join us on [Slack](http://slack.invoiceninja.com), [Discord](https://discord.gg/ZwEdtfCwXA), [Support Forum](https://forum.invoiceninja.com) **Choose your setup**
## Introduction - [Hosted](https://www.invoiceninja.com): Our hosted version is a Software as a Service (SaaS) solution. You're up and running in under 5 minutes, with no need to worry about hosting or server infrastructure.
- [Self-Hosted](https://www.invoiceninja.org): For those who prefer to manage their own hosting and server infrastructure. This version gives you full control and flexibility.
Version 5 of Invoice Ninja is here! All Pro and Enterprise features from the hosted app are included in the open-source code. We offer a $30 per year white-label license to remove the Invoice Ninja branding from client-facing parts of the app.
We took the best parts of version 4 and add the most requested features
to produce a invoicing application like no other.
All Pro and Enterprise features from the hosted app are included in the open code. #### Get social with us
We offer a $30 per year white-label license to remove the Invoice Ninja branding from client facing parts of the app.
* [Videos](https://www.youtube.com/@appinvoiceninja)
* [API Documentation](https://api-docs.invoicing.co/)
* [APP Documentation](https://invoiceninja.github.io/)
* [Support Forum](https://forum.invoiceninja.com) * [Support Forum](https://forum.invoiceninja.com)
* [Slack](http://slack.invoiceninja.com)
* [Discord](https://discord.gg/ZwEdtfCwXA)
* [Instagram](https://www.instagram.com/appinvoiceninja)
## Setup #### Documentation
* [Invoice Ninja - API](https://api-docs.invoicing.co/)
* [Invoice Ninja - Developer Guide](https://invoiceninja.github.io/en/developer-guide/)
* [Invoice Ninja - User Guide](https://invoiceninja.github.io/en/user-guide/)
* [Invoice Ninja - Self-Hosted Installation Guide](https://invoiceninja.github.io/en/self-host-installation/)
## Installation Options and Clients
### Mobile Apps ### Mobile Apps
* [iPhone](https://apps.apple.com/app/id1503970375?platform=iphone) * [iPhone](https://apps.apple.com/app/id1503970375?platform=iphone)
@ -39,22 +44,28 @@ We offer a $30 per year white-label license to remove the Invoice Ninja branding
* [Linux - Snap](https://snapcraft.io/invoiceninja) * [Linux - Snap](https://snapcraft.io/invoiceninja)
* [Linux - Flatpak](https://flathub.org/apps/com.invoiceninja.InvoiceNinja) * [Linux - Flatpak](https://flathub.org/apps/com.invoiceninja.InvoiceNinja)
### Installation Options ### Self-Hosted Server Installation
**Note:** The self-hosted options do support the desktop and mobile apps.
* [Server or VM](https://invoiceninja.github.io/en/self-host-installation/)
* [Docker File](https://hub.docker.com/r/invoiceninja/invoiceninja/) * [Docker File](https://hub.docker.com/r/invoiceninja/invoiceninja/)
* [Cloudron](https://cloudron.io/store/com.invoiceninja.cloudronapp.html) * [Cloudron](https://www.cloudron.io/store/com.invoiceninja.cloudronapp2.html)
* [Softaculous](https://www.softaculous.com/apps/ecommerce/Invoice_Ninja) * [Softaculous](https://www.softaculous.com/apps/ecommerce/Invoice_Ninja)
* [Elestio](https://elest.io/open-source/invoiceninja)
* [YunoHost](https://apps.yunohost.org/app/invoiceninja5)
### Recommended Providers ### Recommended Providers
* [Stripe](https://stripe.com/) * [Stripe](https://stripe.com/)
* [Postmark](https://postmarkapp.com/) * [Postmark](https://postmarkapp.com/)
## Quick Hosting Setup ## [Advanced] Quick Hosting Setup
In addition to the official [Invoice Ninja - Self-Hosted Installation Guide](https://invoiceninja.github.io/en/self-host-installation/) we have a few commands for you.
```sh ```sh
git clone --single-branch --branch v5-stable https://github.com/invoiceninja/invoiceninja.git git clone --single-branch --branch v5-stable https://github.com/invoiceninja/invoiceninja.git
cp .env.example .env cp .env.example .env
composer i -o --no-dev composer i -o --no-dev
php artisan key:generate
``` ```
Please Note: Please Note:
@ -85,6 +96,7 @@ pass: password
``` ```
## Developers Guide ## Developers Guide
In addition to the official [Invoice Ninja - Developer Guide](https://invoiceninja.github.io/en/developer-guide/) we've got your back with some insights.
### App Design ### App Design

View File

@ -1 +1 @@
5.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 { try {
$doc_bin = $document->getFile(); $doc_bin = $document->getFile();
} catch(\Exception $e) { } catch(\Exception $e) {
nlog($e->getMessage()); nlog("Exception:: BackupUpdate::" . $e->getMessage());
} }
if ($doc_bin) { if ($doc_bin) {
@ -184,8 +184,6 @@ class BackupUpdate extends Command
$document->disk = $this->option('disk'); $document->disk = $this->option('disk');
$document->saveQuietly(); $document->saveQuietly();
nlog("Documents - Moving {$document->url} to {$this->option('disk')}");
} }
}); });
@ -199,8 +197,6 @@ class BackupUpdate extends Command
if ($backup_bin) { if ($backup_bin) {
Storage::disk($this->option('disk'))->put($backup->filename, $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; namespace App\Console\Commands;
use App; use App;
use App\Models\User;
use App\Utils\Ninja;
use App\Models\Quote;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Vendor;
use App\Models\Account;
use App\Models\Company;
use App\Models\Contact;
use App\Models\Expense;
use App\Models\Invoice;
use App\Models\Payment;
use App\Libraries\MultiDB;
use App\Models\CompanyUser;
use Illuminate\Support\Str;
use App\Models\CompanyToken;
use App\Models\ClientContact;
use App\Models\CompanyLedger;
use App\Models\PurchaseOrder;
use App\Models\VendorContact;
use App\Models\BankTransaction;
use App\Models\QuoteInvitation;
use Illuminate\Console\Command;
use App\Models\CreditInvitation;
use App\Models\RecurringInvoice;
use App\Models\InvoiceInvitation;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use App\Factory\ClientContactFactory; use App\Factory\ClientContactFactory;
use App\Factory\VendorContactFactory; use App\Factory\VendorContactFactory;
use App\Jobs\Company\CreateCompanyToken; use App\Jobs\Company\CreateCompanyToken;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\BankTransaction;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\CompanyLedger;
use App\Models\CompanyToken;
use App\Models\CompanyUser;
use App\Models\Contact;
use App\Models\Credit;
use App\Models\CreditInvitation;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Models\Payment;
use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Models\QuoteInvitation;
use App\Models\RecurringInvoice;
use App\Models\RecurringInvoiceInvitation; use App\Models\RecurringInvoiceInvitation;
use App\Models\User;
use App\Models\Vendor;
use App\Models\VendorContact;
use App\Utils\Ninja;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
/* /*
@ -130,6 +131,7 @@ class CheckData extends Command
$this->checkContactEmailAndSendEmailStatus(); $this->checkContactEmailAndSendEmailStatus();
$this->checkPaymentCurrency(); $this->checkPaymentCurrency();
$this->checkSubdomainsSet(); $this->checkSubdomainsSet();
$this->checkExpenseCurrency();
if (Ninja::isHosted()) { if (Ninja::isHosted()) {
$this->checkAccountStatuses(); $this->checkAccountStatuses();
@ -172,18 +174,18 @@ class CheckData extends Command
CompanyUser::query()->cursor()->each(function ($cu) { CompanyUser::query()->cursor()->each(function ($cu) {
if (CompanyToken::where('user_id', $cu->user_id)->where('company_id', $cu->company_id)->where('is_system', 1)->doesntExist()) { if (CompanyToken::where('user_id', $cu->user_id)->where('company_id', $cu->company_id)->where('is_system', 1)->doesntExist()) {
if ($cu->company && $cu->user) { if ($cu->company && $cu->user) {
$this->logMessage("Creating missing company token for user # {$cu->user_id} for company id # {$cu->company_id}"); $this->logMessage("Creating missing company token for user # {$cu->user_id} for company id # {$cu->company_id}");
(new CreateCompanyToken($cu->company, $cu->user, 'System'))->handle(); (new CreateCompanyToken($cu->company, $cu->user, 'System'))->handle();
} }
if (!$cu->user) { if (!$cu->user) {
$this->logMessage("No user found for company user - removing company user"); $this->logMessage("No user found for company user - removing company user");
$cu->forceDelete(); $cu->forceDelete();
} }
} }
}); });
} }
@ -477,14 +479,13 @@ class CheckData extends Command
} }
} else { } else {
$this->logMessage("No contact present, so cannot add invitation for {$entity_key} - {$entity->id}"); $this->logMessage("No contact present, so cannot add invitation for {$entity_key} - {$entity->id}");
try{ try {
$entity->service()->createInvitations()->save(); $entity->service()->createInvitations()->save();
} } catch(\Exception $e) {
catch(\Exception $e){
} }
} }
try { try {
@ -949,12 +950,12 @@ class CheckData extends Command
}); });
Company::whereDoesntHave('company_users', function ($query){ Company::whereDoesntHave('company_users', function ($query) {
$query->where('is_owner', 1); $query->where('is_owner', 1);
}) })
->cursor() ->cursor()
->when(Ninja::isHosted()) ->when(Ninja::isHosted())
->each(function ($c){ ->each(function ($c) {
$this->logMessage("Orphan Account # {$c->account_id}"); $this->logMessage("Orphan Account # {$c->account_id}");
@ -963,8 +964,8 @@ class CheckData extends Command
CompanyUser::whereDoesntHave('tokens') CompanyUser::whereDoesntHave('tokens')
->cursor() ->cursor()
->when(Ninja::isHosted()) ->when(Ninja::isHosted())
->each(function ($cu){ ->each(function ($cu) {
$this->logMessage("Missing tokens for Company User # {$cu->id}"); $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->info(date('r').' Create Single Account...');
$this->warmCache();
$this->createAccount(); $this->createAccount();
} }
@ -121,28 +119,6 @@ class CreateAccount extends Command
(new CreateCompanyTaskStatuses($company, $user))->handle(); (new CreateCompanyTaskStatuses($company, $user))->handle();
(new VersionCheck())->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->count = 5;
$this->gateway = $this->argument('gateway'); $this->gateway = $this->argument('gateway');
$this->info('Warming up cache');
$this->warmCache();
$this->createSmallAccount(); $this->createSmallAccount();
@ -389,6 +385,9 @@ class CreateSingleAccount extends Command
}); });
$this->countryClients($company, $user);
$this->info("finished"); $this->info("finished");
} }
@ -774,32 +773,6 @@ class CreateSingleAccount extends Command
return $line_items; 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) private function createGateways($company, $user)
{ {
if (config('ninja.testvars.stripe') && ($this->gateway == 'all' || $this->gateway == 'stripe')) { if (config('ninja.testvars.stripe') && ($this->gateway == 'all' || $this->gateway == 'stripe')) {
@ -1016,7 +989,7 @@ class CreateSingleAccount extends Command
$cg->fees_and_limits = $fees_and_limits; $cg->fees_and_limits = $fees_and_limits;
$cg->save(); $cg->save();
} }
if (config('ninja.testvars.eway') && ($this->gateway == 'all' || $this->gateway == 'eway')) { if (config('ninja.testvars.eway') && ($this->gateway == 'all' || $this->gateway == 'eway')) {
$cg = new CompanyGateway(); $cg = new CompanyGateway();
$cg->company_id = $company->id; $cg->company_id = $company->id;
@ -1038,12 +1011,12 @@ class CreateSingleAccount extends Command
$cg->save(); $cg->save();
} }
if (config('ninja.testvars.gocardless') && ($this->gateway == 'all' || $this->gateway == 'gocardless')) { if (config('ninja.testvars.gocardless') && ($this->gateway == 'all' || $this->gateway == 'gocardless')) {
$c_settings = ClientSettings::defaults(); $c_settings = ClientSettings::defaults();
$c_settings->currency_id = '2'; $c_settings->currency_id = '2';
$client = Client::factory()->create([ $client = Client::factory()->create([
'user_id' => $user->id, 'user_id' => $user->id,
'company_id' => $company->id, 'company_id' => $company->id,
@ -1139,4 +1112,44 @@ class CreateSingleAccount extends Command
event(new RecurringInvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars())); event(new RecurringInvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars()));
} }
private function countryClients($company, $user)
{
Client::unguard();
Client::create([
'company_id' => $company->id,
'user_id' => $user->id,
'name' => 'Swiss Company AG',
'website' => 'https://www.testcompany.ch',
'private_notes' => 'These are some private notes about the test client.',
'balance' => 0,
'paid_to_date' => 0,
'vat_number' => '654321987',
'id_number' => 'CH9300762011623852957', // Sample Swiss IBAN
'custom_value1' => '2024-07-22 10:00:00',
'custom_value2' => 'blue',
'custom_value3' => 'sampleword',
'custom_value4' => 'test@example.com',
'address1' => '123',
'address2' => 'Test Street 45',
'city' => 'Zurich',
'state' => 'Zurich',
'postal_code' => '8001',
'country_id' => '756', // Switzerland
'shipping_address1' => '123',
'shipping_address2' => 'Test Street 45',
'shipping_city' => 'Zurich',
'shipping_state' => 'Zurich',
'shipping_postal_code' => '8001',
'shipping_country_id' => '756', // Switzerland
'settings' => ClientSettings::Defaults(),
'client_hash' => \Illuminate\Support\Str::random(32),
'routing_id' => '',
]);
}
} }

View File

@ -86,8 +86,6 @@ class CreateTestData extends Command
$this->info('Warming up cache'); $this->info('Warming up cache');
$this->warmCache();
$this->createSmallAccount(); $this->createSmallAccount();
$this->createMediumAccount(); $this->createMediumAccount();
$this->createLargeAccount(); $this->createLargeAccount();
@ -673,31 +671,4 @@ class CreateTestData extends Command
return $line_items; 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(); $this->invoice_repo = new InvoiceRepository();
$cached_tables = config('ninja.cached_tables');
$this->info('Migrating'); $this->info('Migrating');
Artisan::call('migrate:fresh --force'); Artisan::call('migrate:fresh --force');
$this->info('Seeding'); $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->info('Seeding Random Data');
$this->createSmallAccount(); $this->createSmallAccount();
@ -623,31 +621,4 @@ class DemoMode extends Command
return $line_items; 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() public function handle()
{ {
$this->buildCache();
if (! MultiDB::userFindAndSetDb($this->option('email'))) { if (! MultiDB::userFindAndSetDb($this->option('email'))) {
$this->info('Could not find a user with that email address'); $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->faker = Factory::create();
$this->buildCache();
$path = $this->option('path') ?? public_path('storage/migrations/import'); $path = $this->option('path') ?? public_path('storage/migrations/import');
$directory = new DirectoryIterator($path); $directory = new DirectoryIterator($path);

View File

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

View File

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

View File

@ -48,37 +48,37 @@ class ReactBuilder extends Command
{ {
if($this->option('type') == 'local') { if($this->option('type') == 'local') {
$includes = '';
$directoryIterator = false; $includes = '';
try { $directoryIterator = false;
$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) { try {
if ($file->getExtension() == 'js') { $directoryIterator = new \RecursiveDirectoryIterator(public_path('react/v'.config('ninja.app_version').'/'), \RecursiveDirectoryIterator::SKIP_DOTS);
if (str_contains($file->getFileName(), 'index-')) { } catch (\Exception $e) {
$includes .= '<script type="module" crossorigin src="/react/v'.config('ninja.app_version').'/'.$file->getFileName().'"></script>'."\n"; $this->error('React files not found');
} else { return;
$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')) { file_put_contents(resource_path('views/react/head.blade.php'), $includes);
$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);
} }
} }
} }

View File

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

View File

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

View File

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

View File

@ -55,7 +55,7 @@ class DbQuery extends GenericMixedMetric
public $string_metric8 = 'client_version'; public $string_metric8 = 'client_version';
public $string_metric9 = 'platform'; public $string_metric9 = 'platform';
/** /**
* The counter * The counter
* set to 1. * set to 1.
@ -66,12 +66,14 @@ class DbQuery extends GenericMixedMetric
public $double_metric2 = 1; 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->int_metric1 = $int_metric1;
$this->string_metric5 = $string_metric5; $this->string_metric5 = $string_metric5;
$this->string_metric6 = $string_metric6; $this->string_metric6 = $string_metric6;
$this->double_metric2 = $double_metric2; $this->double_metric2 = $double_metric2;
$this->string_metric7 = $string_metric7; $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 class BaseSettings
{ {
// //@deprecated
// public function __construct($obj)
// {
// // foreach ($obj as $key => $value) {
// // $obj->{$key} = $value;
// // }
// }
public static function setCasts($obj, $casts) public static function setCasts($obj, $casts)
{ {

View File

@ -29,7 +29,7 @@ class CompanySettings extends BaseSettings
public $besr_id = ''; //@implemented 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 public $enable_client_portal_tasks = false; //@ben to implement
@ -507,7 +507,29 @@ class CompanySettings extends BaseSettings
public int $task_round_to_nearest = 1; 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 = [ 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_up' => 'bool',
'task_round_to_nearest' => 'int', 'task_round_to_nearest' => 'int',
'e_quote_type' => 'string', 'e_quote_type' => 'string',
@ -750,6 +772,8 @@ class CompanySettings extends BaseSettings
'portal_custom_js' => 'string', 'portal_custom_js' => 'string',
'client_portal_enable_uploads' => 'bool', 'client_portal_enable_uploads' => 'bool',
'purchase_order_number_counter' => 'integer', 'purchase_order_number_counter' => 'integer',
'email_template_payment_failed' => 'string',
'email_subject_payment_failed' => 'string',
]; ];
public static $free_plan_casts = [ public static $free_plan_casts = [
@ -962,6 +986,7 @@ class CompanySettings extends BaseSettings
'$invoice.due_date', '$invoice.due_date',
'$invoice.total', '$invoice.total',
'$invoice.balance_due', '$invoice.balance_due',
'$invoice.project',
], ],
'quote_details' => [ 'quote_details' => [
'$quote.number', '$quote.number',
@ -969,6 +994,7 @@ class CompanySettings extends BaseSettings
'$quote.date', '$quote.date',
'$quote.valid_until', '$quote.valid_until',
'$quote.total', '$quote.total',
'$quote.project',
], ],
'credit_details' => [ 'credit_details' => [
'$credit.number', '$credit.number',

View File

@ -30,6 +30,7 @@ class EmailTemplateDefaults
'email_template_custom2', 'email_template_custom2',
'email_template_custom3', 'email_template_custom3',
'email_template_purchase_order', 'email_template_purchase_order',
'email_template_payment_failed'
]; ];
public static function getDefaultTemplate($template, $locale) public static function getDefaultTemplate($template, $locale)
@ -39,6 +40,8 @@ class EmailTemplateDefaults
switch ($template) { switch ($template) {
/* Template */ /* Template */
case 'email_template_payment_failed':
return self::emailPaymentFailedTemplate();
case 'email_template_invoice': case 'email_template_invoice':
return self::emailInvoiceTemplate(); return self::emailInvoiceTemplate();
case 'email_template_quote': case 'email_template_quote':
@ -73,6 +76,9 @@ class EmailTemplateDefaults
case 'email_subject_invoice': case 'email_subject_invoice':
return self::emailInvoiceSubject(); return self::emailInvoiceSubject();
case 'email_subject_payment_failed':
return self::emailPaymentFailedSubject();
case 'email_subject_quote': case 'email_subject_quote':
return self::emailQuoteSubject(); return self::emailQuoteSubject();
@ -115,12 +121,40 @@ class EmailTemplateDefaults
case 'email_vendor_notification_body': case 'email_vendor_notification_body':
return self::emailVendorNotificationBody(); return self::emailVendorNotificationBody();
case 'email_quote_template_reminder1':
return self::emailQuoteReminder1Body();
case 'email_quote_subject_reminder1':
return self::emailQuoteReminder1Subject();
default: default:
return self::emailInvoiceTemplate(); 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() public static function emailVendorNotificationSubject()
{ {
return self::transformText('vendor_notification_subject'); return self::transformText('vendor_notification_subject');
@ -143,14 +177,14 @@ class EmailTemplateDefaults
public static function emailInvoiceTemplate() 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; return $invoice_message;
} }
public static function emailInvoiceReminderTemplate() 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; return $invoice_message;
} }
@ -162,7 +196,7 @@ class EmailTemplateDefaults
public static function emailQuoteTemplate() 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; return $quote_message;
} }
@ -179,28 +213,28 @@ class EmailTemplateDefaults
public static function emailPurchaseOrderTemplate() 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; return $purchase_order_message;
} }
public static function emailPaymentTemplate() 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; return $payment_message;
} }
public static function emailCreditTemplate() 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; return $credit_message;
} }
public static function emailPaymentPartialTemplate() 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; return $payment_message;
} }

View File

@ -64,9 +64,9 @@ class InvoiceItem
public $task_id = ''; public $task_id = '';
public $expense_id = ''; public $expense_id = '';
public $unit_code = 'C62'; public $unit_code = 'C62';
public static $casts = [ public static $casts = [
'task_id' => 'string', 'task_id' => 'string',
'expense_id' => 'string', 'expense_id' => 'string',

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

View File

@ -17,6 +17,7 @@ use App\Models\Invoice;
use App\Models\Product; use App\Models\Product;
use App\DataProviders\USStates; use App\DataProviders\USStates;
use App\DataMapper\Tax\ZipTax\Response; use App\DataMapper\Tax\ZipTax\Response;
use App\Models\RecurringInvoice;
class BaseRule implements RuleInterface class BaseRule implements RuleInterface
{ {
@ -47,6 +48,9 @@ class BaseRule implements RuleInterface
'DK', // Denmark 'DK', // Denmark
'EE', // Estonia 'EE', // Estonia
'ES', // Spain 'ES', // Spain
'ES-CN', // Canary Islands
'ES-CE', // Ceuta
'ES-ML', // Melilla
'FI', // Finland 'FI', // Finland
'FR', // France 'FR', // France
'GR', // Greece 'GR', // Greece
@ -77,6 +81,9 @@ class BaseRule implements RuleInterface
'DK' => 'EU', // Denmark 'DK' => 'EU', // Denmark
'EE' => 'EU', // Estonia 'EE' => 'EU', // Estonia
'ES' => 'EU', // Spain 'ES' => 'EU', // Spain
'ES-CN' => 'EU', // Canary Islands
'ES-CE' => 'EU', // Ceuta
'ES-ML' => 'EU', // Melilla
'FI' => 'EU', // Finland 'FI' => 'EU', // Finland
'FR' => 'EU', // France 'FR' => 'EU', // France
'GR' => 'EU', // Greece 'GR' => 'EU', // Greece
@ -132,7 +139,7 @@ class BaseRule implements RuleInterface
public function shouldCalcTax(): bool public function shouldCalcTax(): bool
{ {
return $this->should_calc_tax; return $this->should_calc_tax && $this->checkIfInvoiceLocked();
} }
/** /**
* Initializes the tax rule for the entity. * Initializes the tax rule for the entity.
@ -215,11 +222,12 @@ class BaseRule implements RuleInterface
$this->invoice->tax_data = $tax_data; $this->invoice->tax_data = $tax_data;
if(\DB::transactionLevel() == 0) { if(\DB::transactionLevel() == 0 && isset($this->invoice->id)) {
try { try {
$this->invoice->saveQuietly(); $this->invoice->saveQuietly();
} catch(\Exception $e) { } catch(\Exception $e) {
nlog("Exception:: BaseRule::" . $e->getMessage());
} }
} }
@ -261,7 +269,7 @@ class BaseRule implements RuleInterface
return $this->client->state; 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) { } catch (\Exception $e) {
return 'CA'; 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))); 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 float $reduced_tax_rate = 0;
public string $tax_name1 = 'MwSt.'; public string $tax_name1 = 'MwSt.';
private string $tax_name;
/** /**
* Initializes the rules and builds any required data. * Initializes the rules and builds any required data.
* *
@ -50,6 +52,7 @@ class Rule extends BaseRule implements RuleInterface
*/ */
public function init(): self public function init(): self
{ {
$this->tax_name = $this->tax_name1;
$this->calculateRates(); $this->calculateRates();
return $this; return $this;
@ -91,6 +94,7 @@ class Rule extends BaseRule implements RuleInterface
*/ */
public function reverseTax($item): self public function reverseTax($item): self
{ {
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = 0; $this->tax_rate1 = 0;
return $this; return $this;
@ -103,6 +107,8 @@ class Rule extends BaseRule implements RuleInterface
*/ */
public function taxReduced($item): self public function taxReduced($item): self
{ {
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = $this->reduced_tax_rate; $this->tax_rate1 = $this->reduced_tax_rate;
return $this; return $this;
@ -115,6 +121,8 @@ class Rule extends BaseRule implements RuleInterface
*/ */
public function zeroRated($item): self public function zeroRated($item): self
{ {
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = 0; $this->tax_rate1 = 0;
return $this; return $this;
@ -142,6 +150,7 @@ class Rule extends BaseRule implements RuleInterface
public function taxDigital($item): self public function taxDigital($item): self
{ {
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = $this->tax_rate; $this->tax_rate1 = $this->tax_rate;
return $this; return $this;
@ -155,6 +164,7 @@ class Rule extends BaseRule implements RuleInterface
public function taxService($item): self public function taxService($item): self
{ {
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = $this->tax_rate; $this->tax_rate1 = $this->tax_rate;
return $this; return $this;
@ -168,6 +178,7 @@ class Rule extends BaseRule implements RuleInterface
public function taxShipping($item): self public function taxShipping($item): self
{ {
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = $this->tax_rate; $this->tax_rate1 = $this->tax_rate;
return $this; return $this;
@ -181,6 +192,7 @@ class Rule extends BaseRule implements RuleInterface
public function taxPhysical($item): self public function taxPhysical($item): self
{ {
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = $this->tax_rate; $this->tax_rate1 = $this->tax_rate;
return $this; return $this;
@ -207,6 +219,14 @@ class Rule extends BaseRule implements RuleInterface
*/ */
public function override($item): self 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; return $this;
} }
@ -221,8 +241,7 @@ class Rule extends BaseRule implements RuleInterface
// nlog("tax exempt"); // nlog("tax exempt");
$this->tax_rate = 0; $this->tax_rate = 0;
$this->reduced_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->vat_number && $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->has_valid_vat_number && $this->eu_business_tax_exempt)
// nlog("euro zone and tax exempt"); // nlog("euro zone and tax exempt");
$this->tax_rate = 0; $this->tax_rate = 0;
$this->reduced_tax_rate = 0; $this->reduced_tax_rate = 0;
@ -232,8 +251,8 @@ class Rule extends BaseRule implements RuleInterface
$this->reduced_tax_rate = 0; $this->reduced_tax_rate = 0;
} elseif(!in_array($this->client_subregion, $this->eu_country_codes)) { } elseif(!in_array($this->client_subregion, $this->eu_country_codes)) {
$this->defaultForeign(); $this->defaultForeign();
} elseif(in_array($this->client_subregion, $this->eu_country_codes) && !$this->client->vat_number) { //eu country / no valid vat } 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) && $this->client->company->tax_data->regions->EU->has_sales_above_threshold) { if($this->client->company->tax_data->seller_subregion != $this->client_subregion) {
// nlog("eu zone with sales above threshold"); // 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->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; $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) public function __construct(public Response $origin)
{ {
// @phpstan-ignore-next-line
foreach($origin as $key => $value) { foreach($origin as $key => $value) {
$this->{$key} = $value; $this->{$key} = $value;
} }

View File

@ -17,7 +17,7 @@ class TaxModel
public string $seller_subregion = 'CA'; public string $seller_subregion = 'CA';
/** @var string $version */ /** @var string $version */
public string $version = 'alpha'; public string $version = 'gamma';
/** @var object $regions */ /** @var object $regions */
public object $regions; public object $regions;
@ -28,15 +28,63 @@ class TaxModel
* @param TaxModel $model * @param TaxModel $model
* @return void * @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(); $this->regions = $this->init();
} else { } 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->reduced_tax_rate = 10;
$this->regions->EU->subregions->ES->apply_tax = false; $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 = new \stdClass();
$this->regions->EU->subregions->FI->tax_rate = 24; $this->regions->EU->subregions->FI->tax_rate = 24;
$this->regions->EU->subregions->FI->tax_name = 'ALV'; $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->reduced_tax_rate = 9;
$this->regions->EU->subregions->NL->apply_tax = false; $this->regions->EU->subregions->NL->apply_tax = false;
$this->regions->EU->subregions->PL = new \stdClass();
$this->regions->EU->subregions->PL->tax_rate = 23;
$this->regions->EU->subregions->PL->tax_name = 'VAT';
$this->regions->EU->subregions->PL->reduced_tax_rate = 8;
$this->regions->EU->subregions->PL->apply_tax = false;
$this->regions->EU->subregions->PT = new \stdClass(); $this->regions->EU->subregions->PT = new \stdClass();
$this->regions->EU->subregions->PT->tax_rate = 23; $this->regions->EU->subregions->PT->tax_rate = 23;
$this->regions->EU->subregions->PT->tax_name = 'IVA'; $this->regions->EU->subregions->PT->tax_name = 'IVA';

View File

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

View File

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

View File

@ -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 <?php
/** /**
* Invoice Ninja (https://invoiceninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
@ -14,14 +15,16 @@ namespace App\Events\Invoice;
use App\Models\Company; use App\Models\Company;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Utils\Traits\Invoice\Broadcasting\DefaultInvoiceBroadcast;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
/** /**
* Class InvoiceWasPaid. * Class InvoiceWasPaid.
*/ */
class InvoiceWasPaid class InvoiceWasPaid implements ShouldBroadcast
{ {
use SerializesModels; use SerializesModels, DefaultInvoiceBroadcast;
/** /**
* @var Invoice * @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\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
use Illuminate\Database\Eloquent\RelationNotFoundException; use Illuminate\Database\Eloquent\RelationNotFoundException;
use Illuminate\Encryption\MissingAppKeyException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Exceptions\ThrottleRequestsException; use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -94,25 +95,16 @@ class Handler extends ExceptionHandler
*/ */
public function report(Throwable $exception) public function report(Throwable $exception)
{ {
if (! Schema::hasTable('accounts')) {
info('account table not found');
return;
}
if (Ninja::isHosted()) { if (Ninja::isHosted()) {
// if($exception instanceof ThrottleRequestsException && class_exists(\Modules\Admin\Events\ThrottledExceptionRaised::class)) {
// $uri = urldecode(request()->getRequestUri());
// event(new \Modules\Admin\Events\ThrottledExceptionRaised(auth()->user()?->account?->key, $uri, request()->ip()));
// }
Integration::configureScope(function (Scope $scope): void { Integration::configureScope(function (Scope $scope): void {
$name = 'hosted@invoiceninja.com'; $name = 'hosted@invoiceninja.com';
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; $name = 'Contact = '.auth()->guard('contact')->user()->email;
$key = auth()->guard('contact')->user()->company->account->key; $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; $name = 'Admin = '.auth()->guard('user')->user()->email;
$key = auth()->user()->account->key; $key = auth()->user()->account->key;
} else { } else {
@ -131,13 +123,14 @@ class Handler extends ExceptionHandler
} }
} elseif (app()->bound('sentry')) { } elseif (app()->bound('sentry')) {
Integration::configureScope(function (Scope $scope): void { 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([ $scope->setUser([
'id' => auth()->guard('contact')->user()->company->account->key, 'id' => auth()->guard('contact')->user()->company->account->key,
'email' => 'anonymous@example.com', 'email' => 'anonymous@example.com',
'name' => 'Anonymous User', '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([ $scope->setUser([
'id' => auth()->user()->account->key, 'id' => auth()->user()->account->key,
'email' => 'anonymous@example.com', 'email' => 'anonymous@example.com',
@ -152,6 +145,10 @@ class Handler extends ExceptionHandler
} }
parent::report($exception); parent::report($exception);
if (Ninja::isSelfHost() && $exception instanceof MissingAppKeyException) {
info('To setup the app run: cp .env.example .env');
}
} }
private function validException($exception) private function validException($exception)

View File

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

View File

@ -172,6 +172,7 @@ class BaseExport
'tax_rate3' => 'invoice.tax_rate3', 'tax_rate3' => 'invoice.tax_rate3',
'recurring_invoice' => 'invoice.recurring_id', 'recurring_invoice' => 'invoice.recurring_id',
'auto_bill' => 'invoice.auto_bill_enabled', 'auto_bill' => 'invoice.auto_bill_enabled',
'project' => 'invoice.project',
]; ];
protected array $recurring_invoice_report_keys = [ protected array $recurring_invoice_report_keys = [
@ -449,6 +450,8 @@ class BaseExport
'status' => 'task.status_id', 'status' => 'task.status_id',
'project' => 'task.project_id', 'project' => 'task.project_id',
'billable' => 'task.billable', 'billable' => 'task.billable',
'item_notes' => 'task.item_notes',
'time_log' => 'task.time_log',
]; ];
protected array $forced_client_fields = [ protected array $forced_client_fields = [
@ -838,12 +841,12 @@ class BaseExport
return ''; return '';
} }
/** /**
* Apply Product Filters * Apply Product Filters
* *
* @param Builder $query * @param \Illuminate\Database\Eloquent\Builder $query
* *
* @return Builder * @return Builder
*/ */
public function applyProductFilters(Builder $query): Builder public function applyProductFilters(Builder $query): Builder
@ -863,13 +866,13 @@ class BaseExport
return $query; return $query;
} }
/** /**
* Add Client Filter * Add Client Filter
* *
* @param Builder $query * @param \Illuminate\Database\Eloquent\Builder $query
* @param mixed $clients * @param mixed $clients
* *
* @return Builder * @return Builder
*/ */
protected function addClientFilter(Builder $query, $clients): Builder protected function addClientFilter(Builder $query, $clients): Builder
@ -886,13 +889,13 @@ class BaseExport
return $query; return $query;
} }
/** /**
* Add Vendor Filter * Add Vendor Filter
* *
* @param Builder $query * @param \Illuminate\Database\Eloquent\Builder $query
* @param string $vendors * @param string $vendors
* *
* @return Builder * @return Builder
*/ */
protected function addVendorFilter(Builder$query, string $vendors): Builder protected function addVendorFilter(Builder$query, string $vendors): Builder
@ -910,13 +913,13 @@ class BaseExport
return $query; return $query;
} }
/** /**
* AddProjectFilter * AddProjectFilter
* *
* @param Builder $query * @param \Illuminate\Database\Eloquent\Builder $query
* @param string $projects * @param string $projects
* *
* @return Builder * @return Builder
*/ */
protected function addProjectFilter(Builder $query, string $projects): Builder protected function addProjectFilter(Builder $query, string $projects): Builder
@ -934,13 +937,13 @@ class BaseExport
return $query; return $query;
} }
/** /**
* Add Category Filter * Add Category Filter
* *
* @param Builder $query * @param \Illuminate\Database\Eloquent\Builder $query
* @param string $expense_categories * @param string $expense_categories
* *
* @return Builder * @return Builder
*/ */
protected function addCategoryFilter(Builder $query, string $expense_categories): Builder protected function addCategoryFilter(Builder $query, string $expense_categories): Builder
@ -959,24 +962,25 @@ class BaseExport
return $query; return $query;
} }
/** /**
* Add Payment Status Filters * Add Payment Status Filters
* *
* @param Builder $query * @param \Illuminate\Database\Eloquent\Builder $query
* @param string $status * @param string $status
* *
* @return Builder * @return Builder
*/ */
protected function addPaymentStatusFilters(Builder $query, string $status): Builder protected function addPaymentStatusFilters(Builder $query, string $status): Builder
{ {
/** @var array $status_parameters */
$status_parameters = explode(',', $status); $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; return $query;
} }
$query->where(function ($query) use ($status_parameters) { $query->where(function ($query) use ($status_parameters) {
$payment_filters = []; $payment_filters = [];
@ -1016,26 +1020,31 @@ class BaseExport
return $query; return $query;
} }
/** /**
* Add RecurringInvoice Status Filter * Add RecurringInvoice Status Filter
* *
* @param Builder $query * @param \Illuminate\Database\Eloquent\Builder $query
* @param string $status * @param string $status
* *
* @return Builder * @return \Illuminate\Database\Eloquent\Builder
*/ */
protected function addRecurringInvoiceStatusFilter(Builder $query, string $status): Builder protected function addRecurringInvoiceStatusFilter(Builder $query, string $status): Builder
{ {
/** @var array $status_parameters */
$status_parameters = explode(',', $status); $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; return $query;
} }
$recurring_filters = []; $recurring_filters = [];
if($this->company->getSetting('report_include_drafts')) {
$recurring_filters[] = RecurringInvoice::STATUS_DRAFT;
}
if (in_array('active', $status_parameters)) { if (in_array('active', $status_parameters)) {
$recurring_filters[] = RecurringInvoice::STATUS_ACTIVE; $recurring_filters[] = RecurringInvoice::STATUS_ACTIVE;
} }
@ -1058,9 +1067,9 @@ class BaseExport
/** /**
* Add QuoteStatus Filter * Add QuoteStatus Filter
* *
* @param Builder $query * @param \Illuminate\Database\Eloquent\Builder $query
* @param string $status * @param string $status
* *
* @return Builder * @return Builder
*/ */
protected function addQuoteStatusFilter(Builder $query, string $status): Builder protected function addQuoteStatusFilter(Builder $query, string $status): Builder
@ -1124,14 +1133,15 @@ class BaseExport
/** /**
* Add PurchaseOrder Status Filter * Add PurchaseOrder Status Filter
* *
* @param Builder $query * @param \Illuminate\Database\Eloquent\Builder $query
* @param string $status * @param string $status
* *
* @return Builder * @return Builder
*/ */
protected function addPurchaseOrderStatusFilter(Builder $query, string $status): Builder protected function addPurchaseOrderStatusFilter(Builder $query, string $status): Builder
{ {
/** @var array $status_parameters */
$status_parameters = explode(',', $status); $status_parameters = explode(',', $status);
if (in_array('all', $status_parameters) || count($status_parameters) == 0) { if (in_array('all', $status_parameters) || count($status_parameters) == 0) {
@ -1173,13 +1183,14 @@ class BaseExport
/** /**
* Add Invoice Status Filter * Add Invoice Status Filter
* *
* @param Builder $query * @param \Illuminate\Database\Eloquent\Builder $query
* @param string $status * @param string $status
* @return Builder * @return Builder
*/ */
protected function addInvoiceStatusFilter(Builder $query, string $status): Builder protected function addInvoiceStatusFilter(Builder $query, string $status): Builder
{ {
/** @var array $status_parameters */
$status_parameters = explode(',', $status); $status_parameters = explode(',', $status);
if(in_array('all', $status_parameters) || count($status_parameters) == 0) { if(in_array('all', $status_parameters) || count($status_parameters) == 0) {
@ -1234,20 +1245,21 @@ class BaseExport
return $query; return $query;
} }
/** /**
* Add Date Range * Add Date Range
* *
* @param Builder $query * @param \Illuminate\Database\Eloquent\Builder $query
* @param ?string $table_name
* @return Builder * @return Builder
*/ */
protected function addDateRange(Builder $query): Builder protected function addDateRange(Builder $query, ?string $table_name = null): Builder
{ {
$query = $this->applyProductFilters($query); $query = $this->applyProductFilters($query);
$date_range = $this->input['date_range']; $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']; $this->date_key = $this->input['date_key'];
} }
@ -1578,7 +1590,7 @@ class BaseExport
public function queueDocuments(Builder $query) public function queueDocuments(Builder $query)
{ {
if($query->getModel() instanceof Document) { if($query->getModel() instanceof Document) {
$documents = $query->pluck('id')->toArray(); $documents = $query->pluck('id')->toArray();
} else { } else {
@ -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() $report = $query->cursor()
->map(function ($client) { ->map(function ($client) {
/** @var \App\Models\Client $client */
$row = $this->buildRow($client); $row = $this->buildRow($client);
return $this->processMetaData($row, $client); return $this->processMetaData($row, $client);
})->toArray(); })->toArray();
@ -127,10 +129,11 @@ class ClientExport extends BaseExport
->withTrashed() ->withTrashed()
->where('company_id', $this->company->id); ->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false) if(!$this->input['include_deleted'] ?? false) {
$query->where('is_deleted', 0); $query->where('is_deleted', 0);
}
$query = $this->addDateRange($query); $query = $this->addDateRange($query, ' clients');
if($this->input['document_email_attachment'] ?? false) { if($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query); $this->queueDocuments($query);
@ -153,6 +156,8 @@ class ClientExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($client) { ->each(function ($client) {
/** @var \App\Models\Client $client */
$this->csv->insertOne($this->buildRow($client)); $this->csv->insertOne($this->buildRow($client));
}); });
@ -242,16 +247,16 @@ class ClientExport extends BaseExport
return $entity; return $entity;
} }
private function calculateStatus($client) // private function calculateStatus($client)
{ // {
if ($client->is_deleted) { // if ($client->is_deleted) {
return ctrans('texts.deleted'); // return ctrans('texts.deleted');
} // }
if ($client->deleted_at) { // if ($client->deleted_at) {
return ctrans('texts.archived'); // 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() $query = ClientContact::query()
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->whereHas('client', function ($q){ ->whereHas('client', function ($q) {
$q->where('is_deleted', false); $q->where('is_deleted', false);
}); });
$query = $this->addDateRange($query); $query = $this->addDateRange($query, 'client_contacts');
return $query; return $query;
@ -82,6 +82,7 @@ class ContactExport extends BaseExport
$this->csv->insertOne($this->buildHeader()); $this->csv->insertOne($this->buildHeader());
$query->cursor()->each(function ($contact) { $query->cursor()->each(function ($contact) {
/** @var \App\Models\ClientContact $contact */
$this->csv->insertOne($this->buildRow($contact)); $this->csv->insertOne($this->buildRow($contact));
}); });
@ -101,6 +102,7 @@ class ContactExport extends BaseExport
$report = $query->cursor() $report = $query->cursor()
->map(function ($contact) { ->map(function ($contact) {
/** @var \App\Models\ClientContact $contact */
$row = $this->buildRow($contact); $row = $this->buildRow($contact);
return $this->processMetaData($row, $contact); return $this->processMetaData($row, $contact);
})->toArray(); })->toArray();
@ -155,7 +157,7 @@ class ContactExport extends BaseExport
} }
if (in_array('client.user_id', $this->input['report_keys'])) { 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'])) { if (in_array('client.assigned_user_id', $this->input['report_keys'])) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -51,6 +51,8 @@ class ProductExport extends BaseExport
$report = $query->cursor() $report = $query->cursor()
->map(function ($resource) { ->map(function ($resource) {
/** @var \App\Models\Product $resource */
$row = $this->buildRow($resource); $row = $this->buildRow($resource);
return $this->processMetaData($row, $resource); return $this->processMetaData($row, $resource);
})->toArray(); })->toArray();
@ -74,13 +76,12 @@ class ProductExport extends BaseExport
$query = Product::query() $query = Product::query()
->withTrashed() ->withTrashed()
->where('company_id', $this->company->id); ->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false) { //@phpstan-ignore-line
if(!$this->input['include_deleted'] ?? false){
$query->where('is_deleted', 0); $query->where('is_deleted', 0);
} }
$query = $this->addDateRange($query); $query = $this->addDateRange($query, 'products');
if($this->input['document_email_attachment'] ?? false) { if($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query); $this->queueDocuments($query);
@ -104,6 +105,8 @@ class ProductExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($entity) { ->each(function ($entity) {
/** @var \App\Models\Product $entity */
$this->csv->insertOne($this->buildRow($entity)); $this->csv->insertOne($this->buildRow($entity));
}); });
@ -133,16 +136,16 @@ class ProductExport extends BaseExport
// return $this->decorateAdvancedFields($product, $entity); // return $this->decorateAdvancedFields($product, $entity);
} }
private function decorateAdvancedFields(Product $product, array $entity): array // private function decorateAdvancedFields(Product $product, array $entity): array
{ // {
if (in_array('vendor_id', $this->input['report_keys'])) { // if (in_array('vendor_id', $this->input['report_keys'])) {
$entity['vendor'] = $product->vendor()->exists() ? $product->vendor->name : ''; // $entity['vendor'] = $product->vendor()->exists() ? $product->vendor->name : '';
} // }
// if (array_key_exists('project_id', $this->input['report_keys'])) { // // if (array_key_exists('project_id', $this->input['report_keys'])) {
// $entity['project'] = $product->project()->exists() ? $product->project->name : ''; // // $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'; public string $date_key = 'created_at';
/** @var Collection<\App\Models\Product> $products*/
protected Collection $products; protected Collection $products;
public Writer $csv; public Writer $csv;
@ -65,11 +66,11 @@ class ProductSalesExport extends BaseExport
'custom_value4' => 'custom_value4', 'custom_value4' => 'custom_value4',
]; ];
private array $decorate_keys = [ // private array $decorate_keys = [
'client', // 'client',
'currency', // 'currency',
'date', // 'date',
]; // ];
public function __construct(Company $company, array $input) public function __construct(Company $company, array $input)
{ {
@ -80,20 +81,20 @@ class ProductSalesExport extends BaseExport
public function filterByProducts($query) public function filterByProducts($query)
{ {
$product_keys = &$this->input['product_key']; $product_keys = &$this->input['product_key'];
if ($product_keys && !empty($this->input['product_key'])) { if ($product_keys && !empty($this->input['product_key'])) {
$keys = explode(",", $product_keys); $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]); $q->orWhereJsonContains('line_items', ['product_key' => $key]);
} }
}); });
} }
return $query; return $query;
@ -121,14 +122,14 @@ class ProductSalesExport extends BaseExport
//insert the header //insert the header
$query = Invoice::query() $query = Invoice::query()
->withTrashed() ->withTrashed()
->whereHas('client', function ($q){ ->whereHas('client', function ($q) {
$q->where('is_deleted', false); $q->where('is_deleted', false);
}) })
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->where('is_deleted', 0) ->where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL, Invoice::STATUS_PAID]); ->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); $query = $this->filterByClients($query);
@ -138,30 +139,29 @@ class ProductSalesExport extends BaseExport
$product_keys = &$this->input['product_key']; $product_keys = &$this->input['product_key'];
if($product_keys){ if($product_keys) {
$product_keys = explode(",", $product_keys); $product_keys = explode(",", $product_keys);
} }
$query->cursor() $query->cursor()
->each(function ($invoice) use($product_keys) { ->each(function ($invoice) use ($product_keys) {
foreach ($invoice->line_items as $item) { foreach ($invoice->line_items as $item) {
if($product_keys) if($product_keys) {
{ if(in_array($item->product_key, $product_keys)) {
if(in_array($item->product_key, $product_keys)) $this->csv->insertOne($this->buildRow($invoice, $item));
$this->csv->insertOne($this->buildRow($invoice, $item)); }
} } else {
else { $this->csv->insertOne($this->buildRow($invoice, $item));
$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; return false;
} }
@ -185,7 +185,8 @@ class ProductSalesExport extends BaseExport
})->reject(function ($value) { })->reject(function ($value) {
return $value === false; return $value === false;
});; });
;
$this->csv->insertOne([]); $this->csv->insertOne([]);
$this->csv->insertOne([]); $this->csv->insertOne([]);
@ -327,10 +328,10 @@ class ProductSalesExport extends BaseExport
* getProduct * getProduct
* *
* @param string $product_key * @param string $product_key
* @return Product * @return ?\Illuminate\Database\Eloquent\Model
*/ */
private function getProduct(string $product_key): ?Product // private function getProduct(string $product_key)
{ // {
return $this->products->firstWhere('product_key', $product_key); // return $this->products->firstWhere('product_key', $product_key);
} // }
} }

View File

@ -58,22 +58,23 @@ class PurchaseOrderExport extends BaseExport
$query = PurchaseOrder::query() $query = PurchaseOrder::query()
->withTrashed() ->withTrashed()
->with('vendor') ->with('vendor')
->whereHas('vendor', function ($q){ ->whereHas('vendor', function ($q) {
$q->where('is_deleted', false); $q->where('is_deleted', false);
}) })
->where('company_id', $this->company->id); ->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->where('is_deleted', 0);
} }
$query = $this->addDateRange($query); $query = $this->addDateRange($query, 'purchase_orders');
$clients = &$this->input['client_id']; $clients = &$this->input['client_id'];
if($clients) if($clients) {
$query = $this->addClientFilter($query, $clients); $query = $this->addClientFilter($query, $clients);
}
$query = $this->addPurchaseOrderStatusFilter($query, $this->input['status'] ?? ''); $query = $this->addPurchaseOrderStatusFilter($query, $this->input['status'] ?? '');
@ -97,6 +98,8 @@ class PurchaseOrderExport extends BaseExport
$report = $query->cursor() $report = $query->cursor()
->map(function ($resource) { ->map(function ($resource) {
/** @var \App\Models\PurchaseOrder $resource */
$row = $this->buildRow($resource); $row = $this->buildRow($resource);
return $this->processMetaData($row, $resource); return $this->processMetaData($row, $resource);
})->toArray(); })->toArray();
@ -118,6 +121,8 @@ class PurchaseOrderExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($purchase_order) { ->each(function ($purchase_order) {
/** @var \App\Models\PurchaseOrder $purchase_order */
$this->csv->insertOne($this->buildRow($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'])) { 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'])) { 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() $query = PurchaseOrder::query()
->withTrashed() ->withTrashed()
->whereHas('vendor', function ($q){ ->whereHas('vendor', function ($q) {
$q->where('is_deleted', false); $q->where('is_deleted', false);
}) })
->with('vendor')->where('company_id', $this->company->id); ->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->where('is_deleted', 0);
} }
$query = $this->addDateRange($query); $query = $this->addDateRange($query, 'purchase_orders');
$clients = &$this->input['client_id']; $clients = &$this->input['client_id'];
@ -101,6 +101,8 @@ class PurchaseOrderItemExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($resource) { ->each(function ($resource) {
/** @var \App\Models\PurchaseOrder $resource */
$this->iterateItems($resource); $this->iterateItems($resource);
foreach($this->storage_array as $row) { foreach($this->storage_array as $row) {
@ -127,6 +129,8 @@ class PurchaseOrderItemExport extends BaseExport
$query->cursor() $query->cursor()
->each(function ($purchase_order) { ->each(function ($purchase_order) {
/** @var \App\Models\PurchaseOrder $purchase_order */
$this->iterateItems($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; // $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)) { // if(array_key_exists('tax_category', $entity)) {
// $entity['tax_category'] = $purchase_order->taxTypeString($entity['tax_category']); // $entity['tax_category'] = $purchase_order->taxTypeString($entity['tax_category']);
// } // }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@ class ContactDecorator implements DecoratorInterface
$contact = $entity->contacts()->first(); $contact = $entity->contacts()->first();
} elseif($entity->client) { } elseif($entity->client) {
$contact = $entity->client->primary_contact->first() ?? $entity->client->contacts()->whereNotNull('email')->first(); $contact = $entity->client->primary_contact->first() ?? $entity->client->contacts()->whereNotNull('email')->first();
} elseif($entity->vendor) { } elseif($entity->vendor) {
$contact = $entity->vendor->primary_contact->first() ?? $entity->vendor->contacts()->whereNotNull('email')->first(); $contact = $entity->vendor->primary_contact->first() ?? $entity->vendor->contacts()->whereNotNull('email')->first();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,7 @@ class CompanyGatewayFactory
$company_gateway->require_shipping_address = false; $company_gateway->require_shipping_address = false;
$company_gateway->config = encrypt(json_encode(new \stdClass())); $company_gateway->config = encrypt(json_encode(new \stdClass()));
$company_gateway->always_show_required_fields = true; $company_gateway->always_show_required_fields = true;
return $company_gateway; return $company_gateway;
} }
} }

View File

@ -16,7 +16,7 @@ use App\Models\Credit;
class CreditFactory 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 = new Credit();
$credit->status_id = Credit::STATUS_DRAFT; $credit->status_id = Credit::STATUS_DRAFT;

View File

@ -76,6 +76,26 @@ class InvoiceItemFactory
$data[] = $item; $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; return $data;
} }

View File

@ -52,7 +52,7 @@ class PurchaseOrderFactory
$purchase_order->exchange_rate = 1; $purchase_order->exchange_rate = 1;
$purchase_order->total_taxes = 0; $purchase_order->total_taxes = 0;
$purchase_order->uses_inclusive_taxes = false; $purchase_order->uses_inclusive_taxes = false;
return $purchase_order; return $purchase_order;
} }
} }

View File

@ -82,11 +82,15 @@ class RecurringExpenseToExpenseFactory
} else { } else {
$locale = $recurring_expense->company->locale(); $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; return $item->id == $recurring_expense->company->settings->date_format_id;
})->first()->format; })->format;
} }
Carbon::setLocale($locale); Carbon::setLocale($locale);
@ -144,45 +148,45 @@ class RecurringExpenseToExpenseFactory
continue; continue;
} }
if (Str::contains($match, '|')) { // if (Str::contains($match, '|')) {
$parts = explode('|', $match); // [ '[MONTH', 'MONTH+2]' ] $parts = explode('|', $match); // [ '[MONTH', 'MONTH+2]' ]
$left = substr($parts[0], 1); // 'MONTH' $left = substr($parts[0], 1); // 'MONTH'
$right = substr($parts[1], 0, -1); // MONTH+2 $right = substr($parts[1], 0, -1); // MONTH+2
// If left side is not part of replacements, skip. // If left side is not part of replacements, skip.
if (! array_key_exists($left, $replacements['ranges'])) { if (! array_key_exists($left, $replacements['ranges'])) {
continue; 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
);
} }
$_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. // Second case with more common calculations.

View File

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

View File

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

View File

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

View File

@ -68,7 +68,7 @@ class BankTransactionFilters extends QueryFilters
*/ */
public function client_status(string $value = ''): Builder public function client_status(string $value = ''): Builder
{ {
if (strlen($value) == 0) { if (strlen($value ?? '') == 0) {
return $this->builder; return $this->builder;
} }
@ -108,13 +108,47 @@ class BankTransactionFilters extends QueryFilters
} }
if (count($debit_or_withdrawal_array) >= 1) { 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; 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. * Sorts the list based on $sort.
* *
@ -132,11 +166,13 @@ class BankTransactionFilters extends QueryFilters
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc'; $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
if ($sort_col[0] == 'deposit') { 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') { 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') { if ($sort_col[0] == 'status') {

View File

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

View File

@ -97,7 +97,15 @@ class CreditFilters extends QueryFilters
$q->where('first_name', 'like', '%'.$filter.'%') $q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%') ->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%'); ->orWhere('email', 'like', '%'.$filter.'%');
}); })
->orWhereRaw("
JSON_UNQUOTE(JSON_EXTRACT(
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

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

View File

@ -49,22 +49,22 @@ class DocumentFilters extends QueryFilters
*/ */
public function client_id(string $client_id = ''): Builder public function client_id(string $client_id = ''): Builder
{ {
return $this->builder->where(function ($query) use ($client_id) { return $this->builder->where(function ($query) use ($client_id) {
$query->whereHasMorph('documentable', [ $query->whereHasMorph('documentable', [
\App\Models\Invoice::class, \App\Models\Invoice::class,
\App\Models\Quote::class, \App\Models\Quote::class,
\App\Models\Credit::class, \App\Models\Credit::class,
\App\Models\Expense::class, \App\Models\Expense::class,
\App\Models\Payment::class, \App\Models\Payment::class,
\App\Models\Task::class, \App\Models\Task::class,
\App\Models\RecurringExpense::class, \App\Models\RecurringExpense::class,
\App\Models\RecurringInvoice::class, \App\Models\RecurringInvoice::class,
\App\Models\Project::class, \App\Models\Project::class,
], function ($q2) use ($client_id) { ], function ($q2) use ($client_id) {
$q2->where('client_id', $this->decodePrimaryKey($client_id)); $q2->where('client_id', $this->decodePrimaryKey($client_id));
})->orWhereHasMorph('documentable', [\App\Models\Client::class], function ($q3) use ($client_id) { })->orWhereHasMorph('documentable', [\App\Models\Client::class], function ($q3) use ($client_id) {
$q3->where('id', $this->decodePrimaryKey($client_id)); $q3->where('id', $this->decodePrimaryKey($client_id));
}); });
}); });
@ -74,8 +74,7 @@ class DocumentFilters extends QueryFilters
{ {
$types = explode(',', $types); $types = explode(',', $types);
foreach ($types as $type) foreach ($types as $type) {
{
match($type) { match($type) {
'private' => $this->builder->where('is_public', 0), 'private' => $this->builder->where('is_public', 0),
'public' => $this->builder->where('is_public', 1), 'public' => $this->builder->where('is_public', 1),
@ -87,7 +86,7 @@ class DocumentFilters extends QueryFilters
} }
return $this->builder; return $this->builder;
} }
/** /**
* Sorts the list based on $sort. * Sorts the list based on $sort.

View File

@ -44,6 +44,9 @@ class ExpenseFilters extends QueryFilters
}) })
->orWhereHas('vendor', function ($q) use ($filter) { ->orWhereHas('vendor', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$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) { $this->builder->where(function ($query) use ($status_parameters) {
if (in_array('logged', $status_parameters)) { if (in_array('logged', $status_parameters)) {
$query->orWhere(function ($query) { $query->orWhere(function ($query) {
$query->where('amount', '>', 0) $query->where('amount', '>=', 0)
->whereNull('invoice_id') ->whereNull('invoice_id')
->whereNull('payment_date') ->whereNull('payment_date')
->where('should_be_invoiced', false); ->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)) { if (in_array('paid', $status_parameters)) {
$query->orWhere(function ($query) { $query->orWhere(function ($query) {
$query->whereNotNull('payment_date'); $query->whereNotNull('payment_date');
@ -108,8 +117,8 @@ class ExpenseFilters extends QueryFilters
}); });
} }
if(in_array('uncategorized', $status_parameters)){ if(in_array('uncategorized', $status_parameters)) {
$query->orWhere(function ($query){ $query->orWhere(function ($query) {
$query->whereNull('category_id'); $query->whereNull('category_id');
}); });
} }
@ -155,6 +164,19 @@ class ExpenseFilters extends QueryFilters
return $this->builder; 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 public function number(string $number = ''): Builder
{ {
if (strlen($number) == 0) { if (strlen($number) == 0) {
@ -202,6 +224,11 @@ class ExpenseFilters extends QueryFilters
->whereColumn('expense_categories.id', 'expenses.category_id'), $sort_col[1]); ->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') { if($sort_col[0] == 'number') {
return $this->builder->orderByRaw("REGEXP_REPLACE(number,'[^0-9]+','')+0 " . $dir); 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.'%') $q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%') ->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%'); ->orWhere('email', 'like', '%'.$filter.'%');
}); })
->orWhereRaw("
JSON_UNQUOTE(JSON_EXTRACT(
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); $date = Carbon::createFromTimestamp((int)$date);
} else { } else {
try{ try {
$date = Carbon::parse($date); $date = Carbon::parse($date);
} } catch(\Exception $e) {
catch(\Exception $e){
return $this->builder; return $this->builder;
} }
} }
@ -259,59 +266,6 @@ class InvoiceFilters extends QueryFilters
return $this->builder->where('due_date', '>=', $date); 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. * Sorts the list based on $sort.
* *
@ -322,7 +276,7 @@ class InvoiceFilters extends QueryFilters
{ {
$sort_col = explode('|', $sort); $sort_col = explode('|', $sort);
if (!is_array($sort_col) || count($sort_col) != 2) { if (!is_array($sort_col) || count($sort_col) != 2 || in_array($sort_col[0], ['documents'])) {
return $this->builder; return $this->builder;
} }
@ -339,10 +293,10 @@ class InvoiceFilters extends QueryFilters
// return $this->builder->orderByRaw('CAST(number AS UNSIGNED), number ' . $dir); // 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("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('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;
} }
return $this->builder;
} }
/** /**

View File

@ -60,7 +60,7 @@ class ProjectFilters extends QueryFilters
{ {
$sort_col = explode('|', $sort); $sort_col = explode('|', $sort);
if (!is_array($sort_col) || count($sort_col) != 2) { if (!is_array($sort_col) || count($sort_col) != 2 || !in_array($sort_col[0], \Illuminate\Support\Facades\Schema::getColumnListing('projects'))) {
return $this->builder; return $this->builder;
} }

View File

@ -96,7 +96,14 @@ class PurchaseOrderFilters extends QueryFilters
->orWhere('custom_value4', 'like', '%'.$filter.'%') ->orWhere('custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('vendor', function ($q) use ($filter) { ->orWhereHas('vendor', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$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]) ->orderByRaw("{$this->with_property} = ? DESC", [$value])
->company(); ->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.'%') $q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%') ->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%'); ->orWhere('email', 'like', '%'.$filter.'%');
}); })
->orWhereRaw("
JSON_UNQUOTE(JSON_EXTRACT(
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.'%']);
}); });
} }

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