diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index 8f9dfe2408f4..0fc9efb9dae4 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 @@ -133,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 @@ -170,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 @@ -276,10 +288,16 @@ class CompanySettings extends BaseSettings public $email_from_name = ''; public $auto_archive_invoice_cancelled = false; - - public $purchase_order_number_counter = 1; //TODO - 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', + 'purchase_order_design_id' => 'string', + 'purchase_order_footer' => 'string', + 'purchase_order_number_pattern' => 'string', + 'purchase_order_number_counter' => 'int', 'page_numbering_alignment' => 'string', 'page_numbering' => 'bool', 'auto_archive_invoice_cancelled' => 'bool', @@ -531,6 +549,7 @@ class CompanySettings extends BaseSettings 'invoice_design_id', 'quote_design_id', 'credit_design_id', + 'purchase_order_design_id', ]; /** @@ -629,6 +648,25 @@ 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', + ], + '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/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/Helpers/Invoice/InvoiceItemSum.php b/app/Helpers/Invoice/InvoiceItemSum.php index 11a4b995da86..ba6e378a9ea2 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->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 8121cbfcc04e..3436610548f0 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->client) + $this->precision = $this->invoice->client->currency()->precision; + else + $this->precision = $this->invoice->vendor->currency()->precision; $this->tax_map = new Collection; } @@ -224,11 +228,19 @@ 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); + $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(); @@ -247,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/Helpers/Invoice/InvoiceSumInclusive.php b/app/Helpers/Invoice/InvoiceSumInclusive.php index 0cdb1638045d..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(); @@ -229,6 +218,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. @@ -240,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/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/app/Http/Controllers/PurchaseOrderController.php b/app/Http/Controllers/PurchaseOrderController.php index 677e00196903..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; @@ -174,8 +176,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() @@ -410,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/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/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->company = $invitation->company; + + $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 VendorHtmlEngine($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) + { + + } + + +} 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 3a82bb9215f8..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; @@ -159,20 +162,20 @@ 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); } 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/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 bae220a60698..a1d98692aa9d 100644 --- a/app/Models/Vendor.php +++ b/app/Models/Vendor.php @@ -11,9 +11,13 @@ namespace App\Models; +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 Illuminate\Support\Facades\Cache; use Laracasts\Presenter\PresentableTrait; class Vendor extends BaseModel @@ -22,6 +26,7 @@ class Vendor extends BaseModel use Filterable; use GeneratesCounter; use PresentableTrait; + use AppSetup; protected $fillable = [ 'name', @@ -95,6 +100,22 @@ class Vendor extends BaseModel return $this->hasMany(Activity::class); } + public function currency() + { + $currencies = Cache::get('currencies'); + + 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(); + } + + public function company() { return $this->belongsTo(Company::class); @@ -109,4 +130,54 @@ 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 ''; + } + + 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..2ffdc0e93576 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,81 @@ 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(); + + /* 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/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/app/Services/PdfMaker/Design.php b/app/Services/PdfMaker/Design.php index 6c349cb07833..9e4b5efeec6b 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; @@ -69,6 +72,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 +118,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,10 +197,29 @@ class Design extends BaseDesign return $elements; } + public function vendorDetails(): array + { + $elements = []; + + if(!$this->vendor) + return $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 = []; + 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']], @@ -224,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()); @@ -258,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/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/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..fa01a8457b27 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(); @@ -40,19 +51,25 @@ class CreateInvitations extends AbstractService $contacts = $this->purchase_order->vendor->contacts; } + $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); + 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(); } @@ -66,7 +83,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() @@ -78,14 +95,13 @@ 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; $ii->save(); } - 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/MarkSent.php b/app/Services/PurchaseOrder/MarkSent.php index fe51346066d0..c3b1df47b0d7 100644 --- a/app/Services/PurchaseOrder/MarkSent.php +++ b/app/Services/PurchaseOrder/MarkSent.php @@ -1,5 +1,13 @@ client = $client; + $this->vendor = $vendor; $this->purchase_order = $purchase_order; } diff --git a/app/Services/PurchaseOrder/PurchaseOrderService.php b/app/Services/PurchaseOrder/PurchaseOrderService.php index b86ced3750c7..69e6066b992a 100644 --- a/app/Services/PurchaseOrder/PurchaseOrderService.php +++ b/app/Services/PurchaseOrder/PurchaseOrderService.php @@ -13,6 +13,9 @@ 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 @@ -21,11 +24,63 @@ class PurchaseOrderService public PurchaseOrder $purchase_order; - public function __construct($purchase_order) + public function __construct(PurchaseOrder $purchase_order) { $this->purchase_order = $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(); + + // //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 (!isset($this->purchase_order->public_notes)) + // $this->purchase_order->public_notes = $this->purchase_order->client->public_notes; + + + 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; + + return $this; + } + + public function markSent() + { + $this->purchase_order = (new MarkSent($this->purchase_order->vendor, $this->purchase_order))->run(); + + return $this; + } + /** * Saves the purchase order. * @return \App\Models\PurchaseOrder object @@ -37,45 +92,4 @@ class PurchaseOrderService return $this->purchase_order; } - public function fillDefaults() - { - $settings = $this->purchase_order->client->getMergedSettings(); - - //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 (!isset($this->purchase_order->public_notes)) - $this->purchase_order->public_notes = $this->purchase_order->client->public_notes; - - - return $this; - } - - public function setStatus($status) - { - $this->purchase_order->status_id = $status; - - return $this; - } - - public function markSent() - { - $this->purchase_order = (new MarkSent($this->purchase_order->client, $this->purchase_order))->run(); - - return $this; - } - /** - * Applies the purchase order number. - * @return $this PurchaseOrderService object - */ - public function applyNumber() - { - $this->purchase_order = (new ApplyNumber($this->purchase_order->client, $this->purchase_order))->run(); - - return $this; - } } diff --git a/app/Utils/Helpers.php b/app/Utils/Helpers.php index ce0312b3fed3..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': @@ -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/Traits/GeneratesCounter.php b/app/Utils/Traits/GeneratesCounter.php index 3dd2bcefb330..939a90849480 100644 --- a/app/Utils/Traits/GeneratesCounter.php +++ b/app/Utils/Traits/GeneratesCounter.php @@ -158,6 +158,10 @@ trait GeneratesCounter return 'purchase_order_number_counter'; break; + case PurchaseOrder::class: + return 'purchase_order_number_counter'; + break; + default: return 'default_number_counter'; break; @@ -192,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); - } /** @@ -363,6 +353,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_counter'); + + $entity_number = $purchase_order_number; + + return $this->replaceUserVars($purchase_order, $entity_number); + + } + /** * Gets the next expense number. * diff --git a/app/Utils/VendorHtmlEngine.php b/app/Utils/VendorHtmlEngine.php new file mode 100644 index 000000000000..7e20a951684e --- /dev/null +++ b/app/Utils/VendorHtmlEngine.php @@ -0,0 +1,714 @@ +invitation = $invitation; + + $this->entity_string = $this->resolveEntityString(); + + $this->entity = $invitation->purchase_order; + + $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['$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['$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')]; + $data['$payment_link'] = ['value' => $this->invitation->getPaymentLink(), 'label' => ctrans('texts.pay_now')]; + + + $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.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' =>'']; + + $data['$entity_number'] = &$data['$number']; + $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')]; + + 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')]; + + 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['$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['$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')]; + + $data['$taxes'] = ['value' => Number::formatMoney($this->entity_calc->getItemTotalTaxes(), $this->vendor) ?: ' ', 'label' => ctrans('texts.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['$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->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->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']; + $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')]; + + 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/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/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..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 @@ -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,8 +38,32 @@ 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'); }); + + Schema::table('purchase_orders', function (Blueprint $table) { + $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 + $settings->email_subject_purchase_order = ''; + $settings->email_template_purchase_order = ''; + $company->settings = $settings; + $company->save(); + }); + } /** @@ -48,6 +73,5 @@ class CreatePurchaseOrderInvitationsTable extends Migration */ public function down() { - Schema::dropIfExists('purchase_order_invitations'); } } 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; 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 f0da1c52d4b7..30d3015c38a2 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) => { 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/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'); 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/Feature/PurchaseOrderTest.php b/tests/Feature/PurchaseOrderTest.php index 63799022b7aa..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 { @@ -40,6 +41,45 @@ class PurchaseOrderTest extends TestCase } + public function testPostNewPurchaseOrderPdf() + { + $purchase_order = [ + 'status_id' => 1, + 'discount' => 0, + 'is_amount_discount' => 1, + '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, + '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); + + $x = $purchase_order->service()->markSent()->getPurchaseOrderPdf(); + + nlog($x); + } + public function testPurchaseOrderRest() { $response = $this->withHeaders([ @@ -83,7 +123,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 +157,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/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)); diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index 2bb170b9388b..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. @@ -294,6 +295,7 @@ trait MockAccountData $this->vendor = Vendor::factory()->create([ 'user_id' => $user_id, 'company_id' => $this->company->id, + 'currency_id' => 1 ]); @@ -450,22 +452,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 = '';