diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index badbdef1d55d..6feefd261a06 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -8,7 +8,7 @@ assignees: '' --- +https://invoiceninja.github.io/en/self-host-troubleshooting/ --> ## Setup - Version: diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index 4ea22fe974ee..0fdee8ad6394 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -451,6 +451,7 @@ class BaseExport 'project' => 'task.project_id', 'billable' => 'task.billable', 'item_notes' => 'task.item_notes', + 'time_log' => 'task.time_log', ]; protected array $forced_client_fields = [ diff --git a/app/Export/CSV/TaskExport.php b/app/Export/CSV/TaskExport.php index 155d61a88f48..3d48b845a240 100644 --- a/app/Export/CSV/TaskExport.php +++ b/app/Export/CSV/TaskExport.php @@ -156,7 +156,7 @@ class TaskExport extends BaseExport $entity[$key] = $transformed_entity[$parts[1]]; } elseif (array_key_exists($key, $transformed_entity)) { $entity[$key] = $transformed_entity[$key]; - } elseif (in_array($key, ['task.start_date', 'task.end_date', 'task.duration', 'task.billable', 'task.item_notes'])) { + } elseif (in_array($key, ['task.start_date', 'task.end_date', 'task.duration', 'task.billable', 'task.item_notes', 'task.time_log'])) { $entity[$key] = ''; } else { $entity[$key] = $this->decorator->transform($key, $task); @@ -207,6 +207,9 @@ class TaskExport extends BaseExport $seconds = $task->calcDuration(); $entity['task.duration'] = $seconds; $entity['task.duration_words'] = $seconds > 86400 ? CarbonInterval::seconds($seconds)->locale($this->company->locale())->cascade()->forHumans() : now()->startOfDay()->addSeconds($seconds)->format('H:i:s'); + + $entity['task.time_log'] = (isset($item[1]) && $item[1] != 0) ? $item[1] - $item[0] : ctrans('texts.is_running'); + } if (in_array('task.billable', $this->input['report_keys']) || in_array('billable', $this->input['report_keys'])) { diff --git a/app/Http/Requests/Task/UpdateTaskRequest.php b/app/Http/Requests/Task/UpdateTaskRequest.php index 0944fb4eee1e..7e9752d29c20 100644 --- a/app/Http/Requests/Task/UpdateTaskRequest.php +++ b/app/Http/Requests/Task/UpdateTaskRequest.php @@ -67,7 +67,7 @@ class UpdateTaskRequest extends Request if(is_string($values)) { $values = json_decode($values, true); } - + if(!is_array($values)) { $fail('The '.$attribute.' must be a valid array.'); return; @@ -133,7 +133,7 @@ class UpdateTaskRequest extends Request } - if(!isset($input['time_log']) || empty($input['time_log']) || $input['time_log'] == '{}') { + if(!isset($input['time_log']) || empty($input['time_log']) || $input['time_log'] == '{}' || $input['time_log'] == '[""]') { $input['time_log'] = json_encode([]); } diff --git a/app/Jobs/Mail/NinjaMailerJob.php b/app/Jobs/Mail/NinjaMailerJob.php index 1124fec9c812..18e4304943a3 100644 --- a/app/Jobs/Mail/NinjaMailerJob.php +++ b/app/Jobs/Mail/NinjaMailerJob.php @@ -254,7 +254,7 @@ class NinjaMailerJob implements ShouldQueue private function incrementEmailCounter(): void { - if(in_array($this->mailer, ['default','mailgun','postmark'])) { + if(in_array($this->nmo->settings->email_sending_method, ['default','mailgun','postmark'])) { Cache::increment("email_quota".$this->company->account->key); } diff --git a/app/Jobs/Ninja/AdjustEmailQuota.php b/app/Jobs/Ninja/AdjustEmailQuota.php index 31a9dc1f2777..62ef46f490b2 100644 --- a/app/Jobs/Ninja/AdjustEmailQuota.php +++ b/app/Jobs/Ninja/AdjustEmailQuota.php @@ -79,7 +79,7 @@ class AdjustEmailQuota implements ShouldQueue /** Use redis pipelines to execute bulk deletes efficiently */ $redis = Redis::connection('sentinel-cache'); - $prefix = config('cache.prefix'). ":email_quota*"; + $prefix = config('cache.prefix'). "email_quota*"; $keys = $redis->keys($prefix); @@ -92,7 +92,7 @@ class AdjustEmailQuota implements ShouldQueue } $keys = null; - $prefix = config('cache.prefix'). ":throttle_notified*"; + $prefix = config('cache.prefix'). "throttle_notified*"; $keys = $redis->keys($prefix); diff --git a/app/Jobs/Payment/EmailPayment.php b/app/Jobs/Payment/EmailPayment.php index 1e93c099181d..5906cce74b17 100644 --- a/app/Jobs/Payment/EmailPayment.php +++ b/app/Jobs/Payment/EmailPayment.php @@ -58,10 +58,6 @@ class EmailPayment implements ShouldQueue */ public function handle() { - if ($this->company->is_disabled || (!$this->contact?->email ?? false)) { - nlog("company disabled - or - contact email not found"); - return; - } MultiDB::setDb($this->company->db); @@ -71,6 +67,11 @@ class EmailPayment implements ShouldQueue $this->contact = $this->payment->client->contacts()->orderBy('is_primary', 'desc')->first(); } + if ($this->company->is_disabled || (!$this->contact?->email ?? false)) { + nlog("company disabled - or - contact email not found"); + return; + } + $this->contact->load('client'); $email_builder = (new PaymentEmailEngine($this->payment, $this->contact))->build(); diff --git a/app/Mail/Engine/PaymentEmailEngine.php b/app/Mail/Engine/PaymentEmailEngine.php index d23772402310..c5832db31fdb 100644 --- a/app/Mail/Engine/PaymentEmailEngine.php +++ b/app/Mail/Engine/PaymentEmailEngine.php @@ -262,6 +262,7 @@ class PaymentEmailEngine extends BaseEmailEngine $data['$client.email'] = &$data['$email']; $data['$client.balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')]; + $data['$client.payment_balance'] = ['value' => Number::formatMoney($this->client->payment_balance, $this->client), 'label' => ctrans('texts.payment_balance_on_file')]; $data['$outstanding'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')]; $data['$client_balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')]; $data['$paid_to_date'] = ['value' => Number::formatMoney($this->client->paid_to_date, $this->client), 'label' => ctrans('texts.paid_to_date')]; diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 2383c0f42af0..fe2b1222f391 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -614,17 +614,17 @@ class Invoice extends BaseModel event(new InvoiceWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $template)); break; case 'reminder1': - event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $template)); + event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $reminder_template)); break; case 'reminder2': - event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $template)); + event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $reminder_template)); break; case 'reminder3': - event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $template)); + event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $reminder_template)); break; case 'reminder_endless': case 'endless_reminder': - event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $template)); + event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $reminder_template)); break; case 'custom1': case 'custom2': diff --git a/app/PaymentDrivers/BaseDriver.php b/app/PaymentDrivers/BaseDriver.php index 4c7dd2691013..26489ff7df13 100644 --- a/app/PaymentDrivers/BaseDriver.php +++ b/app/PaymentDrivers/BaseDriver.php @@ -400,6 +400,7 @@ class BaseDriver extends AbstractPaymentDriver return; $invoices = Invoice::query() + ->where('company_id', $this->company_gateway->company_id) ->whereIn('id', $this->transformKeys(array_column($payment_invoices, 'invoice_id'))) ->whereJsonContains('line_items', ['type_id' => '3']) ->withTrashed(); @@ -407,6 +408,7 @@ class BaseDriver extends AbstractPaymentDriver if($invoices->count() == 0){ $invoice = Invoice::query() + ->where('company_id', $this->company_gateway->company_id) ->whereIn('id', $this->transformKeys(array_column($payment_invoices, 'invoice_id'))) ->orderBy('id','desc') ->withTrashed() diff --git a/app/PaymentDrivers/Rotessa/PaymentMethod.php b/app/PaymentDrivers/Rotessa/PaymentMethod.php index 3e617f76f963..4883490319e7 100755 --- a/app/PaymentDrivers/Rotessa/PaymentMethod.php +++ b/app/PaymentDrivers/Rotessa/PaymentMethod.php @@ -100,8 +100,17 @@ class PaymentMethod implements MethodInterface $customer = array_merge(['address' => $request->only('address_1','address_2','city','postal_code','province_code','country'), 'custom_identifier' => $request->input('custom_identifier') ], $request->all()); - $this->rotessa->findOrCreateCustomer($customer); - + try{ + $this->rotessa->findOrCreateCustomer($customer); + } + catch(\Exception $e){ + + $message = json_decode($e->getMessage(), true); + + return redirect()->route('client.payment_methods.index')->withErrors(array_values($message['errors'])); + + } + return redirect()->route('client.payment_methods.index')->withMessage(ctrans('texts.payment_method_added')); } diff --git a/app/PaymentDrivers/RotessaPaymentDriver.php b/app/PaymentDrivers/RotessaPaymentDriver.php index 040a8c65e935..e52b206e9823 100644 --- a/app/PaymentDrivers/RotessaPaymentDriver.php +++ b/app/PaymentDrivers/RotessaPaymentDriver.php @@ -202,7 +202,6 @@ class RotessaPaymentDriver extends BaseDriver public function findOrCreateCustomer(array $data) { - nlog($data); $result = null; try { @@ -219,7 +218,6 @@ class RotessaPaymentDriver extends BaseDriver if(!isset($data['id'])) { - nlog("no id, lets goo"); $result = $this->gatewayRequest('post', 'customers', $data); if($result->failed()) @@ -252,9 +250,16 @@ class RotessaPaymentDriver extends BaseDriver 'code' => 500 ]; - SystemLogger::dispatch(['server_response' => is_null($result) ? '' : $result->getMessage(), 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, 880 , $this->client, $this->company_gateway->company); + SystemLogger::dispatch(['server_response' => $data, 'data' => []], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, 880 , $this->client, $this->company_gateway->company); - throw $th; + try{ + $errors = explode("422:", $th->getMessage())[1]; + } + catch(\Exception){ + $errors = 'Unknown error occured'; + } + + throw new \Exception($errors, $th->getCode()); } } diff --git a/app/Services/EDocument/Gateway/Storecove/Storecove.php b/app/Services/EDocument/Gateway/Storecove/Storecove.php index 397e8813ff2b..9734abfa587e 100644 --- a/app/Services/EDocument/Gateway/Storecove/Storecove.php +++ b/app/Services/EDocument/Gateway/Storecove/Storecove.php @@ -137,24 +137,29 @@ class Storecove { } - public function sendDocument(string $document, int $routing_id, array $identifiers = []) + public function sendDocument(string $document, int $routing_id, array $override_payload = []) { $payload = [ "legalEntityId" => $routing_id, "idempotencyGuid"=> \Illuminate\Support\Str::uuid(), "routing" => [ - "eIdentifiers" => $identifiers, + "eIdentifiers" => [], "emails" => ["david@invoiceninja.com"] ], "document"=> [ - 'documentType' => 'invoice', - "rawDocumentData"=> [ + + ], + ]; + + $payload = array_merge($payload, $override_payload); + + + $payload['document']['documentType'] = 'invoice'; + $payload['document']["rawDocumentData"] = [ "document" => base64_encode($document), "parse" => true, "parseStrategy"=> "ubl", - ], - ], ]; $uri = "document_submissions"; diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index d6c6e8f538ee..8540e214802e 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -13,22 +13,25 @@ namespace App\Services\EDocument\Standards; use App\Models\Company; use App\Models\Invoice; +use App\Helpers\Invoice\Taxer; use App\Services\AbstractService; use App\Helpers\Invoice\InvoiceSum; use InvoiceNinja\EInvoice\EInvoice; +use App\Utils\Traits\NumberFormatter; use App\Helpers\Invoice\InvoiceSumInclusive; -use App\Helpers\Invoice\Taxer; use InvoiceNinja\EInvoice\Models\Peppol\PaymentMeans; use InvoiceNinja\EInvoice\Models\Peppol\ItemType\Item; use InvoiceNinja\EInvoice\Models\Peppol\PartyType\Party; use InvoiceNinja\EInvoice\Models\Peppol\PriceType\Price; +use InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID; use InvoiceNinja\EInvoice\Models\Peppol\AddressType\Address; use InvoiceNinja\EInvoice\Models\Peppol\ContactType\Contact; use InvoiceNinja\EInvoice\Models\Peppol\CountryType\Country; +use InvoiceNinja\EInvoice\Models\Peppol\PartyIdentification; use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxAmount; +use InvoiceNinja\EInvoice\Models\Peppol\Party as PeppolParty; use InvoiceNinja\EInvoice\Models\Peppol\TaxTotalType\TaxTotal; use App\Services\EDocument\Standards\Settings\PropertyResolver; -use App\Utils\Traits\NumberFormatter; use InvoiceNinja\EInvoice\Models\Peppol\AmountType\PriceAmount; use InvoiceNinja\EInvoice\Models\Peppol\PartyNameType\PartyName; use InvoiceNinja\EInvoice\Models\Peppol\TaxSchemeType\TaxScheme; @@ -42,14 +45,13 @@ use InvoiceNinja\EInvoice\Models\Peppol\TaxScheme as PeppolTaxScheme; use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxExclusiveAmount; use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxInclusiveAmount; use InvoiceNinja\EInvoice\Models\Peppol\AmountType\LineExtensionAmount; +use InvoiceNinja\EInvoice\Models\Peppol\OrderReferenceType\OrderReference; use InvoiceNinja\EInvoice\Models\Peppol\MonetaryTotalType\LegalMonetaryTotal; use InvoiceNinja\EInvoice\Models\Peppol\TaxCategoryType\ClassifiedTaxCategory; use InvoiceNinja\EInvoice\Models\Peppol\CustomerPartyType\AccountingCustomerParty; use InvoiceNinja\EInvoice\Models\Peppol\SupplierPartyType\AccountingSupplierParty; use InvoiceNinja\EInvoice\Models\Peppol\FinancialAccountType\PayeeFinancialAccount; -use InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID; -use InvoiceNinja\EInvoice\Models\Peppol\Party as PeppolParty; -use InvoiceNinja\EInvoice\Models\Peppol\PartyIdentification; +use InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\CustomerAssignedAccountID; class Peppol extends AbstractService { @@ -103,7 +105,7 @@ class Peppol extends AbstractService 'DE' => 'VAT', //tested - requires Payment Means to be defined. 'DK' => 'ERST', 'EE' => 'VAT', - 'ES' => 'VAT', + 'ES' => 'VAT', //tested - B2G pending 'FI' => 'VAT', 'FR' => 'VAT', 'GR' => 'VAT', @@ -165,6 +167,8 @@ class Peppol extends AbstractService private EInvoice $e; + private array $storecove_meta = []; + /** * @param Invoice $invoice */ @@ -649,7 +653,7 @@ class Peppol extends AbstractService { $acp = new AccountingCustomerParty(); - + $party = new Party(); if(strlen($this->invoice->client->vat_number ?? '') > 1) { @@ -740,7 +744,15 @@ class Peppol extends AbstractService return $total; } + + ///////////////// Helper Methods ///////////////////////// + /** + * setInvoiceDefaults + * + * Stubs a default einvoice + * @return self + */ public function setInvoiceDefaults(): self { $settings = [ @@ -769,7 +781,15 @@ class Peppol extends AbstractService return $this; } - + + /** + * getSetting + * + * Attempts to harvest and return a preconfigured prop from company / client / invoice settings + * + * @param string $property_path + * @return mixed + */ public function getSetting(string $property_path): mixed { @@ -783,8 +803,14 @@ class Peppol extends AbstractService return null; } - - public function countryLevelMutators():self + + /** + * countryLevelMutators + * + * Runs country level specific requirements for the e-invoice + * @return self + */ + private function countryLevelMutators():self { if(method_exists($this, $this->invoice->company->country()->iso_3166_2)) @@ -792,7 +818,14 @@ class Peppol extends AbstractService return $this; } - + + /** + * setPaymentMeans + * + * Sets the payment means - if it exists + * @param bool $required + * @return self + */ private function setPaymentMeans(bool $required = false): self { @@ -803,12 +836,152 @@ class Peppol extends AbstractService return $this; } - if($required) - throw new \Exception('e-invoice generation halted:: Payment Means required'); + return $this->checkRequired($required, "Payment Means"); + + } + + /** + * setOrderReference + * + * sets the order reference - if it exists (Never rely on settings for this) + * + * @param bool $required + * @return self + */ + private function setOrderReference(bool $required = false): self + { + $this->p_invoice->BuyerReference = $this->invoice->po_number ?? ''; + + if(strlen($this->invoice->po_number ?? '') > 1) + { + $order_reference = new OrderReference(); + $id = new ID(); + $id->value = $this->invoice->po_number; + + $order_reference->ID = $id; + + $this->p_invoice->OrderReference = $order_reference; + + // $this->setStorecoveMeta(["document" => [ + // "invoice" => [ + // [ + // "references" => [ + // "documentType" => "purchase_order", + // "documentId" => $this->invoice->po_number, + // ], + // ], + // ], + // ] + // ]); + + return $this; + } + + return $this->checkRequired($required, 'Order Reference'); - return $this; } + /** + * setCustomerAssignedAccountId + * + * Sets the client id_number CAN rely on settings + * + * @param bool $required + * @return self + */ + private function setCustomerAssignedAccountId(bool $required = false): self + { + //@phpstan-ignore-next-line + if(isset($this->p_invoice->AccountingCustomerParty->CustomerAssignedAccountID)){ + return $this; + } + elseif($customer_assigned_account_id = $this->getSetting('Invoice.AccountingCustomerParty.CustomerAssignedAccountID')){ + + $this->p_invoice->AccountingCustomerParty->CustomerAssignedAccountID = $customer_assigned_account_id; + return $this; + } + elseif(strlen($this->invoice->client->id_number ?? '') > 1){ + + $customer_assigned_account_id = new CustomerAssignedAccountID(); + $customer_assigned_account_id->value = $this->invoice->client->id_number; + + $this->p_invoice->AccountingCustomerParty->CustomerAssignedAccountID = $customer_assigned_account_id; + return $this; + } + + //@phpstan-ignore-next-line + return $this->checkRequired($required, 'Client ID Number'); + + } + + /** + * Check Required + * + * Throws if a required field is missing. + * + * @param bool $required + * @param string $section + * @return self + */ + private function checkRequired(bool $required, string $section): self + { + + return $required ? throw new \Exception("e-invoice generation halted:: {$section} required") : $this; + + } + + + /** + * Builds the Routing object for StoreCove + * + * @param string $schemeId + * @param string $id + * @return array + */ + private function buildRouting(string $schemeId, string $id): array + { + + return + [ + "routing" => [ + "publicIdentifiers" => [ + [ + "scheme" => $schemeId, + "id" => $id + ] + ] + ] + ]; + } + + /** + * setStorecoveMeta + * + * updates the storecove payload for sending documents + * + * @param array $meta + * @return self + */ + private function setStorecoveMeta(array $meta): self + { + $this->storecove_meta = array_merge($this->storecove_meta, $meta); + + return $this; + } + + public function getStorecoveMeta(): array + { + return $this->storecove_meta; + } + + + + + + + + ////////////////////////// Country level mutators ///////////////////////////////////// + /** * DE * @@ -867,10 +1040,10 @@ class Peppol extends AbstractService * ES * * @Pending + * B2G configuration + * B2G Testing * - * ES:DIRE - routing identifier - * - * testing. //293098 + * testing. // routing identifier - 293098 * * @return self */ @@ -920,20 +1093,45 @@ class Peppol extends AbstractService return $this; } - + + /** + * FR + * @Pending - clarification on codes needed + * + * @return self + */ private function FR(): self { + // When sending invoices to the French government (Chorus Pro): - // All invoices have to be routed to SIRET 0009:11000201100044. There is no test environment for sending to public entities. - // The SIRET / 0009 identifier of the final recipient is to be included in the invoice.accountingCustomerParty.publicIdentifiers array. + if($this->invoice->client->classification == 'government'){ + //route to SIRET 0009:11000201100044 + $this->setStorecoveMeta($this->buildRouting('FR:SIRET', "0009:11000201100044")); + + // The SIRET / 0009 identifier of the final recipient is to be included in the invoice.accountingCustomerParty.publicIdentifiers array. + $this->setCustomerAssignedAccountId(true); + + } + + if(strlen($this->invoice->client->id_number ?? '') == 9) { + //SIREN + $this->setStorecoveMeta($this->buildRouting('FR:SIREN', "0002:{$this->invoice->client->id_number}")); + } + else { + //SIRET + $this->setStorecoveMeta($this->buildRouting('FR:SIRET', "0009:{$this->invoice->client->id_number}")); + } + + // ??????????????????????? //@TODO // The service code must be sent in invoice.buyerReference (deprecated) or the invoice.references array (documentType buyer_reference) - // The commitment number must be sent in the invoice.orderReference (deprecated) or the invoice.references array (documentType purchase_order). + if(strlen($this->invoice->po_number ?? '') >1) { + $this->setOrderReference(false); + } - // Invoices to companies (SIRET / 0009 or SIRENE / 0002) are routed directly to that identifier. return $this; } diff --git a/app/Services/Email/Email.php b/app/Services/Email/Email.php index 77edc44542eb..5202a92dbcfd 100644 --- a/app/Services/Email/Email.php +++ b/app/Services/Email/Email.php @@ -250,7 +250,7 @@ class Email implements ShouldQueue private function incrementEmailCounter(): void { - if(in_array($this->mailer, ['default','mailgun','postmark'])) { + if(in_array($this->email_object->settings->email_sending_method, ['default','mailgun','postmark'])) { Cache::increment("email_quota".$this->company->account->key); } } diff --git a/composer.json b/composer.json index f33899c9ee3c..cba4ad290b11 100644 --- a/composer.json +++ b/composer.json @@ -40,6 +40,7 @@ "asm/php-ansible": "dev-main", "authorizenet/authorizenet": "^2.0", "awobaz/compoships": "^2.1", + "aws/aws-sdk-php": "^3.319", "bacon/bacon-qr-code": "^2.0", "beganovich/snappdf": "dev-master", "braintree/braintree_php": "^6.0", @@ -201,4 +202,4 @@ ], "minimum-stability": "dev", "prefer-stable": true -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 55d090faa651..4fb0ae985936 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6eda3a2962158b87dab46711e65a8438", + "content-hash": "95e7bd229644d1d8e768ecfbc78582cd", "packages": [ { "name": "adrienrn/php-mimetyper", @@ -535,16 +535,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.317.1", + "version": "3.319.0", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "dc1e3031c2721a25beb2e8fbb175b576e3d60ab9" + "reference": "a5c408d4cd1945d5fc817f45e46383634b610497" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/dc1e3031c2721a25beb2e8fbb175b576e3d60ab9", - "reference": "dc1e3031c2721a25beb2e8fbb175b576e3d60ab9", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a5c408d4cd1945d5fc817f45e46383634b610497", + "reference": "a5c408d4cd1945d5fc817f45e46383634b610497", "shasum": "" }, "require": { @@ -597,7 +597,10 @@ ], "psr-4": { "Aws\\": "src/" - } + }, + "exclude-from-classmap": [ + "src/data/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -624,9 +627,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.317.1" + "source": "https://github.com/aws/aws-sdk-php/tree/3.319.0" }, - "time": "2024-08-02T18:09:42+00:00" + "time": "2024-08-07T18:05:51+00:00" }, { "name": "bacon/bacon-qr-code", diff --git a/lang/en/texts.php b/lang/en/texts.php index 6a1dcd6af06a..37bbb24a3f46 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5313,7 +5313,7 @@ $lang = array( 'forever_free' => 'Forever Free', 'comments_only' => 'Comments Only', 'payment_balance_on_file' => 'Payment Balance On File', - + 'ubl_email_attachment_help' => 'For more e-invoice settings please navigate :here', ); return $lang; diff --git a/resources/views/portal/ninja2020/dashboard/index.blade.php b/resources/views/portal/ninja2020/dashboard/index.blade.php index 8bd9314ca76e..21d8919bfd76 100644 --- a/resources/views/portal/ninja2020/dashboard/index.blade.php +++ b/resources/views/portal/ninja2020/dashboard/index.blade.php @@ -2,6 +2,13 @@ @section('meta_title', ctrans('texts.dashboard')) @section('body') + + @if($client->getSetting('custom_message_dashboard')) + @component('portal.ninja2020.components.message') +
{{ $client->getSetting('custom_message_dashboard') }}
+ @endcomponent + @endif +

