Merge pull request #11253 from turbo124/v5-stable

v5.12.23
This commit is contained in:
David Bomba 2025-09-02 07:24:54 +10:00 committed by GitHub
commit 328d0d7482
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2243 changed files with 495169 additions and 413960 deletions

View File

@ -30,6 +30,11 @@ jobs:
id: set_date
run: echo "current_date=$(date '+%Y-%m-%d')" >> $GITHUB_ENV
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Prepare React FrontEnd
run: |
git clone https://${{secrets.commit_secret}}@github.com/invoiceninja/ui.git
@ -58,14 +63,14 @@ jobs:
rm .env || true
rm -rf ui || true
# Set permissions: directories 755, files 644
chmod -R a=r,u+w,a+X .
find . -path ./vendor -prune -o -type f -exec chmod 644 {} \+
find . -path ./vendor -prune -o -type d -exec chmod 755 {} \+
- name: Build project
run: |
shopt -s dotglob
tar --exclude='public/storage' --exclude='invoiceninja.zip' -zcvf /home/runner/work/invoiceninja/invoiceninja.tar *
cd ../
tar --exclude='public/storage' --exclude='invoiceninja.zip' -zcvf invoiceninja.tar.gz invoiceninja/
tar --exclude='public/storage' --exclude='invoiceninja.zip' -zcvf /home/runner/work/invoiceninja/invoiceninja.tar.gz *
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')

View File

@ -1,4 +1,3 @@
# Invoice Ninja Code of Conduct
The development team has invested a tremendous amount of time and energy into this project. While we appreciate that bugs can be frustrating we ask that our community refrain from insults and snide remarks. We're happy to provide support to both our hosted and selfhosted communities but ask that feedback is always polite.

View File

