Merge pull request #11238 from turbo124/v5-develop

v5.12.22
This commit is contained in:
David Bomba 2025-08-29 14:28:55 +10:00 committed by GitHub
commit 90e825662e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 34710 additions and 103 deletions

View File

@ -1 +1 @@
5.12.21
5.12.22

View File

@ -0,0 +1,37 @@
<?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\Events\Client;
use App\Models\Client;
use App\Models\Company;
use Illuminate\Queue\SerializesModels;
/**
* Class ClientWasMerged.
*/
class ClientWasMerged
{
use SerializesModels;
/**
* Create a new event instance.
*
* @param string $mergeable_client
* @param Client $client
* @param Company $company
* @param array $event_vars
*/
public function __construct(public string $mergeable_client, public Client $client, public Company $company, public array $event_vars)
{
}
}

View File

@ -0,0 +1,37 @@
<?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\Events\Client;
use App\Models\User;
use App\Models\Company;
use Illuminate\Queue\SerializesModels;
/**
* Class ClientWasMerged.
*/
class ClientWasPurged
{
use SerializesModels;
/**
* Create a new event instance.
*
* @param string $purged_client
* @param User $user
* @param Company $company
* @param array $event_vars
*/
public function __construct(public string $purged_client, public User $user, public Company $company, public array $event_vars)
{
}
}

37
app/Events/Vendor/VendorWasMerged.php vendored Normal file
View File

@ -0,0 +1,37 @@
<?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\Events\Vendor;
use App\Models\Vendor;
use App\Models\Company;
use Illuminate\Queue\SerializesModels;
/**
* Class ClientWasMerged.
*/
class VendorWasMerged
{
use SerializesModels;
/**
* Create a new event instance.
*
* @param string $mergeable_vendor
* @param Vendor $vendor
* @param Company $company
* @param array $event_vars
*/
public function __construct(public string $mergeable_vendor, public Vendor $vendor, public Company $company, public array $event_vars)
{
}
}

View File

@ -139,7 +139,7 @@ class LoginController extends BaseController
->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version'));
}
} elseif ($user->google_2fa_secret && !$request->has('one_time_password')) {
} elseif (strlen($user->google_2fa_secret ?? '') > 2 && !$request->has('one_time_password')) {
return response()
->json(['message' => ctrans('texts.invalid_one_time_password')], 401)
->header('X-App-Version', config('ninja.app_version'))

View File

@ -26,6 +26,8 @@ class TwilioController extends BaseController
'+23',
'+21',
'+17152567760',
'+93',
'+85',
];
public function __construct()
@ -231,7 +233,7 @@ class TwilioController extends BaseController
return response()->json(['message' => 'SMS verified'], 200);
}
$user->google_2fa_secret = '';
$user->google_2fa_secret = null;
$user->sms_verification_code = '';
$user->save();

View File