{{ $contact->first_name }} {{ $contact->last_name }}

diff --git a/resources/views/portal/ninja2020/payment_methods/index.blade.php b/resources/views/portal/ninja2020/payment_methods/index.blade.php index 8bdde30840f7..9e73a95b3e36 100644 --- a/resources/views/portal/ninja2020/payment_methods/index.blade.php +++ b/resources/views/portal/ninja2020/payment_methods/index.blade.php @@ -2,6 +2,17 @@ @section('meta_title', ctrans('texts.payment_methods')) @section('body') + + @section('header') + @if($errors->any()) +
+ @foreach($errors->all() as $error) +

{{ $error }}

+ @endforeach +
+ @endif + @endsection +
@livewire('payment-methods-table', ['client_id' => $client->id, 'db' => $company->db])
diff --git a/tests/Feature/TaskApiTest.php b/tests/Feature/TaskApiTest.php index 801201b5876a..2db1a6db9f6b 100644 --- a/tests/Feature/TaskApiTest.php +++ b/tests/Feature/TaskApiTest.php @@ -224,6 +224,25 @@ class TaskApiTest extends TestCase ])->postJson("/api/v1/tasks", $data); $response->assertStatus(200); + $arr = $response->json(); + + $data = [ + 'client_id' => $this->client->hashed_id, + 'description' => 'Test Task', + 'time_log' => '[""]', + 'assigned_user' => [], + 'project' => [], + 'user' => [], + ]; + + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson("/api/v1/tasks/".$arr['data']['id'], $data); + + $response->assertStatus(200); + } public function testUserFilters() diff --git a/tests/Integration/Einvoice/Storecove/StorecoveTest.php b/tests/Integration/Einvoice/Storecove/StorecoveTest.php index d227c6848254..e6e619dd8e39 100644 --- a/tests/Integration/Einvoice/Storecove/StorecoveTest.php +++ b/tests/Integration/Einvoice/Storecove/StorecoveTest.php @@ -505,6 +505,117 @@ $x = ' } + private function createFRData() + { + $this->routing_id = 293338; + + $settings = CompanySettings::defaults(); + $settings->company_logo = 'https://pdf.invoicing.co/favicon-v2.png'; + $settings->website = 'www.invoiceninja.de'; + + $settings->address1 = '10 Rue de la Paix'; + $settings->address2 = 'Bâtiment A, Bureau 5'; + $settings->city = 'Paris'; + $settings->state = 'Île-de-France'; + $settings->postal_code = '75002'; + $settings->phone = '01 23456789'; + $settings->email = $this->faker->unique()->safeEmail(); + $settings->country_id = '250'; // France's ISO country code + $settings->vat_number = 'FR82345678911'; + $settings->id_number = '12345678900010'; + $settings->classification = 'business'; + $settings->use_credits_payment = 'always'; + $settings->timezone_id = '1'; // CET (Central European Time) + $settings->entity_send_time = 0; + $settings->e_invoice_type = 'PEPPOL'; + $settings->currency_id = '3'; + + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + ]); + + $this->user->companies()->attach($company->id, [ + 'account_id' => $this->account->id, + 'is_owner' => true, + 'is_admin' => 1, + 'is_locked' => 0, + 'permissions' => '', + 'notifications' => CompanySettings::notificationAdminDefaults(), + 'settings' => null, + ]); + + Client::unguard(); + + $c = + Client::create([ + 'company_id' => $company->id, + 'user_id' => $this->user->id, + 'name' => 'Exemple Société S.A.', + 'website' => 'https://www.exemple-societe.fr', + 'private_notes' => 'Ceci est une note privée pour le client test.', + 'balance' => 0, + 'paid_to_date' => 0, + 'vat_number' => 'FR12345678901', + 'id_number' => '12345678900010', // Typical format for French company registration numbers + 'custom_value1' => '2024-07-22 10:00:00', + 'custom_value2' => 'bleu', + 'custom_value3' => 'motexemple', + 'custom_value4' => 'test@example.com', + 'address1' => '123 Rue de l\'Exemple', + 'address2' => '2ème étage, Bureau 45', + 'city' => 'Paris', + 'state' => 'Île-de-France', + 'postal_code' => '75001', + 'country_id' => '250', // France + 'shipping_address1' => '123 Rue de l\'Exemple', + 'shipping_address2' => '2ème étage, Bureau 45', + 'shipping_city' => 'Paris', + 'shipping_state' => 'Île-de-France', + 'shipping_postal_code' => '75001', + 'shipping_country_id' => '250', // France + 'classification' => 'business', + 'settings' => ClientSettings::Defaults(), + 'client_hash' => \Illuminate\Support\Str::random(32), + 'routing_id' => '', + ]); + + + $item = new InvoiceItem(); + $item->product_key = "Product Key"; + $item->notes = "Product Description"; + $item->cost = 10; + $item->quantity = 10; + $item->tax_rate1 = 20; + $item->tax_name1 = 'VAT'; + + $invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'user_id' => $this->user->id, + 'client_id' => $c->id, + 'discount' => 0, + 'uses_inclusive_taxes' => false, + 'status_id' => 1, + 'tax_rate1' => 0, + 'tax_name1' => '', + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name2' => '', + 'tax_name3' => '', + 'line_items' => [$item], + 'number' => 'DE-'.rand(1000, 100000), + 'date' => now()->format('Y-m-d'), + 'due_date' => now()->addDays(14)->format('Y-m-d'), + ]); + + $invoice = $invoice->calc()->getInvoice(); + $invoice->service()->markSent()->save(); + + + return $invoice; + + } + private function createDEData() { @@ -614,7 +725,39 @@ $x = ' } - public function testEsRules() + public function testFrRules() + { + + $invoice = $this->createFRData(); + + $e_invoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice(); + + $stub = json_decode('{"Invoice":{"Note":"Nooo","PaymentMeans":[{"ID":{"value":"afdasfasdfasdfas"},"PayeeFinancialAccount":{"Name":"PFA-NAME","ID":{"value":"DE89370400440532013000"},"AliasName":"PFA-Alias","AccountTypeCode":{"value":"CHECKING"},"AccountFormatCode":{"value":"IBAN"},"CurrencyCode":{"value":"EUR"},"FinancialInstitutionBranch":{"ID":{"value":"DEUTDEMMXXX"},"Name":"Deutsche Bank"}}}]}}'); + foreach($stub as $key => $value) { + $e_invoice->{$key} = $value; + } + + $invoice->e_invoice = $e_invoice; + $invoice->save(); + + $this->assertInstanceOf(Invoice::class, $invoice); + $this->assertInstanceof(\InvoiceNinja\EInvoice\Models\Peppol\Invoice::class, $e_invoice); + + $p = new Peppol($invoice); + + $p->run(); + $xml = $p->toXml(); + nlog($xml); + + $identifiers = $p->getStorecoveMeta(); + + $sc = new \App\Services\EDocument\Gateway\Storecove\Storecove(); + $sc->sendDocument($xml, $this->routing_id, $identifiers); + + } + + + public function RtestEsRules() { $invoice = $this->createESData(); @@ -639,10 +782,14 @@ $x = ' nlog($xml); $identifiers = [ - [ - 'scheme' => 'ES:VAT', - 'id' => 'ESB53625999' - ], + "routing" => [ + "eIdentifiers" => [ + [ + 'scheme' => 'ES:VAT', + 'id' => 'ESB53625999' + ], + ] + ] ]; $sc = new \App\Services\EDocument\Gateway\Storecove\Storecove(); @@ -650,7 +797,7 @@ $x = ' } - public function testDeRules() + public function RtestDeRules() { $invoice = $this->createDEData(); @@ -673,9 +820,13 @@ $x = ' nlog($xml); $identifiers = [ - [ - 'scheme' => 'DE:VAT', - 'id' => 'DE010101010' + "routing" => [ + "eIdentifiers" => [ + [ + 'scheme' => 'DE:VAT', + 'id' => 'DE010101010' + ] + ] ] ];