@ -66,7 +66,7 @@ A good bug report shouldn't leave others needing to chase you up for more inform
#### How Do I Submit a Good Bug Report?
> You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to <>.
> You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to <contact@invoiceninja.com>.
We use GitHub issues to track bugs and errors. If you run into an issue with the project:
@ -116,4 +116,4 @@ All PRs should be created against the v5-develop branch.
## Attribution
This guide is based on the **contributing.md**. [Make your own](https://contributing.md/)!
This guide is based on the **contributing.md**. [Make your own](https://contributing.md/)!

View File

@ -63,11 +63,13 @@ All Pro and Enterprise features from the hosted app are included in the source-a
In addition to the official [Invoice Ninja - Self-Hosted Installation Guide](https://invoiceninja.github.io/en/self-host-installation/) we have a few commands for you.
```sh
git clone --single-branch --branch v5-stable https://github.com/invoiceninja/invoiceninja.git
git clone --depth 1 -b v5.11.53 https://github.com/invoiceninja/invoiceninja.git
cp .env.example .env
composer i -o --no-dev
```
**Note** replace v5.11.53 with the latest tag version, you will also want to ensure that when performing updates, you use the latest tag version rather than a particular branch, ie v5-develop. This will ensure that you are not pulling in work in progress code.
Please Note:
Your APP_KEY in the .env file is used to encrypt data, if you lose this you will not be able to run the application.
@ -177,4 +179,4 @@ For further information on responsible disclosure please read [here](https://che
## License
Invoice Ninja is released under the Elastic License.
See [LICENSE](LICENSE) for details.
See [LICENSE](LICENSE) for details.

5
SECURITY.md Normal file
View File

@ -0,0 +1,5 @@
## Security
If you find a security issue with this application, please send an email to contact@invoiceninja.com.
Please follow responsible disclosure procedures if you detect an issue.
For further information on responsible disclosure please read [here](https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html).

View File

@ -1 +1 @@
5.11.43
5.12.23

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -37,8 +38,8 @@ class ClientSyncCast implements CastsAttributes
public function set($model, string $key, $value, array $attributes)
{
if (is_null($value)) {
return [$key => null];
}

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -18,7 +19,6 @@ class InvoiceSyncCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
{
if (is_null($value)) {
return null; // Return null if the value is null
}
@ -29,28 +29,24 @@ class InvoiceSyncCast implements CastsAttributes
return null;
}
$is = new InvoiceSync($data);
$is = new InvoiceSync();
$is->qb_id = $data['qb_id'];
return $is;
}
public function set($model, string $key, $value, array $attributes)
{
if (is_null($value)) {
return [$key => null];
}
if (is_null($value)) {
return [$key => null];
}
return [
$key => json_encode([
'qb_id' => $value->qb_id,
])
];
$data = [
'qb_id' => $value->qb_id,
];
return [
$key => json_encode($data)
];
}
}

View File

@ -38,7 +38,7 @@ class PaymentSyncCast implements CastsAttributes
public function set($model, string $key, $value, array $attributes)
{
if (is_null($value)) {
return [$key => null];
}

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -38,10 +39,10 @@ class ProductSyncCast implements CastsAttributes
public function set($model, string $key, $value, array $attributes)
{
if (is_null($value)) {
return [$key => null];
}
if (is_null($value)) {
return [$key => null];
}
return [

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -0,0 +1,52 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Casts;
use App\DataMapper\QuoteSync;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class QuoteSyncCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
{
if (is_null($value)) {
return null; // Return null if the value is null
}
$data = json_decode($value, true);
if (!is_array($data)) {
return null;
}
$is = new QuoteSync($data);
return $is;
}
public function set($model, string $key, $value, array $attributes)
{
if (is_null($value)) {
return [$key => null];
}
return [
$key => json_encode([
'qb_id' => $value->qb_id,
])
];
}
}

View File

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

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -86,7 +87,7 @@ class CheckData extends Command
/**
* @var string
*/
protected $signature = 'ninja:check-data {--database=} {--fix=} {--portal_url=} {--client_id=} {--vendor_id=} {--paid_to_date=} {--client_balance=} {--ledger_balance=} {--balance_status=} {--bank_transaction=} {--line_items=} {--payment_balance=}';
protected $signature = 'ninja:check-data {--database=} {--fix=} {--portal_url=} {--client_id=} {--vendor_id=} {--paid_to_date=} {--client_balance=} {--ledger_balance=} {--balance_status=} {--bank_transaction=} {--line_items=} {--payment_balance=} {--tasks=}';
/**
* @var string
@ -117,6 +118,7 @@ class CheckData extends Command
config(['database.default' => $database]);
}
$this->checkTaskTimeLogs();
$this->checkInvoiceBalances();
$this->checkClientBalanceEdgeCases();
$this->checkPaidToDatesNew();
@ -179,6 +181,37 @@ class CheckData extends Command
$this->log .= $str."\n";
}
private function checkTaskTimeLogs()
{
\App\Models\Task::query()->cursor()->each(function ($task) {
$time_log = json_decode($task->time_log, true);
foreach($time_log as &$log){
if(count($log) > 4){
$this->logMessage("Task #{$task->id} has a time log with more than 4 elements");
if($this->option('tasks') == 'true'){
$log = [(int)$log[0], (int)$log[1], (string)$log[2], (bool)$log[3]];
}
}
elseif(count($log) == 4){
if($this->option('tasks') == 'true'){
$log = [(int)$log[0], (int)$log[1], (string)$log[2], (bool)$log[3]];
}
}
}
unset($log); // Unset the reference variable
if($this->option('tasks') == 'true'){
$task->time_log = json_encode($time_log);
$task->saveQuietly();
}
});
}
private function checkCompanyTokens()
{
CompanyUser::query()->cursor()->each(function ($cu) {
@ -568,6 +601,7 @@ class CheckData extends Command
});
}
//@deprecated
private function clientCreditPaymentables($client)
{
$results = \DB::select("
@ -587,6 +621,28 @@ class CheckData extends Command
return $results;
}
private function clientCreditPaymentablesNew($client)
{
$results = \DB::select("
SELECT
SUM(paymentables.amount - paymentables.refunded) as credit_payment
FROM payments
LEFT JOIN paymentables
ON
payments.id = paymentables.payment_id
WHERE paymentable_type = 'invoices'
AND paymentables.deleted_at is NULL
AND paymentables.amount > 0
AND payments.is_deleted = 0
AND payments.client_id = ?;
", [$client->id]);
return $results;
}
private function checkPaidToDatesNew()
{
$clients_to_check = $this->clientPaidToDateQuery();
@ -599,9 +655,12 @@ class CheckData extends Command
$credits_from_reversal = Credit::withTrashed()->where('client_id', $client->id)->where('is_deleted', 0)->whereNotNull('invoice_id')->sum('amount');
$credits_used_for_payments = $this->clientCreditPaymentables($client);
$total_paid_to_date = $_client->payments_applied + $credits_used_for_payments[0]->credit_payment - $credits_from_reversal;
//2025-03-06 - new method
// $credits_used_for_payments = $this->clientCreditPaymentablesNew($client);
// $total_paid_to_date = $credits_used_for_payments[0]->credit_payment;
if (round($total_paid_to_date, 2) != round($_client->client_paid_to_date, 2)) {
$this->wrong_paid_to_dates++;
@ -911,14 +970,14 @@ class CheckData extends Command
public function checkClientSettings()
{
if ($this->option('fix') == 'true') {
Client::query()->whereNull('country_id')->orWhere('country_id', 0)->cursor()->each(function ($client) {
Client::query()->withTrashed()->whereNull('country_id')->orWhere('country_id', 0)->cursor()->each(function ($client) {
$client->country_id = $client->company->settings->country_id;
$client->saveQuietly();
$this->logMessage("Fixing country for # {$client->id}");
});
Client::query()->whereNull("settings->currency_id")->orWhereJsonContains('settings', ['currency_id' => ''])->cursor()->each(function ($client) {
Client::query()->withTrashed()->whereNull("settings->currency_id")->orWhereJsonContains('settings', ['currency_id' => ''])->cursor()->each(function ($client) {
$settings = $client->settings;
$settings->currency_id = (string)$client->company->settings->currency_id;
$client->settings = $settings;
@ -928,7 +987,7 @@ class CheckData extends Command
});
Payment::withTrashed()->where('exchange_rate', 0)->cursor()->each(function ($payment) {
Payment::withTrashed()->withTrashed()->where('exchange_rate', 0)->cursor()->each(function ($payment) {
$payment->exchange_rate = 1;
$payment->saveQuietly();
@ -941,11 +1000,11 @@ class CheckData extends Command
})
->cursor()
->each(function ($p) {
$p->currency_id = $p->client->settings->currency_id;
$p->currency_id = $p->client->settings->currency_id ?? $p->company->settings->currency_id;
$p->saveQuietly();
$this->logMessage("Fixing currency for # {$p->id}");
$this->logMessage("Fixing currency for # {$p->id} with new currency {$p->currency_id}");
});
@ -956,7 +1015,7 @@ class CheckData extends Command
$c->subdomain = MultiDB::randomSubdomainGenerator();
$c->save();
$this->logMessage("Fixing subdomain for # {$c->id}");
$this->logMessage("Fixing subdomain for # {$c->id} with new subdomain {$c->subdomain}");
});
@ -1017,7 +1076,7 @@ class CheckData extends Command
public function checkVendorSettings()
{
if ($this->option('fix') == 'true') {
Vendor::query()->whereNull('currency_id')->orWhere('currency_id', '')->cursor()->each(function ($vendor) {
Vendor::query()->withTrashed()->whereNull('currency_id')->orWhere('currency_id', '')->cursor()->each(function ($vendor) {
$vendor->currency_id = $vendor->company->settings->currency_id;
$vendor->saveQuietly();
@ -1115,7 +1174,7 @@ class CheckData extends Command
$bt->invoice_ids = collect($bt->payment->invoices)->pluck('hashed_id')->implode(',');
$bt->save();
$this->logMessage("Fixing - {$bt->id}");
$this->logMessage("Fixing - {$bt->id} with new invoice IDs {$bt->invoice_ids}");
}
@ -1136,7 +1195,7 @@ class CheckData extends Command
$c->send_email = false;
$c->saveQuietly();
$this->logMessage("Fixing - {$c->id}");
$this->logMessage("Fixing - {$c->id} with new send email {$c->send_email}");
});
}
@ -1175,11 +1234,11 @@ class CheckData extends Command
if ($this->option('fix') == 'true') {
$p->cursor()->each(function ($c) {
$c->currency_id = $c->client->settings->currency_id ?? $c->company->settings->currency_id;
$c->saveQuietly();
$p->cursor()->each(function ($payment) {
$payment->currency_id = $payment->client->settings->currency_id ?? $payment->company->settings->currency_id;
$payment->saveQuietly();
$this->logMessage("Fixing - {$c->id}");
$this->logMessage("Fixing payment ID - {$payment->id} with new currency {$payment->currency_id}");
});
}

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -32,6 +33,7 @@ use App\Models\TaxRate;
use App\Libraries\MultiDB;
use App\Models\TaskStatus;
use App\Models\CompanyToken;
use App\Models\Subscription;
use App\Models\ClientContact;
use App\Models\VendorContact;
use App\Models\CompanyGateway;
@ -142,6 +144,12 @@ class CreateSingleAccount extends Command
'portal_domain' => 'http://ninja.test:8000',
'track_inventory' => true
]);
$custom_fields = new \stdClass();
$custom_fields->client1 = 'CKey|single_line_text';
$custom_fields->client2 = 'AKey|single_line_text';
$company->custom_fields = $custom_fields;
$faker = \Faker\Factory::create();
$settings = $company->settings;
@ -255,7 +263,11 @@ class CreateSingleAccount extends Command
'company_id' => $company->id,
'name' => 'cypress'
]);
$client->custom_value1 = $company->company_key;
$client->custom_value2 = $account->key;
$client->save();
ClientContact::factory()->create([
'user_id' => $user->id,
'client_id' => $client->id,
@ -406,8 +418,8 @@ class CreateSingleAccount extends Command
'company_id' => $company->id,
'product_key' => 'pro_plan',
'notes' => 'The Pro Plan',
'cost' => 10,
'price' => 10,
'cost' => 12,
'price' => 12,
'quantity' => 1,
]);
@ -416,8 +428,38 @@ class CreateSingleAccount extends Command
'company_id' => $company->id,
'product_key' => 'enterprise_plan',
'notes' => 'The Enterprise Plan',
'cost' => 14,
'price' => 14,
'cost' => 16,
'price' => 16,
'quantity' => 1,
]);
$pe5 = Product::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'product_key' => 'enterprise_plan_5',
'notes' => 'The Enterprise Plan 5',
'cost' => 28,
'price' => 28,
'quantity' => 1,
]);
$pe10 = Product::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'product_key' => 'enterprise_plan_10',
'notes' => 'The Enterprise Plan 10',
'cost' => 56,
'price' => 56,
'quantity' => 1,
]);
$pe20 = Product::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'product_key' => 'enterprise_plan_20',
'notes' => 'The Enterprise Plan 20',
'cost' => 112,
'price' => 112,
'quantity' => 1,
]);
@ -431,6 +473,28 @@ class CreateSingleAccount extends Command
'quantity' => 1,
]);
$p4= Product::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'product_key' => 'docuninja_user',
'notes' => 'The DocuNinja Monthly User Plan',
'cost' => 6,
'price' => 6,
'quantity' => 1,
]);
$p5 = Product::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'product_key' => 'docuninja_user_annual',
'notes' => 'The DocuNinja Annual User Plan',
'cost' => 60,
'price' => 60,
'quantity' => 1,
]);
$webhook_config = [
'post_purchase_url' => 'http://ninja.test:8000/api/admin/plan',
'post_purchase_rest_method' => 'post',
@ -463,8 +527,119 @@ class CreateSingleAccount extends Command
$sub->allow_plan_changes = true;
$sub->frequency_id = RecurringInvoice::FREQUENCY_MONTHLY;
$sub->save();
if(!\App\Models\Subscription::find(6)){
$sub = SubscriptionFactory::create($company->id, $user->id);
$sub->id = 6;
$sub->name = " PRO Pro Plan";
$sub->group_id = $gs->id;
$sub->recurring_product_ids = "{$p1->hashed_id}";
$sub->webhook_configuration = $webhook_config;
$sub->allow_plan_changes = true;
$sub->frequency_id = RecurringInvoice::FREQUENCY_MONTHLY;
$sub->save();
}
if (!\App\Models\Subscription::find(11)) {
$sub = SubscriptionFactory::create($company->id, $user->id);
$sub->id = 11;
$sub->name = " EEE Enterprise Plan";
$sub->group_id = $gs->id;
$sub->recurring_product_ids = "{$p2->hashed_id}";
$sub->webhook_configuration = $webhook_config;
$sub->allow_plan_changes = true;
$sub->frequency_id = RecurringInvoice::FREQUENCY_MONTHLY;
$sub->max_seats_limit =2;
$sub->per_seat_enabled = true;
$sub->save();
}
$_sub = $sub->replicate();
$_sub->id = 41;
$_sub->name = "Enterprise Plan 3-5 Users";
$_sub->recurring_product_ids = "{$pe5->hashed_id}";
$_sub->max_seats_limit =5;
$_sub->per_seat_enabled = true;
$_sub->save();
$_sub = $sub->replicate();
$_sub->id = 46;
$_sub->name = "Enterprise Plan 6-10 Users";
$_sub->recurring_product_ids = "{$pe10->hashed_id}";
$_sub->max_seats_limit =10;
$_sub->per_seat_enabled = true;
$_sub->save();
$_sub = $sub->replicate();
$_sub->id = 51;
$_sub->name = "Enterprise Plan 11-20 Users";
$_sub->recurring_product_ids = "{$pe20->hashed_id}";
$_sub->max_seats_limit =20;
$_sub->per_seat_enabled = true;
$_sub->save();
$sub = SubscriptionFactory::create($company->id, $user->id);
$sub->name = "DocuNinja Monthly Plan";
$sub->group_id = $gs->id;
$sub->recurring_product_ids = "{$p4->hashed_id}";
$sub->webhook_configuration = $webhook_config;
$sub->allow_plan_changes = true;
$sub->frequency_id = RecurringInvoice::FREQUENCY_MONTHLY;
$sub->save();
$sub = SubscriptionFactory::create($company->id, $user->id);
$sub->name = "DocuNinja Annual Plan";
$sub->group_id = $gs->id;
$sub->recurring_product_ids = "{$p5->hashed_id}";
$sub->webhook_configuration = $webhook_config;
$sub->allow_plan_changes = true;
$sub->frequency_id = RecurringInvoice::FREQUENCY_ANNUALLY;
$sub->save();
if($config = config('admin-api.products')){
foreach($config as $key => $product){
if(!$p = Product::where('product_key', $key)->first()){
$p = Product::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'product_key' => $key,
'notes' => $product['description'],
'price' => $product['price']
]);
if(!Subscription::find($product['subscription_id'])){
$sub = SubscriptionFactory::create($company->id, $user->id);
$sub->id = $product['subscription_id'];
$sub->name = $product['description'];
$sub->recurring_product_ids = "{$p->hashed_id}";
$sub->webhook_configuration = $webhook_config;
$sub->allow_plan_changes = true;
$sub->frequency_id = $product['term'] == 'month' ? RecurringInvoice::FREQUENCY_MONTHLY : RecurringInvoice::FREQUENCY_ANNUALLY;
$sub->max_seats_limit = $product['users'] ?? 1;
$sub->per_seat_enabled = true;
$sub->save();
}
}
}
}
}
private function createClient($company, $user)
{
// dispatch(function () use ($company, $user) {

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -70,7 +71,7 @@ class DesignUpdate extends Command
private function handleOnDb()
{
foreach (Design::where('is_custom', false)->get() as $design) {
$invoice_design = new \App\Services\Pdf\DesignExtractor($design->name);
$design_object = new stdClass();

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -56,8 +57,9 @@ class S3Cleanup extends Command
$c1 = Company::on('db-ninja-01')->pluck('company_key');
$c2 = Company::on('db-ninja-02')->pluck('company_key');
$c3 = Company::on('db-ninja-03')->pluck('company_key');
$merged = $c1->merge($c2)->toArray();
$merged = $c1->merge($c2)->merge($c3)->toArray();
$directories = Storage::disk(config('filesystems.default'))->directories();

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -88,9 +89,9 @@ class SendRemindersCron extends Command
});
if ($invoice->invitations->count() > 0) {
// event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(), $reminder_template));
event(new \App\Events\General\EntityWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $reminder_template));
// event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(), $reminder_template));
event(new \App\Events\General\EntityWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $reminder_template));
$invoice->entityEmailEvent($invoice->invitations->first(), $reminder_template);
}

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -35,6 +36,7 @@ class TranslationsExport extends Command
protected $log = '';
private array $langs = [
'af_ZA',
'ar',
'bg',
'ca',
@ -55,6 +57,7 @@ class TranslationsExport extends Command
'he',
'hr',
'hu',
'id_ID',
'it',
'ja',
'km_KH',

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -33,11 +34,13 @@ use App\Jobs\Ninja\BankTransactionSync;
use App\Jobs\Cron\RecurringExpensesCron;
use App\Jobs\Cron\RecurringInvoicesCron;
use App\Jobs\EDocument\EInvoicePullDocs;
use App\Jobs\Cron\InvoiceTaxSummary;
use Illuminate\Console\Scheduling\Schedule;
use App\Jobs\Invoice\InvoiceCheckLateWebhook;
use App\Jobs\Subscription\CleanStaleInvoiceOrder;
use App\PaymentDrivers\Rotessa\Jobs\TransactionReport;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\Console\Commands\CreateElasticIndex;
class Kernel extends ConsoleKernel
{
@ -70,6 +73,29 @@ class Kernel extends ConsoleKernel
/* Checks for scheduled tasks */
$schedule->job(new TaskScheduler())->hourlyAt(10)->withoutOverlapping()->name('task-scheduler-job')->onOneServer();
// Run hourly over 26-hour period for complete timezone coverage
$schedule->job(new InvoiceTaxSummary())
->hourly()
->when(function () {
$now = now();
$hour = $now->hour;
// Run for 26 hours starting from UTC 10:00 on last day of month
// This covers the transition period when timezones move to next month
if ($now->isSameDay($now->copy()->endOfMonth())) {
// Start at UTC 10:00 (when UTC+14 moves to next day)
return $hour >= 10;
} elseif ($now->isSameDay($now->copy()->startOfMonth())) {
// Continue until UTC 12:00 (when UTC-12 moves to next day)
return $hour <= 12;
}
return false;
})
->withoutOverlapping()
->name('invoice-tax-summary-26hour-coverage')
->onOneServer();
/* Checks Rotessa Transactions */
$schedule->job(new TransactionReport())->dailyAt('01:48')->withoutOverlapping()->name('rotessa-transaction-report')->onOneServer();
@ -131,6 +157,8 @@ class Kernel extends ConsoleKernel
$schedule->command('ninja:check-data --database=db-ninja-02')->dailyAt('02:20')->withoutOverlapping()->name('check-data-db-2-job')->onOneServer();
$schedule->command('ninja:check-data --database=db-ninja-03')->dailyAt('02:30')->withoutOverlapping()->name('check-data-db-2-job')->onOneServer();
$schedule->command('ninja:s3-cleanup')->dailyAt('23:15')->withoutOverlapping()->name('s3-cleanup-job')->onOneServer();
}
@ -152,4 +180,5 @@ class Kernel extends ConsoleKernel
require base_path('routes/console.php');
}
}

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -73,7 +74,7 @@ class DbQuery extends GenericMixedMetric
$this->string_metric6 = $string_metric6;
$this->double_metric2 = $double_metric2;
$this->string_metric7 = $string_metric7;
$this->string_metric8 = mb_convert_encoding($string_metric8, "UTF-8");
$this->string_metric9 = mb_convert_encoding($string_metric9, "UTF-8");
$this->string_metric8 = mb_convert_encoding($string_metric8 ?? '', "UTF-8");
$this->string_metric9 = mb_convert_encoding($string_metric9 ?? '', "UTF-8");
}
}

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -75,9 +76,10 @@ class EmailSuccess extends GenericMixedMetric
*/
public $string_metric8 = '';
public function __construct($string_metric7 = '', $string_metric8 = '')
public function __construct($string_metric7 = '', $string_metric8 = '', string $string_metric9 = '')
{
$this->string_metric7 = $string_metric7;
$this->string_metric8 = $string_metric8;
$this->string_metric9 = $string_metric9;
}
}

