From 720e42e35ea58d21ad2f315e73a9fa7d952d45cc Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 31 May 2022 08:28:32 +1000 Subject: [PATCH 01/14] Purchase order scaffold --- app/DataMapper/CompanySettings.php | 3 + app/Helpers/Invoice/InvoiceSum.php | 8 + app/Helpers/Invoice/InvoiceSumInclusive.php | 9 ++ .../Controllers/PurchaseOrderController.php | 2 - .../StorePurchaseOrderRequest.php | 3 +- .../UpdatePurchaseOrderRequest.php | 1 - app/Models/PurchaseOrder.php | 21 ++- app/Models/Vendor.php | 37 +++++ app/Repositories/PurchaseOrderRepository.php | 142 +++++++++++++++++- app/Services/PurchaseOrder/ApplyNumber.php | 88 +++++++++++ .../PurchaseOrder/CreateInvitations.php | 104 +++++++++++++ .../PurchaseOrder/PurchaseOrderService.php | 31 +++- app/Utils/Traits/GeneratesCounter.php | 22 +++ 13 files changed, 453 insertions(+), 18 deletions(-) create mode 100644 app/Services/PurchaseOrder/ApplyNumber.php create mode 100644 app/Services/PurchaseOrder/CreateInvitations.php diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index a3d272a5c410..a3ed7d2fb4d2 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -116,6 +116,9 @@ class CompanySettings extends BaseSettings public $project_number_pattern = ''; //@implemented public $project_number_counter = 1; //@implemented + public $purchase_order_number_pattern = ''; //@implemented + public $purchase_order_number_counter = 1; //@implemented + public $shared_invoice_quote_counter = false; //@implemented public $shared_invoice_credit_counter = false; //@implemented public $recurring_number_prefix = ''; //@implemented diff --git a/app/Helpers/Invoice/InvoiceSum.php b/app/Helpers/Invoice/InvoiceSum.php index 8121cbfcc04e..8e3313fb49f2 100644 --- a/app/Helpers/Invoice/InvoiceSum.php +++ b/app/Helpers/Invoice/InvoiceSum.php @@ -224,6 +224,14 @@ class InvoiceSum return $this->invoice; } + public function getPurchaseOrder() + { + $this->setCalculatedAttributes(); + $this->invoice->saveQuietly(); + + return $this->invoice; + } + public function getRecurringInvoice() { $this->invoice->amount = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision); diff --git a/app/Helpers/Invoice/InvoiceSumInclusive.php b/app/Helpers/Invoice/InvoiceSumInclusive.php index 0cdb1638045d..eeb344a50c3d 100644 --- a/app/Helpers/Invoice/InvoiceSumInclusive.php +++ b/app/Helpers/Invoice/InvoiceSumInclusive.php @@ -229,6 +229,15 @@ class InvoiceSumInclusive return $this->invoice; } + public function getPurchaseOrder() + { + //Build invoice values here and return Invoice + $this->setCalculatedAttributes(); + $this->invoice->saveQuietly(); + + return $this->invoice; + } + /** * Build $this->invoice variables after * calculations have been performed. diff --git a/app/Http/Controllers/PurchaseOrderController.php b/app/Http/Controllers/PurchaseOrderController.php index 677e00196903..b07f068aec3b 100644 --- a/app/Http/Controllers/PurchaseOrderController.php +++ b/app/Http/Controllers/PurchaseOrderController.php @@ -174,8 +174,6 @@ class PurchaseOrderController extends BaseController public function store(StorePurchaseOrderRequest $request) { - $client = Client::find($request->get('client_id')); - $purchase_order = $this->purchase_order_repository->save($request->all(), PurchaseOrderFactory::create(auth()->user()->company()->id, auth()->user()->id)); $purchase_order = $purchase_order->service() diff --git a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php index 1f531178050b..ef97d446ad58 100644 --- a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php @@ -38,8 +38,7 @@ class StorePurchaseOrderRequest extends Request { $rules = []; - $rules['client_id'] = 'required'; - + $rules['vendor_id'] = 'required'; $rules['number'] = ['nullable', Rule::unique('purchase_orders')->where('company_id', auth()->user()->company()->id)]; $rules['discount'] = 'sometimes|numeric'; diff --git a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php index 55e8f5354490..d2e83154fb95 100644 --- a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php @@ -55,7 +55,6 @@ class UpdatePurchaseOrderRequest extends Request $input = $this->decodePrimaryKeys($input); - $input['id'] = $this->purchase_order->id; $this->replace($input); diff --git a/app/Models/PurchaseOrder.php b/app/Models/PurchaseOrder.php index 4d73c609559d..b1e6e87addd7 100644 --- a/app/Models/PurchaseOrder.php +++ b/app/Models/PurchaseOrder.php @@ -12,6 +12,8 @@ namespace App\Models; +use App\Helpers\Invoice\InvoiceSum; +use App\Helpers\Invoice\InvoiceSumInclusive; use App\Services\PurchaseOrder\PurchaseOrderService; use Illuminate\Database\Eloquent\SoftDeletes; @@ -92,8 +94,9 @@ class PurchaseOrder extends BaseModel const STATUS_DRAFT = 1; const STATUS_SENT = 2; - const STATUS_PARTIAL = 3; - const STATUS_APPLIED = 4; + const STATUS_APPROVED = 3; + const STATUS_CONVERTED = 4; + const STATUS_EXPIRED = -1; public function assigned_user() { @@ -130,7 +133,6 @@ class PurchaseOrder extends BaseModel return $this->belongsTo(Client::class)->withTrashed(); } - public function invitations() { return $this->hasMany(CreditInvitation::class); @@ -166,4 +168,17 @@ class PurchaseOrder extends BaseModel return $this->morphMany(Document::class, 'documentable'); } + public function calc() + { + $credit_calc = null; + + if ($this->uses_inclusive_taxes) { + $credit_calc = new InvoiceSumInclusive($this); + } else { + $credit_calc = new InvoiceSum($this); + } + + return $credit_calc->build(); + } + } diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php index bae220a60698..1076ecb29528 100644 --- a/app/Models/Vendor.php +++ b/app/Models/Vendor.php @@ -11,6 +11,7 @@ namespace App\Models; +use App\DataMapper\CompanySettings; use App\Models\Presenters\VendorPresenter; use App\Utils\Traits\GeneratesCounter; use Illuminate\Database\Eloquent\SoftDeletes; @@ -109,4 +110,40 @@ class Vendor extends BaseModel { return ctrans('texts.vendor'); } + + public function setCompanyDefaults($data, $entity_name) :array + { + $defaults = []; + + if (! (array_key_exists('terms', $data) && strlen($data['terms']) > 1)) { + $defaults['terms'] = $this->getSetting($entity_name.'_terms'); + } elseif (array_key_exists('terms', $data)) { + $defaults['terms'] = $data['terms']; + } + + if (! (array_key_exists('footer', $data) && strlen($data['footer']) > 1)) { + $defaults['footer'] = $this->getSetting($entity_name.'_footer'); + } elseif (array_key_exists('footer', $data)) { + $defaults['footer'] = $data['footer']; + } + + if (strlen($this->public_notes) >= 1) { + $defaults['public_notes'] = $this->public_notes; + } + + return $defaults; + } + + public function getSetting($setting) + { + if ((property_exists($this->company->settings, $setting) != false) && (isset($this->company->settings->{$setting}) !== false)) { + return $this->company->settings->{$setting}; + } + + elseif( property_exists(CompanySettings::defaults(), $setting) ) { + return CompanySettings::defaults()->{$setting}; + } + + return ''; + } } diff --git a/app/Repositories/PurchaseOrderRepository.php b/app/Repositories/PurchaseOrderRepository.php index cf0653c25c8a..f6ab93d5b5d3 100644 --- a/app/Repositories/PurchaseOrderRepository.php +++ b/app/Repositories/PurchaseOrderRepository.php @@ -11,8 +11,10 @@ namespace App\Repositories; - +use App\Factory\PurchaseOrderFactory; use App\Models\PurchaseOrder; +use App\Models\Vendor; +use App\Models\VendorContact; use App\Utils\Traits\MakesHash; class PurchaseOrderRepository extends BaseRepository @@ -25,10 +27,144 @@ class PurchaseOrderRepository extends BaseRepository public function save(array $data, PurchaseOrder $purchase_order) : ?PurchaseOrder { - $purchase_order->fill($data); + + if(array_key_exists('vendor_id', $data)) + $purchase_order->vendor_id = $data['vendor_id']; + + $vendor = Vendor::where('id', $purchase_order->vendor_id)->withTrashed()->firstOrFail(); + + $state = []; + + $resource = class_basename($purchase_order); //ie Invoice + + if (! $purchase_order->id) { + $company_defaults = $vendor->setCompanyDefaults($data, lcfirst($resource)); + $purchase_order->uses_inclusive_taxes = $vendor->getSetting('inclusive_taxes'); + $data = array_merge($company_defaults, $data); + } + + $tmp_data = $data; //preserves the $data array + + /* We need to unset some variable as we sometimes unguard the model */ + if (isset($tmp_data['invitations'])) + unset($tmp_data['invitations']); + + if (isset($tmp_data['vendor_contacts'])) + unset($tmp_data['vendor_contacts']); + + $purchase_order->fill($tmp_data); + + $purchase_order->custom_surcharge_tax1 = $vendor->company->custom_surcharge_taxes1; + $purchase_order->custom_surcharge_tax2 = $vendor->company->custom_surcharge_taxes2; + $purchase_order->custom_surcharge_tax3 = $vendor->company->custom_surcharge_taxes3; + $purchase_order->custom_surcharge_tax4 = $vendor->company->custom_surcharge_taxes4; + + if(!$purchase_order->id) + $this->new_model = true; + + $purchase_order->saveQuietly(); + + /* Save any documents */ + if (array_key_exists('documents', $data)) + $this->saveDocuments($data['documents'], $purchase_order); + + if (array_key_exists('file', $data)) + $this->saveDocuments($data['file'], $purchase_order); + + /* If invitations are present we need to filter existing invitations with the new ones */ + if (isset($data['invitations'])) { + $invitations = collect($data['invitations']); + + /* Get array of Keys which have been removed from the invitations array and soft delete each invitation */ + $purchase_order->invitations->pluck('key')->diff($invitations->pluck('key'))->each(function ($invitation) { + $invitation = PurchaseOrderInvitation::where('key', $invitation)->first(); + + if ($invitation) + $invitation->delete(); + + }); + + foreach ($data['invitations'] as $invitation) { + + //if no invitations are present - create one. + if (! $this->getInvitation($invitation, $resource)) { + + if (isset($invitation['id'])) + unset($invitation['id']); + + //make sure we are creating an invite for a contact who belongs to the client only! + $contact = VendorContact::find($invitation['vendor_contact_id']); + + if ($contact && $purchase_order->client_id == $contact->client_id) { + + $new_invitation = PurchaseOrderInvitation::withTrashed() + ->where('vendor_contact_id', $contact->id) + ->where('purchase_order_id', $purchase_order->id) + ->first(); + + if ($new_invitation && $new_invitation->trashed()) { + + $new_invitation->restore(); + + } else { + + $new_invitation = PurchaseOrderFactory::create($purchase_order->company_id, $purchase_order->user_id); + $new_invitation->purchase_order_id = $purchase_order->id; + $new_invitation->vendor_contact_id = $contact->id; + $new_invitation->key = $this->createDbHash($purchase_order->company->db); + $new_invitation->save(); + + } + } + } + } + } + + /* If no invitations have been created, this is our fail safe to maintain state*/ + if ($purchase_order->invitations()->count() == 0) + $purchase_order->service()->createInvitations(); + + /* Apply entity number */ + $purchase_order = $purchase_order->service()->applyNumber()->save(); + + /* Handle attempts where the deposit is greater than the amount/balance of the invoice */ + if((int)$purchase_order->balance != 0 && $purchase_order->partial > $purchase_order->amount) + $purchase_order->partial = min($purchase_order->amount, $purchase_order->balance); + + $purchase_order = $purchase_order->calc()->getPurchaseOrder(); + + if (! $purchase_order->design_id) + $purchase_order->design_id = $this->decodePrimaryKey($client->getSetting('credit_design_id')); + + if(array_key_exists('invoice_id', $data) && $data['invoice_id']) + $purchase_order->invoice_id = $data['invoice_id']; + + if($this->new_model) + event('eloquent.created: App\Models\PurchaseOrder', $purchase_order); + else + event('eloquent.updated: App\Models\PurchaseOrder', $purchase_order); + + $purchase_order->save(); - return $purchase_order; + return $purchase_order->fresh(); + + + // $purchase_order->fill($data); + // $purchase_order->save(); + + // return $purchase_order; } + public function getInvitation($invitation, $resource) + { + // if (is_array($invitation) && ! array_key_exists('key', $invitation)) + // return false; + + // $invitation = PurchaseOrderInvitation::where('key', $invitation['key'])->first(); + + return $invitation; + } + + } diff --git a/app/Services/PurchaseOrder/ApplyNumber.php b/app/Services/PurchaseOrder/ApplyNumber.php new file mode 100644 index 000000000000..bf4a33ddbcdf --- /dev/null +++ b/app/Services/PurchaseOrder/ApplyNumber.php @@ -0,0 +1,88 @@ +vendor = $vendor; + + $this->purchase_order = $purchase_order; + } + + public function run() + { + if ($this->purchase_order->number != '') { + return $this->purchase_order; + } + + switch ($this->client->getSetting('counter_number_applied')) { + case 'when_saved': + $this->trySaving(); + break; + case 'when_sent': + if ($this->purchase_order->status_id == PurchaseOrder::STATUS_SENT) { + $this->trySaving(); + } + break; + + default: + break; + } + + return $this->purchase_order; + } + + private function trySaving() + { + + $x=1; + + do{ + + try{ + + $this->purchase_order->number = $this->getNextPurchaseOrderNumber($this->purchase_order); + $this->purchase_order->saveQuietly(); + + $this->completed = false; + + + } + catch(QueryException $e){ + + $x++; + + if($x>10) + $this->completed = false; + } + + } + while($this->completed); + + } +} diff --git a/app/Services/PurchaseOrder/CreateInvitations.php b/app/Services/PurchaseOrder/CreateInvitations.php new file mode 100644 index 000000000000..301f21806509 --- /dev/null +++ b/app/Services/PurchaseOrder/CreateInvitations.php @@ -0,0 +1,104 @@ +purchase_order = $purchase_order; + } + + public function run() + { + + $contacts = $this->purchase_order->vendor->contacts()->where('send_email', true)->get(); + + if($contacts->count() == 0){ + $this->createBlankContact(); + + $this->purchase_order->refresh(); + $contacts = $this->purchase_order->vendor->contacts; + } + + $contacts->each(function ($contact) { + $invitation = PurchaseOrderInvitation::where('company_id', $this->purchase_order->company_id) + ->where('vendor_contact_id', $contact->id) + ->where('purchase_order_id', $this->purchase_order->id) + ->withTrashed() + ->first(); + + if (! $invitation && $contact->send_email) { + $ii = PurchaseOrderInvitationFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id); + $ii->key = $this->createDbHash($this->purchase_order->company->db); + $ii->purchase_order_id = $this->purchase_order->id; + $ii->vendor_contact_id = $contact->id; + $ii->save(); + } elseif ($invitation && ! $contact->send_email) { + $invitation->delete(); + } + }); + + if($this->purchase_order->invitations()->count() == 0) { + + if($contacts->count() == 0){ + $contact = $this->createBlankContact(); + } + else{ + $contact = $contacts->first(); + + $invitation = PurchaseOrderInvitation::where('company_id', $this->purchase_order->company_id) + ->where('vendor_contact_id', $contact->id) + ->where('purchase_order_id', $this->purchase_order->id) + ->withTrashed() + ->first(); + + if($invitation){ + $invitation->restore(); + return $this->purchase_order; + } + } + + $ii = PurchaseOrderInvitationFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id); + $ii->key = $this->createDbHash($this->purchase_order->company->db); + $ii->purchase_order_id = $this->purchase_order->id; + $ii->vendor_contact_id = $contact->id; + $ii->save(); + } + + return $this->purchase_order; + } + + private function createBlankContact() + { + $new_contact = VendorContactFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id); + $new_contact->vendor_id = $this->purchase_order->vendor_id; + $new_contact->contact_key = Str::random(40); + $new_contact->is_primary = true; + $new_contact->save(); + + return $new_contact; + } +} diff --git a/app/Services/PurchaseOrder/PurchaseOrderService.php b/app/Services/PurchaseOrder/PurchaseOrderService.php index ffb2cb84d164..9328a2a016a9 100644 --- a/app/Services/PurchaseOrder/PurchaseOrderService.php +++ b/app/Services/PurchaseOrder/PurchaseOrderService.php @@ -13,6 +13,8 @@ namespace App\Services\PurchaseOrder; use App\Models\PurchaseOrder; +use App\Services\PurchaseOrder\ApplyNumber; +use App\Services\PurchaseOrder\CreateInvitations; use App\Utils\Traits\MakesHash; class PurchaseOrderService @@ -37,19 +39,34 @@ class PurchaseOrderService return $this->purchase_order; } + public function createInvitations() + { + + $this->purchase_order = (new CreateInvitations($this->purchase_order))->run(); + + return $this; + } + + public function applyNumber() + { + $this->invoice = (new ApplyNumber($this->purchase_order->vendor, $this->purchase_order))->run(); + + return $this; + } + public function fillDefaults() { - $settings = $this->purchase_order->client->getMergedSettings(); + // $settings = $this->purchase_order->client->getMergedSettings(); - //TODO implement design, footer, terms + // //TODO implement design, footer, terms - /* If client currency differs from the company default currency, then insert the client exchange rate on the model.*/ - if (!isset($this->purchase_order->exchange_rate) && $this->purchase_order->client->currency()->id != (int)$this->purchase_order->company->settings->currency_id) - $this->purchase_order->exchange_rate = $this->purchase_order->client->currency()->exchange_rate; + // /* If client currency differs from the company default currency, then insert the client exchange rate on the model.*/ + // if (!isset($this->purchase_order->exchange_rate) && $this->purchase_order->client->currency()->id != (int)$this->purchase_order->company->settings->currency_id) + // $this->purchase_order->exchange_rate = $this->purchase_order->client->currency()->exchange_rate; - if (!isset($this->purchase_order->public_notes)) - $this->purchase_order->public_notes = $this->purchase_order->client->public_notes; + // if (!isset($this->purchase_order->public_notes)) + // $this->purchase_order->public_notes = $this->purchase_order->client->public_notes; return $this; diff --git a/app/Utils/Traits/GeneratesCounter.php b/app/Utils/Traits/GeneratesCounter.php index b9606621246a..6e470292edee 100644 --- a/app/Utils/Traits/GeneratesCounter.php +++ b/app/Utils/Traits/GeneratesCounter.php @@ -18,6 +18,7 @@ use App\Models\Expense; use App\Models\Invoice; use App\Models\Payment; use App\Models\Project; +use App\Models\PurchaseOrder; use App\Models\Quote; use App\Models\RecurringExpense; use App\Models\RecurringInvoice; @@ -154,6 +155,10 @@ trait GeneratesCounter return 'project_number_counter'; break; + case PurchaseOrder::class: + return 'purchase_order_number_counter'; + break; + default: return 'default_number_counter'; break; @@ -345,6 +350,23 @@ trait GeneratesCounter } + public function getNextPurchaseOrderNumber(PurchaseOrder $purchase_order) :string + { + $this->resetCompanyCounters($purchase_order->company); + + $counter = $purchase_order->company->settings->purchase_order_number_counter; + $setting_entity = $purchase_order->company->settings->purchase_order_number_counter; + + $purchase_order_number = $this->checkEntityNumber(PurchaseOrder::class, $purchase_order, $counter, $purchase_order->company->settings->counter_padding, $purchase_order->company->settings->purchase_order_number_pattern); + + $this->incrementCounter($purchase_order->company, 'purchase_order_number_pattern'); + + $entity_number = $purchase_order_number; + + return $this->replaceUserVars($purchase_order, $entity_number); + + } + /** * Gets the next expense number. * From a16e7eaa6283ac637385d69b04f0ffd1400d9c98 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 31 May 2022 08:40:17 +1000 Subject: [PATCH 02/14] Send email set on blank contact vendor --- app/Services/PurchaseOrder/CreateInvitations.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Services/PurchaseOrder/CreateInvitations.php b/app/Services/PurchaseOrder/CreateInvitations.php index 301f21806509..09ff197d94e7 100644 --- a/app/Services/PurchaseOrder/CreateInvitations.php +++ b/app/Services/PurchaseOrder/CreateInvitations.php @@ -13,6 +13,7 @@ namespace App\Services\PurchaseOrder; use App\Factory\ClientContactFactory; use App\Factory\PurchaseOrderInvitationFactory; +use App\Factory\VendorContactFactory; use App\Models\Invoice; use App\Models\PurchaseOrder; use App\Models\PurchaseOrderInvitation; @@ -97,6 +98,7 @@ class CreateInvitations extends AbstractService $new_contact->vendor_id = $this->purchase_order->vendor_id; $new_contact->contact_key = Str::random(40); $new_contact->is_primary = true; + $new_contact->send_email = true; $new_contact->save(); return $new_contact; From ce0327e99e719649dce208946ef1d3085fe5b2ca Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 31 May 2022 08:52:10 +1000 Subject: [PATCH 03/14] Add additional properties --- app/DataMapper/CompanySettings.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index a3ed7d2fb4d2..d2c4231526ac 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -280,6 +280,8 @@ class CompanySettings extends BaseSettings public $auto_archive_invoice_cancelled = false; public static $casts = [ + 'purchase_order_number_pattern' => 'purchase_order_number_pattern', + 'purchase_order_number_counter' => 'int', 'page_numbering_alignment' => 'string', 'page_numbering' => 'bool', 'auto_archive_invoice_cancelled' => 'bool', From f9685035cd5403384f6102e1b6aa96eec4e0abaf Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 5 Jun 2022 19:41:19 +1000 Subject: [PATCH 04/14] Fixes for purchase orders --- app/DataMapper/CompanySettings.php | 5 +--- app/Services/PurchaseOrder/ApplyNumber.php | 21 +++++++++----- .../PurchaseOrder/CreateInvitations.php | 28 +++++++++++++------ app/Services/PurchaseOrder/MarkSent.php | 10 ++++++- .../PurchaseOrder/PurchaseOrderService.php | 22 +++++++-------- app/Utils/Traits/GeneratesCounter.php | 16 +---------- ...reate_purchase_order_invitations_table.php | 5 ++++ tests/Feature/PurchaseOrderTest.php | 4 +-- tests/MockAccountData.php | 12 +------- 9 files changed, 63 insertions(+), 60 deletions(-) diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index a60b6e714ea1..9f2193c8f74a 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -279,11 +279,8 @@ class CompanySettings extends BaseSettings public $email_from_name = ''; public $auto_archive_invoice_cancelled = false; - - public $purchase_order_number_counter = 1; //TODO - public static $casts = [ - 'purchase_order_number_pattern' => 'purchase_order_number_pattern', + 'purchase_order_number_pattern' => 'string', 'purchase_order_number_counter' => 'int', 'page_numbering_alignment' => 'string', 'page_numbering' => 'bool', diff --git a/app/Services/PurchaseOrder/ApplyNumber.php b/app/Services/PurchaseOrder/ApplyNumber.php index c3fd4d5df078..9f47baec4a9c 100644 --- a/app/Services/PurchaseOrder/ApplyNumber.php +++ b/app/Services/PurchaseOrder/ApplyNumber.php @@ -1,11 +1,18 @@ client = $client; + $this->vendor = $vendor; $this->purchase_order = $purchase_order; } @@ -43,7 +50,7 @@ class ApplyNumber extends AbstractService $x=1; do{ try{ - $this->purchase_order->number = $this->getNextPurchaseOrderNumber($this->client, $this->purchase_order); + $this->purchase_order->number = $this->getNextPurchaseOrderNumber($this->purchase_order); $this->purchase_order->saveQuietly(); $this->completed = false; } diff --git a/app/Services/PurchaseOrder/CreateInvitations.php b/app/Services/PurchaseOrder/CreateInvitations.php index 4ae8f85323e6..6365bc912262 100644 --- a/app/Services/PurchaseOrder/CreateInvitations.php +++ b/app/Services/PurchaseOrder/CreateInvitations.php @@ -1,10 +1,19 @@ purchase_order = $purchase_order; } + private function createBlankContact() { - $new_contact = PurchaseOrderInvitationFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id); - $new_contact->client_id = $this->purchase_order->client_id; + $new_contact = VendorContactFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id); + $new_contact->vendor_id = $this->purchase_order->vendor_id; $new_contact->contact_key = Str::random(40); $new_contact->is_primary = true; $new_contact->save(); } public function run() { - $contacts = $this->purchase_order->vendor->contacts; + $contacts = $this->purchase_order->vendor->contacts()->where('send_email', true)->get(); if($contacts->count() == 0){ $this->createBlankContact(); @@ -41,14 +51,14 @@ class CreateInvitations extends AbstractService } $contacts->each(function ($contact) { - $invitation = PurchaseOrderInvitation::whereCompanyId($this->purchase_order->company_id) - ->whereClientContactId($contact->id) - ->whereCreditId($this->purchase_order->id) + $invitation = PurchaseOrderInvitation::where('company_id', $this->purchase_order->company_id) + ->where('vendor_contact_id', $contact->id) + ->where('purchase_order_id', $this->purchase_order->id) ->withTrashed() ->first(); if (! $invitation) { - $ii = PurchaseOrderInvitation::create($this->purchase_order->company_id, $this->purchase_order->user_id); + $ii = PurchaseOrderInvitationFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id); $ii->key = $this->createDbHash($this->purchase_order->company->db); $ii->purchase_order_id = $this->purchase_order->id; $ii->vendor_contact_id = $contact->id; @@ -78,7 +88,7 @@ class CreateInvitations extends AbstractService } } - $ii = PurchaseOrderInvitation::create($this->purchase_order->company_id, $this->purchase_order->user_id); + $ii = PurchaseOrderInvitationFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id); $ii->key = $this->createDbHash($this->purchase_order->company->db); $ii->purchase_order_id = $this->purchase_order->id; $ii->vendor_contact_id = $contact->id; diff --git a/app/Services/PurchaseOrder/MarkSent.php b/app/Services/PurchaseOrder/MarkSent.php index 2938f956557e..c3b1df47b0d7 100644 --- a/app/Services/PurchaseOrder/MarkSent.php +++ b/app/Services/PurchaseOrder/MarkSent.php @@ -1,5 +1,13 @@ purchase_order = $purchase_order; } - /** - * Saves the purchase order. - * @return \App\Models\PurchaseOrder object - */ - public function save(): ?PurchaseOrder - { - $this->purchase_order->saveQuietly(); - - return $this->purchase_order; - } - public function createInvitations() { @@ -86,4 +75,15 @@ class PurchaseOrderService return $this; } + /** + * Saves the purchase order. + * @return \App\Models\PurchaseOrder object + */ + public function save(): ?PurchaseOrder + { + $this->purchase_order->saveQuietly(); + + return $this->purchase_order; + } + } diff --git a/app/Utils/Traits/GeneratesCounter.php b/app/Utils/Traits/GeneratesCounter.php index baaab3c5244d..939a90849480 100644 --- a/app/Utils/Traits/GeneratesCounter.php +++ b/app/Utils/Traits/GeneratesCounter.php @@ -196,20 +196,6 @@ trait GeneratesCounter return $this->replaceUserVars($credit, $entity_number); - } - /** - * Gets the next purchase order number. - * - * @param PurchaseOrder $purchase_order The purchase order - * - * @return string The next purchase order number. - */ - public function getNextPurchaseOrderNumber(Client $client, ?PurchaseOrder $purchase_order) :string - { - $entity_number = $this->getNextEntityNumber(PurchaseOrder::class, $client); - - return $this->replaceUserVars($purchase_order, $entity_number); - } /** @@ -376,7 +362,7 @@ trait GeneratesCounter $purchase_order_number = $this->checkEntityNumber(PurchaseOrder::class, $purchase_order, $counter, $purchase_order->company->settings->counter_padding, $purchase_order->company->settings->purchase_order_number_pattern); - $this->incrementCounter($purchase_order->company, 'purchase_order_number_pattern'); + $this->incrementCounter($purchase_order->company, 'purchase_order_number_counter'); $entity_number = $purchase_order_number; diff --git a/database/migrations/2022_06_01_224339_create_purchase_order_invitations_table.php b/database/migrations/2022_06_01_224339_create_purchase_order_invitations_table.php index 984e6c6cd634..859464145dfa 100644 --- a/database/migrations/2022_06_01_224339_create_purchase_order_invitations_table.php +++ b/database/migrations/2022_06_01_224339_create_purchase_order_invitations_table.php @@ -39,6 +39,11 @@ class CreatePurchaseOrderInvitationsTable extends Migration $table->softDeletes('deleted_at', 6); }); + + Schema::table('purchase_orders', function (Blueprint $table) { + $table->unsignedInteger('client_id')->nullable()->change(); + }); + } /** diff --git a/tests/Feature/PurchaseOrderTest.php b/tests/Feature/PurchaseOrderTest.php index 63799022b7aa..b41141db897f 100644 --- a/tests/Feature/PurchaseOrderTest.php +++ b/tests/Feature/PurchaseOrderTest.php @@ -83,7 +83,7 @@ class PurchaseOrderTest extends TestCase 'custom_value3' => 0, 'custom_value4' => 0, 'status' => 1, - 'client_id' => $this->encodePrimaryKey($this->client->id), + 'vendor_id' => $this->encodePrimaryKey($this->vendor->id), ]; $response = $this->withHeaders([ @@ -117,7 +117,7 @@ class PurchaseOrderTest extends TestCase 'custom_value3' => 0, 'custom_value4' => 0, 'status' => 1, - 'client_id' => $this->encodePrimaryKey($this->client->id), + 'vendor_id' => $this->encodePrimaryKey($this->vendor->id), ]; $response = $this->withHeaders([ diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index 2bb170b9388b..f8767315832c 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -450,22 +450,12 @@ trait MockAccountData $this->quote->save(); - - - - - - - $this->purchase_order = PurchaseOrderFactory::create($this->company->id, $user_id); - $this->purchase_order->client_id = $this->client->id; - + $this->purchase_order->vendor_id = $this->vendor->id; $this->purchase_order->amount = 10; $this->purchase_order->balance = 10; - // $this->credit->due_date = now()->addDays(200); - $this->purchase_order->tax_name1 = ''; $this->purchase_order->tax_name2 = ''; $this->purchase_order->tax_name3 = ''; From feacf65160487a5db64f083619a3501017a2a1fd Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 5 Jun 2022 20:30:56 +1000 Subject: [PATCH 05/14] Stubs for purchase orderS --- app/DataMapper/CompanySettings.php | 12 ++++++++++++ app/Http/Controllers/InvoiceController.php | 1 - ...9_create_purchase_order_invitations_table.php | 16 +++++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index 9f2193c8f74a..05a78f07450c 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -136,6 +136,13 @@ class CompanySettings extends BaseSettings public $invoice_design_id = 'Wpmbk5ezJn'; //@implemented public $quote_design_id = 'Wpmbk5ezJn'; //@implemented public $credit_design_id = 'Wpmbk5ezJn'; //@implemented + + public $purchase_order_design_id = 'Wpmbk5ezJn'; + public $purchase_order_footer = ''; //@implemented + public $purchase_order_terms = ''; //@implemented + public $purchase_order_public_notes = ''; //@implemented + public $require_purchase_order_signature = false; //@TODO ben to confirm + public $invoice_footer = ''; //@implemented public $credit_footer = ''; //@implemented public $credit_terms = ''; //@implemented @@ -280,6 +287,11 @@ class CompanySettings extends BaseSettings public $auto_archive_invoice_cancelled = false; public static $casts = [ + 'require_purchase_order_signature' => 'bool', + 'purchase_order_public_notes' => 'string', + 'purchase_order_terms' => 'string', + 'purchase_order_design_id' => 'string', + 'purchase_order_footer' => 'string', 'purchase_order_number_pattern' => 'string', 'purchase_order_number_counter' => 'int', 'page_numbering_alignment' => 'string', diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 78332a2ae52c..0ebfcbafdaaa 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -709,7 +709,6 @@ class InvoiceController extends BaseController echo Storage::get($file); }, basename($file), ['Content-Type' => 'application/pdf']); - break; case 'restore': $this->invoice_repo->restore($invoice); diff --git a/database/migrations/2022_06_01_224339_create_purchase_order_invitations_table.php b/database/migrations/2022_06_01_224339_create_purchase_order_invitations_table.php index 859464145dfa..60f95f0900c0 100644 --- a/database/migrations/2022_06_01_224339_create_purchase_order_invitations_table.php +++ b/database/migrations/2022_06_01_224339_create_purchase_order_invitations_table.php @@ -44,6 +44,21 @@ class CreatePurchaseOrderInvitationsTable extends Migration $table->unsignedInteger('client_id')->nullable()->change(); }); + + Company::cursor()->each(function ($company){ + + $settings = $company->settings; + + $settings->purchase_order_design_id = 'Wpmbk5ezJn'; + $settings->purchase_order_footer = ''; //@implemented + $settings->purchase_order_terms = ''; //@implemented + $settings->purchase_order_public_notes = ''; //@implemented + $settings->purchase_order_number_pattern = ''; //@implemented + $settings->purchase_order_number_counter = 1; //@implemented + + $company->settings = $settings; + $company->save(); + }) } /** @@ -53,6 +68,5 @@ class CreatePurchaseOrderInvitationsTable extends Migration */ public function down() { - Schema::dropIfExists('purchase_order_invitations'); } } From 888dfd30926bdfa92c994fabfe0dd3b4222b1766 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 5 Jun 2022 20:48:49 +1000 Subject: [PATCH 06/14] Scaffold for PDF generation for purchase orders --- app/Models/PurchaseOrder.php | 2 +- app/Models/Vendor.php | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/Models/PurchaseOrder.php b/app/Models/PurchaseOrder.php index 3a82bb9215f8..9f8a46f50405 100644 --- a/app/Models/PurchaseOrder.php +++ b/app/Models/PurchaseOrder.php @@ -159,7 +159,7 @@ class PurchaseOrder extends BaseModel if(!$invitation) throw new \Exception('Hard fail, could not create an invitation - is there a valid contact?'); - $file_path = $this->client->credit_filepath($invitation).$this->numberFormatter().'.pdf'; + $file_path = $this->vendor->purchase_order_filepath($invitation).$this->numberFormatter().'.pdf'; if(Ninja::isHosted() && $portal && Storage::disk(config('filesystems.default'))->exists($file_path)){ return Storage::disk(config('filesystems.default'))->{$type}($file_path); diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php index 1076ecb29528..3a23d5d7b8bf 100644 --- a/app/Models/Vendor.php +++ b/app/Models/Vendor.php @@ -146,4 +146,10 @@ class Vendor extends BaseModel return ''; } + + public function purchase_order_filepath($invitation) + { + $contact_key = $invitation->contact->contact_key; + return $this->company->company_key.'/'.$this->vendor_hash.'/'.$contact_key.'/purchase_orders/'; + } } From 57f8bddd04d026f79cb9d37459fedce14cb01a19 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 6 Jun 2022 08:49:41 +1000 Subject: [PATCH 07/14] Vendor PDFs --- app/Jobs/Entity/CreateEntityPdf.php | 6 +- app/Jobs/Vendor/CreatePurchaseOrderPdf.php | 219 +++++++++++++++++++++ 2 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 app/Jobs/Vendor/CreatePurchaseOrderPdf.php diff --git a/app/Jobs/Entity/CreateEntityPdf.php b/app/Jobs/Entity/CreateEntityPdf.php index a0821fab9773..4a5be2b5887a 100644 --- a/app/Jobs/Entity/CreateEntityPdf.php +++ b/app/Jobs/Entity/CreateEntityPdf.php @@ -1,11 +1,11 @@ invitation = $invitation; + + $this->entity = $invitation->purchase_order; + $this->entity_string = 'purchase_order'; + + $this->contact = $invitation->contact; + + $this->vendor = $invitation->contact->vendor; + $this->vendor->load('company'); + + $this->disk = Ninja::isHosted() ? config('filesystems.default') : $disk; + + } + + public function handle() + { + + MultiDB::setDb($this->company->db); + + /* Forget the singleton*/ + App::forgetInstance('translator'); + + /* Init a new copy of the translator*/ + $t = app('translator'); + /* Set the locale*/ + App::setLocale($this->company->locale()); + + /* Set customized translations _NOW_ */ + $t->replace(Ninja::transformTranslations($this->company->settings)); + + if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') { + return (new Phantom)->generate($this->invitation); + } + + $entity_design_id = ''; + + $path = $this->vendor->purchase_order_filepath($this->invitation); + $entity_design_id = 'purchase_order_design_id'; + + $file_path = $path.$this->entity->numberFormatter().'.pdf'; + + $entity_design_id = $this->entity->design_id ? $this->entity->design_id : $this->decodePrimaryKey('Wpmbk5ezJn'); + + $design = Design::find($entity_design_id); + + /* Catch all in case migration doesn't pass back a valid design */ + if(!$design) + $design = Design::find(2); + + $html = new HtmlEngine($this->invitation); + + if ($design->is_custom) { + $options = [ + 'custom_partials' => json_decode(json_encode($design->design), true) + ]; + $template = new PdfMakerDesign(PdfDesignModel::CUSTOM, $options); + } else { + $template = new PdfMakerDesign(strtolower($design->name)); + } + + $variables = $html->generateLabelsAndValues(); + + $state = [ + 'template' => $template->elements([ + 'client' => null, + 'vendor' => $this->vendor, + 'entity' => $this->entity, + 'pdf_variables' => (array) $this->company->settings->pdf_variables, + '$product' => $design->design->product, + 'variables' => $variables, + ]), + 'variables' => $variables, + 'options' => [ + 'all_pages_header' => $this->entity->company->getSetting('all_pages_header'), + 'all_pages_footer' => $this->entity->company->getSetting('all_pages_footer'), + ], + 'process_markdown' => $this->entity->company->markdown_enabled, + ]; + + $maker = new PdfMakerService($state); + + $maker + ->design($template) + ->build(); + + $pdf = null; + + try { + + if(config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja'){ + $pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true)); + + $numbered_pdf = $this->pageNumbering($pdf, $this->company); + + if($numbered_pdf) + $pdf = $numbered_pdf; + + } + else { + + $pdf = $this->makePdf(null, null, $maker->getCompiledHTML(true)); + + $numbered_pdf = $this->pageNumbering($pdf, $this->company); + + if($numbered_pdf) + $pdf = $numbered_pdf; + + + } + + } catch (\Exception $e) { + nlog(print_r($e->getMessage(), 1)); + } + + if (config('ninja.log_pdf_html')) { + info($maker->getCompiledHTML()); + } + + if ($pdf) { + + try{ + + if(!Storage::disk($this->disk)->exists($path)) + Storage::disk($this->disk)->makeDirectory($path, 0775); + + Storage::disk($this->disk)->put($file_path, $pdf, 'public'); + + } + catch(\Exception $e) + { + + throw new FilePermissionsFailure($e->getMessage()); + + } + } + + return $file_path; + } + + public function failed($e) + { + + } + + +} From 614987a55eabf0ba140e3c0b1e948773f80f6ea3 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 6 Jun 2022 13:28:10 +1000 Subject: [PATCH 08/14] Vendor PDFs --- app/DataMapper/CompanySettings.php | 11 + app/Jobs/Vendor/CreatePurchaseOrderPdf.php | 5 +- app/Models/PurchaseOrderInvitation.php | 26 + app/Models/Vendor.php | 18 +- app/Services/PdfMaker/Design.php | 19 + app/Utils/Helpers.php | 18 +- app/Utils/VendorHtmlEngine.php | 781 +++++++++++++++++++++ resources/views/pdf-designs/clean.html | 4 +- 8 files changed, 869 insertions(+), 13 deletions(-) create mode 100644 app/Utils/VendorHtmlEngine.php diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index 05a78f07450c..4eb8e06b2e67 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -643,6 +643,17 @@ class CompanySettings extends BaseSettings '$client.phone', '$contact.email', ], + 'vendor_details' => [ + '$vendor.name', + '$vendor.number', + '$vendor.vat_number', + '$vendor.address1', + '$vendor.address2', + '$vendor.city_state_postal', + '$vendor.country', + '$vendor.phone', + '$contact.email', + ], 'company_details' => [ '$company.name', '$company.id_number', diff --git a/app/Jobs/Vendor/CreatePurchaseOrderPdf.php b/app/Jobs/Vendor/CreatePurchaseOrderPdf.php index bf4664662606..a15e8444f592 100644 --- a/app/Jobs/Vendor/CreatePurchaseOrderPdf.php +++ b/app/Jobs/Vendor/CreatePurchaseOrderPdf.php @@ -33,9 +33,10 @@ use App\Utils\PhantomJS\Phantom; use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesInvoiceHtml; use App\Utils\Traits\NumberFormatter; -use App\Utils\Traits\Pdf\PageNumbering; use App\Utils\Traits\Pdf\PDF; +use App\Utils\Traits\Pdf\PageNumbering; use App\Utils\Traits\Pdf\PdfMaker; +use App\Utils\VendorHtmlEngine; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -120,7 +121,7 @@ class CreatePurchaseOrderPdf implements ShouldQueue if(!$design) $design = Design::find(2); - $html = new HtmlEngine($this->invitation); + $html = new VendorHtmlEngine($this->invitation); if ($design->is_custom) { $options = [ diff --git a/app/Models/PurchaseOrderInvitation.php b/app/Models/PurchaseOrderInvitation.php index c0601653e559..beff77059650 100644 --- a/app/Models/PurchaseOrderInvitation.php +++ b/app/Models/PurchaseOrderInvitation.php @@ -4,6 +4,7 @@ namespace App\Models; +use App\Utils\Ninja; use App\Utils\Traits\Inviteable; use App\Utils\Traits\MakesDates; use Carbon\Carbon; @@ -77,5 +78,30 @@ class PurchaseOrderInvitation extends BaseModel $this->save(); } + public function getPortalLink() :string + { + + if(Ninja::isHosted()) + $domain = $this->company->domain(); + else + $domain = config('ninja.app_url'); + + switch ($this->company->portal_mode) { + case 'subdomain': + return $domain.'/vendor/'; + break; + case 'iframe': + return $domain.'/vendor/'; + break; + case 'domain': + return $domain.'/vendor/'; + break; + + default: + return ''; + break; + } + + } } diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php index 3a23d5d7b8bf..2f63a7d5644d 100644 --- a/app/Models/Vendor.php +++ b/app/Models/Vendor.php @@ -13,9 +13,11 @@ namespace App\Models; use App\DataMapper\CompanySettings; use App\Models\Presenters\VendorPresenter; +use App\Utils\Traits\AppSetup; use App\Utils\Traits\GeneratesCounter; use Illuminate\Database\Eloquent\SoftDeletes; use Laracasts\Presenter\PresentableTrait; +use Illuminate\Support\Facades\Cache; class Vendor extends BaseModel { @@ -23,7 +25,8 @@ class Vendor extends BaseModel use Filterable; use GeneratesCounter; use PresentableTrait; - + use AppSetup; + protected $fillable = [ 'name', 'assigned_user_id', @@ -96,6 +99,19 @@ class Vendor extends BaseModel return $this->hasMany(Activity::class); } + public function currency() + { + $currencies = Cache::get('currencies'); + + if(!$currencies) + $this->buildCache(true); + + return $currencies->filter(function ($item) { + return $item->id == $this->currency_id; + })->first(); + } + + public function company() { return $this->belongsTo(Company::class); diff --git a/app/Services/PdfMaker/Design.php b/app/Services/PdfMaker/Design.php index 6c349cb07833..d8dc215c7cdd 100644 --- a/app/Services/PdfMaker/Design.php +++ b/app/Services/PdfMaker/Design.php @@ -69,6 +69,8 @@ class Design extends BaseDesign const DELIVERY_NOTE = 'delivery_note'; const STATEMENT = 'statement'; + const PURCHASE_ORDER = 'purchase_order'; + public function __construct(string $design = null, array $options = []) { @@ -113,6 +115,10 @@ class Design extends BaseDesign 'id' => 'client-details', 'elements' => $this->clientDetails(), ], + 'vendor-details' => [ + 'id' => 'vendor-details', + 'elements' => $this->vendorDetails(), + ], 'entity-details' => [ 'id' => 'entity-details', 'elements' => $this->entityDetails(), @@ -188,6 +194,19 @@ class Design extends BaseDesign return $elements; } + public function vendorDetails(): array + { + $elements = []; + + $variables = $this->context['pdf_variables']['vendor_details']; + + foreach ($variables as $variable) { + $elements[] = ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'vendor_details-' . substr($variable, 1)]]; + } + + return $elements; + } + public function clientDetails(): array { $elements = []; diff --git a/app/Utils/Helpers.php b/app/Utils/Helpers.php index ce0312b3fed3..8623a452cfce 100644 --- a/app/Utils/Helpers.php +++ b/app/Utils/Helpers.php @@ -104,15 +104,15 @@ class Helpers * Process reserved keywords on PDF. * * @param string $value - * @param Client $client + * @param Client|Company $entity * @return null|string */ - public static function processReservedKeywords(?string $value, Client $client): ?string + public static function processReservedKeywords(?string $value, $entity): ?string { if(!$value) return ''; - Carbon::setLocale($client->locale()); + Carbon::setLocale($entity->locale()); $replacements = [ 'literal' => [ @@ -121,21 +121,21 @@ class Helpers ':QUARTER' => 'Q' . now()->quarter, ':WEEK_BEFORE' => \sprintf( '%s %s %s', - Carbon::now()->subDays(7)->translatedFormat($client->date_format()), + Carbon::now()->subDays(7)->translatedFormat($entity->date_format()), ctrans('texts.to'), - Carbon::now()->translatedFormat($client->date_format()) + Carbon::now()->translatedFormat($entity->date_format()) ), ':WEEK_AHEAD' => \sprintf( '%s %s %s', - Carbon::now()->addDays(7)->translatedFormat($client->date_format()), + Carbon::now()->addDays(7)->translatedFormat($entity->date_format()), ctrans('texts.to'), - Carbon::now()->addDays(14)->translatedFormat($client->date_format()) + Carbon::now()->addDays(14)->translatedFormat($entity->date_format()) ), ':WEEK' => \sprintf( '%s %s %s', - Carbon::now()->translatedFormat($client->date_format()), + Carbon::now()->translatedFormat($entity->date_format()), ctrans('texts.to'), - Carbon::now()->addDays(7)->translatedFormat($client->date_format()) + Carbon::now()->addDays(7)->translatedFormat($entity->date_format()) ), ], 'raw' => [ diff --git a/app/Utils/VendorHtmlEngine.php b/app/Utils/VendorHtmlEngine.php new file mode 100644 index 000000000000..955a12ac507c --- /dev/null +++ b/app/Utils/VendorHtmlEngine.php @@ -0,0 +1,781 @@ +invitation = $invitation; + + $this->entity_string = $this->resolveEntityString(); + + $this->entity = $invitation->{$this->entity_string}; + + $this->company = $invitation->company; + + $this->contact = $invitation->contact->load('vendor'); + + $this->vendor = $this->contact->vendor->load('company','country'); + + $this->entity->load('vendor'); + + $this->settings = $this->company->settings; + + $this->entity_calc = $this->entity->calc(); + + $this->helpers = new Helpers(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private function resolveEntityString() + { + switch ($this->invitation) { + case ($this->invitation instanceof InvoiceInvitation): + return 'invoice'; + break; + case ($this->invitation instanceof CreditInvitation): + return 'credit'; + break; + case ($this->invitation instanceof QuoteInvitation): + return 'quote'; + break; + case ($this->invitation instanceof RecurringInvoiceInvitation): + return 'recurring_invoice'; + break; + case ($this->invitation instanceof PurchaseOrderInvitation): + return 'purchase_order'; + break; + default: + # code... + break; + } + } + + public function buildEntityDataArray() :array + { + if (! $this->vendor->currency) { + throw new Exception(debug_backtrace()[1]['function'], 1); + exit; + } + + App::forgetInstance('translator'); + $t = app('translator'); + App::setLocale($this->company->locale()); + $t->replace(Ninja::transformTranslations($this->settings)); + + $data = []; + $data['$global_margin'] = ['value' => '6.35mm', 'label' => '']; + $data['$tax'] = ['value' => '', 'label' => ctrans('texts.tax')]; + $data['$app_url'] = ['value' => $this->generateAppUrl(), 'label' => '']; + $data['$from'] = ['value' => '', 'label' => ctrans('texts.from')]; + $data['$to'] = ['value' => '', 'label' => ctrans('texts.to')]; + $data['$total_tax_labels'] = ['value' => $this->totalTaxLabels(), 'label' => ctrans('texts.taxes')]; + $data['$total_tax_values'] = ['value' => $this->totalTaxValues(), 'label' => ctrans('texts.taxes')]; + $data['$line_tax_labels'] = ['value' => $this->lineTaxLabels(), 'label' => ctrans('texts.taxes')]; + $data['$line_tax_values'] = ['value' => $this->lineTaxValues(), 'label' => ctrans('texts.taxes')]; + $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.date')]; + + $data['$invoice.date'] = &$data['$date']; + $data['$invoiceDate'] = &$data['$date']; + $data['$due_date'] = ['value' => $this->translateDate($this->entity->due_date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')]; + + $data['$partial_due_date'] = ['value' => $this->translateDate($this->entity->partial_due_date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')]; + + $data['$dueDate'] = &$data['$due_date']; + + $data['$payment_due'] = ['value' => $this->translateDate($this->entity->due_date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.payment_due')]; + $data['$invoice.due_date'] = &$data['$due_date']; + $data['$invoice.number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number')]; + $data['$invoice.po_number'] = ['value' => $this->entity->po_number ?: ' ', 'label' => ctrans('texts.po_number')]; + $data['$poNumber'] = &$data['$invoice.po_number']; + $data['$entity.datetime'] = ['value' => $this->formatDatetime($this->entity->created_at, $this->company->date_format(), $this->company->locale()), 'label' => ctrans('texts.date')]; + $data['$invoice.datetime'] = &$data['$entity.datetime']; + $data['$quote.datetime'] = &$data['$entity.datetime']; + $data['$credit.datetime'] = &$data['$entity.datetime']; + $data['$payment_button'] = ['value' => ''.ctrans('texts.pay_now').'', 'label' => ctrans('texts.pay_now')]; + $data['$payment_link'] = ['value' => $this->invitation->getPaymentLink(), 'label' => ctrans('texts.pay_now')]; + + + if ($this->entity_string == 'invoice' || $this->entity_string == 'recurring_invoice') { + $data['$entity'] = ['value' => '', 'label' => ctrans('texts.invoice')]; + $data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number')]; + $data['$number_short'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number_short')]; + $data['$entity.terms'] = ['value' => Helpers::processReservedKeywords(\nl2br($this->entity->terms), $this->company) ?: '', 'label' => ctrans('texts.invoice_terms')]; + $data['$terms'] = &$data['$entity.terms']; + $data['$view_link'] = ['value' => ''.ctrans('texts.view_invoice').'', 'label' => ctrans('texts.view_invoice')]; + $data['$viewLink'] = &$data['$view_link']; + $data['$viewButton'] = &$data['$view_link']; + $data['$view_button'] = &$data['$view_link']; + $data['$paymentButton'] = &$data['$payment_button']; + $data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_invoice')]; + $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.invoice_date')]; + + if($this->entity->project) { + $data['$project.name'] = ['value' => $this->entity->project->name, 'label' => ctrans('texts.project_name')]; + $data['$invoice.project'] = &$data['$project.name']; + } + + if($this->entity->vendor) { + $data['$invoice.vendor'] = ['value' => $this->entity->vendor->present()->name(), 'label' => ctrans('texts.vendor_name')]; + } + } + + $data['$portal_url'] = ['value' => $this->invitation->getPortalLink(), 'label' =>'']; + + $data['$entity_number'] = &$data['$number']; + $data['$invoice.discount'] = ['value' => Number::formatMoney($this->entity_calc->getTotalDiscount(), $this->vendor) ?: ' ', 'label' => ctrans('texts.discount')]; + $data['$discount'] = &$data['$invoice.discount']; + $data['$subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getSubTotal(), $this->vendor) ?: ' ', 'label' => ctrans('texts.subtotal')]; + $data['$gross_subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getGrossSubTotal(), $this->vendor) ?: ' ', 'label' => ctrans('texts.subtotal')]; + + if($this->entity->uses_inclusive_taxes) + $data['$net_subtotal'] = ['value' => Number::formatMoney(($this->entity_calc->getSubTotal() - $this->entity->total_taxes - $this->entity_calc->getTotalDiscount()), $this->vendor) ?: ' ', 'label' => ctrans('texts.net_subtotal')]; + else + $data['$net_subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getSubTotal() - $this->entity_calc->getTotalDiscount(), $this->vendor) ?: ' ', 'label' => ctrans('texts.net_subtotal')]; + + $data['$invoice.subtotal'] = &$data['$subtotal']; + + if ($this->entity->partial > 0) { + $data['$balance_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->vendor) ?: ' ', 'label' => ctrans('texts.partial_due')]; + $data['$balance_due_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.partial_due')]; + $data['$amount_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.partial_due')]; + $data['$due_date'] = ['value' => $this->translateDate($this->entity->partial_due_date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')]; + + } else { + + if($this->entity->status_id == 1){ + $data['$balance_due'] = ['value' => Number::formatMoney($this->entity->amount, $this->vendor) ?: ' ', 'label' => ctrans('texts.balance_due')]; + $data['$balance_due_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.balance_due')]; + $data['$amount_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.amount')]; + } + else{ + $data['$balance_due'] = ['value' => Number::formatMoney($this->entity->balance, $this->vendor) ?: ' ', 'label' => ctrans('texts.balance_due')]; + $data['$balance_due_raw'] = ['value' => $this->entity->balance, 'label' => ctrans('texts.balance_due')]; + $data['$amount_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.amount')]; + } + } + + $data['$quote.balance_due'] = &$data['$balance_due']; + $data['$invoice.balance_due'] = &$data['$balance_due']; + + + if ($this->entity_string == 'credit') { + $data['$balance_due'] = ['value' => Number::formatMoney($this->entity->balance, $this->vendor) ?: ' ', 'label' => ctrans('texts.credit_balance')]; + $data['$balance_due_raw'] = ['value' => $this->entity->balance, 'label' => ctrans('texts.credit_balance')]; + $data['$amount_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.amount')]; + } + + // $data['$balance_due'] = $data['$balance_due']; + $data['$outstanding'] = &$data['$balance_due']; + $data['$partial_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->vendor) ?: ' ', 'label' => ctrans('texts.partial_due')]; + $data['$partial'] = &$data['$partial_due']; + + $data['$total'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->vendor) ?: ' ', 'label' => ctrans('texts.total')]; + $data['$amount'] = &$data['$total']; + $data['$amount_due'] = ['value' => &$data['$total']['value'], 'label' => ctrans('texts.amount_due')]; + $data['$quote.total'] = &$data['$total']; + $data['$invoice.total'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->vendor) ?: ' ', 'label' => ctrans('texts.invoice_total')]; + $data['$invoice_total_raw'] = ['value' => $this->entity_calc->getTotal(), 'label' => ctrans('texts.invoice_total')]; + $data['$invoice.amount'] = &$data['$total']; + $data['$quote.amount'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->vendor) ?: ' ', 'label' => ctrans('texts.quote_total')]; + $data['$credit.total'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->vendor) ?: ' ', 'label' => ctrans('texts.credit_total')]; + $data['$credit.number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.credit_number')]; + $data['$credit.total'] = &$data['$credit.total']; + $data['$credit.po_number'] = &$data['$invoice.po_number']; + $data['$credit.date'] = ['value' => $this->translateDate($this->entity->date, $this->company->date_format(), $this->company->locale()), 'label' => ctrans('texts.credit_date')]; + $data['$balance'] = ['value' => Number::formatMoney($this->entity_calc->getBalance(), $this->vendor) ?: ' ', 'label' => ctrans('texts.balance')]; + $data['$credit.balance'] = &$data['$balance']; + + $data['$invoice.balance'] = &$data['$balance']; + $data['$taxes'] = ['value' => Number::formatMoney($this->entity_calc->getItemTotalTaxes(), $this->vendor) ?: ' ', 'label' => ctrans('texts.taxes')]; + $data['$invoice.taxes'] = &$data['$taxes']; + + $data['$user.name'] = ['value' => $this->entity->user->present()->name(), 'label' => ctrans('texts.name')]; + $data['$user.first_name'] = ['value' => $this->entity->user->first_name, 'label' => ctrans('texts.first_name')]; + $data['$user.last_name'] = ['value' => $this->entity->user->last_name, 'label' => ctrans('texts.last_name')]; + $data['$created_by_user'] = &$data['$user.name']; + $data['$assigned_to_user'] = ['value' => $this->entity->assigned_user ? $this->entity->assigned_user->present()->name() : '', 'label' => ctrans('texts.name')]; + + $data['$entity.public_notes'] = &$data['$invoice.public_notes']; + $data['$public_notes'] = &$data['$invoice.public_notes']; + $data['$notes'] = &$data['$public_notes']; + + $data['$entity_issued_to'] = ['value' => '', 'label' => ctrans("texts.{$this->entity_string}_issued_to")]; + $data['$your_entity'] = ['value' => '', 'label' => ctrans("texts.your_{$this->entity_string}")]; + + => ctrans('texts.valid_until')]; + $data['$valid_until'] = &$data['$quote.valid_until']; + $data['$credit_amount'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->vendor) ?: ' ', 'label' => ctrans('texts.credit_amount')]; + $data['$credit_balance'] = ['value' => Number::formatMoney($this->entity->balance, $this->vendor) ?: ' ', 'label' => ctrans('texts.credit_balance')]; + $data['$purchase_order.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order1', $this->entity->custom_value1, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order1')]; + $data['$purchase_order.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order2', $this->entity->custom_value2, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order2')]; + $data['$purchase_order.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order3', $this->entity->custom_value3, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order3')]; + $data['$purchase_order.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order4', $this->entity->custom_value4, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order4')]; + + $data['$credit_number'] = &$data['$number']; + $data['$credit_no'] = &$data['$number']; + $data['$credit.credit_no'] = &$data['$number']; + + $data['$invoice_no'] = &$data['$number']; + $data['$invoice.invoice_no'] = &$data['$number']; + $data['$vendor1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor1', $this->vendor->custom_value1, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor1')]; + $data['$vendor2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor2', $this->vendor->custom_value2, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor2')]; + $data['$vendor3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor3', $this->vendor->custom_value3, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor3')]; + $data['$vendor4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor4', $this->vendor->custom_value4, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor4')]; + $data['$vendor.custom1'] = &$data['$vendor1']; + $data['$vendor.custom2'] = &$data['$vendor2']; + $data['$vendor.custom3'] = &$data['$vendor3']; + $data['$vendor.custom4'] = &$data['$vendor4']; + $data['$address1'] = ['value' => $this->vendor->address1 ?: ' ', 'label' => ctrans('texts.address1')]; + $data['$address2'] = ['value' => $this->vendor->address2 ?: ' ', 'label' => ctrans('texts.address2')]; + $data['$id_number'] = ['value' => $this->vendor->id_number ?: ' ', 'label' => ctrans('texts.id_number')]; + $data['$vendor.number'] = ['value' => $this->vendor->number ?: ' ', 'label' => ctrans('texts.number')]; + $data['$vat_number'] = ['value' => $this->vendor->vat_number ?: ' ', 'label' => ctrans('texts.vat_number')]; + $data['$website'] = ['value' => $this->vendor->present()->website() ?: ' ', 'label' => ctrans('texts.website')]; + $data['$phone'] = ['value' => $this->vendor->present()->phone() ?: ' ', 'label' => ctrans('texts.phone')]; + $data['$country'] = ['value' => isset($this->vendor->country->name) ? ctrans('texts.country_' . $this->vendor->country->name) : '', 'label' => ctrans('texts.country')]; + $data['$country_2'] = ['value' => isset($this->vendor->country) ? $this->vendor->country->iso_3166_2 : '', 'label' => ctrans('texts.country')]; + $data['$email'] = ['value' => isset($this->contact) ? $this->contact->email : 'no contact email on record', 'label' => ctrans('texts.email')]; + + if(str_contains($data['$email']['value'], 'example.com')) + $data['$email'] = ['value' => '', 'label' => ctrans('texts.email')]; + + $data['$vendor_name'] = ['value' => $this->vendor->present()->name() ?: ' ', 'label' => ctrans('texts.vendor_name')]; + $data['$vendor.name'] = &$data['$vendor_name']; + $data['$vendor'] = &$data['$vendor_name']; + + $data['$vendor.address1'] = &$data['$address1']; + $data['$vendor.address2'] = &$data['$address2']; + $data['$vendor_address'] = ['value' => $this->vendor->present()->address() ?: ' ', 'label' => ctrans('texts.address')]; + $data['$vendor.address'] = &$data['$vendor_address']; + $data['$vendor.postal_code'] = ['value' => $this->vendor->postal_code ?: ' ', 'label' => ctrans('texts.postal_code')]; + $data['$vendor.public_notes'] = ['value' => $this->vendor->public_notes ?: ' ', 'label' => ctrans('texts.notes')]; + $data['$vendor.city'] = ['value' => $this->vendor->city ?: ' ', 'label' => ctrans('texts.city')]; + $data['$vendor.state'] = ['value' => $this->vendor->state ?: ' ', 'label' => ctrans('texts.state')]; + $data['$vendor.id_number'] = &$data['$id_number']; + $data['$vendor.vat_number'] = &$data['$vat_number']; + $data['$vendor.website'] = &$data['$website']; + $data['$vendor.phone'] = &$data['$phone']; + $data['$city_state_postal'] = ['value' => $this->vendor->present()->cityStateZip($this->vendor->city, $this->vendor->state, $this->vendor->postal_code, false) ?: ' ', 'label' => ctrans('texts.city_state_postal')]; + $data['$vendor.city_state_postal'] = &$data['$city_state_postal']; + $data['$postal_city_state'] = ['value' => $this->vendor->present()->cityStateZip($this->vendor->city, $this->vendor->state, $this->vendor->postal_code, true) ?: ' ', 'label' => ctrans('texts.postal_city_state')]; + $data['$vendor.postal_city_state'] = &$data['$postal_city_state']; + $data['$vendor.country'] = &$data['$country']; + $data['$vendor.email'] = &$data['$email']; + + $data['$vendor.billing_address'] = &$data['$vendor_address']; + $data['$vendor.billing_address1'] = &$data['$vendor.address1']; + $data['$vendor.billing_address2'] = &$data['$vendor.address2']; + $data['$vendor.billing_city'] = &$data['$vendor.city']; + $data['$vendor.billing_state'] = &$data['$vendor.state']; + $data['$vendor.billing_postal_code'] = &$data['$vendor.postal_code']; + $data['$vendor.billing_country'] = &$data['$vendor.country']; + + $data['$vendor.currency'] = ['value' => $this->vendor->currency()->code, 'label' => '']; + + $data['$paid_to_date'] = ['value' => Number::formatMoney($this->entity->paid_to_date, $this->vendor), 'label' => ctrans('texts.paid_to_date')]; + + $data['$contact.full_name'] = ['value' => $this->contact->present()->name(), 'label' => ctrans('texts.name')]; + $data['$contact'] = &$data['$contact.full_name']; + + $data['$contact.email'] = &$data['$email']; + $data['$contact.phone'] = ['value' => $this->contact->phone, 'label' => ctrans('texts.phone')]; + + $data['$contact.name'] = ['value' => isset($this->contact) ? $this->contact->present()->name() : $this->vendor->present()->name(), 'label' => ctrans('texts.contact_name')]; + $data['$contact.first_name'] = ['value' => isset($this->contact) ? $this->contact->first_name : '', 'label' => ctrans('texts.first_name')]; + $data['$firstName'] = &$data['$contact.first_name']; + + $data['$contact.last_name'] = ['value' => isset($this->contact) ? $this->contact->last_name : '', 'label' => ctrans('texts.last_name')]; + + $data['$contact.custom1'] = ['value' => isset($this->contact) ? $this->contact->custom_value1 : ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'contact1')]; + $data['$contact.custom2'] = ['value' => isset($this->contact) ? $this->contact->custom_value2 : ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'contact2')]; + $data['$contact.custom3'] = ['value' => isset($this->contact) ? $this->contact->custom_value3 : ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'contact3')]; + $data['$contact.custom4'] = ['value' => isset($this->contact) ? $this->contact->custom_value4 : ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'contact4')]; + + $data['$company.city_state_postal'] = ['value' => $this->company->present()->cityStateZip($this->settings->city, $this->settings->state, $this->settings->postal_code, false) ?: ' ', 'label' => ctrans('texts.city_state_postal')]; + $data['$company.postal_city_state'] = ['value' => $this->company->present()->cityStateZip($this->settings->city, $this->settings->state, $this->settings->postal_code, true) ?: ' ', 'label' => ctrans('texts.postal_city_state')]; + $data['$company.name'] = ['value' => $this->settings->name ?: ctrans('texts.untitled_account'), 'label' => ctrans('texts.company_name')]; + $data['$account'] = &$data['$company.name']; + + $data['$company.address1'] = ['value' => $this->settings->address1 ?: ' ', 'label' => ctrans('texts.address1')]; + $data['$company.address2'] = ['value' => $this->settings->address2 ?: ' ', 'label' => ctrans('texts.address2')]; + $data['$company.city'] = ['value' => $this->settings->city ?: ' ', 'label' => ctrans('texts.city')]; + $data['$company.state'] = ['value' => $this->settings->state ?: ' ', 'label' => ctrans('texts.state')]; + $data['$company.postal_code'] = ['value' => $this->settings->postal_code ?: ' ', 'label' => ctrans('texts.postal_code')]; + $data['$company.country'] = ['value' => $this->getCountryName(), 'label' => ctrans('texts.country')]; + $data['$company.country_2'] = ['value' => $this->getCountryCode(), 'label' => ctrans('texts.country')]; + $data['$company.phone'] = ['value' => $this->settings->phone ?: ' ', 'label' => ctrans('texts.phone')]; + $data['$company.email'] = ['value' => $this->settings->email ?: ' ', 'label' => ctrans('texts.email')]; + $data['$company.vat_number'] = ['value' => $this->settings->vat_number ?: ' ', 'label' => ctrans('texts.vat_number')]; + $data['$company.id_number'] = ['value' => $this->settings->id_number ?: ' ', 'label' => ctrans('texts.id_number')]; + $data['$company.website'] = ['value' => $this->settings->website ?: ' ', 'label' => ctrans('texts.website')]; + $data['$company.address'] = ['value' => $this->company->present()->address($this->settings) ?: ' ', 'label' => ctrans('texts.address')]; + + $data['$signature'] = ['value' => $this->settings->email_signature ?: ' ', 'label' => '']; + $data['$emailSignature'] = &$data['$signature']; + + $logo = $this->company->present()->logo_base64($this->settings); + + $data['$company.logo'] = ['value' => $logo ?: ' ', 'label' => ctrans('texts.logo')]; + $data['$company_logo'] = &$data['$company.logo']; + $data['$company1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company1', $this->settings->custom_value1, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company1')]; + $data['$company2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company2', $this->settings->custom_value2, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company2')]; + $data['$company3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company3', $this->settings->custom_value3, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company3')]; + $data['$company4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company4', $this->settings->custom_value4, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company4')]; + + $data['$company.custom1'] = &$data['$company1']; + $data['$company.custom2'] = &$data['$company2']; + $data['$company.custom3'] = &$data['$company3']; + $data['$company.custom4'] = &$data['$company4']; + + $data['$custom_surcharge1'] = ['value' => Number::formatMoney($this->entity->custom_surcharge1, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'surcharge1')]; + $data['$custom_surcharge2'] = ['value' => Number::formatMoney($this->entity->custom_surcharge2, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'surcharge2')]; + $data['$custom_surcharge3'] = ['value' => Number::formatMoney($this->entity->custom_surcharge3, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'surcharge3')]; + $data['$custom_surcharge4'] = ['value' => Number::formatMoney($this->entity->custom_surcharge4, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'surcharge4')]; + + $data['$product.item'] = ['value' => '', 'label' => ctrans('texts.item')]; + $data['$product.date'] = ['value' => '', 'label' => ctrans('texts.date')]; + $data['$product.discount'] = ['value' => '', 'label' => ctrans('texts.discount')]; + $data['$product.product_key'] = ['value' => '', 'label' => ctrans('texts.product_key')]; + $data['$product.description'] = ['value' => '', 'label' => ctrans('texts.description')]; + $data['$product.unit_cost'] = ['value' => '', 'label' => ctrans('texts.unit_cost')]; + $data['$product.quantity'] = ['value' => '', 'label' => ctrans('texts.quantity')]; + $data['$product.tax_name1'] = ['value' => '', 'label' => ctrans('texts.tax')]; + $data['$product.tax'] = ['value' => '', 'label' => ctrans('texts.tax')]; + $data['$product.tax_name2'] = ['value' => '', 'label' => ctrans('texts.tax')]; + $data['$product.tax_name3'] = ['value' => '', 'label' => ctrans('texts.tax')]; + $data['$product.line_total'] = ['value' => '', 'label' => ctrans('texts.line_total')]; + $data['$product.gross_line_total'] = ['value' => '', 'label' => ctrans('texts.gross_line_total')]; + $data['$product.description'] = ['value' => '', 'label' => ctrans('texts.description')]; + $data['$product.unit_cost'] = ['value' => '', 'label' => ctrans('texts.unit_cost')]; + $data['$product.product1'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'product1')]; + $data['$product.product2'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'product2')]; + $data['$product.product3'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'product3')]; + $data['$product.product4'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'product4')]; + + $data['$task.date'] = ['value' => '', 'label' => ctrans('texts.date')]; + $data['$task.discount'] = ['value' => '', 'label' => ctrans('texts.discount')]; + $data['$task.service'] = ['value' => '', 'label' => ctrans('texts.service')]; + $data['$task.description'] = ['value' => '', 'label' => ctrans('texts.description')]; + $data['$task.rate'] = ['value' => '', 'label' => ctrans('texts.rate')]; + $data['$task.cost'] = ['value' => '', 'label' => ctrans('texts.rate')]; + $data['$task.hours'] = ['value' => '', 'label' => ctrans('texts.hours')]; + $data['$task.tax'] = ['value' => '', 'label' => ctrans('texts.tax')]; + $data['$task.tax_name1'] = ['value' => '', 'label' => ctrans('texts.tax')]; + $data['$task.tax_name2'] = ['value' => '', 'label' => ctrans('texts.tax')]; + $data['$task.tax_name3'] = ['value' => '', 'label' => ctrans('texts.tax')]; + $data['$task.line_total'] = ['value' => '', 'label' => ctrans('texts.line_total')]; + $data['$task.gross_line_total'] = ['value' => '', 'label' => ctrans('texts.gross_line_total')]; + $data['$task.service'] = ['value' => '', 'label' => ctrans('texts.service')]; + $data['$task.task1'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'task1')]; + $data['$task.task2'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'task2')]; + $data['$task.task3'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'task3')]; + $data['$task.task4'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'task4')]; + + if ($this->settings->signature_on_pdf) { + $data['$contact.signature'] = ['value' => $this->invitation->signature_base64, 'label' => ctrans('texts.signature')]; + } else { + $data['$contact.signature'] = ['value' => '', 'label' => '']; + } + + $data['$thanks'] = ['value' => '', 'label' => ctrans('texts.thanks')]; + $data['$from'] = ['value' => '', 'label' => ctrans('texts.from')]; + $data['$to'] = ['value' => '', 'label' => ctrans('texts.to')]; + $data['$details'] = ['value' => '', 'label' => ctrans('texts.details')]; + + $data['_rate1'] = ['value' => '', 'label' => ctrans('texts.tax')]; + $data['_rate2'] = ['value' => '', 'label' => ctrans('texts.tax')]; + $data['_rate3'] = ['value' => '', 'label' => ctrans('texts.tax')]; + + $data['$font_size'] = ['value' => $this->settings->font_size . 'px', 'label' => '']; + $data['$font_name'] = ['value' => Helpers::resolveFont($this->settings->primary_font)['name'], 'label' => '']; + $data['$font_url'] = ['value' => Helpers::resolveFont($this->settings->primary_font)['url'], 'label' => '']; + + $data['$invoiceninja.whitelabel'] = ['value' => 'https://raw.githubusercontent.com/invoiceninja/invoiceninja/v5-develop/public/images/new_logo.png', 'label' => '']; + + $data['$primary_color'] = ['value' => $this->settings->primary_color, 'label' => '']; + $data['$secondary_color'] = ['value' => $this->settings->secondary_color, 'label' => '']; + + $data['$item'] = ['value' => '', 'label' => ctrans('texts.item')]; + $data['$description'] = ['value' => '', 'label' => ctrans('texts.description')]; + + $data['$entity_footer'] = ['value' => Helpers::processReservedKeywords(\nl2br($this->entity->footer), $this->company), 'label' => '']; + $data['$footer'] = &$data['$entity_footer']; + + $data['$page_size'] = ['value' => $this->settings->page_size, 'label' => '']; + $data['$page_layout'] = ['value' => property_exists($this->settings, 'page_layout') ? $this->settings->page_layout : 'Portrait', 'label' => '']; + + $data['$tech_hero_image'] = ['value' => asset('images/pdf-designs/tech-hero-image.jpg'), 'label' => '']; + $data['$autoBill'] = ['value' => ctrans('texts.auto_bill_notification_placeholder'), 'label' => '']; + $data['$auto_bill'] = &$data['$autoBill']; + + /*Payment Aliases*/ + $data['$paymentLink'] = &$data['$payment_link']; + $data['$payment_url'] = &$data['$payment_link']; + $data['$portalButton'] = &$data['$paymentLink']; + + $data['$dir'] = ['value' => optional($this->company->language())->locale === 'ar' ? 'rtl' : 'ltr', 'label' => '']; + $data['$dir_text_align'] = ['value' => optional($this->company->language())->locale === 'ar' ? 'right' : 'left', 'label' => '']; + + $data['$payment.date'] = ['value' => ' ', 'label' => ctrans('texts.payment_date')]; + $data['$method'] = ['value' => ' ', 'label' => ctrans('texts.method')]; + + $data['$statement_amount'] = ['value' => '', 'label' => ctrans('texts.amount')]; + $data['$statement'] = ['value' => '', 'label' => ctrans('texts.statement')]; + + $data['$entity_images'] = ['value' => $this->generateEntityImagesMarkup(), 'label' => '']; + + $data['$payments'] = ['value' => '', 'label' => ctrans('texts.payments')]; + + $arrKeysLength = array_map('strlen', array_keys($data)); + array_multisort($arrKeysLength, SORT_DESC, $data); + + return $data; + } + + public function makeValues() :array + { + $data = []; + + $values = $this->buildEntityDataArray(); + + foreach ($values as $key => $value) { + $data[$key] = $value['value']; + } + + return $data; + } + + public function generateLabelsAndValues() + { + $data = []; + + $values = $this->buildEntityDataArray(); + + foreach ($values as $key => $value) { + $data['values'][$key] = $value['value']; + $data['labels'][$key.'_label'] = $value['label']; + } + + return $data; + } + + private function totalTaxLabels() :string + { + $data = ''; + + if (! $this->entity_calc->getTotalTaxMap()) { + return $data; + } + + foreach ($this->entity_calc->getTotalTaxMap() as $tax) { + $data .= ''.$tax['name'].''; + } + + return $data; + } + + private function totalTaxValues() :string + { + $data = ''; + + if (! $this->entity_calc->getTotalTaxMap()) { + return $data; + } + + foreach ($this->entity_calc->getTotalTaxMap() as $tax) { + $data .= ''.Number::formatMoney($tax['total'], $this->vendor).''; + } + + return $data; + } + + private function lineTaxLabels() :string + { + $tax_map = $this->entity_calc->getTaxMap(); + + $data = ''; + + foreach ($tax_map as $tax) { + $data .= ''.$tax['name'].''; + } + + return $data; + } + + private function getCountryName() :string + { + $country = Country::find($this->settings->country_id); + + if ($country) { + return ctrans('texts.country_' . $country->name); + } + + return ' '; + } + + + private function getCountryCode() :string + { + $country = Country::find($this->settings->country_id); + + if($country) + return $country->iso_3166_2; + // if ($country) { + // return ctrans('texts.country_' . $country->iso_3166_2); + // } + + return ' '; + } + /** + * Due to the way we are compiling the blade template we + * have no ability to iterate, so in the case + * of line taxes where there are multiple rows, + * we use this function to format a section of rows. + * + * @return string a collection of rows with line item + * aggregate data + */ + private function makeLineTaxes() :string + { + $tax_map = $this->entity_calc->getTaxMap(); + + $data = ''; + + foreach ($tax_map as $tax) { + $data .= ''; + $data .= ''.$tax['name'].''; + $data .= ''.Number::formatMoney($tax['total'], $this->vendor).''; + } + + return $data; + } + + private function lineTaxValues() :string + { + $tax_map = $this->entity_calc->getTaxMap(); + + $data = ''; + + foreach ($tax_map as $tax) { + $data .= ''.Number::formatMoney($tax['total'], $this->vendor).''; + } + + return $data; + } + + private function makeTotalTaxes() :string + { + $data = ''; + + if (! $this->entity_calc->getTotalTaxMap()) { + return $data; + } + + foreach ($this->entity_calc->getTotalTaxMap() as $tax) { + $data .= ''; + $data .= ''; + $data .= ''.$tax['name'].''; + $data .= ''.Number::formatMoney($tax['total'], $this->vendor).''; + } + + return $data; + } + + private function parseLabelsAndValues($labels, $values, $section) :string + { + $section = strtr($section, $labels); + + return strtr($section, $values); + } + + /* + | Ensures the URL doesn't have duplicated trailing slash + */ + public function generateAppUrl() + { + //return rtrim(config('ninja.app_url'), "/"); + return config('ninja.app_url'); + } + + /** + * Builds CSS to assist with the generation + * of Repeating headers and footers on the PDF. + * @return string The css string + */ + private function generateCustomCSS() :string + { + $header_and_footer = ' +.header, .header-space { + height: 160px; +} + +.footer, .footer-space { + height: 160px; +} + +.footer { + position: fixed; + bottom: 0; + width: 100%; +} + +.header { + position: fixed; + top: 0mm; + width: 100%; +} + +@media print { + thead {display: table-header-group;} + tfoot {display: table-footer-group;} + button {display: none;} + body {margin: 0;} +}'; + + $header = ' +.header, .header-space { + height: 160px; +} + +.header { + position: fixed; + top: 0mm; + width: 100%; +} + +@media print { + thead {display: table-header-group;} + button {display: none;} + body {margin: 0;} +}'; + + $footer = ' + +.footer, .footer-space { + height: 160px; +} + +.footer { + position: fixed; + bottom: 0; + width: 100%; +} + +@media print { + tfoot {display: table-footer-group;} + button {display: none;} + body {margin: 0;} +}'; + $css = ''; + + if ($this->settings->all_pages_header && $this->settings->all_pages_footer) { + $css .= $header_and_footer; + } elseif ($this->settings->all_pages_header && ! $this->settings->all_pages_footer) { + $css .= $header; + } elseif (! $this->settings->all_pages_header && $this->settings->all_pages_footer) { + $css .= $footer; + } + + $css .= ' +.page { + page-break-after: always; +} + +@page { + margin: 0mm +} + +html { + '; + + $css .= 'font-size:'.$this->settings->font_size.'px;'; +// $css .= 'font-size:14px;'; + + $css .= '}'; + + return $css; + } + + /** + * Generate markup for HTML images on entity. + * + * @return string|void + */ + protected function generateEntityImagesMarkup() + { + if ($this->company->getSetting('embed_documents') === false) { + return ''; + } + + $dom = new \DOMDocument('1.0', 'UTF-8'); + + $container = $dom->createElement('div'); + $container->setAttribute('style', 'display:grid; grid-auto-flow: row; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(2, 1fr);'); + + foreach ($this->entity->documents as $document) { + if (!$document->isImage()) { + continue; + } + + $image = $dom->createElement('img'); + + $image->setAttribute('src', $document->generateUrl()); + $image->setAttribute('style', 'max-height: 100px; margin-top: 20px;'); + + $container->appendChild($image); + } + + $dom->appendChild($container); + + return $dom->saveHTML(); + } +} diff --git a/resources/views/pdf-designs/clean.html b/resources/views/pdf-designs/clean.html index f0da1c52d4b7..35e9e2e4afe4 100644 --- a/resources/views/pdf-designs/clean.html +++ b/resources/views/pdf-designs/clean.html @@ -295,6 +295,7 @@
+
@@ -333,7 +334,8 @@ $entity_images let tables = [ 'product-table', 'task-table', 'delivery-note-table', 'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals', - 'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table' + 'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table', + 'client-details','vendor-details' ]; tables.forEach((tableIdentifier) => { From 04e55836ea6abb3be39781cb7e68c2e0d12fa86d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 6 Jun 2022 19:05:08 +1000 Subject: [PATCH 09/14] Translations for purchase orders --- app/Utils/VendorHtmlEngine.php | 105 ++++++--------------------------- resources/lang/en/texts.php | 4 +- 2 files changed, 20 insertions(+), 89 deletions(-) diff --git a/app/Utils/VendorHtmlEngine.php b/app/Utils/VendorHtmlEngine.php index 955a12ac507c..3241551e7d4e 100644 --- a/app/Utils/VendorHtmlEngine.php +++ b/app/Utils/VendorHtmlEngine.php @@ -55,7 +55,7 @@ class VendorHtmlEngine $this->entity_string = $this->resolveEntityString(); - $this->entity = $invitation->{$this->entity_string}; + $this->entity = $invitation->purchase_order; $this->company = $invitation->company; @@ -101,7 +101,7 @@ class VendorHtmlEngine public function buildEntityDataArray() :array { - if (! $this->vendor->currency) { + if (! $this->vendor->currency()) { throw new Exception(debug_backtrace()[1]['function'], 1); exit; } @@ -123,55 +123,39 @@ class VendorHtmlEngine $data['$line_tax_values'] = ['value' => $this->lineTaxValues(), 'label' => ctrans('texts.taxes')]; $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.date')]; - $data['$invoice.date'] = &$data['$date']; - $data['$invoiceDate'] = &$data['$date']; - $data['$due_date'] = ['value' => $this->translateDate($this->entity->due_date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')]; + $data['$due_date'] = ['value' => $this->translateDate($this->entity->due_date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.due_date')]; $data['$partial_due_date'] = ['value' => $this->translateDate($this->entity->partial_due_date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')]; $data['$dueDate'] = &$data['$due_date']; $data['$payment_due'] = ['value' => $this->translateDate($this->entity->due_date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.payment_due')]; - $data['$invoice.due_date'] = &$data['$due_date']; - $data['$invoice.number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number')]; - $data['$invoice.po_number'] = ['value' => $this->entity->po_number ?: ' ', 'label' => ctrans('texts.po_number')]; $data['$poNumber'] = &$data['$invoice.po_number']; $data['$entity.datetime'] = ['value' => $this->formatDatetime($this->entity->created_at, $this->company->date_format(), $this->company->locale()), 'label' => ctrans('texts.date')]; - $data['$invoice.datetime'] = &$data['$entity.datetime']; - $data['$quote.datetime'] = &$data['$entity.datetime']; - $data['$credit.datetime'] = &$data['$entity.datetime']; + $data['$payment_button'] = ['value' => ''.ctrans('texts.pay_now').'', 'label' => ctrans('texts.pay_now')]; $data['$payment_link'] = ['value' => $this->invitation->getPaymentLink(), 'label' => ctrans('texts.pay_now')]; - if ($this->entity_string == 'invoice' || $this->entity_string == 'recurring_invoice') { - $data['$entity'] = ['value' => '', 'label' => ctrans('texts.invoice')]; - $data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number')]; - $data['$number_short'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number_short')]; - $data['$entity.terms'] = ['value' => Helpers::processReservedKeywords(\nl2br($this->entity->terms), $this->company) ?: '', 'label' => ctrans('texts.invoice_terms')]; - $data['$terms'] = &$data['$entity.terms']; - $data['$view_link'] = ['value' => ''.ctrans('texts.view_invoice').'', 'label' => ctrans('texts.view_invoice')]; - $data['$viewLink'] = &$data['$view_link']; - $data['$viewButton'] = &$data['$view_link']; - $data['$view_button'] = &$data['$view_link']; - $data['$paymentButton'] = &$data['$payment_button']; - $data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_invoice')]; - $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.invoice_date')]; + $data['$entity'] = ['value' => '', 'label' => ctrans('texts.purchase_order')]; + $data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.purchase_order_number')]; + $data['$number_short'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.purchase_order_number_short')]; + $data['$entity.terms'] = ['value' => Helpers::processReservedKeywords(\nl2br($this->entity->terms), $this->company) ?: '', 'label' => ctrans('texts.invoice_terms')]; + $data['$terms'] = &$data['$entity.terms']; + $data['$view_link'] = ['value' => ''.ctrans('texts.view_invoice').'', 'label' => ctrans('texts.view_invoice')]; + $data['$viewLink'] = &$data['$view_link']; + $data['$viewButton'] = &$data['$view_link']; + $data['$view_button'] = &$data['$view_link']; + $data['$paymentButton'] = &$data['$payment_button']; + $data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_invoice')]; + $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.invoice_date')]; - if($this->entity->project) { - $data['$project.name'] = ['value' => $this->entity->project->name, 'label' => ctrans('texts.project_name')]; - $data['$invoice.project'] = &$data['$project.name']; - } - if($this->entity->vendor) { - $data['$invoice.vendor'] = ['value' => $this->entity->vendor->present()->name(), 'label' => ctrans('texts.vendor_name')]; - } - } + $data['$portal_url'] = ['value' => $this->invitation->getPortalLink(), 'label' =>'']; $data['$entity_number'] = &$data['$number']; - $data['$invoice.discount'] = ['value' => Number::formatMoney($this->entity_calc->getTotalDiscount(), $this->vendor) ?: ' ', 'label' => ctrans('texts.discount')]; $data['$discount'] = &$data['$invoice.discount']; $data['$subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getSubTotal(), $this->vendor) ?: ' ', 'label' => ctrans('texts.subtotal')]; $data['$gross_subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getGrossSubTotal(), $this->vendor) ?: ' ', 'label' => ctrans('texts.subtotal')]; @@ -203,16 +187,6 @@ class VendorHtmlEngine } } - $data['$quote.balance_due'] = &$data['$balance_due']; - $data['$invoice.balance_due'] = &$data['$balance_due']; - - - if ($this->entity_string == 'credit') { - $data['$balance_due'] = ['value' => Number::formatMoney($this->entity->balance, $this->vendor) ?: ' ', 'label' => ctrans('texts.credit_balance')]; - $data['$balance_due_raw'] = ['value' => $this->entity->balance, 'label' => ctrans('texts.credit_balance')]; - $data['$amount_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.amount')]; - } - // $data['$balance_due'] = $data['$balance_due']; $data['$outstanding'] = &$data['$balance_due']; $data['$partial_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->vendor) ?: ' ', 'label' => ctrans('texts.partial_due')]; @@ -221,22 +195,9 @@ class VendorHtmlEngine $data['$total'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->vendor) ?: ' ', 'label' => ctrans('texts.total')]; $data['$amount'] = &$data['$total']; $data['$amount_due'] = ['value' => &$data['$total']['value'], 'label' => ctrans('texts.amount_due')]; - $data['$quote.total'] = &$data['$total']; - $data['$invoice.total'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->vendor) ?: ' ', 'label' => ctrans('texts.invoice_total')]; - $data['$invoice_total_raw'] = ['value' => $this->entity_calc->getTotal(), 'label' => ctrans('texts.invoice_total')]; - $data['$invoice.amount'] = &$data['$total']; - $data['$quote.amount'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->vendor) ?: ' ', 'label' => ctrans('texts.quote_total')]; - $data['$credit.total'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->vendor) ?: ' ', 'label' => ctrans('texts.credit_total')]; - $data['$credit.number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.credit_number')]; - $data['$credit.total'] = &$data['$credit.total']; - $data['$credit.po_number'] = &$data['$invoice.po_number']; - $data['$credit.date'] = ['value' => $this->translateDate($this->entity->date, $this->company->date_format(), $this->company->locale()), 'label' => ctrans('texts.credit_date')]; $data['$balance'] = ['value' => Number::formatMoney($this->entity_calc->getBalance(), $this->vendor) ?: ' ', 'label' => ctrans('texts.balance')]; - $data['$credit.balance'] = &$data['$balance']; - $data['$invoice.balance'] = &$data['$balance']; $data['$taxes'] = ['value' => Number::formatMoney($this->entity_calc->getItemTotalTaxes(), $this->vendor) ?: ' ', 'label' => ctrans('texts.taxes')]; - $data['$invoice.taxes'] = &$data['$taxes']; $data['$user.name'] = ['value' => $this->entity->user->present()->name(), 'label' => ctrans('texts.name')]; $data['$user.first_name'] = ['value' => $this->entity->user->first_name, 'label' => ctrans('texts.first_name')]; @@ -248,24 +209,11 @@ class VendorHtmlEngine $data['$public_notes'] = &$data['$invoice.public_notes']; $data['$notes'] = &$data['$public_notes']; - $data['$entity_issued_to'] = ['value' => '', 'label' => ctrans("texts.{$this->entity_string}_issued_to")]; - $data['$your_entity'] = ['value' => '', 'label' => ctrans("texts.your_{$this->entity_string}")]; - - => ctrans('texts.valid_until')]; - $data['$valid_until'] = &$data['$quote.valid_until']; - $data['$credit_amount'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->vendor) ?: ' ', 'label' => ctrans('texts.credit_amount')]; - $data['$credit_balance'] = ['value' => Number::formatMoney($this->entity->balance, $this->vendor) ?: ' ', 'label' => ctrans('texts.credit_balance')]; $data['$purchase_order.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order1', $this->entity->custom_value1, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order1')]; $data['$purchase_order.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order2', $this->entity->custom_value2, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order2')]; $data['$purchase_order.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order3', $this->entity->custom_value3, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order3')]; $data['$purchase_order.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order4', $this->entity->custom_value4, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order4')]; - $data['$credit_number'] = &$data['$number']; - $data['$credit_no'] = &$data['$number']; - $data['$credit.credit_no'] = &$data['$number']; - - $data['$invoice_no'] = &$data['$number']; - $data['$invoice.invoice_no'] = &$data['$number']; $data['$vendor1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor1', $this->vendor->custom_value1, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor1')]; $data['$vendor2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor2', $this->vendor->custom_value2, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor2')]; $data['$vendor3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor3', $this->vendor->custom_value3, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor3')]; @@ -401,25 +349,6 @@ class VendorHtmlEngine $data['$product.product3'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'product3')]; $data['$product.product4'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'product4')]; - $data['$task.date'] = ['value' => '', 'label' => ctrans('texts.date')]; - $data['$task.discount'] = ['value' => '', 'label' => ctrans('texts.discount')]; - $data['$task.service'] = ['value' => '', 'label' => ctrans('texts.service')]; - $data['$task.description'] = ['value' => '', 'label' => ctrans('texts.description')]; - $data['$task.rate'] = ['value' => '', 'label' => ctrans('texts.rate')]; - $data['$task.cost'] = ['value' => '', 'label' => ctrans('texts.rate')]; - $data['$task.hours'] = ['value' => '', 'label' => ctrans('texts.hours')]; - $data['$task.tax'] = ['value' => '', 'label' => ctrans('texts.tax')]; - $data['$task.tax_name1'] = ['value' => '', 'label' => ctrans('texts.tax')]; - $data['$task.tax_name2'] = ['value' => '', 'label' => ctrans('texts.tax')]; - $data['$task.tax_name3'] = ['value' => '', 'label' => ctrans('texts.tax')]; - $data['$task.line_total'] = ['value' => '', 'label' => ctrans('texts.line_total')]; - $data['$task.gross_line_total'] = ['value' => '', 'label' => ctrans('texts.gross_line_total')]; - $data['$task.service'] = ['value' => '', 'label' => ctrans('texts.service')]; - $data['$task.task1'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'task1')]; - $data['$task.task2'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'task2')]; - $data['$task.task3'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'task3')]; - $data['$task.task4'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'task4')]; - if ($this->settings->signature_on_pdf) { $data['$contact.signature'] = ['value' => $this->invitation->signature_base64, 'label' => ctrans('texts.signature')]; } else { diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index bf22e6982ee8..f999624d2027 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4607,7 +4607,9 @@ $LANG = array( 'enable_tooltips_help' => 'Show tooltips when hovering the mouse', 'multiple_client_error' => 'Error: records belong to more than one client', 'login_label' => 'Login to an existing account', - + 'purchase_order' => 'Purchase Order', + 'purchase_order_number' => 'Purchase Order Number', + 'purchase_order_number_short' => 'Purchase Order #', ); return $LANG; From c0ba8aa822bde2b082f1f8992b8d419c328d0787 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 6 Jun 2022 22:27:17 +1000 Subject: [PATCH 10/14] TDD for purchase order PDFs --- app/Helpers/Invoice/InvoiceItemSum.php | 5 +- app/Helpers/Invoice/InvoiceSum.php | 18 +++-- app/Jobs/Vendor/CreatePurchaseOrderPdf.php | 3 +- .../Presenters/VendorContactPresenter.php | 42 +++++++++++ app/Models/PurchaseOrder.php | 22 +++++- app/Models/Vendor.php | 13 +++- app/Models/VendorContact.php | 3 + app/Repositories/PurchaseOrderRepository.php | 73 ++++++++++++++++++- app/Services/PdfMaker/Design.php | 9 +++ .../Designs/Utilities/DesignHelpers.php | 4 + .../PurchaseOrder/CreateInvitations.php | 20 ++++- .../PurchaseOrder/GetPurchaseOrderPdf.php | 58 +++++++++++++++ .../PurchaseOrder/PurchaseOrderService.php | 8 +- app/Utils/Helpers.php | 4 +- app/Utils/VendorHtmlEngine.php | 29 ++++---- ...reate_purchase_order_invitations_table.php | 12 ++- tests/Feature/PurchaseOrderTest.php | 33 +++++++++ tests/MockAccountData.php | 1 + 18 files changed, 320 insertions(+), 37 deletions(-) create mode 100644 app/Models/Presenters/VendorContactPresenter.php create mode 100644 app/Services/PurchaseOrder/GetPurchaseOrderPdf.php diff --git a/app/Helpers/Invoice/InvoiceItemSum.php b/app/Helpers/Invoice/InvoiceItemSum.php index 11a4b995da86..720b2466447d 100644 --- a/app/Helpers/Invoice/InvoiceItemSum.php +++ b/app/Helpers/Invoice/InvoiceItemSum.php @@ -52,7 +52,10 @@ class InvoiceItemSum $this->invoice = $invoice; - $this->currency = $this->invoice->client->currency(); + if($this->invoice->vendor) + $this->currency = $this->invoice->vendor->currency(); + else + $this->currency = $this->invoice->client->currency(); $this->line_items = []; } diff --git a/app/Helpers/Invoice/InvoiceSum.php b/app/Helpers/Invoice/InvoiceSum.php index 8e3313fb49f2..5c2f72238ea7 100644 --- a/app/Helpers/Invoice/InvoiceSum.php +++ b/app/Helpers/Invoice/InvoiceSum.php @@ -44,6 +44,8 @@ class InvoiceSum private $gross_sub_total; + private $precision; + /** * Constructs the object with Invoice and Settings object. * @@ -53,8 +55,10 @@ class InvoiceSum { $this->invoice = $invoice; - // if(!$this->invoice->relationLoaded('client')) - // $this->invoice->load('client'); + if($this->invoice->vendor) + $this->precision = $this->invoice->vendor->currency()->precision; + else + $this->precision = $this->invoice->client->currency()->precision; $this->tax_map = new Collection; } @@ -234,9 +238,9 @@ class InvoiceSum public function getRecurringInvoice() { - $this->invoice->amount = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision); + $this->invoice->amount = $this->formatValue($this->getTotal(), $this->precision); $this->invoice->total_taxes = $this->getTotalTaxes(); - $this->invoice->balance = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision); + $this->invoice->balance = $this->formatValue($this->getTotal(), $this->precision); $this->invoice->saveQuietly(); @@ -255,13 +259,13 @@ class InvoiceSum if ($this->invoice->amount != $this->invoice->balance) { $paid_to_date = $this->invoice->amount - $this->invoice->balance; - $this->invoice->balance = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision) - $paid_to_date; + $this->invoice->balance = $this->formatValue($this->getTotal(), $this->precision) - $paid_to_date; } else { - $this->invoice->balance = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision); + $this->invoice->balance = $this->formatValue($this->getTotal(), $this->precision); } } /* Set new calculated total */ - $this->invoice->amount = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision); + $this->invoice->amount = $this->formatValue($this->getTotal(), $this->precision); $this->invoice->total_taxes = $this->getTotalTaxes(); diff --git a/app/Jobs/Vendor/CreatePurchaseOrderPdf.php b/app/Jobs/Vendor/CreatePurchaseOrderPdf.php index a15e8444f592..2b601a8d1fd5 100644 --- a/app/Jobs/Vendor/CreatePurchaseOrderPdf.php +++ b/app/Jobs/Vendor/CreatePurchaseOrderPdf.php @@ -73,7 +73,8 @@ class CreatePurchaseOrderPdf implements ShouldQueue public function __construct($invitation, $disk = 'public') { $this->invitation = $invitation; - + $this->company = $invitation->company; + $this->entity = $invitation->purchase_order; $this->entity_string = 'purchase_order'; diff --git a/app/Models/Presenters/VendorContactPresenter.php b/app/Models/Presenters/VendorContactPresenter.php new file mode 100644 index 000000000000..7f140f932ab5 --- /dev/null +++ b/app/Models/Presenters/VendorContactPresenter.php @@ -0,0 +1,42 @@ +entity->first_name.' '.$this->entity->last_name; + + if (strlen($contact_name) > 1) { + return $contact_name; + } + + return $this->entity->vendor->present()->name(); + } + + public function first_name() + { + return $this->entity->first_name ?: ''; + } + + public function last_name() + { + return $this->entity->last_name ?: ''; + } +} diff --git a/app/Models/PurchaseOrder.php b/app/Models/PurchaseOrder.php index 9f8a46f50405..67e8a2d2a63e 100644 --- a/app/Models/PurchaseOrder.php +++ b/app/Models/PurchaseOrder.php @@ -12,7 +12,10 @@ namespace App\Models; +use App\Helpers\Invoice\InvoiceSum; +use App\Helpers\Invoice\InvoiceSumInclusive; use App\Jobs\Entity\CreateEntityPdf; +use App\Jobs\Vendor\CreatePurchaseOrderPdf; use App\Services\PurchaseOrder\PurchaseOrderService; use App\Utils\Ninja; use Illuminate\Database\Eloquent\SoftDeletes; @@ -165,14 +168,14 @@ class PurchaseOrder extends BaseModel return Storage::disk(config('filesystems.default'))->{$type}($file_path); } elseif(Ninja::isHosted() && $portal){ - $file_path = CreateEntityPdf::dispatchNow($invitation,config('filesystems.default')); + $file_path = CreatePurchaseOrderPdf::dispatchNow($invitation,config('filesystems.default')); return Storage::disk(config('filesystems.default'))->{$type}($file_path); } if(Storage::disk('public')->exists($file_path)) return Storage::disk('public')->{$type}($file_path); - $file_path = CreateEntityPdf::dispatchNow($invitation); + $file_path = CreatePurchaseOrderPdf::dispatchNow($invitation); return Storage::disk('public')->{$type}($file_path); } @@ -193,7 +196,7 @@ class PurchaseOrder extends BaseModel public function service() { - return new PurchaseOrderService($this); + return new PurchaseOrderService($this); } public function invoices() @@ -211,4 +214,17 @@ class PurchaseOrder extends BaseModel return $this->morphMany(Document::class, 'documentable'); } + public function calc() + { + $purchase_order_calc = null; + + if ($this->uses_inclusive_taxes) { + $purchase_order_calc = new InvoiceSumInclusive($this); + } else { + $purchase_order_calc = new InvoiceSum($this); + } + + return $purchase_order_calc->build(); + } + } diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php index 2f63a7d5644d..836ec380b741 100644 --- a/app/Models/Vendor.php +++ b/app/Models/Vendor.php @@ -15,9 +15,10 @@ use App\DataMapper\CompanySettings; use App\Models\Presenters\VendorPresenter; use App\Utils\Traits\AppSetup; use App\Utils\Traits\GeneratesCounter; +use App\Utils\Traits\NumberFormatter; use Illuminate\Database\Eloquent\SoftDeletes; -use Laracasts\Presenter\PresentableTrait; use Illuminate\Support\Facades\Cache; +use Laracasts\Presenter\PresentableTrait; class Vendor extends BaseModel { @@ -26,7 +27,7 @@ class Vendor extends BaseModel use GeneratesCounter; use PresentableTrait; use AppSetup; - + protected $fillable = [ 'name', 'assigned_user_id', @@ -166,6 +167,14 @@ class Vendor extends BaseModel public function purchase_order_filepath($invitation) { $contact_key = $invitation->contact->contact_key; + return $this->company->company_key.'/'.$this->vendor_hash.'/'.$contact_key.'/purchase_orders/'; } + + public function country() + { + return $this->belongsTo(Country::class); + } + + } diff --git a/app/Models/VendorContact.php b/app/Models/VendorContact.php index 2b2fcd3f1252..2f5f9447cc04 100644 --- a/app/Models/VendorContact.php +++ b/app/Models/VendorContact.php @@ -11,6 +11,7 @@ namespace App\Models; +use App\Models\Presenters\VendorContactPresenter; use App\Notifications\ClientContactResetPassword; use App\Utils\Traits\MakesHash; use Illuminate\Contracts\Translation\HasLocalePreference; @@ -35,6 +36,8 @@ class VendorContact extends Authenticatable implements HasLocalePreference protected $touches = ['vendor']; + protected $presenter = VendorContactPresenter::class; + /* Allow microtime timestamps */ protected $dateFormat = 'Y-m-d H:i:s.u'; diff --git a/app/Repositories/PurchaseOrderRepository.php b/app/Repositories/PurchaseOrderRepository.php index d620fdcdd142..ff508f1f44d5 100644 --- a/app/Repositories/PurchaseOrderRepository.php +++ b/app/Repositories/PurchaseOrderRepository.php @@ -11,9 +11,10 @@ namespace App\Repositories; - +use App\Factory\PurchaseOrderInvitationFactory; use App\Models\PurchaseOrder; use App\Models\PurchaseOrderInvitation; +use App\Models\VendorContact; use App\Utils\Traits\MakesHash; class PurchaseOrderRepository extends BaseRepository @@ -27,13 +28,83 @@ class PurchaseOrderRepository extends BaseRepository public function save(array $data, PurchaseOrder $purchase_order) : ?PurchaseOrder { $purchase_order->fill($data); + $purchase_order->save(); + if (isset($data['invitations'])) { + $invitations = collect($data['invitations']); + + /* Get array of Keys which have been removed from the invitations array and soft delete each invitation */ + $purchase_order->invitations->pluck('key')->diff($invitations->pluck('key'))->each(function ($invitation) { + + $invitation = PurchaseOrderInvitation::where('key', $invitation)->first(); + + if ($invitation) + $invitation->delete(); + + }); + + foreach ($data['invitations'] as $invitation) { + + //if no invitations are present - create one. + if (! $this->getInvitation($invitation)) { + + if (isset($invitation['id'])) + unset($invitation['id']); + + //make sure we are creating an invite for a contact who belongs to the client only! + $contact = VendorContact::find($invitation['vendor_contact_id']); + + if ($contact && $purchase_order->vendor_id == $contact->vendor_id) { + + $new_invitation = PurchaseOrderInvitation::withTrashed() + ->where('vendor_contact_id', $contact->id) + ->where('purchase_order_id', $purchase_order->id) + ->first(); + + if ($new_invitation && $new_invitation->trashed()) { + + $new_invitation->restore(); + + } else { + + $new_invitation = PurchaseOrderInvitationFactory::create($purchase_order->company_id, $purchase_order->user_id); + $new_invitation->purchase_order_id = $purchase_order->id; + $new_invitation->vendor_contact_id = $contact->id; + $new_invitation->key = $this->createDbHash($purchase_order->company->db); + $new_invitation->save(); + + } + } + } + } + } + + /* If no invitations have been created, this is our fail safe to maintain state*/ + if ($purchase_order->invitations()->count() == 0) + $purchase_order->service()->createInvitations(); + +nlog("4"); + + /* Recalculate invoice amounts */ + $purchase_order = $purchase_order->calc()->getPurchaseOrder(); + return $purchase_order; } + public function getInvitationByKey($key) :?PurchaseOrderInvitation { return PurchaseOrderInvitation::where('key', $key)->first(); } + public function getInvitation($invitation, $resource=null) + { + if (is_array($invitation) && ! array_key_exists('key', $invitation)) + return false; + + $invitation = PurchaseOrderInvitation::where('key', $invitation['key'])->first(); + + return $invitation; + } + } diff --git a/app/Services/PdfMaker/Design.php b/app/Services/PdfMaker/Design.php index d8dc215c7cdd..c7e34e3997d6 100644 --- a/app/Services/PdfMaker/Design.php +++ b/app/Services/PdfMaker/Design.php @@ -35,6 +35,9 @@ class Design extends BaseDesign /** @var App\Models\Client */ public $client; + /** @var App\Models\Vendor */ + public $vendor; + /** Global state of the design, @var array */ public $context; @@ -198,6 +201,9 @@ class Design extends BaseDesign { $elements = []; + if(!$this->vendor) + return $elements; + $variables = $this->context['pdf_variables']['vendor_details']; foreach ($variables as $variable) { @@ -211,6 +217,9 @@ class Design extends BaseDesign { $elements = []; + if(!$this->client) + return $elements; + if ($this->type == self::DELIVERY_NOTE) { $elements = [ ['element' => 'p', 'content' => ctrans('texts.delivery_note'), 'properties' => ['data-ref' => 'delivery_note-label', 'style' => 'font-weight: bold; text-transform: uppercase']], diff --git a/app/Services/PdfMaker/Designs/Utilities/DesignHelpers.php b/app/Services/PdfMaker/Designs/Utilities/DesignHelpers.php index a10579b9f24d..944c518db166 100644 --- a/app/Services/PdfMaker/Designs/Utilities/DesignHelpers.php +++ b/app/Services/PdfMaker/Designs/Utilities/DesignHelpers.php @@ -30,6 +30,10 @@ trait DesignHelpers { $this->syncPdfVariables(); + if (isset($this->context['vendor'])) { + $this->vendor = $this->context['vendor']; + } + if (isset($this->context['client'])) { $this->client = $this->context['client']; } diff --git a/app/Services/PurchaseOrder/CreateInvitations.php b/app/Services/PurchaseOrder/CreateInvitations.php index 6365bc912262..141914faa8ed 100644 --- a/app/Services/PurchaseOrder/CreateInvitations.php +++ b/app/Services/PurchaseOrder/CreateInvitations.php @@ -39,10 +39,13 @@ class CreateInvitations extends AbstractService $new_contact->is_primary = true; $new_contact->save(); } + public function run() { $contacts = $this->purchase_order->vendor->contacts()->where('send_email', true)->get(); +nlog("a"); + if($contacts->count() == 0){ $this->createBlankContact(); @@ -50,6 +53,9 @@ class CreateInvitations extends AbstractService $contacts = $this->purchase_order->vendor->contacts; } +nlog("b"); +nlog($contacts->count()); + $contacts->each(function ($contact) { $invitation = PurchaseOrderInvitation::where('company_id', $this->purchase_order->company_id) ->where('vendor_contact_id', $contact->id) @@ -58,16 +64,24 @@ class CreateInvitations extends AbstractService ->first(); if (! $invitation) { + try{ $ii = PurchaseOrderInvitationFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id); $ii->key = $this->createDbHash($this->purchase_order->company->db); $ii->purchase_order_id = $this->purchase_order->id; $ii->vendor_contact_id = $contact->id; $ii->save(); + } + catch(\Exception $e){ + nlog($e->getMessage()); + } } elseif (! $contact->send_email) { $invitation->delete(); } }); +nlog("c"); + + if($this->purchase_order->invitations()->count() == 0) { if($contacts->count() == 0){ @@ -76,7 +90,7 @@ class CreateInvitations extends AbstractService else{ $contact = $contacts->first(); - $invitation = PurchaseOrder::where('company_id', $this->purchase_order->company_id) + $invitation = PurchaseOrderInvitation::where('company_id', $this->purchase_order->company_id) ->where('vendor_contact_id', $contact->id) ->where('purchase_order_id', $this->purchase_order->id) ->withTrashed() @@ -88,6 +102,9 @@ class CreateInvitations extends AbstractService } } +nlog("d"); + + $ii = PurchaseOrderInvitationFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id); $ii->key = $this->createDbHash($this->purchase_order->company->db); $ii->purchase_order_id = $this->purchase_order->id; @@ -95,6 +112,7 @@ class CreateInvitations extends AbstractService $ii->save(); } +nlog("e"); return $this->purchase_order; } diff --git a/app/Services/PurchaseOrder/GetPurchaseOrderPdf.php b/app/Services/PurchaseOrder/GetPurchaseOrderPdf.php new file mode 100644 index 000000000000..8c75b87899a9 --- /dev/null +++ b/app/Services/PurchaseOrder/GetPurchaseOrderPdf.php @@ -0,0 +1,58 @@ +purchase_order = $purchase_order; + + $this->contact = $contact; + } + + public function run() + { + + if (! $this->contact) { + $this->contact = $this->purchase_order->vendor->contacts()->where('send_email', true)->first(); + } + + $invitation = $this->purchase_order->invitations()->where('vendor_contact_id', $this->contact->id)->first(); + + if(!$invitation) + $invitation = $this->purchase_order->invitations()->first(); + + $path = $this->purchase_order->vendor->purchase_order_filepath($invitation); + + $file_path = $path.$this->purchase_order->numberFormatter().'.pdf'; + + // $disk = 'public'; + $disk = config('filesystems.default'); + + $file = Storage::disk($disk)->exists($file_path); + + if (! $file) { + $file_path = CreatePurchaseOrderPdf::dispatchNow($invitation); + } + + return $file_path; + + } +} diff --git a/app/Services/PurchaseOrder/PurchaseOrderService.php b/app/Services/PurchaseOrder/PurchaseOrderService.php index 01991331317f..69e6066b992a 100644 --- a/app/Services/PurchaseOrder/PurchaseOrderService.php +++ b/app/Services/PurchaseOrder/PurchaseOrderService.php @@ -15,6 +15,7 @@ namespace App\Services\PurchaseOrder; use App\Models\PurchaseOrder; use App\Services\PurchaseOrder\ApplyNumber; use App\Services\PurchaseOrder\CreateInvitations; +use App\Services\PurchaseOrder\GetPurchaseOrderPdf; use App\Utils\Traits\MakesHash; class PurchaseOrderService @@ -23,7 +24,7 @@ class PurchaseOrderService public PurchaseOrder $purchase_order; - public function __construct($purchase_order) + public function __construct(PurchaseOrder $purchase_order) { $this->purchase_order = $purchase_order; } @@ -61,6 +62,11 @@ class PurchaseOrderService return $this; } + public function getPurchaseOrderPdf($contact = null) + { + return (new GetPurchaseOrderPdf($this->purchase_order, $contact))->run(); + } + public function setStatus($status) { $this->purchase_order->status_id = $status; diff --git a/app/Utils/Helpers.php b/app/Utils/Helpers.php index 8623a452cfce..28cf1adc2596 100644 --- a/app/Utils/Helpers.php +++ b/app/Utils/Helpers.php @@ -52,7 +52,7 @@ class Helpers * * @return null|string */ - public function formatCustomFieldValue($custom_fields, $field, $value, Client $client = null): ?string + public function formatCustomFieldValue($custom_fields, $field, $value, $entity = null): ?string { $custom_field = ''; @@ -67,7 +67,7 @@ class Helpers switch ($custom_field) { case 'date': - return is_null($client) ? $value : $this->translateDate($value, $client->date_format(), $client->locale()); + return is_null($entity) ? $value : $this->translateDate($value, $entity->date_format(), $entity->locale()); break; case 'switch': diff --git a/app/Utils/VendorHtmlEngine.php b/app/Utils/VendorHtmlEngine.php index 3241551e7d4e..f56b68245e9b 100644 --- a/app/Utils/VendorHtmlEngine.php +++ b/app/Utils/VendorHtmlEngine.php @@ -130,7 +130,8 @@ class VendorHtmlEngine $data['$dueDate'] = &$data['$due_date']; $data['$payment_due'] = ['value' => $this->translateDate($this->entity->due_date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.payment_due')]; - $data['$poNumber'] = &$data['$invoice.po_number']; + $data['$poNumber'] = ['value' => $this->entity->po_number, 'label' => ctrans('texts.po_number')]; + $data['$entity.datetime'] = ['value' => $this->formatDatetime($this->entity->created_at, $this->company->date_format(), $this->company->locale()), 'label' => ctrans('texts.date')]; $data['$payment_button'] = ['value' => ''.ctrans('texts.pay_now').'', 'label' => ctrans('texts.pay_now')]; @@ -156,7 +157,7 @@ class VendorHtmlEngine $data['$portal_url'] = ['value' => $this->invitation->getPortalLink(), 'label' =>'']; $data['$entity_number'] = &$data['$number']; - $data['$discount'] = &$data['$invoice.discount']; + $data['$discount'] = ['value' => $this->entity->discount, 'label' => ctrans('texts.discount')]; $data['$subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getSubTotal(), $this->vendor) ?: ' ', 'label' => ctrans('texts.subtotal')]; $data['$gross_subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getGrossSubTotal(), $this->vendor) ?: ' ', 'label' => ctrans('texts.subtotal')]; @@ -165,8 +166,6 @@ class VendorHtmlEngine else $data['$net_subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getSubTotal() - $this->entity_calc->getTotalDiscount(), $this->vendor) ?: ' ', 'label' => ctrans('texts.net_subtotal')]; - $data['$invoice.subtotal'] = &$data['$subtotal']; - if ($this->entity->partial > 0) { $data['$balance_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->vendor) ?: ' ', 'label' => ctrans('texts.partial_due')]; $data['$balance_due_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.partial_due')]; @@ -205,19 +204,19 @@ class VendorHtmlEngine $data['$created_by_user'] = &$data['$user.name']; $data['$assigned_to_user'] = ['value' => $this->entity->assigned_user ? $this->entity->assigned_user->present()->name() : '', 'label' => ctrans('texts.name')]; - $data['$entity.public_notes'] = &$data['$invoice.public_notes']; - $data['$public_notes'] = &$data['$invoice.public_notes']; + $data['$public_notes'] = ['value' => $this->entity->public_notes, 'label' => ctrans("texts.public_notes")]; + $data['$entity.public_notes'] = &$data['$public_notes']; $data['$notes'] = &$data['$public_notes']; - $data['$purchase_order.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order1', $this->entity->custom_value1, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order1')]; - $data['$purchase_order.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order2', $this->entity->custom_value2, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order2')]; - $data['$purchase_order.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order3', $this->entity->custom_value3, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order3')]; - $data['$purchase_order.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order4', $this->entity->custom_value4, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order4')]; + $data['$purchase_order.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order1', $this->entity->custom_value1, $this->company) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order1')]; + $data['$purchase_order.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order2', $this->entity->custom_value2, $this->company) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order2')]; + $data['$purchase_order.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order3', $this->entity->custom_value3, $this->company) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order3')]; + $data['$purchase_order.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order4', $this->entity->custom_value4, $this->company) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order4')]; - $data['$vendor1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor1', $this->vendor->custom_value1, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor1')]; - $data['$vendor2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor2', $this->vendor->custom_value2, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor2')]; - $data['$vendor3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor3', $this->vendor->custom_value3, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor3')]; - $data['$vendor4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor4', $this->vendor->custom_value4, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor4')]; + $data['$vendor1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor1', $this->vendor->custom_value1, $this->company) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor1')]; + $data['$vendor2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor2', $this->vendor->custom_value2, $this->company) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor2')]; + $data['$vendor3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor3', $this->vendor->custom_value3, $this->company) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor3')]; + $data['$vendor4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor4', $this->vendor->custom_value4, $this->company) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor4')]; $data['$vendor.custom1'] = &$data['$vendor1']; $data['$vendor.custom2'] = &$data['$vendor2']; $data['$vendor.custom3'] = &$data['$vendor3']; @@ -430,6 +429,8 @@ class VendorHtmlEngine $values = $this->buildEntityDataArray(); foreach ($values as $key => $value) { + nlog($key); + nlog($value); $data['values'][$key] = $value['value']; $data['labels'][$key.'_label'] = $value['label']; } diff --git a/database/migrations/2022_06_01_224339_create_purchase_order_invitations_table.php b/database/migrations/2022_06_01_224339_create_purchase_order_invitations_table.php index 60f95f0900c0..0412abb708ec 100644 --- a/database/migrations/2022_06_01_224339_create_purchase_order_invitations_table.php +++ b/database/migrations/2022_06_01_224339_create_purchase_order_invitations_table.php @@ -1,5 +1,6 @@ id(); - $table->unsignedInteger('company_id')->index(); + $table->unsignedInteger('company_id'); $table->unsignedInteger('user_id'); - $table->unsignedInteger('vendor_contact_id')->unique(); - $table->unsignedBigInteger('purchase_order_id')->index()->unique(); + $table->unsignedInteger('vendor_contact_id'); + $table->unsignedBigInteger('purchase_order_id')->index(); $table->string('key')->index(); $table->string('transaction_reference')->nullable(); $table->string('message_id')->nullable()->index(); @@ -37,6 +38,8 @@ class CreatePurchaseOrderInvitationsTable extends Migration $table->timestamps(6); $table->softDeletes('deleted_at', 6); + $table->unique(['vendor_contact_id', 'purchase_order_id'], 'vendor_purchase_unique'); + $table->index(['deleted_at', 'purchase_order_id', 'company_id'], 'vendor_purchase_company_index'); }); @@ -58,7 +61,8 @@ class CreatePurchaseOrderInvitationsTable extends Migration $company->settings = $settings; $company->save(); - }) + }); + } /** diff --git a/tests/Feature/PurchaseOrderTest.php b/tests/Feature/PurchaseOrderTest.php index b41141db897f..3450147e8865 100644 --- a/tests/Feature/PurchaseOrderTest.php +++ b/tests/Feature/PurchaseOrderTest.php @@ -40,6 +40,39 @@ class PurchaseOrderTest extends TestCase } + public function testPostNewPurchaseOrderPdf() + { + $purchase_order = [ + 'status_id' => 1, + 'discount' => 0, + 'is_amount_discount' => 1, + 'number' => '34343xx43', + 'public_notes' => 'notes', + 'is_deleted' => 0, + 'custom_value1' => 0, + 'custom_value2' => 0, + 'custom_value3' => 0, + 'custom_value4' => 0, + 'status' => 1, + 'vendor_id' => $this->encodePrimaryKey($this->vendor->id), + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/purchase_orders/', $purchase_order) + ->assertStatus(200); + + $arr = $response->json(); + + $purchase_order = PurchaseOrder::find($this->decodePrimaryKey($arr['data']['id'])); + + $this->assertNotNull($purchase_order); + + $purchase_order->service()->markSent()->getPurchaseOrderPdf(); + + } + public function testPurchaseOrderRest() { $response = $this->withHeaders([ diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index f8767315832c..b9bc2957d2aa 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -294,6 +294,7 @@ trait MockAccountData $this->vendor = Vendor::factory()->create([ 'user_id' => $user_id, 'company_id' => $this->company->id, + 'currency_id' => 1 ]); From a6327b76944f15f07acc1c2f4ee3b4963b2a4d40 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 7 Jun 2022 08:12:06 +1000 Subject: [PATCH 11/14] Purchase Order Actions --- app/DataMapper/CompanySettings.php | 2 + .../Controllers/PurchaseOrderController.php | 219 ++++++++++++++++++ .../ActionPurchaseOrderRequest.php | 61 +++++ app/Jobs/Ninja/SystemMaintenance.php | 4 +- ...reate_purchase_order_invitations_table.php | 3 +- routes/api.php | 2 + 6 files changed, 288 insertions(+), 3 deletions(-) create mode 100644 app/Http/Requests/PurchaseOrder/ActionPurchaseOrderRequest.php diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index 4eb8e06b2e67..bd202b8a9141 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -180,6 +180,8 @@ class CompanySettings extends BaseSettings public $email_subject_payment = ''; //@implemented public $email_subject_payment_partial = ''; //@implemented public $email_subject_statement = ''; //@implemented + public $email_subject_purchase_order = ''; //@implemented + public $email_template_purchase_order = ''; //@implemented public $email_template_invoice = ''; //@implemented public $email_template_credit = ''; //@implemented public $email_template_quote = ''; //@implemented diff --git a/app/Http/Controllers/PurchaseOrderController.php b/app/Http/Controllers/PurchaseOrderController.php index b07f068aec3b..60c8a33138c0 100644 --- a/app/Http/Controllers/PurchaseOrderController.php +++ b/app/Http/Controllers/PurchaseOrderController.php @@ -14,12 +14,14 @@ namespace App\Http\Controllers; use App\Factory\PurchaseOrderFactory; use App\Filters\PurchaseOrderFilters; +use App\Http\Requests\PurchaseOrder\ActionPurchaseOrderRequest; use App\Http\Requests\PurchaseOrder\CreatePurchaseOrderRequest; use App\Http\Requests\PurchaseOrder\DestroyPurchaseOrderRequest; use App\Http\Requests\PurchaseOrder\EditPurchaseOrderRequest; use App\Http\Requests\PurchaseOrder\ShowPurchaseOrderRequest; use App\Http\Requests\PurchaseOrder\StorePurchaseOrderRequest; use App\Http\Requests\PurchaseOrder\UpdatePurchaseOrderRequest; +use App\Jobs\Invoice\ZipInvoices; use App\Models\Client; use App\Models\PurchaseOrder; use App\Repositories\PurchaseOrderRepository; @@ -408,4 +410,221 @@ class PurchaseOrderController extends BaseController return $this->itemResponse($purchase_order->fresh()); } + + /** + * Perform bulk actions on the list view. + * + * @return Collection + * + * @OA\Post( + * path="/api/v1/purchase_orders/bulk", + * operationId="bulkPurchaseOrderss", + * tags={"purchase_orders"}, + * summary="Performs bulk actions on an array of purchase_orders", + * description="", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/index"), + * @OA\RequestBody( + * description="Purchase Order IDS", + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema( + * type="array", + * @OA\Items( + * type="integer", + * description="Array of hashed IDs to be bulk 'actioned", + * example="[0,1,2,3]", + * ), + * ) + * ) + * ), + * @OA\Response( + * response=200, + * description="The Bulk Action response", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function bulk() + { + + $action = request()->input('action'); + + $ids = request()->input('ids'); + + $purchase_orders = PurchaseOrder::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()->get(); + + if (! $invoices) { + return response()->json(['message' => 'No Purchase Orders Found']); + } + + /* + * Download Purchase Order/s + */ + + if ($action == 'bulk_download' && $purchase_orders->count() > 1) { + $purchase_orders->each(function ($purchase_order) { + if (auth()->user()->cannot('view', $purchase_order)) { + nlog("access denied"); + return response()->json(['message' => ctrans('text.access_denied')]); + } + }); + + ZipInvoices::dispatch($purchase_orders, $purchase_orders->first()->company, auth()->user()); + + return response()->json(['message' => ctrans('texts.sent_message')], 200); + } + + /* + * Send the other actions to the switch + */ + $purchase_orders->each(function ($purchase_order, $key) use ($action) { + if (auth()->user()->can('edit', $purchase_order)) { + $this->performAction($purchase_order, $action, true); + } + }); + + /* Need to understand which permission are required for the given bulk action ie. view / edit */ + + return $this->listResponse(PurchaseOrder::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()); + } + + /** + * @OA\Get( + * path="/api/v1/purchase_orders/{id}/{action}", + * operationId="actionPurchaseOrder", + * tags={"purchase_orders"}, + * summary="Performs a custom action on an purchase order", + * description="Performs a custom action on an purchase order. + * + * The current range of actions are as follows + * - mark_paid + * - download + * - archive + * - delete + * - email", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The Purchase Order Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Parameter( + * name="action", + * in="path", + * description="The action string to be performed", + * example="clone_to_quote", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the invoice object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/Invoice"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + * @param ActionPurchaseOrderRequest $request + * @param PurchaseOrder $purchase_order + * @param $action + * @return \App\Http\Controllers\Response|\Illuminate\Http\JsonResponse|Response|mixed|\Symfony\Component\HttpFoundation\StreamedResponse + */ + public function action(ActionPurchaseOrderRequest $request, PurchaseOrder $purchase_order, $action) + { + return $this->performAction($invoice, $action); + } + + private function performAction(PurchaseOrder $purchase_order, $action, $bulk = false) + { + /*If we are using bulk actions, we don't want to return anything */ + switch ($action) { + case 'mark_sent': + $purchase_order->service()->markSent()->save(); + + if (! $bulk) { + return $this->itemResponse($purchase_order); + } + break; + case 'download': + + $file = $purchase_order->service()->getPurchaseOrderPdf(); + + return response()->streamDownload(function () use($file) { + echo Storage::get($file); + }, basename($file), ['Content-Type' => 'application/pdf']); + + break; + case 'restore': + $this->purchase_order_repository->restore($purchase_order); + + if (! $bulk) { + return $this->listResponse($purchase_order); + } + break; + case 'archive': + $this->purchase_order_repository->archive($purchase_order); + + if (! $bulk) { + return $this->listResponse($purchase_order); + } + break; + case 'delete': + + $this->purchase_order_repository->delete($purchase_order); + + if (! $bulk) { + return $this->listResponse($purchase_order); + } + break; + + case 'email': + //check query parameter for email_type and set the template else use calculateTemplate + + + default: + return response()->json(['message' => ctrans('texts.action_unavailable', ['action' => $action])], 400); + break; + } + } } diff --git a/app/Http/Requests/PurchaseOrder/ActionPurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/ActionPurchaseOrderRequest.php new file mode 100644 index 000000000000..6768f171316d --- /dev/null +++ b/app/Http/Requests/PurchaseOrder/ActionPurchaseOrderRequest.php @@ -0,0 +1,61 @@ +user()->can('edit', $this->purchase_order); + } + + public function rules() + { + return [ + 'action' => 'required' + ]; + } + + protected function prepareForValidation() + { + $input = $this->all(); + + if($this->action){ + $input['action'] = $this->action; + } elseif (!array_key_exists('action', $input) ) { + $this->error_msg = 'Action is a required field'; + } + + $this->replace($input); + } + + public function messages() + { + return [ + 'action' => $this->error_msg, + ]; + } +} diff --git a/app/Jobs/Ninja/SystemMaintenance.php b/app/Jobs/Ninja/SystemMaintenance.php index b2221aad93a9..003dd172972d 100644 --- a/app/Jobs/Ninja/SystemMaintenance.php +++ b/app/Jobs/Ninja/SystemMaintenance.php @@ -49,8 +49,8 @@ class SystemMaintenance implements ShouldQueue nlog("Starting System Maintenance"); - // if(Ninja::isHosted()) - // return; + if(Ninja::isHosted()) + return; $delete_pdf_days = config('ninja.maintenance.delete_pdfs'); diff --git a/database/migrations/2022_06_01_224339_create_purchase_order_invitations_table.php b/database/migrations/2022_06_01_224339_create_purchase_order_invitations_table.php index 0412abb708ec..fc01d62b5a28 100644 --- a/database/migrations/2022_06_01_224339_create_purchase_order_invitations_table.php +++ b/database/migrations/2022_06_01_224339_create_purchase_order_invitations_table.php @@ -58,7 +58,8 @@ class CreatePurchaseOrderInvitationsTable extends Migration $settings->purchase_order_public_notes = ''; //@implemented $settings->purchase_order_number_pattern = ''; //@implemented $settings->purchase_order_number_counter = 1; //@implemented - + $settings->email_subject_purchase_order = ''; + $settings->email_template_purchase_order = ''; $company->settings = $settings; $company->save(); }); diff --git a/routes/api.php b/routes/api.php index b0fcc58d0045..4c4b07fab02e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -207,6 +207,8 @@ Route::group(['middleware' => ['throttle:100,1', 'api_db', 'token_auth', 'locale Route::put('vendors/{vendor}/upload', 'VendorController@upload'); Route::resource('purchase_orders', 'PurchaseOrderController'); + Route::post('purchase_orders/bulk', 'PurchaseOrderController@bulk')->name('purchase_orders.bulk'); + Route::get('purchase_orders/{purchase_order}/{action}', 'PurchaseOrderController@action')->name('purchase_orders.action'); Route::get('users', 'UserController@index'); Route::get('users/{user}', 'UserController@show')->middleware('password_protected'); From d7c7289ca962de41f2828f963fca4c34ff4fca79 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 7 Jun 2022 20:36:47 +1000 Subject: [PATCH 12/14] Update for invoice designs --- app/DataMapper/CompanySettings.php | 8 ++++++++ app/Repositories/PurchaseOrderRepository.php | 2 -- app/Services/PdfMaker/Design.php | 8 ++++++++ app/Services/PurchaseOrder/CreateInvitations.php | 12 ------------ app/Utils/VendorHtmlEngine.php | 13 ++++++++----- database/factories/CompanyFactory.php | 3 +++ database/factories/VendorFactory.php | 3 +++ resources/views/pdf-designs/bold.html | 4 +++- resources/views/pdf-designs/business.html | 2 ++ resources/views/pdf-designs/clean.html | 2 +- resources/views/pdf-designs/creative.html | 4 +++- resources/views/pdf-designs/elegant.html | 4 +++- resources/views/pdf-designs/hipster.html | 4 +++- resources/views/pdf-designs/modern.html | 4 +++- resources/views/pdf-designs/plain.html | 4 +++- resources/views/pdf-designs/playful.html | 4 +++- resources/views/pdf-designs/tech.html | 4 +++- tests/Feature/PurchaseOrderTest.php | 11 +++++++++-- tests/MockAccountData.php | 1 + 19 files changed, 67 insertions(+), 30 deletions(-) diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index bd202b8a9141..b81ad887b555 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -656,6 +656,14 @@ class CompanySettings extends BaseSettings '$vendor.phone', '$contact.email', ], + 'purchase_order_details' => [ + '$purchase_order.number', + '$purchase_order.po_number', + '$purchase_order.date', + '$purchase_order.due_date', + '$purchase_order.total', + '$purchase_order.balance_due', + ], 'company_details' => [ '$company.name', '$company.id_number', diff --git a/app/Repositories/PurchaseOrderRepository.php b/app/Repositories/PurchaseOrderRepository.php index ff508f1f44d5..2ffdc0e93576 100644 --- a/app/Repositories/PurchaseOrderRepository.php +++ b/app/Repositories/PurchaseOrderRepository.php @@ -83,8 +83,6 @@ class PurchaseOrderRepository extends BaseRepository /* If no invitations have been created, this is our fail safe to maintain state*/ if ($purchase_order->invitations()->count() == 0) $purchase_order->service()->createInvitations(); - -nlog("4"); /* Recalculate invoice amounts */ $purchase_order = $purchase_order->calc()->getPurchaseOrder(); diff --git a/app/Services/PdfMaker/Design.php b/app/Services/PdfMaker/Design.php index c7e34e3997d6..9e4b5efeec6b 100644 --- a/app/Services/PdfMaker/Design.php +++ b/app/Services/PdfMaker/Design.php @@ -252,6 +252,8 @@ class Design extends BaseDesign public function entityDetails(): array { + + if ($this->type === 'statement') { $s_date = $this->translateDate(now(), $this->client->date_format(), $this->client->locale()); @@ -286,6 +288,12 @@ class Design extends BaseDesign $variables = $this->context['pdf_variables']['credit_details']; } + if($this->vendor){ + + $variables = $this->context['pdf_variables']['purchase_order_details']; + + } + $elements = []; // We don't want to show account balance or invoice total on PDF.. or any amount with currency. diff --git a/app/Services/PurchaseOrder/CreateInvitations.php b/app/Services/PurchaseOrder/CreateInvitations.php index 141914faa8ed..fa01a8457b27 100644 --- a/app/Services/PurchaseOrder/CreateInvitations.php +++ b/app/Services/PurchaseOrder/CreateInvitations.php @@ -44,8 +44,6 @@ class CreateInvitations extends AbstractService { $contacts = $this->purchase_order->vendor->contacts()->where('send_email', true)->get(); -nlog("a"); - if($contacts->count() == 0){ $this->createBlankContact(); @@ -53,8 +51,6 @@ nlog("a"); $contacts = $this->purchase_order->vendor->contacts; } -nlog("b"); -nlog($contacts->count()); $contacts->each(function ($contact) { $invitation = PurchaseOrderInvitation::where('company_id', $this->purchase_order->company_id) @@ -79,9 +75,6 @@ nlog($contacts->count()); } }); -nlog("c"); - - if($this->purchase_order->invitations()->count() == 0) { if($contacts->count() == 0){ @@ -102,9 +95,6 @@ nlog("c"); } } -nlog("d"); - - $ii = PurchaseOrderInvitationFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id); $ii->key = $this->createDbHash($this->purchase_order->company->db); $ii->purchase_order_id = $this->purchase_order->id; @@ -112,8 +102,6 @@ nlog("d"); $ii->save(); } -nlog("e"); - return $this->purchase_order; } } diff --git a/app/Utils/VendorHtmlEngine.php b/app/Utils/VendorHtmlEngine.php index f56b68245e9b..7e20a951684e 100644 --- a/app/Utils/VendorHtmlEngine.php +++ b/app/Utils/VendorHtmlEngine.php @@ -149,10 +149,12 @@ class VendorHtmlEngine $data['$view_button'] = &$data['$view_link']; $data['$paymentButton'] = &$data['$payment_button']; $data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_invoice')]; - $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.invoice_date')]; + $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.date')]; - - + $data['$purchase_order.number'] = &$data['$number']; + $data['$purchase_order.date'] = &$data['$date']; + $data['$purchase_order.po_number'] = &$data['$poNumber']; + $data['$purchase_order.due_date'] = &$data['$due_date']; $data['$portal_url'] = ['value' => $this->invitation->getPortalLink(), 'label' =>'']; @@ -192,6 +194,9 @@ class VendorHtmlEngine $data['$partial'] = &$data['$partial_due']; $data['$total'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->vendor) ?: ' ', 'label' => ctrans('texts.total')]; + + $data['$purchase_order.total'] = &$data['$total']; + $data['$amount'] = &$data['$total']; $data['$amount_due'] = ['value' => &$data['$total']['value'], 'label' => ctrans('texts.amount_due')]; $data['$balance'] = ['value' => Number::formatMoney($this->entity_calc->getBalance(), $this->vendor) ?: ' ', 'label' => ctrans('texts.balance')]; @@ -429,8 +434,6 @@ class VendorHtmlEngine $values = $this->buildEntityDataArray(); foreach ($values as $key => $value) { - nlog($key); - nlog($value); $data['values'][$key] = $value['value']; $data['labels'][$key.'_label'] = $value['label']; } diff --git a/database/factories/CompanyFactory.php b/database/factories/CompanyFactory.php index 6fb2d23ef793..c2298fd718c5 100644 --- a/database/factories/CompanyFactory.php +++ b/database/factories/CompanyFactory.php @@ -12,10 +12,12 @@ namespace Database\Factories; use App\DataMapper\CompanySettings; use App\Models\Company; +use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Factories\Factory; class CompanyFactory extends Factory { + use MakesHash; /** * The name of the factory's corresponding model. * @@ -41,6 +43,7 @@ class CompanyFactory extends Factory 'enabled_modules' => config('ninja.enabled_modules'), 'custom_fields' => (object) [ ], + 'company_key' => $this->createHash(), ]; } } diff --git a/database/factories/VendorFactory.php b/database/factories/VendorFactory.php index 1bca58436fac..152848e545ba 100644 --- a/database/factories/VendorFactory.php +++ b/database/factories/VendorFactory.php @@ -12,6 +12,7 @@ namespace Database\Factories; use App\Models\Vendor; use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Support\Str; class VendorFactory extends Factory { @@ -45,6 +46,8 @@ class VendorFactory extends Factory 'state' => $this->faker->state, 'postal_code' => $this->faker->postcode, 'country_id' => 4, + 'vendor_hash' => Str::random(40), + ]; } } diff --git a/resources/views/pdf-designs/bold.html b/resources/views/pdf-designs/bold.html index d648ae8fc87b..de716f40d374 100644 --- a/resources/views/pdf-designs/bold.html +++ b/resources/views/pdf-designs/bold.html @@ -331,6 +331,7 @@

$entity_label

+
@@ -380,7 +381,8 @@ $entity_images let tables = [ 'product-table', 'task-table', 'delivery-note-table', 'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals', - 'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table' + 'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table', + 'vendor-details', 'client-details' ]; tables.forEach((tableIdentifier) => { diff --git a/resources/views/pdf-designs/business.html b/resources/views/pdf-designs/business.html index 84a1cd981512..9c6fbbb7d304 100644 --- a/resources/views/pdf-designs/business.html +++ b/resources/views/pdf-designs/business.html @@ -324,6 +324,7 @@

$entity_issued_to_label:

+
@@ -365,6 +366,7 @@ $entity_images let tables = [ 'product-table', 'task-table', 'delivery-note-table', 'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals', + 'vendor-details','client-details' ]; tables.forEach((tableIdentifier) => { diff --git a/resources/views/pdf-designs/clean.html b/resources/views/pdf-designs/clean.html index 35e9e2e4afe4..30d3015c38a2 100644 --- a/resources/views/pdf-designs/clean.html +++ b/resources/views/pdf-designs/clean.html @@ -295,7 +295,7 @@
-
+
diff --git a/resources/views/pdf-designs/creative.html b/resources/views/pdf-designs/creative.html index c68b769f1fab..5d37e6d72faf 100644 --- a/resources/views/pdf-designs/creative.html +++ b/resources/views/pdf-designs/creative.html @@ -267,6 +267,7 @@
+
@@ -322,7 +323,8 @@ $entity_images let tables = [ 'product-table', 'task-table', 'delivery-note-table', 'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals', - 'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table' + 'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table', + 'vendor-details', 'client-details' ]; tables.forEach((tableIdentifier) => { diff --git a/resources/views/pdf-designs/elegant.html b/resources/views/pdf-designs/elegant.html index 3843b50c50f7..a9c0fcffa5bb 100644 --- a/resources/views/pdf-designs/elegant.html +++ b/resources/views/pdf-designs/elegant.html @@ -279,6 +279,7 @@

$to_label

+

$from_label

@@ -328,7 +329,8 @@ $entity_images let tables = [ 'product-table', 'task-table', 'delivery-note-table', 'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals', - 'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table' + 'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table', + 'client-details','vendor-details' ]; tables.forEach((tableIdentifier) => { diff --git a/resources/views/pdf-designs/hipster.html b/resources/views/pdf-designs/hipster.html index 63de4dd2a15d..02c452e383df 100644 --- a/resources/views/pdf-designs/hipster.html +++ b/resources/views/pdf-designs/hipster.html @@ -297,6 +297,7 @@

$to_label:

+
{ diff --git a/resources/views/pdf-designs/modern.html b/resources/views/pdf-designs/modern.html index da83b85bed89..51476ddec1b4 100644 --- a/resources/views/pdf-designs/modern.html +++ b/resources/views/pdf-designs/modern.html @@ -320,6 +320,7 @@
+
@@ -364,7 +365,8 @@ $entity_images let tables = [ 'product-table', 'task-table', 'delivery-note-table', 'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals', - 'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table' + 'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table', + 'client-details','vendor-details' ]; tables.forEach((tableIdentifier) => { diff --git a/resources/views/pdf-designs/plain.html b/resources/views/pdf-designs/plain.html index c1a5ab55dade..a529de6c93f6 100644 --- a/resources/views/pdf-designs/plain.html +++ b/resources/views/pdf-designs/plain.html @@ -270,6 +270,7 @@
+
@@ -306,7 +307,8 @@ $entity_images let tables = [ 'product-table', 'task-table', 'delivery-note-table', 'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals', - 'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table' + 'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table', + 'client-details','vendor-details' ]; tables.forEach((tableIdentifier) => { diff --git a/resources/views/pdf-designs/playful.html b/resources/views/pdf-designs/playful.html index 43d0d0a46842..85739d84ccfa 100644 --- a/resources/views/pdf-designs/playful.html +++ b/resources/views/pdf-designs/playful.html @@ -349,6 +349,7 @@

$to_label:

+
@@ -409,7 +410,8 @@ $entity_images let tables = [ 'product-table', 'task-table', 'delivery-note-table', 'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals', - 'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table' + 'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table', + 'client-details','vendor-details' ]; tables.forEach((tableIdentifier) => { diff --git a/resources/views/pdf-designs/tech.html b/resources/views/pdf-designs/tech.html index 2fb542856370..2eb7c69385cb 100644 --- a/resources/views/pdf-designs/tech.html +++ b/resources/views/pdf-designs/tech.html @@ -321,6 +321,7 @@
$from_label:
+
$to_label: @@ -369,7 +370,8 @@ $entity_images let tables = [ 'product-table', 'task-table', 'delivery-note-table', 'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals', - 'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table' + 'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table', + 'client-details','vendor-details' ]; tables.forEach((tableIdentifier) => { diff --git a/tests/Feature/PurchaseOrderTest.php b/tests/Feature/PurchaseOrderTest.php index 3450147e8865..75e441c93fed 100644 --- a/tests/Feature/PurchaseOrderTest.php +++ b/tests/Feature/PurchaseOrderTest.php @@ -19,6 +19,7 @@ use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\Session; +use Illuminate\Support\Str; class PurchaseOrderTest extends TestCase { @@ -46,7 +47,12 @@ class PurchaseOrderTest extends TestCase 'status_id' => 1, 'discount' => 0, 'is_amount_discount' => 1, - 'number' => '34343xx43', + 'number' => Str::random(10), + 'po_number' => Str::random(5), + 'due_date' => '2022-01-01', + 'date' => '2022-01-01', + 'balance' => 100, + 'amount' => 100, 'public_notes' => 'notes', 'is_deleted' => 0, 'custom_value1' => 0, @@ -69,8 +75,9 @@ class PurchaseOrderTest extends TestCase $this->assertNotNull($purchase_order); - $purchase_order->service()->markSent()->getPurchaseOrderPdf(); + $x = $purchase_order->service()->markSent()->getPurchaseOrderPdf(); + nlog($x); } public function testPurchaseOrderRest() diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index b9bc2957d2aa..93f22890117c 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -56,6 +56,7 @@ use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; /** * Class MockAccountData. From 25b48994d63d43e131eaf805e951e039c999b327 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 7 Jun 2022 21:07:14 +1000 Subject: [PATCH 13/14] Fixes for invoice sum calculations --- app/Helpers/Invoice/InvoiceItemSum.php | 6 ++-- .../Invoice/InvoiceItemSumInclusive.php | 5 ++- app/Helpers/Invoice/InvoiceSum.php | 6 ++-- app/Helpers/Invoice/InvoiceSumInclusive.php | 33 +++++++------------ app/Models/Vendor.php | 3 ++ 5 files changed, 24 insertions(+), 29 deletions(-) diff --git a/app/Helpers/Invoice/InvoiceItemSum.php b/app/Helpers/Invoice/InvoiceItemSum.php index 720b2466447d..ba6e378a9ea2 100644 --- a/app/Helpers/Invoice/InvoiceItemSum.php +++ b/app/Helpers/Invoice/InvoiceItemSum.php @@ -52,10 +52,10 @@ class InvoiceItemSum $this->invoice = $invoice; - if($this->invoice->vendor) - $this->currency = $this->invoice->vendor->currency(); - else + if($this->invoice->client) $this->currency = $this->invoice->client->currency(); + else + $this->currency = $this->invoice->vendor->currency(); $this->line_items = []; } diff --git a/app/Helpers/Invoice/InvoiceItemSumInclusive.php b/app/Helpers/Invoice/InvoiceItemSumInclusive.php index 0af268d25ee8..0b5628229353 100644 --- a/app/Helpers/Invoice/InvoiceItemSumInclusive.php +++ b/app/Helpers/Invoice/InvoiceItemSumInclusive.php @@ -46,7 +46,10 @@ class InvoiceItemSumInclusive $this->invoice = $invoice; - $this->currency = $this->invoice->client->currency(); + if($this->invoice->client) + $this->currency = $this->invoice->client->currency(); + else + $this->currency = $this->invoice->vendor->currency(); $this->line_items = []; } diff --git a/app/Helpers/Invoice/InvoiceSum.php b/app/Helpers/Invoice/InvoiceSum.php index 5c2f72238ea7..3436610548f0 100644 --- a/app/Helpers/Invoice/InvoiceSum.php +++ b/app/Helpers/Invoice/InvoiceSum.php @@ -55,10 +55,10 @@ class InvoiceSum { $this->invoice = $invoice; - if($this->invoice->vendor) - $this->precision = $this->invoice->vendor->currency()->precision; - else + if($this->invoice->client) $this->precision = $this->invoice->client->currency()->precision; + else + $this->precision = $this->invoice->vendor->currency()->precision; $this->tax_map = new Collection; } diff --git a/app/Helpers/Invoice/InvoiceSumInclusive.php b/app/Helpers/Invoice/InvoiceSumInclusive.php index eeb344a50c3d..5e09187303cc 100644 --- a/app/Helpers/Invoice/InvoiceSumInclusive.php +++ b/app/Helpers/Invoice/InvoiceSumInclusive.php @@ -41,6 +41,7 @@ class InvoiceSumInclusive private $sub_total; + private $precision; /** * Constructs the object with Invoice and Settings object. * @@ -50,6 +51,11 @@ class InvoiceSumInclusive { $this->invoice = $invoice; + if($this->invoice->client) + $this->precision = $this->invoice->client->currency()->precision; + else + $this->precision = $this->invoice->vendor->currency()->precision; + $this->tax_map = new Collection; } @@ -164,32 +170,15 @@ class InvoiceSumInclusive private function calculateTotals() { - //$this->total += $this->total_taxes; - - // if (is_numeric($this->invoice->custom_value1)) { - // $this->total += $this->invoice->custom_value1; - // } - - // if (is_numeric($this->invoice->custom_value2)) { - // $this->total += $this->invoice->custom_value2; - // } - - // if (is_numeric($this->invoice->custom_value3)) { - // $this->total += $this->invoice->custom_value3; - // } - - // if (is_numeric($this->invoice->custom_value4)) { - // $this->total += $this->invoice->custom_value4; - // } return $this; } public function getRecurringInvoice() { - $this->invoice->amount = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision); + $this->invoice->amount = $this->formatValue($this->getTotal(), $this->precision); $this->invoice->total_taxes = $this->getTotalTaxes(); - $this->invoice->balance = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision); + $this->invoice->balance = $this->formatValue($this->getTotal(), $this->precision); $this->invoice->saveQuietly(); @@ -249,14 +238,14 @@ class InvoiceSumInclusive if ($this->invoice->amount != $this->invoice->balance) { $paid_to_date = $this->invoice->amount - $this->invoice->balance; - $this->invoice->balance = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision) - $paid_to_date; + $this->invoice->balance = $this->formatValue($this->getTotal(), $this->precision) - $paid_to_date; } else { - $this->invoice->balance = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision); + $this->invoice->balance = $this->formatValue($this->getTotal(), $this->precision); } } /* Set new calculated total */ - $this->invoice->amount = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision); + $this->invoice->amount = $this->formatValue($this->getTotal(), $this->precision); $this->invoice->total_taxes = $this->getTotalTaxes(); diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php index 836ec380b741..a1d98692aa9d 100644 --- a/app/Models/Vendor.php +++ b/app/Models/Vendor.php @@ -107,6 +107,9 @@ class Vendor extends BaseModel if(!$currencies) $this->buildCache(true); + if(!$this->currency_id) + $this->currency_id = 1; + return $currencies->filter(function ($item) { return $item->id == $this->currency_id; })->first(); From 21c5bdcd90776ece172f6c60a7ed0c617a53ca17 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 8 Jun 2022 08:27:47 +1000 Subject: [PATCH 14/14] Fixes for tests --- app/DataMapper/CompanySettings.php | 3 +++ app/Events/Invoice/InvoiceWasEmailedAndFailed.php | 5 ++--- app/Services/Invoice/InvoiceService.php | 2 +- tests/Feature/CompanySettingsTest.php | 5 +++++ tests/Integration/HtmlGenerationTest.php | 3 +++ 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index b81ad887b555..0fc9efb9dae4 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -289,6 +289,8 @@ class CompanySettings extends BaseSettings public $auto_archive_invoice_cancelled = false; public static $casts = [ + 'email_subject_purchase_order' => 'string', + 'email_template_purchase_order' => 'string', 'require_purchase_order_signature' => 'bool', 'purchase_order_public_notes' => 'string', 'purchase_order_terms' => 'string', @@ -547,6 +549,7 @@ class CompanySettings extends BaseSettings 'invoice_design_id', 'quote_design_id', 'credit_design_id', + 'purchase_order_design_id', ]; /** diff --git a/app/Events/Invoice/InvoiceWasEmailedAndFailed.php b/app/Events/Invoice/InvoiceWasEmailedAndFailed.php index a83bc07467e5..138f8960207f 100644 --- a/app/Events/Invoice/InvoiceWasEmailedAndFailed.php +++ b/app/Events/Invoice/InvoiceWasEmailedAndFailed.php @@ -12,7 +12,6 @@ namespace App\Events\Invoice; use App\Models\Company; -use App\Models\InvoiceInvitation; use Illuminate\Queue\SerializesModels; /** @@ -35,12 +34,12 @@ class InvoiceWasEmailedAndFailed /** * Create a new event instance. * - * @param InvoiceInvitation $invitation + * @param $invitation * @param Company $company * @param string $errors * @param array $event_vars */ - public function __construct(InvoiceInvitation $invitation, Company $company, string $message, string $template, array $event_vars) + public function __construct($invitation, Company $company, string $message, string $template, array $event_vars) { $this->invitation = $invitation; diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index df74ee4fb73b..0eae2bc67984 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -179,7 +179,7 @@ class InvoiceService $this->invoice = (new MarkSent($this->invoice->client, $this->invoice))->run(); $this->setExchangeRate(); - + return $this; } diff --git a/tests/Feature/CompanySettingsTest.php b/tests/Feature/CompanySettingsTest.php index c521802586af..3c9a854329c0 100644 --- a/tests/Feature/CompanySettingsTest.php +++ b/tests/Feature/CompanySettingsTest.php @@ -78,10 +78,15 @@ class CompanySettingsTest extends TestCase $this->company->saveSettings($settings, $this->company); + try { $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-Token' => $this->token, ])->put('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $this->company->toArray()); + } catch (ValidationException $e) { + $message = json_decode($e->validator->getMessageBag(), 1); + nlog($message); + } $response->assertStatus(200); diff --git a/tests/Integration/HtmlGenerationTest.php b/tests/Integration/HtmlGenerationTest.php index eb31de791fff..c977ade9820e 100644 --- a/tests/Integration/HtmlGenerationTest.php +++ b/tests/Integration/HtmlGenerationTest.php @@ -13,6 +13,7 @@ namespace Tests\Integration; use App\Models\Credit; use App\Models\Design; use App\Models\Invoice; +use App\Models\PurchaseOrder; use App\Models\Quote; use App\Models\RecurringInvoice; use App\Services\PdfMaker\Design as PdfDesignModel; @@ -57,6 +58,8 @@ class HtmlGenerationTest extends TestCase $entity_design_id = 'quote_design_id'; } elseif ($entity instanceof Credit) { $entity_design_id = 'credit_design_id'; + } elseif ($entity instanceof PurchaseOrder) { + $entity_design_id = 'purchase_order_design_id'; } $entity_design_id = $entity->design_id ? $entity->design_id : $this->decodePrimaryKey($entity->client->getSetting($entity_design_id));