@ -29,7 +29,7 @@ class TwoFactorController extends BaseController
/** @var \App\Models\User $user */
$user = auth()->user();
if ($user->google_2fa_secret) {
if (strlen($user->google_2fa_secret ?? '') > 2) {
return response()->json(['message' => '2FA already enabled'], 400);
} elseif (Ninja::isSelfHost()) {

View File

@ -22,6 +22,7 @@ class BlackListRule implements ValidationRule
{
/** Bad domains +/- disposable email domains */
private array $blacklist = [
"mailshan.com",
"tabletship.com",
"tiktook.lol",
"0-mail.com",

View File

@ -346,18 +346,13 @@ class NinjaMailerJob implements ShouldQueue
$t = app('translator');
$t->replace(Ninja::transformTranslations($this->nmo->settings));
if(Ninja::isHosted() && $this->nmo?->transport == 'default') {
$this->mailer = config('mail.default');
return $this;
}
/** Force free/trials onto specific mail driver */
if ($this->nmo->settings->email_sending_method == 'default' && $this->company->account->isNewHostedAccount()) {
if(Ninja::isHosted() && $this->nmo?->transport == 'default' && ($this->company->account->isNewHostedAccount() || !$this->company->account->isPaid())) {
$this->mailer = 'mailgun';
$this->setHostedMailgunMailer();
return $this;
}
if (Ninja::isHosted() && $this->company->account->isPaid() && $this->nmo->settings->email_sending_method == 'default') {
//check if outlook.
@ -399,10 +394,6 @@ class NinjaMailerJob implements ShouldQueue
$this->mailer = 'mailgun';
$this->setHostedMailgunMailer();
return $this;
case 'ses':
$this->mailer = 'ses';
$this->setHostedSesMailer();
return $this;
case 'gmail':
$this->mailer = 'gmail';
$this->setGmailMailer();
@ -584,20 +575,6 @@ class NinjaMailerJob implements ShouldQueue
}
private function setHostedSesMailer()
{
if (property_exists($this->nmo->settings, 'email_from_name') && strlen($this->nmo->settings->email_from_name) > 1) {
$email_from_name = $this->nmo->settings->email_from_name;
} else {
$email_from_name = $this->company->present()->name();
}
$this->nmo
->mailable
->from(config('services.ses.from.address'), $email_from_name);
}
/**
* Configures Mailgun using client supplied secret
* as the Mailer

View File

@ -0,0 +1,61 @@
<?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\Listeners\Activity;
use App\Libraries\MultiDB;
use App\Models\Activity;
use App\Repositories\ActivityRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use stdClass;
class ClientMergedActivity implements ShouldQueue
{
protected $activity_repo;
/**
* Create the event listener.
*
* @param ActivityRepository $activity_repo
*/
public function __construct(ActivityRepository $activity_repo)
{
$this->activity_repo = $activity_repo;
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
MultiDB::setDb($event->company->db);
$client = $event->client;
$fields = new stdClass();
$user_id = array_key_exists('user_id', $event->event_vars)
? $event->event_vars['user_id']
: $event->client->user_id;
$fields->client_id = $client->id;
$fields->user_id = $user_id;
$fields->company_id = $client->company_id;
$fields->activity_type_id = Activity::MERGE_CLIENT;
$fields->notes = $event->mergeable_client;
$this->activity_repo->save($fields, $client, $event->event_vars);
}
}

View File

@ -0,0 +1,58 @@
<?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\Listeners\Activity;
use App\Libraries\MultiDB;
use App\Models\Activity;
use App\Repositories\ActivityRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use stdClass;
class ClientPurgedActivity implements ShouldQueue
{
protected $activity_repo;
/**
* Create the event listener.
*
* @param ActivityRepository $activity_repo
*/
public function __construct(ActivityRepository $activity_repo)
{
$this->activity_repo = $activity_repo;
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
MultiDB::setDb($event->company->db);
$fields = new stdClass();
$user_id = array_key_exists('user_id', $event->event_vars)
? $event->event_vars['user_id']
: $event->user->id;
$fields->user_id = $user_id;
$fields->company_id = $event->company->id;
$fields->activity_type_id = Activity::PURGE_CLIENT;
$fields->notes = $event->purged_client;
$this->activity_repo->save($fields, $event->user, $event->event_vars);
}
}

View File

@ -0,0 +1,61 @@
<?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\Listeners\Activity;
use App\Libraries\MultiDB;
use App\Models\Activity;
use App\Repositories\ActivityRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use stdClass;
class VendorMergedActivity implements ShouldQueue
{
protected $activity_repo;
/**
* Create the event listener.
*
* @param ActivityRepository $activity_repo
*/
public function __construct(ActivityRepository $activity_repo)
{
$this->activity_repo = $activity_repo;
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
MultiDB::setDb($event->company->db);
$vendor = $event->vendor;
$fields = new stdClass();
$user_id = array_key_exists('user_id', $event->event_vars)
? $event->event_vars['user_id']
: $event->vendor->user_id;
$fields->vendor_id = $vendor->id;
$fields->user_id = $user_id;
$fields->company_id = $vendor->company_id;
$fields->activity_type_id = Activity::MERGE_VENDOR;
$fields->notes = $event->mergeable_vendor;
$this->activity_repo->save($fields, $vendor, $event->event_vars);
}
}

View File

@ -225,7 +225,7 @@ class RequiredClientInfo extends Component
$hash = Cache::get(request()->input('hash'));
/** @var \App\Models\Invoice $invoice */
$invoice = Invoice::find($this->decodePrimaryKey($hash['invoice_id']));
$invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($hash['invoice_id']));
$this->invoice_terms = $invoice->terms;
}

View File

@ -282,6 +282,13 @@ class Activity extends StaticModel
public const ACCOUNT_DELETED = 150;
public const MERGE_CLIENT = 151;
public const MERGE_VENDOR = 152;
public const PURGE_CLIENT = 153;
protected $casts = [
'is_system' => 'boolean',
'updated_at' => 'timestamp',

View File

@ -197,7 +197,7 @@ class ACH implements MethodInterface, LivewireMethodInterface
private function processUnsuccessfulPayment($response)
{
$this->braintree->sendFailureMail($response->transaction->additionalProcessorResponse);
$this->braintree->sendFailureMail($response->transaction->additionalProcessorResponse ?? 'Unknown error occurred');
$message = [
'server_response' => $response,

View File

@ -61,17 +61,22 @@ use App\Events\Task\TaskWasRestored;
use App\Events\User\UserLoginFailed;
use App\Events\User\UserWasArchived;
use App\Events\User\UserWasRestored;
use App\Listeners\LogRequestSending;
use App\Events\Quote\QuoteWasCreated;
use App\Events\Quote\QuoteWasDeleted;
use App\Events\Quote\QuoteWasEmailed;
use App\Events\Quote\QuoteWasUpdated;
use App\Events\Account\AccountCreated;
use App\Events\Account\AccountDeleted;
use App\Events\Client\ClientWasMerged;
use App\Events\Client\ClientWasPurged;
use App\Events\Credit\CreditWasViewed;
use App\Events\Invoice\InvoiceWasPaid;
use App\Events\Quote\QuoteWasApproved;
use App\Events\Quote\QuoteWasArchived;
use App\Events\Quote\QuoteWasRestored;
use App\Events\Vendor\VendorWasMerged;
use App\Listeners\LogResponseReceived;
use Illuminate\Queue\Events\JobFailed;
use App\Events\Client\ClientWasCreated;
use App\Events\Client\ClientWasDeleted;
@ -166,12 +171,15 @@ use App\Listeners\Activity\TaskUpdatedActivity;
use App\Listeners\Invoice\InvoiceEmailActivity;
use App\Listeners\SendVerificationNotification;
use App\Events\Credit\CreditWasEmailedAndFailed;
use App\Listeners\Activity\ClientMergedActivity;
use App\Listeners\Activity\ClientPurgedActivity;
use App\Listeners\Activity\CreatedQuoteActivity;
use App\Listeners\Activity\DeleteClientActivity;
use App\Listeners\Activity\DeleteCreditActivity;
use App\Listeners\Activity\QuoteUpdatedActivity;
use App\Listeners\Activity\TaskArchivedActivity;
use App\Listeners\Activity\TaskRestoredActivity;
use App\Listeners\Activity\VendorMergedActivity;
use App\Listeners\Credit\CreditRestoredActivity;
use App\Listeners\Invoice\CreateInvoiceActivity;
use App\Listeners\Invoice\InvoiceViewedActivity;
@ -194,6 +202,7 @@ use App\Listeners\Payment\PaymentBalanceActivity;
use App\Listeners\Payment\PaymentEmailedActivity;
use App\Listeners\Quote\QuoteCreatedNotification;
use App\Listeners\Quote\QuoteEmailedNotification;
use Illuminate\Http\Client\Events\RequestSending;
use App\Events\Invoice\InvoiceWasEmailedAndFailed;
use App\Events\Payment\PaymentWasEmailedAndFailed;
use App\Listeners\Activity\ArchivedClientActivity;
@ -212,6 +221,8 @@ use App\Listeners\Invoice\InvoiceRestoredActivity;
use App\Listeners\Invoice\InvoiceReversedActivity;
use App\Listeners\Payment\PaymentRestoredActivity;
use App\Listeners\Quote\QuoteApprovedNotification;
use SocialiteProviders\Apple\AppleExtendSocialite;
use SocialiteProviders\Manager\SocialiteWasCalled;
use App\Events\Subscription\SubscriptionWasCreated;
use App\Events\Subscription\SubscriptionWasDeleted;
use App\Events\Subscription\SubscriptionWasUpdated;
@ -223,6 +234,7 @@ use App\Listeners\Credit\CreditCreatedNotification;
use App\Listeners\Credit\CreditEmailedNotification;
use App\Listeners\Invoice\InvoiceCancelledActivity;
use App\Listeners\Quote\QuoteReminderEmailActivity;
use Illuminate\Http\Client\Events\ResponseReceived;
use App\Events\PurchaseOrder\PurchaseOrderWasViewed;
use App\Events\Subscription\SubscriptionWasArchived;
use App\Events\Subscription\SubscriptionWasRestored;
@ -238,6 +250,7 @@ use App\Listeners\Statement\StatementEmailedActivity;
use App\Events\PurchaseOrder\PurchaseOrderWasAccepted;
use App\Events\PurchaseOrder\PurchaseOrderWasArchived;
use App\Events\PurchaseOrder\PurchaseOrderWasRestored;
use App\Listeners\Payment\PaymentEmailFailureActivity;
use App\Listeners\Vendor\UpdateVendorContactLastLogin;
use App\Events\RecurringQuote\RecurringQuoteWasCreated;
use App\Events\RecurringQuote\RecurringQuoteWasDeleted;
@ -254,6 +267,7 @@ use App\Listeners\Activity\SubscriptionRestoredActivity;
use App\Listeners\Invoice\InvoiceAutoBillFailedActivity;
use App\Listeners\Invoice\InvoiceAutoBillSuccessActivity;
use App\Listeners\Invoice\InvoiceFailedEmailNotification;
use SocialiteProviders\Microsoft\MicrosoftExtendSocialite;
use App\Events\RecurringExpense\RecurringExpenseWasCreated;
use App\Events\RecurringExpense\RecurringExpenseWasDeleted;
use App\Events\RecurringExpense\RecurringExpenseWasUpdated;
@ -385,6 +399,12 @@ class EventServiceProvider extends ServiceProvider
ClientWasRestored::class => [
RestoreClientActivity::class,
],
ClientWasMerged::class => [
ClientMergedActivity::class,
],
ClientWasPurged::class => [
ClientPurgedActivity::class,
],
// Documents
DocumentWasCreated::class => [
],
@ -671,6 +691,9 @@ class EventServiceProvider extends ServiceProvider
VendorContactLoggedIn::class => [
UpdateVendorContactLastLogin::class,
],
VendorWasMerged::class => [
VendorMergedActivity::class,
],
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
// ... Manager won't register drivers that are not added to this listener.
\SocialiteProviders\Apple\AppleExtendSocialite::class.'@handle',

View File

@ -147,6 +147,17 @@ class ClientRepository extends BaseRepository
public function purge($client)
{
$purged_client = $client->present()->name();
$purged_client_hash = $client->client_hash;
$user = auth()->user() ?? $client->user;
$company = $client->company;
$event_vars = \App\Utils\Ninja::eventVars(auth()->user() ? auth()->user()->id : null);
$event_vars['client_hash'] = $purged_client_hash;
event(new \App\Events\Client\ClientWasPurged($purged_client, $user, $company, $event_vars));
nlog("Purging client id => {$client->id} => {$client->number}");
$client->contacts()->forceDelete();

View File

@ -36,6 +36,8 @@ class Merge extends AbstractService
nlog("balance pre {$this->client->balance}");
nlog("paid_to_date pre {$this->client->paid_to_date}");
$mergeable_client = $this->mergable_client->present()->name();
$this->client->balance += $this->mergable_client->balance;
$this->client->paid_to_date += $this->mergable_client->paid_to_date;
$this->client->save();
@ -43,6 +45,9 @@ class Merge extends AbstractService
nlog("balance post {$this->client->balance}");
nlog("paid_to_date post {$this->client->paid_to_date}");
$event_vars = \App\Utils\Ninja::eventVars(auth()->user() ? auth()->user()->id : null);
$event_vars['client_hash'] = $this->mergable_client->client_hash;
$this->updateLedger($this->mergable_client->balance);
$this->mergable_client->activities()->update(['client_id' => $this->client->id]);
@ -71,11 +76,14 @@ class Merge extends AbstractService
}
});
$this->mergable_client->forceDelete();
$this->client->credit_balance = $this->client->service()->getCreditBalance();
$this->client->saveQuietly();
event(new \App\Events\Client\ClientWasMerged($mergeable_client, $this->client, $this->client->company, $event_vars));
return $this->client;
}

View File

@ -246,7 +246,7 @@ class OrderXDocument extends AbstractService
} elseif (in_array($this->document->client->country->iso_3166_2, ["ES-CE", "ES-ML"])) {
$tax_type = OrderDutyTaxFeeCategories::TAX_FOR_PRODUCTION_SERVICES_AND_IMPORTATION_IN_CEUTA_AND_MELILLA;
} else {
nlog("Unkown tax case for xinvoice");
// nlog("Unkown tax case for xinvoice");
$tax_type = OrderDutyTaxFeeCategories::STANDARD_RATE;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -104,22 +104,22 @@ class ZugferdEDocument extends AbstractService
if ($this->document->custom_surcharge1 > 0) {
$surcharge = $this->document->uses_inclusive_taxes ? ($this->document->custom_surcharge1 / (1 + ($item["tax_rate"] / 100))) : $this->document->custom_surcharge1;
$this->xdocument->addDocumentAllowanceCharge($surcharge, true, $tax_code, "VAT", $item["tax_rate"]);
$this->xdocument->addDocumentAllowanceCharge($surcharge, true, $tax_code, "VAT", $item["tax_rate"],null,null,null,null,null,null, ctrans('texts.surcharge'));
}
if ($this->document->custom_surcharge2 > 0) {
$surcharge = $this->document->uses_inclusive_taxes ? ($this->document->custom_surcharge2 / (1 + ($item["tax_rate"] / 100))) : $this->document->custom_surcharge2;
$this->xdocument->addDocumentAllowanceCharge($surcharge, true, $tax_code, "VAT", $item["tax_rate"]);
$this->xdocument->addDocumentAllowanceCharge($surcharge, true, $tax_code, "VAT", $item["tax_rate"],null,null,null,null,null,null, ctrans('texts.surcharge'));
}
if ($this->document->custom_surcharge3 > 0) {
$surcharge = $this->document->uses_inclusive_taxes ? ($this->document->custom_surcharge3 / (1 + ($item["tax_rate"] / 100))) : $this->document->custom_surcharge3;
$this->xdocument->addDocumentAllowanceCharge($surcharge, true, $tax_code, "VAT", $item["tax_rate"]);
$this->xdocument->addDocumentAllowanceCharge($surcharge, true, $tax_code, "VAT", $item["tax_rate"],null,null,null,null,null,null, ctrans('texts.surcharge'));
}
if ($this->document->custom_surcharge4 > 0) {
$surcharge = $this->document->uses_inclusive_taxes ? ($this->document->custom_surcharge4 / (1 + ($item["tax_rate"] / 100))) : $this->document->custom_surcharge4;
$this->xdocument->addDocumentAllowanceCharge($surcharge, true, $tax_code, "VAT", $item["tax_rate"]);
$this->xdocument->addDocumentAllowanceCharge($surcharge, true, $tax_code, "VAT", $item["tax_rate"],null,null,null,null,null,null, ctrans('texts.surcharge'));
}
return $this;
@ -147,7 +147,8 @@ class ZugferdEDocument extends AbstractService
*/
private function setDocumentTaxes(): self
{
if ($this->document->total_taxes == 0) {
if ((string) $this->document->total_taxes == '0') {
$base_amount = 0;
$tax_amount = 0;
@ -175,7 +176,8 @@ class ZugferdEDocument extends AbstractService
false,
$this->tax_code,
"VAT",
0
0,
null,null,null,null,null,null, ctrans('texts.discount')
);
}
@ -215,7 +217,8 @@ class ZugferdEDocument extends AbstractService
false,
$this->getTaxType($item["tax_id"] ?? '2'),
"VAT",
$item["tax_rate"]
$item["tax_rate"],
null,null,null,null,null,null,ctrans('texts.discount')
);
}
@ -260,6 +263,7 @@ class ZugferdEDocument extends AbstractService
$item = $this->document->line_items[0] ?? null;
if (is_null($item)) {
$this->tax_code = ZugferdDutyTaxFeeCategories::EXEMPT_FROM_TAX;
return $this;
}
@ -270,7 +274,7 @@ class ZugferdEDocument extends AbstractService
$this->tax_code = ZugferdDutyTaxFeeCategories::EXEMPT_FROM_TAX;
// $this->exemption_reason_code = "VATEX-EU-NOT-TAX";
$this->exemption_reason_code = "VATEX-EU-O";
nlog("exemption_reason_code: {$this->exemption_reason_code}");
// nlog("exemption_reason_code: {$this->exemption_reason_code}");
} elseif ($item->tax_id == '9') { //reverse charge
$this->tax_code = ZugferdDutyTaxFeeCategories::VAT_REVERSE_CHARGE;
$this->exemption_reason_code = "VATEX-EU-AE";
@ -587,7 +591,7 @@ class ZugferdEDocument extends AbstractService
} elseif (in_array($this->document->client->country->iso_3166_2, ["ES-CE", "ES-ML"])) {
$tax_type = ZugferdDutyTaxFeeCategories::TAX_FOR_PRODUCTION_SERVICES_AND_IMPORTATION_IN_CEUTA_AND_MELILLA;
} else {
nlog("Unkown tax case for xinvoice");
// nlog("Unkown tax case for xinvoice");
$tax_type = ZugferdDutyTaxFeeCategories::STANDARD_RATE;
}
}

