mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2026-03-03 02:57:01 +00:00
Add PurchaseOrderSync classes
This commit is contained in:
@@ -43,6 +43,7 @@ class InvoiceSyncCast implements CastsAttributes
|
||||
'qb_id' => $value->qb_id,
|
||||
'invitations' => $value->invitations,
|
||||
'dn_completed' => $value->dn_completed,
|
||||
'dn_document_hashed_id' => $value->dn_document_hashed_id,
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
50
app/Casts/PurchaseOrderSyncCast.php
Normal file
50
app/Casts/PurchaseOrderSyncCast.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2026. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Casts;
|
||||
|
||||
use App\DataMapper\PurchaseOrderSync;
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
|
||||
class PurchaseOrderSyncCast 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) || empty($data)) {
|
||||
return null; // Return null if decoded data is not an array or is empty
|
||||
}
|
||||
|
||||
return PurchaseOrderSync::fromArray($data);
|
||||
}
|
||||
|
||||
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,
|
||||
'invitations' => $value->invitations,
|
||||
'dn_completed' => $value->dn_completed,
|
||||
'dn_document_hashed_id' => $value->dn_document_hashed_id,
|
||||
])
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ class QuoteSyncCast implements CastsAttributes
|
||||
'qb_id' => $value->qb_id,
|
||||
'invitations' => $value->invitations,
|
||||
'dn_completed' => $value->dn_completed,
|
||||
'dn_document_hashed_id' => $value->dn_document_hashed_id,
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ class InvoiceSync implements Castable
|
||||
public string $qb_id = '',
|
||||
public array $invitations = [],
|
||||
public bool $dn_completed = false,
|
||||
public string $dn_document_hashed_id = '',
|
||||
){}
|
||||
/**
|
||||
* Get the name of the caster class to use when casting from / to this cast target.
|
||||
@@ -43,6 +44,7 @@ class InvoiceSync implements Castable
|
||||
qb_id: $data['qb_id'] ?? '',
|
||||
invitations: $data['invitations'] ?? [],
|
||||
dn_completed: $data['dn_completed'] ?? false,
|
||||
dn_document_hashed_id: $data['dn_document_hashed_id'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
100
app/DataMapper/PurchaseOrderSync.php
Normal file
100
app/DataMapper/PurchaseOrderSync.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2026. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\DataMapper;
|
||||
|
||||
use App\Casts\PurchaseOrderSyncCast;
|
||||
use Illuminate\Contracts\Database\Eloquent\Castable;
|
||||
|
||||
/**
|
||||
* PurchaseOrderSync.
|
||||
*/
|
||||
class PurchaseOrderSync implements Castable
|
||||
{
|
||||
public function __construct(
|
||||
public string $qb_id = '',
|
||||
public array $invitations = [],
|
||||
public bool $dn_completed = false,
|
||||
public string $dn_document_hashed_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 PurchaseOrderSyncCast::class;
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
qb_id: $data['qb_id'] ?? '',
|
||||
invitations: $data['invitations'] ?? [],
|
||||
dn_completed: $data['dn_completed'] ?? false,
|
||||
dn_document_hashed_id: $data['dn_document_hashed_id'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an invitation to the invitations array
|
||||
*
|
||||
* @param string $invitation_key The invitation key
|
||||
* @param string $dn_id The DocuNinja ID
|
||||
* @param string $dn_invitation_id The DocuNinja invitation ID
|
||||
* @param string $dn_sig The DocuNinja signature
|
||||
*/
|
||||
public function addInvitation(
|
||||
string $invitation_key,
|
||||
string $dn_id,
|
||||
string $dn_invitation_id,
|
||||
string $dn_sig
|
||||
): void {
|
||||
$this->invitations[] = [
|
||||
'invitation_key' => $invitation_key,
|
||||
'dn_id' => $dn_id,
|
||||
'dn_invitation_id' => $dn_invitation_id,
|
||||
'dn_sig' => $dn_sig,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invitation data by invitation key
|
||||
*
|
||||
* @param string $invitation_key The invitation key
|
||||
* @return array|null The invitation data or null if not found
|
||||
*/
|
||||
public function getInvitation(string $invitation_key): ?array
|
||||
{
|
||||
foreach ($this->invitations as $invitation) {
|
||||
if ($invitation['invitation_key'] === $invitation_key) {
|
||||
return $invitation;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an invitation by invitation key
|
||||
*
|
||||
* @param string $invitation_key The invitation key
|
||||
*/
|
||||
public function removeInvitation(string $invitation_key): void
|
||||
{
|
||||
$this->invitations = array_filter($this->invitations, function($invitation) use ($invitation_key) {
|
||||
return $invitation['invitation_key'] !== $invitation_key;
|
||||
});
|
||||
// Re-index the array to maintain numeric keys
|
||||
$this->invitations = array_values($this->invitations);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ class QuoteSync implements Castable
|
||||
public string $qb_id = '',
|
||||
public array $invitations = [],
|
||||
public bool $dn_completed = false,
|
||||
public string $dn_document_hashed_id = '',
|
||||
){}
|
||||
/**
|
||||
* Get the name of the caster class to use when casting from / to this cast target.
|
||||
@@ -41,6 +42,7 @@ class QuoteSync implements Castable
|
||||
qb_id: $data['qb_id'] ?? '',
|
||||
invitations: $data['invitations'] ?? [],
|
||||
dn_completed: $data['dn_completed'] ?? false,
|
||||
dn_document_hashed_id: $data['dn_document_hashed_id'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,26 @@ class CreateRawPdf
|
||||
public function handle()
|
||||
{
|
||||
|
||||
/** Serve DocuNinja signed PDF if signing is complete */
|
||||
if (in_array($this->entity_string, ['invoice', 'quote', 'purchase_order'])
|
||||
&& $this->company->docuninjaActive()
|
||||
&& $this->entity->sync?->dn_completed
|
||||
) {
|
||||
$document = $this->entity->getSignedPdfDocument();
|
||||
|
||||
if ($document) {
|
||||
try {
|
||||
$pdf = $document->getFile();
|
||||
|
||||
if ($pdf && strlen($pdf) > 0) {
|
||||
return $pdf;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
nlog("Failed to retrieve signed PDF for {$this->entity_string} {$this->entity->id}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pdf = $this->generatePdf();
|
||||
|
||||
if ($this->isBlankPdf($pdf)) {
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace App\Livewire\Flow2;
|
||||
use Livewire\Component;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\DataMapper\InvoiceSync;
|
||||
use App\DataMapper\PurchaseOrderSync;
|
||||
use App\DataMapper\QuoteSync;
|
||||
use App\Models\QuoteInvitation;
|
||||
use App\Models\CreditInvitation;
|
||||
use App\Models\InvoiceInvitation;
|
||||
@@ -91,7 +93,11 @@ class DocuNinjaLoader extends Component
|
||||
|
||||
$signable = $invitation->{$this->entity_type}->service()->getDocuNinjaSignable($invitation);
|
||||
|
||||
$sync = new InvoiceSync(qb_id: '', dn_completed: false);
|
||||
$sync = match($this->entity_type) {
|
||||
'quote' => new QuoteSync(qb_id: '', dn_completed: false),
|
||||
'purchase_order' => new PurchaseOrderSync(qb_id: '', dn_completed: false),
|
||||
default => new InvoiceSync(qb_id: '', dn_completed: false),
|
||||
};
|
||||
$sync->addInvitation(
|
||||
$signable['invitation_key'],
|
||||
$signable['document_id'],
|
||||
|
||||
@@ -395,6 +395,25 @@ class BaseModel extends Model
|
||||
return \App\Services\Pdf\Purify::clean(html_entity_decode($parsed));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the DocuNinja signed PDF document for this entity.
|
||||
*
|
||||
* Uses the dn_document_hashed_id stored on the entity's sync object
|
||||
* to look up the Document record.
|
||||
*
|
||||
* @return \App\Models\Document|null
|
||||
*/
|
||||
public function getSignedPdfDocument(): ?\App\Models\Document
|
||||
{
|
||||
if (!$this->sync?->dn_document_hashed_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->documents()
|
||||
->where('id', $this->decodePrimaryKey($this->sync->dn_document_hashed_id))
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Merged PDFs associated with the entity / company
|
||||
* into a single document
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace App\Models;
|
||||
use App\Utils\Ninja;
|
||||
use App\Utils\Number;
|
||||
use Illuminate\Support\Carbon;
|
||||
use App\DataMapper\InvoiceSync;
|
||||
use App\DataMapper\PurchaseOrderSync;
|
||||
use App\Helpers\Invoice\InvoiceSum;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Elastic\ScoutDriverPlus\Searchable;
|
||||
@@ -113,7 +113,7 @@ use App\Events\PurchaseOrder\PurchaseOrderWasEmailed;
|
||||
* @property object|null $tax_data
|
||||
* @property object|null $e_invoice
|
||||
* @property int|null $location_id
|
||||
* @property \App\DataMapper\InvoiceSync|null $sync
|
||||
* @property \App\DataMapper\PurchaseOrderSync|null $sync
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|PurchaseOrder exclude($columns)
|
||||
* @method static \Database\Factories\PurchaseOrderFactory factory($count = null, $state = [])
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|PurchaseOrder filter(\App\Filters\QueryFilters $filters)
|
||||
@@ -218,7 +218,7 @@ class PurchaseOrder extends BaseModel
|
||||
'deleted_at' => 'timestamp',
|
||||
'is_amount_discount' => 'bool',
|
||||
'e_invoice' => 'object',
|
||||
'sync' => InvoiceSync::class,
|
||||
'sync' => PurchaseOrderSync::class,
|
||||
];
|
||||
|
||||
public const STATUS_DRAFT = 1;
|
||||
|
||||
@@ -273,7 +273,7 @@ class UserRepository extends BaseRepository
|
||||
|
||||
$notes = $user->present()->name();
|
||||
|
||||
\DB::transaction(function () use ($user, $new_owner_user, $notes) {
|
||||
\DB::transaction(function () use ($user, $new_owner_user) {
|
||||
|
||||
// Relations to transfer user_id to new owner
|
||||
$allRelations = [
|
||||
@@ -314,8 +314,9 @@ class UserRepository extends BaseRepository
|
||||
|
||||
$user->forceDelete();
|
||||
|
||||
event(new UserWasPurged($new_owner_user, $notes, auth()->user()->company(), Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
|
||||
});
|
||||
|
||||
$company = $new_owner_user->account->default_company ?? $new_owner_user->companies->first();
|
||||
event(new UserWasPurged($new_owner_user, $notes, $company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,8 @@
|
||||
"twig/twig": "^3.14",
|
||||
"twilio/sdk": "^6.40",
|
||||
"wikimedia/composer-merge-plugin": "^2.1",
|
||||
"wildbit/postmark-php": "^4.0"
|
||||
"wildbit/postmark-php": "^4.0",
|
||||
"invoiceninja/admin-api": "dev-main"
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-debugbar": "^3.6",
|
||||
@@ -223,6 +224,10 @@
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/turbo124/snappdf"
|
||||
},
|
||||
{
|
||||
"type": "path",
|
||||
"url": "../admin-api"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "dev",
|
||||
|
||||
82
composer.lock
generated
82
composer.lock
generated
@@ -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": "1b74189cd9dbfc430f4a65c9c3e0f784",
|
||||
"content-hash": "05003792e6ff08c3d34abc489a7003f1",
|
||||
"packages": [
|
||||
{
|
||||
"name": "afosto/yaac",
|
||||
@@ -4482,6 +4482,65 @@
|
||||
],
|
||||
"time": "2022-05-21T17:30:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "invoiceninja/admin-api",
|
||||
"version": "dev-main",
|
||||
"dist": {
|
||||
"type": "path",
|
||||
"url": "../admin-api",
|
||||
"reference": "520d230d7f022515fdc35d43ca9702ae886fba01"
|
||||
},
|
||||
"require": {
|
||||
"afosto/yaac": "^1.5",
|
||||
"asm/php-ansible": "dev-main",
|
||||
"ext-curl": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-json": "*",
|
||||
"ext-libxml": "*",
|
||||
"ext-simplexml": "*",
|
||||
"illuminate/database": "^11",
|
||||
"illuminate/support": "^11",
|
||||
"imdhemy/laravel-purchases": "^1.7",
|
||||
"php": "^8.2|^8.3|^8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"larastan/larastan": "^3.0",
|
||||
"orchestra/testbench": "^9.0",
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"phpunit/phpunit": "^11.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"InvoiceNinja\\AdminApi\\Providers\\AdminApiServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"InvoiceNinja\\AdminApi\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"license": [
|
||||
"Elastic"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "David Bomba",
|
||||
"email": "turbo124@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "API endpoints for the admin interface",
|
||||
"transport-options": {
|
||||
"relative": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "invoiceninja/einvoice",
|
||||
"version": "dev-main",
|
||||
@@ -21697,23 +21756,23 @@
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-ignition",
|
||||
"version": "2.10.0",
|
||||
"version": "2.11.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/laravel-ignition.git",
|
||||
"reference": "2abefdcca6074a9155f90b4ccb3345af8889d5f5"
|
||||
"reference": "11f38d1ff7abc583a61c96bf3c1b03610a69cccd"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/2abefdcca6074a9155f90b4ccb3345af8889d5f5",
|
||||
"reference": "2abefdcca6074a9155f90b4ccb3345af8889d5f5",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/11f38d1ff7abc583a61c96bf3c1b03610a69cccd",
|
||||
"reference": "11f38d1ff7abc583a61c96bf3c1b03610a69cccd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"illuminate/support": "^11.0|^12.0",
|
||||
"illuminate/support": "^11.0|^12.0|^13.0",
|
||||
"nesbot/carbon": "^2.72|^3.0",
|
||||
"php": "^8.2",
|
||||
"spatie/ignition": "^1.15.1",
|
||||
@@ -21721,10 +21780,10 @@
|
||||
"symfony/var-dumper": "^7.4|^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"livewire/livewire": "^3.7.0|^4.0",
|
||||
"livewire/livewire": "^3.7.0|^4.0|dev-josh/v3-laravel-13-support",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"openai-php/client": "^0.10.3",
|
||||
"orchestra/testbench": "^v9.16.0|^10.6",
|
||||
"openai-php/client": "^0.10.3|^0.19",
|
||||
"orchestra/testbench": "^v9.16.0|^10.6|^11.0",
|
||||
"pestphp/pest": "^3.7|^4.0",
|
||||
"phpstan/extension-installer": "^1.4.3",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0.3",
|
||||
@@ -21785,7 +21844,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-20T13:16:11+00:00"
|
||||
"time": "2026-02-22T19:14:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spaze/phpstan-stripe",
|
||||
@@ -22104,7 +22163,8 @@
|
||||
"asm/php-ansible": 20,
|
||||
"beganovich/snappdf": 20,
|
||||
"invoiceninja/einvoice": 20,
|
||||
"socialiteproviders/apple": 20
|
||||
"socialiteproviders/apple": 20,
|
||||
"invoiceninja/admin-api": 20
|
||||
},
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
|
||||
@@ -929,6 +929,7 @@ class UserTest extends TestCase
|
||||
|
||||
// Perform the purge
|
||||
$user_repo = new UserRepository();
|
||||
$owner_user->setCompany($company);
|
||||
$user_repo->purge($secondary_user, $owner_user);
|
||||
|
||||
// Assert secondary user is deleted
|
||||
|
||||
Reference in New Issue
Block a user