View File

@ -0,0 +1,60 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\Analytics;
use Turbo124\Beacon\ExampleMetric\GenericMixedMetric;
class JobFailureAnalytics 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 = 'job.failed';
/**
* 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 = 'name';
public $string_metric6 = 'exception';
public $int_metric1 = 1;
public function __construct($string_metric5, $string_metric6)
{
$this->string_metric5 = $string_metric5;
$this->string_metric6 = $string_metric6;
}
}

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -11,8 +12,9 @@
namespace App\DataMapper;
use App\Utils\Traits\MakesHash;
use stdClass;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
/**
* CompanySettings.
@ -229,7 +231,7 @@ class CompanySettings extends BaseSettings
public $require_quote_signature = false; //@TODO ben to confirm
//email settings
public $email_sending_method = 'default'; //enum 'default','gmail','office365' 'client_postmark', 'client_mailgun', 'mailgun', 'client_brevo' //@implemented
public $email_sending_method = 'default'; //enum 'default','gmail','office365' 'client_postmark', 'client_mailgun', 'mailgun', 'client_brevo', 'client_ses', 'ses' //@implemented
public $gmail_sending_user_id = '0'; //@implemented
@ -528,7 +530,18 @@ class CompanySettings extends BaseSettings
public bool $unlock_invoice_documents_after_payment = false;
public string $ses_secret_key = '';
public string $ses_access_key = '';
public string $ses_region = '';
public string $ses_topic_arn = '';
public string $ses_from_address = '';
public static $casts = [
'ses_from_address' => 'string',
'ses_topic_arn' => 'string',
'ses_secret_key' => 'string',
'ses_access_key' => 'string',
'ses_region' => 'string',
'unlock_invoice_documents_after_payment' => 'bool',
'preference_product_notes_for_html_view' => 'bool',
'enable_client_profile_update' => 'bool',
@ -930,7 +943,10 @@ class CompanySettings extends BaseSettings
{
$notification = new stdClass();
$notification->email = [];
$notification->email = ['invoice_sent_all', 'payment_success_all', 'payment_manual_all'];
if(Ninja::isSelfHost()) {
$notification->email = ['invoice_sent_all', 'payment_success_all', 'payment_manual_all'];
}
return $notification;
}

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -12,6 +13,7 @@
namespace App\DataMapper;
use App\Casts\InvoiceSyncCast;
use App\DataMapper\TaxReport\TaxReport;
use Illuminate\Contracts\Database\Eloquent\Castable;
/**
@ -23,9 +25,7 @@ class InvoiceSync implements Castable
public function __construct(array $attributes = [])
{
$this->qb_id = $attributes['qb_id'] ?? '';
}
/**

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -0,0 +1,46 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper;
use App\Casts\QuoteSyncCast;
use Illuminate\Contracts\Database\Eloquent\Castable;
/**
* QuoteSync.
*/
class QuoteSync implements Castable
{
public string $qb_id;
public function __construct(array $attributes = [])
{
$this->qb_id = $attributes['qb_id'] ?? '';
}
/**
* 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 QuoteSyncCast::class;
}
public static function fromArray(array $data): self
{
return new self($data);
}
}

View File

@ -18,9 +18,9 @@ class ReferralEarning
public string $version = 'alpha';
public string $referral_start_date = ''; // The date this referral was registered.
public string $qualifies_after = ''; // The date the payout qualifies after (5 months / 1 year)
public string $period_ending = ''; // The Date this set relates to. ie 2024-07-31 = July 2024
public string $account_key = '';

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -0,0 +1,57 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\Schedule;
class InvoiceOutstandingTasks
{
/**
* Defines the template name
*
* @var string
*/
public string $template = 'invoice_outstanding_tasks';
/**
* The date range the report should include
*
* @var string
*/
public string $date_range = 'this_month';
/**
* An array of clients hashed_ids
*
* Leave blank if this action should apply to all clients
*
* @var array
*/
public array $clients = [];
/**
* If true, the invoice will be auto-sent
* else it will be generated and kept in a draft state
*
* @var bool
*/
public bool $auto_send = false;
/**
* If true, the project tasks will be included in the report
*
* @var bool
*/
public bool $include_project_tasks = false;
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\DataMapper\Schedule;
class PaymentSchedule
{
/**
* The template name
*
* @var string
*/
public string $template = 'payment_schedule';
/**
*
* @var array(
* 'id' => int,
* 'date' => string,
* 'amount' => float,
* 'is_amount' => bool
* )
*/
public array $schedule = [];
/**
* The invoice id
*
* @var string
*/
public string $invoice_id = '';
/**
* Whether to auto bill the invoice
*
* @var bool
*/
public bool $auto_bill = false;
}

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -111,7 +112,7 @@ class BaseRule implements RuleInterface
];
/** EU TAXES */
/** Supported E Delivery Countries */
public array $peppol_business_countries = [
'AT',
@ -252,10 +253,10 @@ class BaseRule implements RuleInterface
$tax_data = $company->origin_tax_data;
} elseif($this->client->location && $this->client->location->is_shipping_location && $this->client->location->tax_data){
} elseif ($this->invoice->location && $this->invoice->location->is_shipping_location && $this->invoice->location->tax_data) {
$tax_data = $this->invoice->location->tax_data;
$tax_data = $this->client->location->tax_data;
} elseif ($this->client->tax_data) {
@ -334,6 +335,7 @@ class BaseRule implements RuleInterface
public function defaultForeign(): self
{
// nlog("default foreign");
if ($this->invoice->client->is_tax_exempt) {
$this->tax_rate1 = 0;

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*

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