View File

@ -350,7 +350,7 @@ class ZugferdEDokument extends AbstractService
} elseif (in_array($this->document->client->country->iso_3166_2, ["ES-CE", "ES-ML"])) {
$tax_type = ZugferdDutyTaxFeeCategories::TAX_FOR_PRODUCTION_SERVICES_AND_IMPORTATION_IN_CEUTA_AND_MELILLA;
} else {
nlog("Unkown tax case for xinvoice");
// nlog("Unkown tax case for xinvoice");
$tax_type = ZugferdDutyTaxFeeCategories::STANDARD_RATE;
}
}

View File

@ -546,19 +546,6 @@ class Email implements ShouldQueue
}
private function setHostedSesMailer()
{
if (property_exists($this->email_object->settings, 'email_from_name') && strlen($this->email_object->settings->email_from_name) > 1) {
$email_from_name = $this->email_object->settings->email_from_name;
} else {
$email_from_name = $this->company->present()->name();
}
$this->mailable
->from(config('services.ses.from.address'), $email_from_name);
}
/**
* Sets the mail driver to use and applies any specific configuration
@ -568,7 +555,7 @@ class Email implements ShouldQueue
{
/** Force free/trials onto specific mail driver */
if ($this->email_object->settings->email_sending_method == 'default' && $this->company->account->isNewHostedAccount()) {
if ($this->email_object->settings->email_sending_method == 'default' && (!$this->company->account->isPaid() || $this->company->account->isNewHostedAccount())) {
$this->mailer = 'mailgun';
$this->setHostedMailgunMailer();
return $this;
@ -614,10 +601,6 @@ class Email implements ShouldQueue
$this->mailer = 'mailgun';
$this->setHostedMailgunMailer();
return $this;
case 'ses':
$this->mailer = 'ses';
$this->setHostedSesMailer();
return $this;
case 'gmail':
$this->mailer = 'gmail';
$this->setGmailMailer();

View File

@ -82,6 +82,11 @@ class PdfDesigner
$html .= $partials['body'];
$html .= $partials['footer'];
// Valid HTML is always required.
if(strlen($html) == 0){
return '<p></p>';
}
return $html;
}
}

View File

@ -95,10 +95,11 @@ class ProjectReport extends BaseExport
$ts = new TemplateService();
/** @var Project $_project */
/** @var ?Project $_project */
$_project = $query->first();
$currency_code = $_project ? $_project->company->currency()->code : $this->company->currency()->code;
$currency_code = $_project?->client ? $_project->client->currency()->code : $this->company->currency()->code;
$ts_instance = $ts->setCompany($this->company)
// ->setData($data)
->processData($data)

File diff suppressed because one or more lines are too long

View File

@ -33,6 +33,10 @@ class Merge extends AbstractService
public function run()
{
$_mergeable_vendor = $this->mergable_vendor->present()->name();
$event_vars = \App\Utils\Ninja::eventVars(auth()->user() ? auth()->user()->id : null);
$event_vars['vendor_hash'] = $this->mergable_vendor->vendor_hash;
$this->mergable_vendor->activities()->update(['vendor_id' => $this->vendor->id]);
$this->mergable_vendor->contacts()->update(['vendor_id' => $this->vendor->id]);
$this->mergable_vendor->credits()->update(['vendor_id' => $this->vendor->id]);
@ -56,6 +60,8 @@ class Merge extends AbstractService
$this->mergable_vendor->forceDelete();
event(new \App\Events\Vendor\VendorWasMerged($_mergeable_vendor, $this->vendor, $this->vendor->company, $event_vars));
return $this->vendor;
}

View File

@ -36,23 +36,21 @@ class PDF extends FPDI
// Calculate X position with offset
$base_x = config('ninja.pdf_page_numbering_x_alignment');
$x_position = $base_x + $this->x_offset;
// Set X position based on alignment
if ($this->text_alignment == 'L') {
$this->SetX($x_position);
$this->SetX($this->GetX() + $base_x);
// Adjust cell width to account for X offset
$cell_width = $this->GetPageWidth() - $x_position - 10;
$cell_width = $this->GetPageWidth();
$this->Cell($cell_width, 5, $trans, 0, 0, 'L');
} elseif ($this->text_alignment == 'R') {
$this->SetX($x_position);
$this->SetX($this->GetX() + $base_x);
// For right alignment, calculate width from X position to right edge
$cell_width = $this->GetPageWidth() - $x_position;
$cell_width = $this->GetPageWidth();
$this->Cell($cell_width, 5, $trans, 0, 0, 'R');
} else {
$this->SetX($x_position);
// For center alignment, calculate appropriate width
$cell_width = $this->GetPageWidth() - ($x_position * 2);
$cell_width = $this->GetPageWidth();
$this->Cell($cell_width, 5, $trans, 0, 0, 'C');
}
}

38
composer.lock generated
View File

@ -11410,16 +11410,16 @@
},
{
"name": "sentry/sentry",
"version": "4.15.0",
"version": "4.15.1",
"source": {
"type": "git",
"url": "https://github.com/getsentry/sentry-php.git",
"reference": "b2d84de69f3eda8ca22b0b00e9f923be3b837355"
"reference": "0d09baf3700869ec4b723c95eb466de56c3d74b6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/getsentry/sentry-php/zipball/b2d84de69f3eda8ca22b0b00e9f923be3b837355",
"reference": "b2d84de69f3eda8ca22b0b00e9f923be3b837355",
"url": "https://api.github.com/repos/getsentry/sentry-php/zipball/0d09baf3700869ec4b723c95eb466de56c3d74b6",
"reference": "0d09baf3700869ec4b723c95eb466de56c3d74b6",
"shasum": ""
},
"require": {
@ -11483,7 +11483,7 @@
],
"support": {
"issues": "https://github.com/getsentry/sentry-php/issues",
"source": "https://github.com/getsentry/sentry-php/tree/4.15.0"
"source": "https://github.com/getsentry/sentry-php/tree/4.15.1"
},
"funding": [
{
@ -11495,7 +11495,7 @@
"type": "custom"
}
],
"time": "2025-08-20T14:26:37+00:00"
"time": "2025-08-28T15:45:14+00:00"
},
{
"name": "sentry/sentry-laravel",
@ -19338,16 +19338,16 @@
},
{
"name": "phpunit/phpunit",
"version": "11.5.34",
"version": "11.5.35",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "3e4c6ef395f7cb61a6206c23e0e04b31724174f2"
"reference": "d341ee94ee5007b286fc7907b383aae6b5b3cc91"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3e4c6ef395f7cb61a6206c23e0e04b31724174f2",
"reference": "3e4c6ef395f7cb61a6206c23e0e04b31724174f2",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d341ee94ee5007b286fc7907b383aae6b5b3cc91",
"reference": "d341ee94ee5007b286fc7907b383aae6b5b3cc91",
"shasum": ""
},
"require": {
@ -19361,7 +19361,7 @@
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.2",
"phpunit/php-code-coverage": "^11.0.10",
"phpunit/php-code-coverage": "^11.0.11",
"phpunit/php-file-iterator": "^5.1.0",
"phpunit/php-invoker": "^5.0.1",
"phpunit/php-text-template": "^4.0.1",
@ -19419,7 +19419,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.34"
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.35"
},
"funding": [
{
@ -19443,7 +19443,7 @@
"type": "tidelift"
}
],
"time": "2025-08-20T14:41:45+00:00"
"time": "2025-08-28T05:13:54+00:00"
},
{
"name": "react/cache",
@ -20947,16 +20947,16 @@
},
{
"name": "spatie/backtrace",
"version": "1.8.0",
"version": "1.8.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/backtrace.git",
"reference": "1607d8870bf597fc4ad79a6945cf0b2e584c2669"
"reference": "8c0f16a59ae35ec8c62d85c3c17585158f430110"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/backtrace/zipball/1607d8870bf597fc4ad79a6945cf0b2e584c2669",
"reference": "1607d8870bf597fc4ad79a6945cf0b2e584c2669",
"url": "https://api.github.com/repos/spatie/backtrace/zipball/8c0f16a59ae35ec8c62d85c3c17585158f430110",
"reference": "8c0f16a59ae35ec8c62d85c3c17585158f430110",
"shasum": ""
},
"require": {
@ -20995,7 +20995,7 @@
],
"support": {
"issues": "https://github.com/spatie/backtrace/issues",
"source": "https://github.com/spatie/backtrace/tree/1.8.0"
"source": "https://github.com/spatie/backtrace/tree/1.8.1"
},
"funding": [
{
@ -21007,7 +21007,7 @@
"type": "other"
}
],
"time": "2025-08-25T16:16:45+00:00"
"time": "2025-08-26T08:22:30+00:00"
},
{
"name": "spatie/error-solutions",

View File

@ -17,8 +17,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => env('APP_VERSION', '5.12.21'),
'app_tag' => env('APP_TAG', '5.12.21'),
'app_version' => env('APP_VERSION', '5.12.22'),
'app_tag' => env('APP_TAG', '5.12.22'),
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false),
@ -257,7 +257,7 @@ return [
'storecove_email_catchall' => env('STORECOVE_CATCHALL_EMAIL',false),
'qvalia_api_key' => env('QVALIA_API_KEY', false),
'qvalia_partner_number' => env('QVALIA_PARTNER_NUMBER', false),
'pdf_page_numbering_x_alignment' => env('PDF_PAGE_NUMBER_X', 0),
'pdf_page_numbering_x_alignment' => env('PDF_PAGE_NUMBER_X', -10),
'pdf_page_numbering_y_alignment' => env('PDF_PAGE_NUMBER_Y', -6),
'hosted_einvoice_secret' => env('HOSTED_EINVOICE_SECRET', null),
'e_invoice_quota_warning' => env('E_INVOICE_QUOTA_WARNING', 15),

View File

@ -5617,8 +5617,11 @@ $lang = array(
'creator' => 'Created by',
'ses_topic_arn_help' => 'The SES topic (optional, only for webhook tracking)',
'ses_region_help' => 'The AWS region, ie us-east-1',
'ses_secret_key' => 'SES Secret Key',
'ses_access_key' => 'SES Access Key ID'
'ses_secret_key' => 'SES Secret Key',
'ses_access_key' => 'SES Access Key ID',
'activity_151' => 'Client :notes merged into :client by :user',
'activity_152' => 'Vendor :notes merged into :vendor by :user',
'activity_153' => 'Client :notes purged by :user',
);
return $lang;

View File

@ -415,7 +415,7 @@ Route::group(['middleware' => ['throttle:api', 'token_auth', 'valid_json','local
Route::post('settings/disable_two_factor', [TwoFactorController::class, 'disableTwoFactor']);
Route::post('verify', [TwilioController::class, 'generate'])->name('verify.generate')->middleware('throttle:1,1');
Route::post('verify/confirm', [TwilioController::class, 'confirm'])->name('verify.confirm');
Route::post('verify/confirm', [TwilioController::class, 'confirm'])->name('verify.confirm')->middleware('throttle:2,1');
Route::resource('vendors', VendorController::class); // name = (vendors. index / create / show / update / destroy / edit
Route::post('vendors/bulk', [VendorController::class, 'bulk'])->name('vendors.bulk');

View File

@ -68,6 +68,10 @@ class ZugferdTest extends TestCase
private string $zug_16931 = 'Services/EDocument/Standards/Validation/Zugferd/zugferd_16931.xslt';
private string $zf_extended_wl = 'Services/EDocument/Standards/Validation/Zugferd/FACTUR-X_EXTENDED.xslt';
private string $extended_profile = 'XInvoice-Extended';
protected function setUp(): void
{
parent::setUp();
@ -96,7 +100,7 @@ class ZugferdTest extends TestCase
$settings->classification = $params['company_classification'] ?? 'business';
$settings->country_id = Country::where('iso_3166_2', 'DE')->first()->id;
$settings->email = $this->faker->safeEmail();
$settings->e_invoice_type = 'XInvoice_3_0';
$settings->e_invoice_type = $params['e_invoice_type'] ?? 'XInvoice_3_0';
$settings->currency_id = '3';
$settings->name = 'Test Company';
$settings->address1 = 'Line 1 of address of the seller';
@ -255,6 +259,66 @@ class ZugferdTest extends TestCase
$this->assertCount(0, $validator->getErrors());
}
}
public function testDeTodeTaxExemptExtendedProfile()
{
$scenario = [
'company_vat' => 'DE923356489',
'company_country' => 'DE',
'client_country' => 'DE',
'client_vat' => 'DE923356488',
'classification' => 'business',
'has_valid_vat' => true,
'over_threshold' => true,
'legal_entity_id' => 290868,
'e_invoice_type' => $this->extended_profile,
];
$data = $this->setupTestData($scenario);
$invoice = $data['invoice'];
$repo = new InvoiceRepository();
foreach($this->inclusive_scenarios as $scenario){
$invoice_data = json_decode($scenario, true);
$line_items = $invoice_data['line_items'];
foreach ($line_items as &$item) {
$item['tax_rate1'] = 0;
$item['tax_name1'] = 'VAT';
$item['tax_id'] = '5';
}
unset($item);
$invoice_data['line_items'] = array_values($line_items);
$invoice_data['uses_inclusive_taxes'] = false;
$invoice = $repo->save($invoice_data, $invoice);
$invoice = $invoice->calc()->getInvoice();
$xml = $invoice->service()->getEInvoice();
$validator = new \App\Services\EDocument\Standards\Validation\XsltDocumentValidator($xml);
$validator->setStyleSheets([$this->zf_extended_wl]);
$validator->setXsd('/Services/EDocument/Standards/Validation/Zugferd/Schema/XSD/CrossIndustryInvoice_100pD22B.xsd');
$validator->validate();
if (count($validator->getErrors()) > 0) {
nlog($invoice->withoutRelations()->toArray());
nlog($xml);
nlog($validator->getErrors());
}
$this->assertCount(0, $validator->getErrors());
}
}
@ -585,6 +649,7 @@ class ZugferdTest extends TestCase
{
$zug_16931 = 'Services/EDocument/Standards/Validation/Zugferd/zugferd_16931.xslt';
// $xr_cii = 'Services/EDocument/Standards/Validation/Zugferd/xrechnung_cii.xslt';
// $zug_16931 = 'Services/EDocument/Standards/Validation/Zugferd/FACTUR-X_MINIMUM.xslt';
@ -621,6 +686,8 @@ class ZugferdTest extends TestCase
$validator = new \App\Services\EDocument\Standards\Validation\XsltDocumentValidator($xml);
$validator->setStyleSheets([$this->zug_16931]);
$validator->setXsd('/Services/EDocument/Standards/Validation/Zugferd/Schema/XSD/CrossIndustryInvoice_100pD22B.xsd');
$validator->validate();
@ -684,5 +751,399 @@ class ZugferdTest extends TestCase
}
// ============================================================================
// EXTENDED PROFILE TEST METHODS - Duplicates using extended profile and XSLT
// ============================================================================
public function testDeToNlReverseTaxExtendedProfile()
{
$scenario = [
'company_vat' => 'DE923356489',
'company_country' => 'DE',
'client_country' => 'NL',
'client_vat' => 'NL808436332B01',
'classification' => 'business',
'has_valid_vat' => true,
'over_threshold' => true,
'legal_entity_id' => 290868,
'e_invoice_type' => $this->extended_profile,
];
$data = $this->setupTestData($scenario);
$invoice = $data['invoice'];
$repo = new InvoiceRepository();
foreach($this->inclusive_scenarios as $scenario){
$invoice_data = json_decode($scenario, true);
$line_items = $invoice_data['line_items'];
foreach ($line_items as &$item) {
$item['tax_rate1'] = 0;
$item['tax_name1'] = '';
$item['tax_id'] = '9';
}
unset($item);
$invoice_data['line_items'] = array_values($line_items);
$invoice_data['uses_inclusive_taxes'] = false;
$invoice = $repo->save($invoice_data, $invoice);
$invoice = $invoice->calc()->getInvoice();
$xml = $invoice->service()->getEInvoice();
$validator = new \App\Services\EDocument\Standards\Validation\XsltDocumentValidator($xml);
$validator->setStyleSheets([$this->zf_extended_wl]);
$validator->setXsd('/Services/EDocument/Standards/Validation/Zugferd/Schema/XSD/CrossIndustryInvoice_100pD22B.xsd');
$validator->validate();
if (count($validator->getErrors()) > 0) {
nlog($invoice->withoutRelations()->toArray());
nlog($xml);
nlog($validator->getErrors());
}
$this->assertCount(0, $validator->getErrors());
}
}
public function testInclusiveScenariosExtendedProfile()
{
$scenario = [
'company_vat' => 'DE923356489',
'company_country' => 'DE',
'client_country' => 'DE',
'client_vat' => 'DE923356488',
'classification' => 'business',
'has_valid_vat' => true,
'over_threshold' => true,
'legal_entity_id' => 290868,
'e_invoice_type' => $this->extended_profile,
];
$data = $this->setupTestData($scenario);
$invoice = $data['invoice'];
$repo = new InvoiceRepository();
foreach($this->inclusive_scenarios as $scenario){
$invoice_data = json_decode($scenario, true);
$invoice = $repo->save($invoice_data, $invoice);
$invoice = $invoice->calc()->getInvoice();
$xml = $invoice->service()->getEInvoice();
$validator = new \App\Services\EDocument\Standards\Validation\XsltDocumentValidator($xml);
$validator->setStyleSheets([$this->zf_extended_wl]);
$validator->setXsd('/Services/EDocument/Standards/Validation/Zugferd/Schema/XSD/CrossIndustryInvoice_100pD22B.xsd');
$validator->validate();
if (count($validator->getErrors()) > 0) {
nlog($invoice->withoutRelations()->toArray());
nlog($xml);
nlog($validator->getErrors());
}
$this->assertCount(0, $validator->getErrors());
}
}
public function testExclusiveScenariosExtendedProfile()
{
$scenario = [
'company_vat' => 'DE923356489',
'company_country' => 'DE',
'client_country' => 'DE',
'client_vat' => 'DE923356488',
'classification' => 'business',
'has_valid_vat' => true,
'over_threshold' => true,
'legal_entity_id' => 290868,
'e_invoice_type' => $this->extended_profile,
];
$data = $this->setupTestData($scenario);
$invoice = $data['invoice'];
$repo = new InvoiceRepository();
foreach($this->inclusive_scenarios as $scenario){
$invoice_data = json_decode($scenario, true);
$invoice_data['uses_inclusive_taxes'] = false;
$invoice = $repo->save($invoice_data, $invoice);
$invoice = $invoice->calc()->getInvoice();
$xml = $invoice->service()->getEInvoice();
$validator = new \App\Services\EDocument\Standards\Validation\XsltDocumentValidator($xml);
$validator->setStyleSheets([$this->zf_extended_wl]);
$validator->setXsd('/Services/EDocument/Standards/Validation/Zugferd/Schema/XSD/CrossIndustryInvoice_100pD22B.xsd');
$validator->validate();
if (count($validator->getErrors()) > 0) {
nlog($invoice->withoutRelations()->toArray());
nlog($xml);
nlog($validator->getErrors());
}
$this->assertCount(0, $validator->getErrors());
}
}
public function testZugFerdValidationExtendedProfile()
{
$scenario = [
'company_vat' => 'DE923356489',
'company_country' => 'DE',
'client_country' => 'DE',
'client_vat' => 'DE923356488',
'classification' => 'business',
'has_valid_vat' => true,
'over_threshold' => true,
'legal_entity_id' => 290868,
'e_invoice_type' => $this->extended_profile,
];
$data = $this->setupTestData($scenario);
$invoice = $data['invoice'];
$xml = $invoice->service()->getEInvoice();
$validator = new \App\Services\EDocument\Standards\Validation\XsltDocumentValidator($xml);
$validator->setStyleSheets([$this->zf_extended_wl]);
$validator->setXsd('/Services/EDocument/Standards/Validation/Zugferd/Schema/XSD/CrossIndustryInvoice_100pD22B.xsd');
$validator->validate();
if (count($validator->getErrors()) > 0) {
nlog($xml);
nlog($validator->getErrors());
}
$this->assertCount(0, $validator->getErrors());
}
public function testZugFerdValidationWithInclusiveTaxesExtendedProfile()
{
$scenario = [
'company_vat' => 'DE923356489',
'company_country' => 'DE',
'client_country' => 'DE',
'client_vat' => 'DE923356488',
'classification' => 'business',
'has_valid_vat' => true,
'over_threshold' => true,
'legal_entity_id' => 290868,
'e_invoice_type' => $this->extended_profile,
];
$data = $this->setupTestData($scenario);
$invoice = $data['invoice'];
$invoice->uses_inclusive_taxes = true;
$invoice = $invoice->calc()->getInvoice();
$xml = $invoice->service()->getEInvoice();
$validator = new \App\Services\EDocument\Standards\Validation\XsltDocumentValidator($xml);
$validator->setStyleSheets([$this->zf_extended_wl]);
$validator->setXsd('/Services/EDocument/Standards/Validation/Zugferd/Schema/XSD/CrossIndustryInvoice_100pD22B.xsd');
$validator->validate();
if (count($validator->getErrors()) > 0) {
nlog($xml);
nlog($validator->getErrors());
}
$this->assertCount(0, $validator->getErrors());
}
public function testZugFerdValidationWithInclusiveTaxesAndTotalAmountDiscountExtendedProfile()
{
$scenario = [
'company_vat' => 'DE923356489',
'company_country' => 'DE',
'client_country' => 'DE',
'client_vat' => 'DE923356488',
'classification' => 'business',
'has_valid_vat' => true,
'over_threshold' => true,
'legal_entity_id' => 290868,
'e_invoice_type' => $this->extended_profile,
];
$data = $this->setupTestData($scenario);
$invoice = $data['invoice'];
$invoice->uses_inclusive_taxes = true;
$invoice = $invoice->calc()->getInvoice();
$invoice->discount=20;
$invoice->is_amount_discount = true;
$xml = $invoice->service()->getEInvoice();
$validator = new \App\Services\EDocument\Standards\Validation\XsltDocumentValidator($xml);
$validator->setStyleSheets([$this->zf_extended_wl]);
$validator->setXsd('/Services/EDocument/Standards/Validation/Zugferd/Schema/XSD/CrossIndustryInvoice_100pD22B.xsd');
$validator->validate();
if (count($validator->getErrors()) > 0) {
nlog($xml);
nlog($validator->getErrors());
}
$this->assertCount(0, $validator->getErrors());
}
public function testZugFerdValidationWithInclusiveTaxesAndTotalPercentDiscountExtendedProfile()
{
$scenario = [
'company_vat' => 'DE923356489',
'company_country' => 'DE',
'client_country' => 'DE',
'client_vat' => 'DE923356488',
'classification' => 'business',
'has_valid_vat' => true,
'over_threshold' => true,
'legal_entity_id' => 290868,
'e_invoice_type' => $this->extended_profile,
];
$data = $this->setupTestData($scenario);
$invoice = $data['invoice'];
$invoice->uses_inclusive_taxes = true;
$invoice = $invoice->calc()->getInvoice();
$invoice->discount=20;
$invoice->is_amount_discount = false;
$xml = $invoice->service()->getEInvoice();
$validator = new \App\Services\EDocument\Standards\Validation\XsltDocumentValidator($xml);
$validator->setStyleSheets([$this->zf_extended_wl]);
$validator->setXsd('/Services/EDocument/Standards/Validation/Zugferd/Schema/XSD/CrossIndustryInvoice_100pD22B.xsd');
$validator->validate();
if (count($validator->getErrors()) > 0) {
nlog($xml);
nlog($validator->getErrors());
}
$this->assertCount(0, $validator->getErrors());
}
public function testZugFerdValidationWithInclusiveTaxesAndTotalPercentDiscountOnLineItemsAlsoExtendedProfile()
{
$scenario = [
'company_vat' => 'DE923356489',
'company_country' => 'DE',
'client_country' => 'DE',
'client_vat' => 'DE923356488',
'classification' => 'business',
'has_valid_vat' => true,
'over_threshold' => true,
'legal_entity_id' => 290868,
'e_invoice_type' => $this->extended_profile,
];
$data = $this->setupTestData($scenario);
$invoice = $data['invoice'];
$invoice->uses_inclusive_taxes = true;
$invoice = $invoice->calc()->getInvoice();
$invoice->discount=20;
$invoice->is_amount_discount = false;
$items = $invoice->line_items;
foreach($items as &$item){
$item->discount=10;
$item->is_amount_discount = false;
}
unset($item);
$invoice->line_items = $items;
$xml = $invoice->service()->getEInvoice();
$validator = new \App\Services\EDocument\Standards\Validation\XsltDocumentValidator($xml);
$validator->setStyleSheets([$this->zf_extended_wl]);
$validator->setXsd('/Services/EDocument/Standards/Validation/Zugferd/Schema/XSD/CrossIndustryInvoice_100pD22B.xsd');
$validator->validate();
if (count($validator->getErrors()) > 0) {
nlog($xml);
nlog($validator->getErrors());
}
$this->assertCount(0, $validator->getErrors());
}
public function testZugFerdValidationWithInclusiveTaxesAndTotalAmountDiscountOnLineItemsAlsoExtendedProfile()
{
$scenario = [
'company_vat' => 'DE923356489',
'company_country' => 'DE',
'client_country' => 'DE',
'client_vat' => 'DE923356488',
'classification' => 'business',
'has_valid_vat' => true,
'over_threshold' => true,
'legal_entity_id' => 290868,
'e_invoice_type' => $this->extended_profile,
];
$data = $this->setupTestData($scenario);
$invoice = $data['invoice'];
$invoice->uses_inclusive_taxes = true;
$invoice = $invoice->calc()->getInvoice();
$invoice->discount=20;
$invoice->is_amount_discount = true;
$items = $invoice->line_items;
foreach($items as &$item){
$item->discount=5;
$item->is_amount_discount = true;
}
unset($item);
$invoice->line_items = $items;
$xml = $invoice->service()->getEInvoice();
$validator = new \App\Services\EDocument\Standards\Validation\XsltDocumentValidator($xml);
$validator->setStyleSheets([$this->zf_extended_wl]);
$validator->setXsd('/Services/EDocument/Standards/Validation/Zugferd/Schema/XSD/CrossIndustryInvoice_100pD22B.xsd');
$validator->validate();
if (count($validator->getErrors()) > 0) {
nlog($xml);
nlog($validator->getErrors());
}
$this->assertCount(0, $validator->getErrors());
}
}