From 75549a726ce414d8cb81a280279284aa55ca7bd6 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 12 Jul 2023 19:15:52 +1000 Subject: [PATCH 01/22] Add thai baht currency code. --- app/Casts/EncryptedCast.php | 2 +- ...2_074829_add_thai_baht_currency_symbol.php | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 database/migrations/2023_07_12_074829_add_thai_baht_currency_symbol.php diff --git a/app/Casts/EncryptedCast.php b/app/Casts/EncryptedCast.php index e13c69625d32..7be926072c3a 100644 --- a/app/Casts/EncryptedCast.php +++ b/app/Casts/EncryptedCast.php @@ -17,7 +17,7 @@ class EncryptedCast implements CastsAttributes { public function get($model, string $key, $value, array $attributes) { - return strlen($value) > 1 ? decrypt($value) : null; + return is_string($value) && strlen($value) > 1 ? decrypt($value) : null; } public function set($model, string $key, $value, array $attributes) diff --git a/database/migrations/2023_07_12_074829_add_thai_baht_currency_symbol.php b/database/migrations/2023_07_12_074829_add_thai_baht_currency_symbol.php new file mode 100644 index 000000000000..fe959e037436 --- /dev/null +++ b/database/migrations/2023_07_12_074829_add_thai_baht_currency_symbol.php @@ -0,0 +1,34 @@ +symbol = '฿'; + $tb->save(); + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +}; From d40de524f4dc6269501a86946fd7d266273e26a2 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 12 Jul 2023 19:16:19 +1000 Subject: [PATCH 02/22] Updates for html cp --- database/seeders/CurrenciesSeeder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/seeders/CurrenciesSeeder.php b/database/seeders/CurrenciesSeeder.php index 71a482d726c1..82d92b6595b7 100644 --- a/database/seeders/CurrenciesSeeder.php +++ b/database/seeders/CurrenciesSeeder.php @@ -43,7 +43,7 @@ class CurrenciesSeeder extends Seeder ['id' => 18, 'name' => 'Guatemalan Quetzal', 'code' => 'GTQ', 'symbol' => 'Q', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['id' => 19, 'name' => 'Malaysian Ringgit', 'code' => 'MYR', 'symbol' => 'RM', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['id' => 20, 'name' => 'Brazilian Real', 'code' => 'BRL', 'symbol' => 'R$', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ','], - ['id' => 21, 'name' => 'Thai Baht', 'code' => 'THB', 'symbol' => '', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], + ['id' => 21, 'name' => 'Thai Baht', 'code' => 'THB', 'symbol' => '฿', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['id' => 22, 'name' => 'Nigerian Naira', 'code' => 'NGN', 'symbol' => '', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['id' => 23, 'name' => 'Argentine Peso', 'code' => 'ARS', 'symbol' => '$', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ','], ['id' => 24, 'name' => 'Bangladeshi Taka', 'code' => 'BDT', 'symbol' => 'Tk', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], From 348b9259479a9f82888f79481a19da31c81dafc4 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 12 Jul 2023 21:19:27 +1000 Subject: [PATCH 03/22] Fixes for credit imports --- app/Jobs/Util/Import.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/Jobs/Util/Import.php b/app/Jobs/Util/Import.php index e4de8ecc5244..1df1683a212e 100644 --- a/app/Jobs/Util/Import.php +++ b/app/Jobs/Util/Import.php @@ -1173,11 +1173,26 @@ class Import implements ShouldQueue unset($modified['id']); + $credit = $credit_repository->save( $modified, CreditFactory::create($this->company->id, $modified['user_id']) ); + if($credit->status_id == 4) + { + + $client = $credit->client; + $client->balance -= $credit->balance; + $client->credit_balance -= $credit->amount; + $client->saveQuietly(); + + $credit->paid_to_date = $credit->amount; + $credit->balance = 0; + $credit->saveQuietly(); + + } + //remove credit balance from ledger if ($credit->balance > 0 && $credit->client->balance > 0) { $client = $credit->client; From 484d090faef974798b08ae9048c428925ebdc3c0 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 13 Jul 2023 10:59:10 +1000 Subject: [PATCH 04/22] Minor fixes --- app/Filters/InvoiceFilters.php | 4 ++ app/Http/Controllers/ActivityController.php | 42 +-------------------- app/Utils/Ninja.php | 2 +- lang/cs/texts.php | 2 +- lang/en/texts.php | 2 +- lang/en_GB/texts.php | 2 +- lang/fr_CA/texts.php | 33 ++++++++-------- lang/ja/texts.php | 2 +- lang/lt/texts.php | 2 +- lang/nb_NO/texts.php | 2 +- lang/sq/texts.php | 2 +- lang/tr_TR/texts.php | 2 +- 12 files changed, 32 insertions(+), 65 deletions(-) diff --git a/app/Filters/InvoiceFilters.php b/app/Filters/InvoiceFilters.php index e7ada0ce11ac..3f1feccc3610 100644 --- a/app/Filters/InvoiceFilters.php +++ b/app/Filters/InvoiceFilters.php @@ -55,6 +55,10 @@ class InvoiceFilters extends QueryFilters $this->builder->where(function ($query) use ($status_parameters) { $invoice_filters = []; + if (in_array('draft', $status_parameters)) { + $invoice_filters[] = Invoice::STATUS_DRAFT; + } + if (in_array('paid', $status_parameters)) { $invoice_filters[] = Invoice::STATUS_PAID; } diff --git a/app/Http/Controllers/ActivityController.php b/app/Http/Controllers/ActivityController.php index ac850442125e..cf69f531cbfd 100644 --- a/app/Http/Controllers/ActivityController.php +++ b/app/Http/Controllers/ActivityController.php @@ -91,47 +91,7 @@ class ActivityController extends BaseController ->orderBy('created_at', 'DESC') ->company() ->take($default_activities); - - // if ($request->has('react')) { - - // /** @var \App\Models\User auth()->user() */ - // $user = auth()->user(); - - // if (!$user->isAdmin()) { - // $activities->where('user_id', auth()->user()->id); - // } - - // $system = ctrans('texts.system'); - - // $data = $activities->cursor()->map(function ($activity) { - - // $arr = - // [ - // 'client' => $activity->client ? $activity->client : '', - // 'contact' => $activity->client ? $activity->contact : '', - // 'quote' => $activity->quote ? $activity->quote : '', - // 'user' => $activity->user ? $activity->user : '', - // 'expense' => $activity->expense ? $activity->expense : '', - // 'invoice' => $activity->invoice ? $activity->invoice : '', - // 'recurring_invoice' => $activity->recurring_invoice ? $activity->recurring_invoice : '', - // 'payment' => $activity->payment ? $activity->payment : '', - // 'credit' => $activity->credit ? $activity->credit : '', - // 'task' => $activity->task ? $activity->task : '', - // 'vendor' => $activity->vendor ? $activity->vendor : '', - // 'purchase_order' => $activity->purchase_order ? $activity->purchase_order : '', - // 'subscription' => $activity->subscription ? $activity->subscription : '', - // 'vendor_contact' => $activity->vendor_contact ? $activity->vendor_contact : '', - // 'recurring_expense' => $activity->recurring_expense ? $activity->recurring_expense : '', - // ]; - - // $activity_array = $activity->toArray(); - - // return array_merge($arr, $activity_array); - // }); - - // return response()->json(['data' => $data->toArray()], 200); - // } - // else + if($request->has('reactv2')) { /** @var \App\Models\User auth()->user() */ diff --git a/app/Utils/Ninja.php b/app/Utils/Ninja.php index 16f357d4df0b..42e6ee271254 100644 --- a/app/Utils/Ninja.php +++ b/app/Utils/Ninja.php @@ -132,7 +132,7 @@ class Ninja 'ip' => $ip, 'token' => request()->header('X-API-TOKEN'), 'is_system' => app()->runningInConsole(), - 'user_id' => $user_id, + 'user_id' => ($ip == '127.0.0.1') ? null : $user_id, ]; } diff --git a/lang/cs/texts.php b/lang/cs/texts.php index b9bb377be16c..e28dc4e0770a 100644 --- a/lang/cs/texts.php +++ b/lang/cs/texts.php @@ -753,7 +753,7 @@ $LANG = array( 'activity_7' => 'Klient :contact zobrazil fakturu :invoice pro :client', 'activity_8' => ':user archivoval fakturu :invoice', 'activity_9' => ':user smazal fakturu :invoice', - 'activity_10' => ':contact entered payment :payment for :payment_amount on invoice :invoice for :client', + 'activity_10' => ':user entered payment :payment for :payment_amount on invoice :invoice for :client', 'activity_11' => ':user změnil platbu :payment', 'activity_12' => ':user archivoval platbu :payment', 'activity_13' => ':user smazal platbu :payment', diff --git a/lang/en/texts.php b/lang/en/texts.php index 69b4e7e79fa1..d17b5e0ef1fb 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5122,7 +5122,7 @@ $LANG = array( 'lang_French - Swiss' => 'French - Swiss', 'currency_swazi_lilangeni' => 'Swazi Lilangeni', 'income' => 'Income', - + 'amount_received_help' => 'Enter a value here if the total amount received was MORE than the invoice amount, or when recording a payment with no invoices. Otherwise this field should be left blank.', ); diff --git a/lang/en_GB/texts.php b/lang/en_GB/texts.php index 27268d7857c3..c687b7b8d9cd 100644 --- a/lang/en_GB/texts.php +++ b/lang/en_GB/texts.php @@ -761,7 +761,7 @@ $LANG = array( 'activity_7' => ':contact viewed invoice :invoice for :client', 'activity_8' => ':user archived invoice :invoice', 'activity_9' => ':user deleted invoice :invoice', - 'activity_10' => ':contact entered payment :payment for :payment_amount on invoice :invoice for :client', + 'activity_10' => ':user entered payment :payment for :payment_amount on invoice :invoice for :client', 'activity_11' => ':user updated payment :payment', 'activity_12' => ':user archived payment :payment', 'activity_13' => ':user deleted payment :payment', diff --git a/lang/fr_CA/texts.php b/lang/fr_CA/texts.php index 9d812017526c..94189ac73af5 100644 --- a/lang/fr_CA/texts.php +++ b/lang/fr_CA/texts.php @@ -748,7 +748,7 @@ $LANG = array( 'activity_7' => ':contact a visualisé la facture :invoice pour :client', 'activity_8' => ':user a archivé la facture :invoice', 'activity_9' => ':user a supprimé la facture :invoice', - 'activity_10' => ':contact a saisi le paiement :payment de :payment_amount de la facture :invoice pour :client', + 'activity_10' => ':user a saisi le paiement :payment de :payment_amount de la facture :invoice pour :client', 'activity_11' => ':user a mis à jour le paiement :payment', 'activity_12' => ':user a archivé le paiement :payment', 'activity_13' => ':user a supprimé le paiement :payment', @@ -1433,7 +1433,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'payment_type_Swish' => 'Swish', 'payment_type_Alipay' => 'Alipay', 'payment_type_Sofort' => 'Sofort', - 'payment_type_SEPA' => 'SEPA Prélèvement automatique', + 'payment_type_SEPA' => 'Prélèvement automatique SEPA', 'payment_type_Bitcoin' => 'Bitcoin', 'payment_type_GoCardless' => 'GoCardless', 'payment_type_Zelle' => 'Zelle', @@ -2448,7 +2448,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'alipay' => 'Alipay', 'sofort' => 'Sofort', - 'sepa' => 'SEPA Prélèvement automatique', + 'sepa' => 'Prélèvement automatique SEPA', 'name_without_special_characters' => 'Veuillez entrer un nom en utilisant seulement les lettres de a à z et des espaces.', 'enable_alipay' => 'Accepter Alipay', 'enable_sofort' => 'Accepter les transferts d\'institutions bancaires de l\'Union européenne', @@ -3130,7 +3130,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'number_padding' => 'Disposition du numéro', 'general' => 'Général', 'surcharge_field' => 'Champ Surcharge', - 'company_value' => 'Valeur de compagnie', + 'company_value' => 'Valeur de l\'entreprise', 'credit_field' => 'Champ Crédit', 'payment_field' => 'Champ Paiement', 'group_field' => 'Champ Groupe', @@ -3510,7 +3510,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'search_expenses' => 'Recherche de dépenses', 'search_payments' => 'Recherche de paiements', 'search_groups' => 'Recherche de groupes', - 'search_company' => 'Recherche d\'entreprises', + 'search_company' => 'Recherche d\'une entreprise', 'cancelled_invoice' => 'La facture a été annulée', 'cancelled_invoices' => 'Les factures ont été annulées', 'reversed_invoice' => 'La facture a été inversée', @@ -3972,8 +3972,8 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'account_balance' => 'Solde de compte', 'thanks' => 'Merci', 'minimum_required_payment' => 'Le paiement minimum requis est :amount', - 'under_payments_disabled' => 'La société ne tolère pas le sous-paiement.', - 'over_payments_disabled' => 'La société ne tolère pas le sur-paiement.', + 'under_payments_disabled' => 'L\'entreprise ne tolère pas le sous-paiement.', + 'over_payments_disabled' => 'L\'entreprise ne tolère pas le sur-paiement.', 'saved_at' => 'Enregistré à :time', 'credit_payment' => 'Le crédit a été appliqué à la facture :invoice_number', 'credit_subject' => 'Nouveau crédit :credit de :account', @@ -4109,7 +4109,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'migration_api_secret_notice' => 'Vous pouvez trouver API_SECRET dans le fichier .env ou Invoice Ninja v5. Si la propriété est manquante, laissez le champ vide.', 'billing_coupon_notice' => 'Votre rabais sera appliqué au moment de régler votre facture.', 'use_last_email' => 'Utiliser le dernier e-mail', - 'activate_company' => 'Activer la société', + 'activate_company' => 'Activer l\'entreprise', 'activate_company_help' => 'Activez les courriels, les factures récurrentes et les notifications', 'an_error_occurred_try_again' => 'Une erreur s\'est produite, veuillez réessayer', 'please_first_set_a_password' => 'Veuillez d\'abord définir un mot de passe', @@ -4242,9 +4242,9 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'giropay_law' => 'En saisissant vos information de client (tel que votre nom et numéro de compte), vous (le client) agréez à donner cette information volontairement.', 'klarna' => 'Klarna', 'eps' => 'EPS', - 'becs' => 'BECS Prélèvement automatique', - 'bacs' => 'Débit direct BACS', - 'payment_type_BACS' => 'Débit direct BACS', + 'becs' => 'Prélèvement automatique BECS', + 'bacs' => 'Prélèvement automatique BACS', + 'payment_type_BACS' => 'Prélèvement automatique BACS', 'missing_payment_method' => 'Veuillez ajouter une méthode de paiement avant de payer.', 'becs_mandate' => 'En fournissant vos coordonnées bancaires, vous acceptez l\'entente de service de requête de débit direct, et autorisez Stripe Payments Australia Pty Ltd ACN 160 180 343 Direct Debit User ID number 507156 (“Stripe”) à prélever votre compte par le Bulk Electronic Clearing System (BECS) de la part du :company (“Merchant”) pour tout montant séparément communiqué à vous par le Marchand. Vous certifiez que vous êtes propriétaire du compte ou que vous êtes un signataire autorisé sur le compte indiqué plus haut.', 'you_need_to_accept_the_terms_before_proceeding' => 'Vous devez accepter les conditions pour continuer.', @@ -4257,12 +4257,12 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'browser_pay' => 'Google Pay, Apple Pay et Microsoft Pay', 'no_available_methods' => 'Cartes de crédit introuvable sur cet appareil. Plus d\'information.', 'gocardless_mandate_not_ready' => 'La demande de paiement n\'est pas prête. Veuillez essayer de nouveau plus tard.', - 'payment_type_instant_bank_pay' => 'Interac', + 'payment_type_instant_bank_pay' => 'Instant Bank Pay', 'payment_type_iDEAL' => 'iDEAL', 'payment_type_Przelewy24' => 'Przelewy24', 'payment_type_Mollie Bank Transfer' => 'Virement bancaire Mollie', 'payment_type_KBC/CBC' => 'KBC/CBC', - 'payment_type_Instant Bank Pay' => 'Interac', + 'payment_type_Instant Bank Pay' => 'Instant Bank Pay', 'payment_type_Hosted Page' => 'Page hébergée', 'payment_type_GiroPay' => 'GiroPay', 'payment_type_EPS' => 'EPS', @@ -4279,7 +4279,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'show_pdf_preview_help' => 'Afficher l\'aperçu PDF lors de la rédaction des factures', 'print_pdf' => 'Imprimer le PDF', 'remind_me' => 'Rappel', - 'instant_bank_pay' => 'Interac', + 'instant_bank_pay' => 'Instant Bank Pay', 'click_selected' => 'Cliquer sur la sélection', 'hide_preview' => 'Cacher l\'aperçu', 'edit_record' => 'Modifier l\'enregistrement', @@ -5111,7 +5111,10 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'admin_initiated_payments_help' => 'Permet la saisie d\'un paiement dans le portal d\'administration sans facture', 'paid_date' => 'Date de paiement', 'downloaded_entities' => 'Un courriel sera envoyé avec le ou les PDF', - + 'lang_French - Swiss' => 'Français - Suisse', + 'currency_swazi_lilangeni' => 'Lilangeni Eswatinien', + 'income' => 'Revenu', + ); diff --git a/lang/ja/texts.php b/lang/ja/texts.php index a68c333c9d73..ba1fecbd22ff 100644 --- a/lang/ja/texts.php +++ b/lang/ja/texts.php @@ -753,7 +753,7 @@ $LANG = array( 'activity_7' => ':contact が :client の請求書 :invoice を閲覧しました', 'activity_8' => ':user は 請求書 :invoice をアーカイブしました。', 'activity_9' => ':user は 請求書 :invoice をアーカイブしました。', - 'activity_10' => ':contact は、:client の請求書 :invoice に :payment_amount の支払い :payment を入力しました', + 'activity_10' => ':user は、:client の請求書 :invoice に :payment_amount の支払い :payment を入力しました', 'activity_11' => ':user updated payment :payment', 'activity_12' => ':user archived payment :payment', 'activity_13' => ':user deleted payment :payment', diff --git a/lang/lt/texts.php b/lang/lt/texts.php index cc10c19bdfc3..bc2567e18b5e 100644 --- a/lang/lt/texts.php +++ b/lang/lt/texts.php @@ -754,7 +754,7 @@ $LANG = array( 'activity_7' => ':contact viewed invoice :invoice for :client', 'activity_8' => ':user archived invoice :invoice', 'activity_9' => ':user deleted invoice :invoice', - 'activity_10' => ':contact entered payment :payment for :payment_amount on invoice :invoice for :client', + 'activity_10' => ':user entered payment :payment for :payment_amount on invoice :invoice for :client', 'activity_11' => ':user atnaujino mokėjimą :payment', 'activity_12' => ':user archived payment :payment', 'activity_13' => ':user deleted payment :payment', diff --git a/lang/nb_NO/texts.php b/lang/nb_NO/texts.php index 5f0c308c69a9..c74cc5ae23c2 100644 --- a/lang/nb_NO/texts.php +++ b/lang/nb_NO/texts.php @@ -754,7 +754,7 @@ $LANG = array( 'activity_7' => ':contact har sett fakturaen :invoice for :client', 'activity_8' => ':user arkiverte faktura :invoice', 'activity_9' => ':user slettet faktura :invoice', - 'activity_10' => ':contact la inn betaling :payment på :payment_amount', + 'activity_10' => ':user la inn betaling :payment på :payment_amount', 'activity_11' => ':user oppdaterte betaling :payment', 'activity_12' => ':user arkiverte betaling :payment', 'activity_13' => ':user slettet betaling :payment', diff --git a/lang/sq/texts.php b/lang/sq/texts.php index df7d91d568eb..f706e13e1de3 100644 --- a/lang/sq/texts.php +++ b/lang/sq/texts.php @@ -751,7 +751,7 @@ $LANG = array( 'activity_7' => ':contact viewed invoice :invoice for :client', 'activity_8' => ':user ka arkivuar faturën :invoice', 'activity_9' => ':user ka fshirë faturën :invoice', - 'activity_10' => ':contact entered payment :payment for :payment_amount on invoice :invoice for :client', + 'activity_10' => ':user entered payment :payment for :payment_amount on invoice :invoice for :client', 'activity_11' => ':user ka perditesuar pagesën :payment', 'activity_12' => ':user ka arkivuar pagesën :payment', 'activity_13' => ':user ka fshirë pagesën :payment', diff --git a/lang/tr_TR/texts.php b/lang/tr_TR/texts.php index e85d8f2d30bb..aad64be4899c 100644 --- a/lang/tr_TR/texts.php +++ b/lang/tr_TR/texts.php @@ -753,7 +753,7 @@ adresine gönderildi. Müthiş tüm özelliklerin kilidini açmak için lütfen 'activity_7' => ':contact viewed invoice :invoice for :client', 'activity_8' => ':user :invoice nolu faturayı arşivledi', 'activity_9' => ':user :invoice nolu faturayı sildi', - 'activity_10' => ':contact entered payment :payment for :payment_amount on invoice :invoice for :client', + 'activity_10' => ':user entered payment :payment for :payment_amount on invoice :invoice for :client', 'activity_11' => ':user :payment tutarlı ödemeyi güncelledi', 'activity_12' => ':user :payment tutarlı ödemeyi arşivledi', 'activity_13' => ':user :payment tutarlı ödemeyi sildi', From 08c66fc545d6806ab4ce261b4f0fbf06233267ae Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 13 Jul 2023 12:43:54 +1000 Subject: [PATCH 05/22] Tune show/hide parameters for html invoices --- app/Http/Livewire/PdfSlot.php | 13 ++++++++++++- .../ninja2020/components/html-viewer.blade.php | 10 +++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/Http/Livewire/PdfSlot.php b/app/Http/Livewire/PdfSlot.php index 8eda7b1c4ded..b28379338550 100644 --- a/app/Http/Livewire/PdfSlot.php +++ b/app/Http/Livewire/PdfSlot.php @@ -48,6 +48,10 @@ class PdfSlot extends Component protected $listeners = ['viewportChanged' => 'getPdf']; + public $show_cost = true; + + public $show_quantity = true; + public function mount() { MultiDB::setDb($this->db); @@ -82,6 +86,14 @@ class PdfSlot extends Component $this->settings = $this->entity->client ? $this->entity->client->getMergedSettings() : $this->entity->company->settings; + $this->show_cost = in_array('$product.unit_cost', $this->settings->pdf_variables->product_columns); + $this->show_quantity = in_array('$product.quantity', $this->settings->pdf_variables->product_columns); + + if($this->entity_type == 'quote' && !$this->settings->sync_invoice_quote_columns ){ + $this->show_cost = in_array('$product.unit_cost', $this->settings->pdf_variables->product_quote_columns); + $this->show_quantity = in_array('$product.quantity', $this->settings->pdf_variables->product_quote_columns); + } + $this->html_variables = $this->entity->client ? (new HtmlEngine($this->invitation))->generateLabelsAndValues() : (new VendorHtmlEngine($this->invitation))->generateLabelsAndValues(); @@ -100,7 +112,6 @@ class PdfSlot extends Component 'entity_details' => $this->getEntityDetails(), 'user_details' => $this->getUserDetails(), 'user_name' => $this->getUserName(), - ]); } diff --git a/resources/views/portal/ninja2020/components/html-viewer.blade.php b/resources/views/portal/ninja2020/components/html-viewer.blade.php index 8b27738e3a54..b459ca03e435 100644 --- a/resources/views/portal/ninja2020/components/html-viewer.blade.php +++ b/resources/views/portal/ninja2020/components/html-viewer.blade.php @@ -77,7 +77,15 @@ span {
-

{{ $product['quantity'] }} × {{ $product['cost'] }}

+

+ @if($show_quantity) + {{ $product['quantity'] }} x + @endif + + @if($show_cost) + {{ $product['cost'] }} + @endif +

{{ $product['notes'] }}

From 4f8ba1c9307a237ed9dc41a0912eaacda381f3c6 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 13 Jul 2023 15:23:57 +1000 Subject: [PATCH 06/22] Additional Reports --- app/Export/CSV/BaseExport.php | 134 ++++++++++ app/Export/CSV/ClientExport.php | 2 +- app/Export/CSV/PurchaseOrderExport.php | 172 ++++++++++++ app/Export/CSV/PurchaseOrderItemExport.php | 246 ++++++++++++++++++ app/Export/CSV/VendorExport.php | 172 ++++++++++++ .../RecurringInvoiceReportController.php | 31 --- app/Models/Vendor.php | 44 ---- lang/en/texts.php | 1 + routes/api.php | 10 +- tests/Feature/Export/ClientCsvTest.php | 48 ++++ 10 files changed, 781 insertions(+), 79 deletions(-) create mode 100644 app/Export/CSV/PurchaseOrderExport.php create mode 100644 app/Export/CSV/PurchaseOrderItemExport.php create mode 100644 app/Export/CSV/VendorExport.php diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index d9731c49b680..e98b4bbeeeff 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -42,6 +42,37 @@ class BaseExport public array $forced_keys = []; + protected array $vendor_report_keys = [ + 'address1' => 'vendor.address1', + 'address2' => 'vendor.address2', + 'city' => 'vendor.city', + 'country' => 'vendor.country_id', + 'custom_value1' => 'vendor.custom_value1', + 'custom_value2' => 'vendor.custom_value2', + 'custom_value3' => 'vendor.custom_value3', + 'custom_value4' => 'vendor.custom_value4', + 'id_number' => 'vendor.id_number', + 'name' => 'vendor.name', + 'number' => 'vendor.number', + 'client_phone' => 'vendor.phone', + 'postal_code' => 'vendor.postal_code', + 'private_notes' => 'vendor.private_notes', + 'public_notes' => 'vendor.public_notes', + 'state' => 'vendor.state', + 'vat_number' => 'vendor.vat_number', + 'website' => 'vendor.website', + 'currency' => 'vendor.currency', + 'first_name' => 'vendor_contact.first_name', + 'last_name' => 'vendor_contact.last_name', + 'contact_phone' => 'vendor_contact.phone', + 'contact_custom_value1' => 'vendor_contact.custom_value1', + 'contact_custom_value2' => 'vendor_contact.custom_value2', + 'contact_custom_value3' => 'vendor_contact.custom_value3', + 'contact_custom_value4' => 'vendor_contact.custom_value4', + 'email' => 'vendor_contact.email', + 'status' => 'vendor.status', + ]; + protected array $client_report_keys = [ "name" => "client.name", "user" => "client.user_id", @@ -103,6 +134,42 @@ class BaseExport "user" => "invoice.user_id", ]; + protected array $purchase_order_report_keys = [ + 'amount' => 'purchase_order.amount', + 'balance' => 'purchase_order.balance', + 'vendor' => 'purchase_order.vendor_id', + // 'custom_surcharge1' => 'purchase_order.custom_surcharge1', + // 'custom_surcharge2' => 'purchase_order.custom_surcharge2', + // 'custom_surcharge3' => 'purchase_order.custom_surcharge3', + // 'custom_surcharge4' => 'purchase_order.custom_surcharge4', + 'custom_value1' => 'purchase_order.custom_value1', + 'custom_value2' => 'purchase_order.custom_value2', + 'custom_value3' => 'purchase_order.custom_value3', + 'custom_value4' => 'purchase_order.custom_value4', + 'date' => 'purchase_order.date', + 'discount' => 'purchase_order.discount', + 'due_date' => 'purchase_order.due_date', + 'exchange_rate' => 'purchase_order.exchange_rate', + 'footer' => 'purchase_order.footer', + 'number' => 'purchase_order.number', + 'paid_to_date' => 'purchase_order.paid_to_date', + 'partial' => 'purchase_order.partial', + 'partial_due_date' => 'purchase_order.partial_due_date', + 'po_number' => 'purchase_order.po_number', + 'private_notes' => 'purchase_order.private_notes', + 'public_notes' => 'purchase_order.public_notes', + 'status' => 'purchase_order.status_id', + 'tax_name1' => 'purchase_order.tax_name1', + 'tax_name2' => 'purchase_order.tax_name2', + 'tax_name3' => 'purchase_order.tax_name3', + 'tax_rate1' => 'purchase_order.tax_rate1', + 'tax_rate2' => 'purchase_order.tax_rate2', + 'tax_rate3' => 'purchase_order.tax_rate3', + 'terms' => 'purchase_order.terms', + 'total_taxes' => 'purchase_order.total_taxes', + 'currency_id' => 'purchase_order.currency_id', + ]; + protected array $item_report_keys = [ "quantity" => "item.quantity", "cost" => "item.cost", @@ -222,7 +289,10 @@ class BaseExport match($parts[0]) { 'contact' => $value = $this->resolveClientContactKey($parts[1], $entity, $transformer), 'client' => $value = $this->resolveClientKey($parts[1], $entity, $transformer), + 'vendor' => $value = $this->resolveVendorKey($parts[1], $entity, $transformer), + 'vendor_contact' => $value = $this->resolveVendorContactKey($parts[1], $entity, $transformer), 'invoice' => $value = $this->resolveInvoiceKey($parts[1], $entity, $transformer), + 'purchase_order' => $value = $this->resolvePurchaseOrderKey($parts[1], $entity, $transformer), 'payment' => $value = $this->resolvePaymentKey($parts[1], $entity, $transformer), default => $value = '' }; @@ -239,6 +309,51 @@ class BaseExport } + private function resolveVendorContactKey($column, $entity, $transformer) + { + + $primary_contact = $entity->vendor->primary_contact()->first() ?? $entity->vendor->contacts()->first(); + + return $primary_contact?->{$column} ?? ''; + + } + + private function resolveVendorKey($column, $entity, $transformer) + { + $transformed_entity = $transformer->includeVendor($entity); + + $manager = new Manager(); + $manager->setSerializer(new ArraySerializer()); + $transformed_entity = $manager->createData($transformed_entity)->toArray(); + + if($column == 'name') + return $transformed_entity['display_name']; + + if($column == 'user_id') + return $entity->vendor->user->present()->name(); + + if($column == 'country_id') + return $entity->vendor->country ? ctrans("texts.country_{$entity->vendor->country->name}") : ''; + + if ($column == 'currency_id') { + return $entity->vendor->currency() ? $entity->vendor->currency()->code : $entity->company->currency()->code; + } + + if($column == 'status') + return $entity->stringStatus($entity->status_id); + + + + if(array_key_exists($column, $transformed_entity)) + return $transformed_entity[$column]; + + nlog("export: Could not resolve vendor key: {$column}"); + + return ''; + + } + + private function resolveClientKey($column, $entity, $transformer) { $transformed_client = $transformer->includeClient($entity); @@ -282,6 +397,18 @@ class BaseExport } + private function resolvePurchaseOrderKey($column, $entity, $transformer) + { + nlog("searching for {$column}"); + + $transformed_entity = $transformer->transform($entity); + + if($column == 'status') + return $entity->stringStatus($entity->status_id); + + return ''; + } + private function resolveInvoiceKey($column, $entity, $transformer) { nlog("searching for {$column}"); @@ -495,6 +622,8 @@ class BaseExport public function buildHeader() :array { $header = []; + + nlog($this->input['report_keys']); foreach (array_merge($this->input['report_keys'], $this->forced_keys) as $value) { @@ -538,8 +667,13 @@ class BaseExport } $key = str_replace('item.', '', $key); + $key = str_replace('recurring_invoice.', '', $key); $key = str_replace('invoice.', '', $key); + $key = str_replace('quote.', '', $key); + $key = str_replace('credit.', '', $key); + $key = str_replace('task.', '', $key); $key = str_replace('client.', '', $key); + $key = str_replace('vendor.', '', $key); $key = str_replace('contact.', '', $key); $key = str_replace('payment.', '', $key); diff --git a/app/Export/CSV/ClientExport.php b/app/Export/CSV/ClientExport.php index 1f45c2b97569..c41dcc7a63df 100644 --- a/app/Export/CSV/ClientExport.php +++ b/app/Export/CSV/ClientExport.php @@ -185,7 +185,7 @@ class ClientExport extends BaseExport } if ($client->deleted_at) { - return ctrans('texts.arcvived'); + return ctrans('texts.archived'); } return ctrans('texts.active'); diff --git a/app/Export/CSV/PurchaseOrderExport.php b/app/Export/CSV/PurchaseOrderExport.php new file mode 100644 index 000000000000..0b4202ce5af0 --- /dev/null +++ b/app/Export/CSV/PurchaseOrderExport.php @@ -0,0 +1,172 @@ + 'amount', + 'balance' => 'balance', + 'vendor' => 'vendor_id', + 'custom_surcharge1' => 'custom_surcharge1', + 'custom_surcharge2' => 'custom_surcharge2', + 'custom_surcharge3' => 'custom_surcharge3', + 'custom_surcharge4' => 'custom_surcharge4', + 'custom_value1' => 'custom_value1', + 'custom_value2' => 'custom_value2', + 'custom_value3' => 'custom_value3', + 'custom_value4' => 'custom_value4', + 'date' => 'date', + 'discount' => 'discount', + 'due_date' => 'due_date', + 'exchange_rate' => 'exchange_rate', + 'footer' => 'footer', + 'number' => 'number', + 'paid_to_date' => 'paid_to_date', + 'partial' => 'partial', + 'partial_due_date' => 'partial_due_date', + 'po_number' => 'po_number', + 'private_notes' => 'private_notes', + 'public_notes' => 'public_notes', + 'status' => 'status_id', + 'tax_name1' => 'tax_name1', + 'tax_name2' => 'tax_name2', + 'tax_name3' => 'tax_name3', + 'tax_rate1' => 'tax_rate1', + 'tax_rate2' => 'tax_rate2', + 'tax_rate3' => 'tax_rate3', + 'terms' => 'terms', + 'total_taxes' => 'total_taxes', + 'currency_id' => 'currency_id', + ]; + + private array $decorate_keys = [ + 'country', + 'currency_id', + 'status', + 'vendor', + 'project', + ]; + + + public function __construct(Company $company, array $input) + { + $this->company = $company; + $this->input = $input; + $this->purchase_order_transformer = new PurchaseOrderTransformer(); + } + + public function run() + { + MultiDB::setDb($this->company->db); + App::forgetInstance('translator'); + App::setLocale($this->company->locale()); + $t = app('translator'); + $t->replace(Ninja::transformTranslations($this->company->settings)); + + //load the CSV document from a string + $this->csv = Writer::createFromString(); + + if (count($this->input['report_keys']) == 0) { + $this->input['report_keys'] = array_values($this->entity_keys); + } + + //insert the header + $this->csv->insertOne($this->buildHeader()); + + $query = PurchaseOrder::query() + ->withTrashed() + ->with('vendor') + ->where('company_id', $this->company->id) + ->where('is_deleted', 0); + + $query = $this->addDateRange($query); + + // if(isset($this->input['status'])) { + // $query = $this->addPurchaseOrderStatusFilter($query, $this->input['status']); + // } + + $query->cursor() + ->each(function ($purchase_order) { + $this->csv->insertOne($this->buildRow($purchase_order)); + }); + + return $this->csv->toString(); + } + + private function buildRow(PurchaseOrder $purchase_order) :array + { + $transformed_purchase_order = $this->purchase_order_transformer->transform($purchase_order); + + $entity = []; + + foreach (array_values($this->input['report_keys']) as $key) { + $keyval = array_search($key, $this->entity_keys); + + if(!$keyval) { + $keyval = array_search(str_replace("invoice.", "", $key), $this->entity_keys) ?? $key; + } + + if(!$keyval) { + $keyval = $key; + } + + if (array_key_exists($key, $transformed_purchase_order)) { + $entity[$keyval] = $transformed_purchase_order[$key]; + } elseif (array_key_exists($keyval, $transformed_purchase_order)) { + $entity[$keyval] = $transformed_purchase_order[$keyval]; + } else { + $entity[$keyval] = $this->resolveKey($keyval, $purchase_order, $this->purchase_order_transformer); + } + } + + return $this->decorateAdvancedFields($purchase_order, $entity); + } + + private function decorateAdvancedFields(PurchaseOrder $purchase_order, array $entity) :array + { + if (in_array('country_id', $this->input['report_keys'])) { + $entity['country'] = $purchase_order->vendor->country ? ctrans("texts.country_{$purchase_order->vendor->country->name}") : ''; + } + + if (in_array('currency_id', $this->input['report_keys'])) { + $entity['currency_id'] = $purchase_order->vendor->currency() ? $purchase_order->vendor->currency()->code : $purchase_order->company->currency()->code; + } + + if (in_array('vendor_id', $this->input['report_keys'])) { + $entity['vendor'] = $purchase_order->vendor->present()->name(); + } + + if (in_array('status_id', $this->input['report_keys'])) { + $entity['status'] = $purchase_order->stringStatus($purchase_order->status_id); + } + + return $entity; + } +} diff --git a/app/Export/CSV/PurchaseOrderItemExport.php b/app/Export/CSV/PurchaseOrderItemExport.php new file mode 100644 index 000000000000..4847a162f9c0 --- /dev/null +++ b/app/Export/CSV/PurchaseOrderItemExport.php @@ -0,0 +1,246 @@ + 'amount', + 'balance' => 'balance', + 'vendor' => 'vendor_id', + 'vendor_number' => 'vendor.number', + 'vendor_id_number' => 'vendor.id_number', + 'custom_surcharge1' => 'custom_surcharge1', + 'custom_surcharge2' => 'custom_surcharge2', + 'custom_surcharge3' => 'custom_surcharge3', + 'custom_surcharge4' => 'custom_surcharge4', + // 'custom_value1' => 'custom_value1', + // 'custom_value2' => 'custom_value2', + // 'custom_value3' => 'custom_value3', + // 'custom_value4' => 'custom_value4', + 'date' => 'date', + 'discount' => 'discount', + 'due_date' => 'due_date', + 'exchange_rate' => 'exchange_rate', + 'footer' => 'footer', + 'number' => 'number', + 'paid_to_date' => 'paid_to_date', + 'partial' => 'partial', + 'partial_due_date' => 'partial_due_date', + 'po_number' => 'po_number', + 'private_notes' => 'private_notes', + 'public_notes' => 'public_notes', + 'status' => 'status_id', + 'tax_name1' => 'tax_name1', + 'tax_name2' => 'tax_name2', + 'tax_name3' => 'tax_name3', + 'tax_rate1' => 'tax_rate1', + 'tax_rate2' => 'tax_rate2', + 'tax_rate3' => 'tax_rate3', + 'terms' => 'terms', + 'total_taxes' => 'total_taxes', + 'currency' => 'currency_id', + 'quantity' => 'item.quantity', + 'cost' => 'item.cost', + 'product_key' => 'item.product_key', + 'buy_price' => 'item.product_cost', + 'notes' => 'item.notes', + 'discount' => 'item.discount', + 'is_amount_discount' => 'item.is_amount_discount', + 'tax_rate1' => 'item.tax_rate1', + 'tax_rate2' => 'item.tax_rate2', + 'tax_rate3' => 'item.tax_rate3', + 'tax_name1' => 'item.tax_name1', + 'tax_name2' => 'item.tax_name2', + 'tax_name3' => 'item.tax_name3', + 'line_total' => 'item.line_total', + 'gross_line_total' => 'item.gross_line_total', + // 'invoice1' => 'item.custom_value1', + // 'invoice2' => 'item.custom_value2', + // 'invoice3' => 'item.custom_value3', + // 'invoice4' => 'item.custom_value4', + 'tax_category' => 'item.tax_id', + 'type' => 'item.type_id', + ]; + + private array $decorate_keys = [ + 'client', + 'currency_id', + 'status' + ]; + + public function __construct(Company $company, array $input) + { + $this->company = $company; + $this->input = $input; + $this->purchase_order_transformer = new PurchaseOrderTransformer(); + } + + public function run() + { + MultiDB::setDb($this->company->db); + App::forgetInstance('translator'); + App::setLocale($this->company->locale()); + $t = app('translator'); + $t->replace(Ninja::transformTranslations($this->company->settings)); + + //load the CSV document from a string + $this->csv = Writer::createFromString(); + + if (count($this->input['report_keys']) == 0) { + $this->force_keys = true; + $this->input['report_keys'] = array_values($this->entity_keys); + } + + //insert the header + $this->csv->insertOne($this->buildHeader()); + + $query = PurchaseOrder::query() + ->withTrashed() + ->with('vendor')->where('company_id', $this->company->id) + ->where('is_deleted', 0); + + $query = $this->addDateRange($query); + + $query->cursor() + ->each(function ($purchase_order) { + $this->iterateItems($purchase_order); + }); + + return $this->csv->toString(); + } + + private function iterateItems(PurchaseOrder $purchase_order) + { + $transformed_invoice = $this->buildRow($purchase_order); + + $transformed_items = []; + + foreach ($purchase_order->line_items as $item) { + $item_array = []; + + foreach (array_values($this->input['report_keys']) as $key) { //items iterator produces item array + + if (str_contains($key, "item.")) { + + $key = str_replace("item.", "", $key); + + $keyval = $key; + + $keyval = str_replace("custom_value", "invoice", $key); + + if($key == 'type_id') { + $keyval = 'type'; + } + + if($key == 'tax_id') { + $keyval = 'tax_category'; + } + + if (property_exists($item, $key)) { + $item_array[$keyval] = $item->{$key}; + } else { + $item_array[$keyval] = ''; + } + } + } + + $entity = []; + + foreach (array_values($this->input['report_keys']) as $key) { //create an array of report keys only + $keyval = array_search($key, $this->entity_keys); + + if (array_key_exists($key, $transformed_items)) { + $entity[$keyval] = $transformed_items[$key]; + } else { + $entity[$keyval] = ""; + } + } + + $transformed_items = array_merge($transformed_invoice, $item_array); + $entity = $this->decorateAdvancedFields($purchase_order, $transformed_items); + + $this->csv->insertOne($entity); + } + } + + private function buildRow(PurchaseOrder $purchase_order) :array + { + $transformed_invoice = $this->purchase_order_transformer->transform($purchase_order); + + $entity = []; + + foreach (array_values($this->input['report_keys']) as $key) { + $keyval = array_search($key, $this->entity_keys); + + if(!$keyval) { + $keyval = array_search(str_replace("invoice.", "", $key), $this->entity_keys) ?? $key; + } + + if(!$keyval) { + $keyval = $key; + } + + if (array_key_exists($key, $transformed_invoice)) { + $entity[$keyval] = $transformed_invoice[$key]; + } elseif (array_key_exists($keyval, $transformed_invoice)) { + $entity[$keyval] = $transformed_invoice[$keyval]; + } else { + $entity[$keyval] = $this->resolveKey($keyval, $purchase_order, $this->purchase_order_transformer); + } + } + + return $this->decorateAdvancedFields($purchase_order, $entity); + } + + private function decorateAdvancedFields(PurchaseOrder $purchase_order, array $entity) :array + { + if (in_array('currency_id', $this->input['report_keys'])) { + $entity['currency'] = $purchase_order->vendor->currency() ? $purchase_order->vendor->currency()->code : $purchase_order->company->currency()->code; + } + + if(array_key_exists('type', $entity)) { + $entity['type'] = $purchase_order->typeIdString($entity['type']); + } + + if(array_key_exists('tax_category', $entity)) { + $entity['tax_category'] = $purchase_order->taxTypeString($entity['tax_category']); + } + + if($this->force_keys) { + $entity['vendor'] = $purchase_order->vendor->present()->name(); + $entity['vendor_id_number'] = $purchase_order->vendor->id_number; + $entity['vendor_number'] = $purchase_order->vendor->number; + $entity['status'] = $purchase_order->stringStatus($purchase_order->status_id); + } + + return $entity; + } +} diff --git a/app/Export/CSV/VendorExport.php b/app/Export/CSV/VendorExport.php new file mode 100644 index 000000000000..872b818ef762 --- /dev/null +++ b/app/Export/CSV/VendorExport.php @@ -0,0 +1,172 @@ + 'vendor.address1', + 'address2' => 'vendor.address2', + 'city' => 'vendor.city', + 'country' => 'vendor.country_id', + 'custom_value1' => 'vendor.custom_value1', + 'custom_value2' => 'vendor.custom_value2', + 'custom_value3' => 'vendor.custom_value3', + 'custom_value4' => 'vendor.custom_value4', + 'id_number' => 'vendor.id_number', + 'name' => 'vendor.name', + 'number' => 'vendor.number', + 'client_phone' => 'vendor.phone', + 'postal_code' => 'vendor.postal_code', + 'private_notes' => 'vendor.private_notes', + 'public_notes' => 'vendor.public_notes', + 'state' => 'vendor.state', + 'vat_number' => 'vendor.vat_number', + 'website' => 'vendor.website', + 'currency' => 'vendor.currency', + 'first_name' => 'vendor_contact.first_name', + 'last_name' => 'vendor_contact.last_name', + 'contact_phone' => 'vendor_contact.phone', + 'contact_custom_value1' => 'vendor_contact.custom_value1', + 'contact_custom_value2' => 'vendor_contact.custom_value2', + 'contact_custom_value3' => 'vendor_contact.custom_value3', + 'contact_custom_value4' => 'vendor_contact.custom_value4', + 'email' => 'vendor_contact.email', + 'status' => 'vendor.status' + ]; + + private array $decorate_keys = [ + 'vendor.country_id', + 'vendor.currency', + ]; + + public array $forced_keys = [ + // 'vendor.status' + ]; + + public function __construct(Company $company, array $input) + { + $this->company = $company; + $this->input = $input; + $this->vendor_transformer = new VendorTransformer(); + $this->contact_transformer = new VendorContactTransformer(); + } + + public function run() + { + MultiDB::setDb($this->company->db); + App::forgetInstance('translator'); + App::setLocale($this->company->locale()); + $t = app('translator'); + $t->replace(Ninja::transformTranslations($this->company->settings)); + + //load the CSV document from a string + $this->csv = Writer::createFromString(); + + if (count($this->input['report_keys']) == 0) { + $this->input['report_keys'] = array_values($this->entity_keys); + } + + //insert the header + $this->csv->insertOne($this->buildHeader()); + + $query = Vendor::query()->with('contacts') + ->withTrashed() + ->where('company_id', $this->company->id) + ->where('is_deleted', 0); + + $query = $this->addDateRange($query); + + $query->cursor() + ->each(function ($vendor) { + $this->csv->insertOne($this->buildRow($vendor)); + }); + + return $this->csv->toString(); + } + + private function buildRow(Vendor $vendor) :array + { + $transformed_contact = false; + + $transformed_vendor = $this->vendor_transformer->transform($vendor); + + if ($contact = $vendor->contacts()->first()) { + $transformed_contact = $this->contact_transformer->transform($contact); + } + + $entity = []; + + foreach (array_values($this->input['report_keys']) as $key) { + $parts = explode('.', $key); + + $keyval = array_search($key, $this->entity_keys); + + if (is_array($parts) && $parts[0] == 'vendor' && array_key_exists($parts[1], $transformed_vendor)) { + $entity[$keyval] = $transformed_vendor[$parts[1]]; + } elseif (is_array($parts) && $parts[0] == 'vendor_contact' && array_key_exists($parts[1], $transformed_contact)) { + $entity[$keyval] = $transformed_contact[$parts[1]]; + } else { + $entity[$keyval] = ''; + } + } + + return $this->decorateAdvancedFields($vendor, $entity); + } + + private function decorateAdvancedFields(Vendor $vendor, array $entity) :array + { + if (in_array('vendor.country_id', $this->input['report_keys'])) { + $entity['country'] = $vendor->country ? ctrans("texts.country_{$vendor->country->name}") : ''; + } + + if (in_array('vendor.currency', $this->input['report_keys'])) { + $entity['currency'] = $vendor->currency() ? $vendor->currency()->code : $vendor->company->currency()->code; + } + + $entity['status'] = $this->calculateStatus($vendor); + + return $entity; + } + + private function calculateStatus($vendor) + { + if ($vendor->is_deleted) { + return ctrans('texts.deleted'); + } + + if ($vendor->deleted_at) { + return ctrans('texts.archived'); + } + + return ctrans('texts.active'); + } +} diff --git a/app/Http/Controllers/Reports/RecurringInvoiceReportController.php b/app/Http/Controllers/Reports/RecurringInvoiceReportController.php index 01d9a0f1f72a..eaae55bd1351 100644 --- a/app/Http/Controllers/Reports/RecurringInvoiceReportController.php +++ b/app/Http/Controllers/Reports/RecurringInvoiceReportController.php @@ -29,37 +29,6 @@ class RecurringInvoiceReportController extends BaseController parent::__construct(); } - /** - * @OA\Post( - * path="/api/v1/reports/recurring_invoices", - * operationId="getRecurringInvoiceReport", - * tags={"reports"}, - * summary="Recurring Invoice reports", - * description="Export recurring invoice reports", - * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), - * @OA\RequestBody( - * required=true, - * @OA\JsonContent(ref="#/components/schemas/GenericReportSchema") - * ), - * @OA\Response( - * response=200, - * description="success", - * @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 __invoke(GenericReportRequest $request) { if ($request->has('send_email') && $request->get('send_email')) { diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php index e7eeae12c41a..84ca29d6f31d 100644 --- a/app/Models/Vendor.php +++ b/app/Models/Vendor.php @@ -108,50 +108,6 @@ use Laracasts\Presenter\PresentableTrait; * @property-read \Illuminate\Database\Eloquent\Collection $contacts * @property-read \Illuminate\Database\Eloquent\Collection $documents * @property-read \Illuminate\Database\Eloquent\Collection $primary_contact - * @property-read \Illuminate\Database\Eloquent\Collection $activities - * @property-read \Illuminate\Database\Eloquent\Collection $contacts - * @property-read \Illuminate\Database\Eloquent\Collection $documents - * @property-read \Illuminate\Database\Eloquent\Collection $primary_contact - * @property-read \Illuminate\Database\Eloquent\Collection $activities - * @property-read \Illuminate\Database\Eloquent\Collection $contacts - * @property-read \Illuminate\Database\Eloquent\Collection $documents - * @property-read \Illuminate\Database\Eloquent\Collection $primary_contact - * @property-read \Illuminate\Database\Eloquent\Collection $activities - * @property-read \Illuminate\Database\Eloquent\Collection $contacts - * @property-read \Illuminate\Database\Eloquent\Collection $documents - * @property-read \Illuminate\Database\Eloquent\Collection $primary_contact - * @property-read \Illuminate\Database\Eloquent\Collection $activities - * @property-read \Illuminate\Database\Eloquent\Collection $contacts - * @property-read \Illuminate\Database\Eloquent\Collection $documents - * @property-read \Illuminate\Database\Eloquent\Collection $primary_contact - * @property-read \Illuminate\Database\Eloquent\Collection $activities - * @property-read \Illuminate\Database\Eloquent\Collection $contacts - * @property-read \Illuminate\Database\Eloquent\Collection $documents - * @property-read \Illuminate\Database\Eloquent\Collection $primary_contact - * @property-read \Illuminate\Database\Eloquent\Collection $activities - * @property-read \Illuminate\Database\Eloquent\Collection $contacts - * @property-read \Illuminate\Database\Eloquent\Collection $documents - * @property-read \Illuminate\Database\Eloquent\Collection $primary_contact - * @property-read \Illuminate\Database\Eloquent\Collection $activities - * @property-read \Illuminate\Database\Eloquent\Collection $contacts - * @property-read \Illuminate\Database\Eloquent\Collection $documents - * @property-read \Illuminate\Database\Eloquent\Collection $primary_contact - * @property-read \Illuminate\Database\Eloquent\Collection $activities - * @property-read \Illuminate\Database\Eloquent\Collection $contacts - * @property-read \Illuminate\Database\Eloquent\Collection $documents - * @property-read \Illuminate\Database\Eloquent\Collection $primary_contact - * @property-read \Illuminate\Database\Eloquent\Collection $activities - * @property-read \Illuminate\Database\Eloquent\Collection $contacts - * @property-read \Illuminate\Database\Eloquent\Collection $documents - * @property-read \Illuminate\Database\Eloquent\Collection $primary_contact - * @property-read \Illuminate\Database\Eloquent\Collection $activities - * @property-read \Illuminate\Database\Eloquent\Collection $contacts - * @property-read \Illuminate\Database\Eloquent\Collection $documents - * @property-read \Illuminate\Database\Eloquent\Collection $primary_contact - * @property-read \Illuminate\Database\Eloquent\Collection $activities - * @property-read \Illuminate\Database\Eloquent\Collection $contacts - * @property-read \Illuminate\Database\Eloquent\Collection $documents - * @property-read \Illuminate\Database\Eloquent\Collection $primary_contact * @mixin \Eloquent */ class Vendor extends BaseModel diff --git a/lang/en/texts.php b/lang/en/texts.php index d17b5e0ef1fb..de8f17ea971e 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5123,6 +5123,7 @@ $LANG = array( 'currency_swazi_lilangeni' => 'Swazi Lilangeni', 'income' => 'Income', 'amount_received_help' => 'Enter a value here if the total amount received was MORE than the invoice amount, or when recording a payment with no invoices. Otherwise this field should be left blank.', + 'vendor_phone' => 'Vendor Phone', ); diff --git a/routes/api.php b/routes/api.php index b2ef6d3d5a15..97c8550b3417 100644 --- a/routes/api.php +++ b/routes/api.php @@ -75,6 +75,7 @@ use App\Http\Controllers\HostedMigrationController; use App\Http\Controllers\ConnectedAccountController; use App\Http\Controllers\RecurringExpenseController; use App\Http\Controllers\RecurringInvoiceController; +use App\Http\Controllers\ProtectedDownloadController; use App\Http\Controllers\ClientGatewayTokenController; use App\Http\Controllers\Reports\TaskReportController; use App\Http\Controllers\Auth\ForgotPasswordController; @@ -85,6 +86,7 @@ use App\Http\Controllers\Auth\PasswordTimeoutController; use App\Http\Controllers\PreviewPurchaseOrderController; use App\Http\Controllers\Reports\ClientReportController; use App\Http\Controllers\Reports\CreditReportController; +use App\Http\Controllers\Reports\VendorReportController; use App\Http\Controllers\Reports\ExpenseReportController; use App\Http\Controllers\Reports\InvoiceReportController; use App\Http\Controllers\Reports\PaymentReportController; @@ -101,11 +103,12 @@ use App\Http\Controllers\Support\Messages\SendingController; use App\Http\Controllers\Reports\ClientSalesReportController; use App\Http\Controllers\Reports\InvoiceItemReportController; use App\Http\Controllers\PaymentNotificationWebhookController; -use App\Http\Controllers\ProtectedDownloadController; use App\Http\Controllers\Reports\ProductSalesReportController; use App\Http\Controllers\Reports\ClientBalanceReportController; use App\Http\Controllers\Reports\ClientContactReportController; +use App\Http\Controllers\Reports\PurchaseOrderReportController; use App\Http\Controllers\Reports\RecurringInvoiceReportController; +use App\Http\Controllers\Reports\PurchaseOrderItemReportController; Route::group(['middleware' => ['throttle:api', 'api_secret_check']], function () { Route::post('api/v1/signup', [AccountController::class, 'store'])->name('signup.submit'); @@ -272,7 +275,6 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'] Route::post('recurring_expenses/bulk', [RecurringExpenseController::class, 'bulk'])->name('recurring_expenses.bulk'); Route::put('recurring_expenses/{recurring_expense}/upload', [RecurringExpenseController::class, 'upload']); - Route::resource('recurring_invoices', RecurringInvoiceController::class); // name = (recurring_invoices. index / create / show / update / destroy / edit Route::post('recurring_invoices/bulk', [RecurringInvoiceController::class, 'bulk'])->name('recurring_invoices.bulk'); Route::put('recurring_invoices/{recurring_invoice}/upload', [RecurringInvoiceController::class, 'upload']); @@ -291,6 +293,8 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'] Route::post('reports/expenses', ExpenseReportController::class)->middleware('throttle:20,1'); Route::post('reports/invoices', InvoiceReportController::class)->middleware('throttle:20,1'); Route::post('reports/invoice_items', InvoiceItemReportController::class)->middleware('throttle:20,1'); + Route::post('reports/purchase_orders', PurchaseOrderReportController::class)->middleware('throttle:20,1'); + Route::post('reports/purchase_order_items', PurchaseOrderItemReportController::class)->middleware('throttle:20,1'); Route::post('reports/quotes', QuoteReportController::class)->middleware('throttle:20,1'); Route::post('reports/quote_items', QuoteItemReportController::class)->middleware('throttle:20,1'); Route::post('reports/recurring_invoices', RecurringInvoiceReportController::class)->middleware('throttle:20,1'); @@ -298,7 +302,7 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'] Route::post('reports/products', ProductReportController::class)->middleware('throttle:20,1'); Route::post('reports/product_sales', ProductSalesReportController::class)->middleware('throttle:20,1'); Route::post('reports/tasks', TaskReportController::class)->middleware('throttle:20,1'); - + Route::post('reports/vendors', VendorReportController::class)->middleware('throttle:20,1'); Route::post('reports/profitloss', ProfitAndLossController::class); Route::post('reports/ar_detail_report', ARDetailReportController::class); Route::post('reports/ar_summary_report', ARSummaryReportController::class); diff --git a/tests/Feature/Export/ClientCsvTest.php b/tests/Feature/Export/ClientCsvTest.php index 5e3ef755a660..5361b946b532 100644 --- a/tests/Feature/Export/ClientCsvTest.php +++ b/tests/Feature/Export/ClientCsvTest.php @@ -37,6 +37,54 @@ class ClientCsvTest extends TestCase $this->withoutExceptionHandling(); } + public function testRecurringInvoiceExportCsv() + { + $data = [ + 'date_range' => 'this_year', + 'report_keys' => [], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/recurring_invoices', $data); + + $response->assertStatus(200); + } + + public function testVendorExportCsv() + { + $data = [ + 'date_range' => 'this_year', + 'report_keys' => [], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/vendors', $data); + + $response->assertStatus(200); + } + + public function testPurchaseOrderExportCsv() + { + $data = [ + 'date_range' => 'this_year', + 'report_keys' => [], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/purchase_orders', $data); + + $response->assertStatus(200); + } + public function testClientExportCsv() { $data = [ From df4620ac62153b7bb6f1f30541002736adfbb8dd Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 13 Jul 2023 21:10:54 +1000 Subject: [PATCH 07/22] Report extension --- app/Export/CSV/BaseExport.php | 130 +++++++++++++++++++++++++++++-- app/Export/CSV/ExpenseExport.php | 110 +++++++++++++++----------- app/Models/Expense.php | 15 ++++ 3 files changed, 200 insertions(+), 55 deletions(-) diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index e98b4bbeeeff..2398e64a6fc1 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -264,6 +264,58 @@ class BaseExport "assigned_user" => "payment.assigned_user_id", ]; + protected array $expense_report_keys = [ + 'amount' => 'expense.amount', + 'category' => 'expense.category_id', + 'client' => 'expense.client_id', + 'custom_value1' => 'expense.custom_value1', + 'custom_value2' => 'expense.custom_value2', + 'custom_value3' => 'expense.custom_value3', + 'custom_value4' => 'expense.custom_value4', + 'currency' => 'expense.currency_id', + 'date' => 'expense.date', + 'exchange_rate' => 'expense.exchange_rate', + 'converted_amount' => 'expense.foreign_amount', + 'invoice_currency_id' => 'expense.invoice_currency_id', + 'payment_date' => 'expense.payment_date', + 'number' => 'expense.number', + 'payment_type_id' => 'expense.payment_type_id', + 'private_notes' => 'expense.private_notes', + 'project' => 'expense.project_id', + 'public_notes' => 'expense.public_notes', + 'tax_amount1' => 'expense.tax_amount1', + 'tax_amount2' => 'expense.tax_amount2', + 'tax_amount3' => 'expense.tax_amount3', + 'tax_name1' => 'expense.tax_name1', + 'tax_name2' => 'expense.tax_name2', + 'tax_name3' => 'expense.tax_name3', + 'tax_rate1' => 'expense.tax_rate1', + 'tax_rate2' => 'expense.tax_rate2', + 'tax_rate3' => 'expense.tax_rate3', + 'transaction_reference' => 'expense.transaction_reference', + 'vendor' => 'expense.vendor_id', + 'invoice' => 'expense.invoice_id', + 'user' => 'expense.user', + 'assigned_user' => 'expense.assigned_user', + ]; + + protected array $task_report_keys = [ + 'start_date' => 'task.start_date', + 'end_date' => 'task.end_date', + 'duration' => 'task.duration', + 'rate' => 'task.rate', + 'number' => 'task.number', + 'description' => 'task.description', + 'custom_value1' => 'task.custom_value1', + 'custom_value2' => 'task.custom_value2', + 'custom_value3' => 'task.custom_value3', + 'custom_value4' => 'task.custom_value4', + 'status' => 'task.status_id', + 'project' => 'task.project_id', + 'invoice' => 'task.invoice_id', + 'client' => 'task.client_id', + ]; + protected function filterByClients($query) { if (isset($this->input['client_id']) && $this->input['client_id'] != 'all') { @@ -289,6 +341,7 @@ class BaseExport match($parts[0]) { 'contact' => $value = $this->resolveClientContactKey($parts[1], $entity, $transformer), 'client' => $value = $this->resolveClientKey($parts[1], $entity, $transformer), + 'expense' => $value = $this->resolveExpenseKey($parts[1], $entity, $transformer), 'vendor' => $value = $this->resolveVendorKey($parts[1], $entity, $transformer), 'vendor_contact' => $value = $this->resolveVendorContactKey($parts[1], $entity, $transformer), 'invoice' => $value = $this->resolveInvoiceKey($parts[1], $entity, $transformer), @@ -302,24 +355,62 @@ class BaseExport private function resolveClientContactKey($column, $entity, $transformer) { + + if(!$entity->client) { + return ""; + } $primary_contact = $entity->client->primary_contact()->first() ?? $entity->client->contacts()->first(); - return $primary_contact?->{$column} ?? ''; + return $primary_contact ? $primary_contact?->{$column} ?? '' : ''; } private function resolveVendorContactKey($column, $entity, $transformer) { + if(!$entity->vendor) + return ""; $primary_contact = $entity->vendor->primary_contact()->first() ?? $entity->vendor->contacts()->first(); - return $primary_contact?->{$column} ?? ''; + return $primary_contact ? $primary_contact?->{$column} ?? '' : ''; + + } + + + private function resolveExpenseKey($column, $entity, $transformer) + { + // $transformed_entity = $transformer->includeExpense($entity); + + // $manager = new Manager(); + // $manager->setSerializer(new ArraySerializer()); + // $transformed_entity = $manager->createData($transformed_entity)->toArray(); + + if($column == 'user' && $entity?->expense?->user) + return $entity->expense->user->present()->name() ?? ' '; + + if($column == 'assigned_user' && $entity?->expense?->assigned_user) + return $entity->expense->assigned_user->present()->name() ?? ' '; + + if($column == 'category' && $entity->expense) { + return $entity->expense->category?->name ?? ' '; + } + + if(property_exists($entity, $column)) + return $entity?->{$column} ?? ''; + + nlog("export: Could not resolve expense key: {$column}"); + + return ''; } private function resolveVendorKey($column, $entity, $transformer) { + + if(!$entity->vendor) + return ''; + $transformed_entity = $transformer->includeVendor($entity); $manager = new Manager(); @@ -327,10 +418,10 @@ class BaseExport $transformed_entity = $manager->createData($transformed_entity)->toArray(); if($column == 'name') - return $transformed_entity['display_name']; + return $entity->vendor->present()->name() ?: ''; if($column == 'user_id') - return $entity->vendor->user->present()->name(); + return $entity->vendor->user->present()->name() ?: ''; if($column == 'country_id') return $entity->vendor->country ? ctrans("texts.country_{$entity->vendor->country->name}") : ''; @@ -340,9 +431,7 @@ class BaseExport } if($column == 'status') - return $entity->stringStatus($entity->status_id); - - + return $entity->stringStatus($entity->status_id) ?: ''; if(array_key_exists($column, $transformed_entity)) return $transformed_entity[$column]; @@ -356,6 +445,10 @@ class BaseExport private function resolveClientKey($column, $entity, $transformer) { + + if(!$entity->client) + return ''; + $transformed_client = $transformer->includeClient($entity); $manager = new Manager(); @@ -622,7 +715,7 @@ class BaseExport public function buildHeader() :array { $header = []; - + nlog($this->input['report_keys']); foreach (array_merge($this->input['report_keys'], $this->forced_keys) as $value) { @@ -662,6 +755,26 @@ class BaseExport $key = array_search($value, $this->item_report_keys); } + if(!$key) { + $prefix = ctrans('texts.expense'); + $key = array_search($value, $this->expense_report_keys); + } + + if(!$key) { + $prefix = ctrans('texts.task'); + $key = array_search($value, $this->task_report_keys); + } + + if(!$key) { + $prefix = ctrans('texts.vendor'); + $key = array_search($value, $this->vendor_report_keys); + } + + if(!$key) { + $prefix = ctrans('texts.purchase_order'); + $key = array_search($value, $this->purchase_order_report_keys); + } + if(!$key) { $prefix = ''; } @@ -676,6 +789,7 @@ class BaseExport $key = str_replace('vendor.', '', $key); $key = str_replace('contact.', '', $key); $key = str_replace('payment.', '', $key); + $key = str_replace('expense.', '', $key); $header[] = "{$prefix} " . ctrans("texts.{$key}"); } diff --git a/app/Export/CSV/ExpenseExport.php b/app/Export/CSV/ExpenseExport.php index a1d9411ea5af..4f0cb2a799f2 100644 --- a/app/Export/CSV/ExpenseExport.php +++ b/app/Export/CSV/ExpenseExport.php @@ -30,36 +30,38 @@ class ExpenseExport extends BaseExport public Writer $csv; public array $entity_keys = [ - 'amount' => 'amount', - 'category' => 'category_id', - 'client' => 'client_id', - 'custom_value1' => 'custom_value1', - 'custom_value2' => 'custom_value2', - 'custom_value3' => 'custom_value3', - 'custom_value4' => 'custom_value4', - 'currency' => 'currency_id', - 'date' => 'date', - 'exchange_rate' => 'exchange_rate', - 'converted_amount' => 'foreign_amount', - 'invoice_currency_id' => 'invoice_currency_id', - 'payment_date' => 'payment_date', - 'number' => 'number', - 'payment_type_id' => 'payment_type_id', - 'private_notes' => 'private_notes', - 'project' => 'project_id', - 'public_notes' => 'public_notes', - 'tax_amount1' => 'tax_amount1', - 'tax_amount2' => 'tax_amount2', - 'tax_amount3' => 'tax_amount3', - 'tax_name1' => 'tax_name1', - 'tax_name2' => 'tax_name2', - 'tax_name3' => 'tax_name3', - 'tax_rate1' => 'tax_rate1', - 'tax_rate2' => 'tax_rate2', - 'tax_rate3' => 'tax_rate3', - 'transaction_reference' => 'transaction_reference', - 'vendor' => 'vendor_id', - 'invoice' => 'invoice_id', + 'amount' => 'expense.amount', + 'category' => 'expense.category', + 'client' => 'expense.client_id', + 'custom_value1' => 'expense.custom_value1', + 'custom_value2' => 'expense.custom_value2', + 'custom_value3' => 'expense.custom_value3', + 'custom_value4' => 'expense.custom_value4', + 'currency' => 'expense.currency_id', + 'date' => 'expense.date', + 'exchange_rate' => 'expense.exchange_rate', + 'converted_amount' => 'expense.foreign_amount', + 'invoice_currency_id' => 'expense.invoice_currency_id', + 'payment_date' => 'expense.payment_date', + 'number' => 'expense.number', + 'payment_type_id' => 'expense.payment_type_id', + 'private_notes' => 'expense.private_notes', + 'project' => 'expense.project_id', + 'public_notes' => 'expense.public_notes', + 'tax_amount1' => 'expense.tax_amount1', + 'tax_amount2' => 'expense.tax_amount2', + 'tax_amount3' => 'expense.tax_amount3', + 'tax_name1' => 'expense.tax_name1', + 'tax_name2' => 'expense.tax_name2', + 'tax_name3' => 'expense.tax_name3', + 'tax_rate1' => 'expense.tax_rate1', + 'tax_rate2' => 'expense.tax_rate2', + 'tax_rate3' => 'expense.tax_rate3', + 'transaction_reference' => 'expense.transaction_reference', + 'vendor' => 'expense.vendor_id', + 'invoice' => 'expense.invoice_id', + 'user' => 'expense.user', + 'assigned_user' => 'expense.assigned_user', ]; private array $decorate_keys = [ @@ -120,13 +122,17 @@ class ExpenseExport extends BaseExport $entity = []; foreach (array_values($this->input['report_keys']) as $key) { + $parts = explode('.', $key); $keyval = array_search($key, $this->entity_keys); - if (array_key_exists($key, $transformed_expense)) { - $entity[$keyval] = $transformed_expense[$key]; + if (is_array($parts) && $parts[0] == 'expense' && array_key_exists($parts[1], $transformed_expense)) { + $entity[$key] = $transformed_expense[$parts[1]]; + } elseif (array_key_exists($key, $transformed_expense)) { + $entity[$key] = $transformed_expense[$key]; } else { - $entity[$keyval] = ''; + $entity[$key] = $this->resolveKey($key, $expense, $this->expense_transformer); } + } return $this->decorateAdvancedFields($expense, $entity); @@ -134,34 +140,44 @@ class ExpenseExport extends BaseExport private function decorateAdvancedFields(Expense $expense, array $entity) :array { - if (in_array('currency_id', $this->input['report_keys'])) { - $entity['currency'] = $expense->currency ? $expense->currency->code : ''; + if (in_array('expense.currency_id', $this->input['report_keys'])) { + $entity['expense.currency_id'] = $expense->currency ? $expense->currency->code : ''; } - if (in_array('client_id', $this->input['report_keys'])) { - $entity['client'] = $expense->client ? $expense->client->present()->name() : ''; + if (in_array('expense.client_id', $this->input['report_keys'])) { + $entity['expense.client'] = $expense->client ? $expense->client->present()->name() : ''; } - if (in_array('invoice_id', $this->input['report_keys'])) { - $entity['invoice'] = $expense->invoice ? $expense->invoice->number : ''; + if (in_array('expense.invoice_id', $this->input['report_keys'])) { + $entity['expense.invoice_id'] = $expense->invoice ? $expense->invoice->number : ''; } - if (in_array('category_id', $this->input['report_keys'])) { - $entity['category'] = $expense->category ? $expense->category->name : ''; + if (in_array('expense.category', $this->input['report_keys'])) { + $entity['expense.category'] = $expense->category ? $expense->category->name : ''; } - if (in_array('vendor_id', $this->input['report_keys'])) { - $entity['vendor'] = $expense->vendor ? $expense->vendor->name : ''; + if (in_array('expense.vendor_id', $this->input['report_keys'])) { + $entity['expense.vendor'] = $expense->vendor ? $expense->vendor->name : ''; } - if (in_array('payment_type_id', $this->input['report_keys'])) { - $entity['payment_type'] = $expense->payment_type ? $expense->payment_type->name : ''; + if (in_array('expense.payment_type_id', $this->input['report_keys'])) { + $entity['expense.payment_type_id'] = $expense->payment_type ? $expense->payment_type->name : ''; } - if (in_array('project_id', $this->input['report_keys'])) { - $entity['project'] = $expense->project ? $expense->project->name : ''; + if (in_array('expense.project_id', $this->input['report_keys'])) { + $entity['expense.project_id'] = $expense->project ? $expense->project->name : ''; } + if (in_array('expense.user', $this->input['report_keys'])) { + $entity['expense.user'] = $expense->user ? $expense->user->present()->name() : ''; + } + + if (in_array('expense.assigned_user', $this->input['report_keys'])) { + $entity['expense.assigned_user'] = $expense->assigned_user ? $expense->assigned_user->present()->name() : ''; + } + + nlog($entity); + return $entity; } } diff --git a/app/Models/Expense.php b/app/Models/Expense.php index 00a16129fc9f..3bb6ff894ba4 100644 --- a/app/Models/Expense.php +++ b/app/Models/Expense.php @@ -275,4 +275,19 @@ class Expense extends BaseModel return $this->belongsTo(BankTransaction::class); } + public function stringStatus() + { + if($this->is_deleted) + return ctrans('texts.deleted'); + elseif($this->payment_date) + return ctrans('texts.paid'); + elseif($this->invoice_id) + return ctrans('texts.invoiced'); + elseif($this->should_be_invoiced) + return ctrans('texts.pending'); + elseif($this->trashed()) + return ctrans('texts.archived'); + + return ctrans('texts.logged'); + } } From 82fa7cbae03a8e84cf1aec2e341adfb272a2db72 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 13 Jul 2023 21:52:39 +1000 Subject: [PATCH 08/22] Export maps --- app/Export/CSV/BaseExport.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index 2398e64a6fc1..ca5e9c1a0953 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -355,7 +355,7 @@ class BaseExport private function resolveClientContactKey($column, $entity, $transformer) { - + if(!$entity->client) { return ""; } @@ -380,11 +380,11 @@ class BaseExport private function resolveExpenseKey($column, $entity, $transformer) { - // $transformed_entity = $transformer->includeExpense($entity); + $transformed_entity = $transformer->includeExpense($entity); - // $manager = new Manager(); - // $manager->setSerializer(new ArraySerializer()); - // $transformed_entity = $manager->createData($transformed_entity)->toArray(); + $manager = new Manager(); + $manager->setSerializer(new ArraySerializer()); + $transformed_entity = $manager->createData($transformed_entity)->toArray(); if($column == 'user' && $entity?->expense?->user) return $entity->expense->user->present()->name() ?? ' '; @@ -396,6 +396,9 @@ class BaseExport return $entity->expense->category?->name ?? ' '; } + if(array_key_exists($column, $transformed_entity)) + return $transformed_entity[$column]; + if(property_exists($entity, $column)) return $entity?->{$column} ?? ''; From 552c630a634526ac079b7a823ba2747dd35dd90b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 13 Jul 2023 22:01:22 +1000 Subject: [PATCH 09/22] Add in controllers for reports --- app/Export/CSV/VendorExport.php | 2 +- .../PurchaseOrderItemReportController.php | 53 +++++++++++++++++++ .../Reports/PurchaseOrderReportController.php | 53 +++++++++++++++++++ .../Reports/VendorReportController.php | 53 +++++++++++++++++++ 4 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/Reports/PurchaseOrderItemReportController.php create mode 100644 app/Http/Controllers/Reports/PurchaseOrderReportController.php create mode 100644 app/Http/Controllers/Reports/VendorReportController.php diff --git a/app/Export/CSV/VendorExport.php b/app/Export/CSV/VendorExport.php index 872b818ef762..e978c36e1432 100644 --- a/app/Export/CSV/VendorExport.php +++ b/app/Export/CSV/VendorExport.php @@ -44,7 +44,7 @@ class VendorExport extends BaseExport 'id_number' => 'vendor.id_number', 'name' => 'vendor.name', 'number' => 'vendor.number', - 'client_phone' => 'vendor.phone', + 'phone' => 'vendor.phone', 'postal_code' => 'vendor.postal_code', 'private_notes' => 'vendor.private_notes', 'public_notes' => 'vendor.public_notes', diff --git a/app/Http/Controllers/Reports/PurchaseOrderItemReportController.php b/app/Http/Controllers/Reports/PurchaseOrderItemReportController.php new file mode 100644 index 000000000000..a708f1300ab4 --- /dev/null +++ b/app/Http/Controllers/Reports/PurchaseOrderItemReportController.php @@ -0,0 +1,53 @@ +has('send_email') && $request->get('send_email')) { + SendToAdmin::dispatch(auth()->user()->company(), $request->all(), PurchaseOrderItemExport::class, $this->filename); + + return response()->json(['message' => 'working...'], 200); + } + // expect a list of visible fields, or use the default + + $export = new PurchaseOrderItemExport(auth()->user()->company(), $request->all()); + + $csv = $export->run(); + + $headers = [ + 'Content-Disposition' => 'attachment', + 'Content-Type' => 'text/csv', + ]; + + return response()->streamDownload(function () use ($csv) { + echo $csv; + }, $this->filename, $headers); + } +} diff --git a/app/Http/Controllers/Reports/PurchaseOrderReportController.php b/app/Http/Controllers/Reports/PurchaseOrderReportController.php new file mode 100644 index 000000000000..4bd0057aa91a --- /dev/null +++ b/app/Http/Controllers/Reports/PurchaseOrderReportController.php @@ -0,0 +1,53 @@ +has('send_email') && $request->get('send_email')) { + SendToAdmin::dispatch(auth()->user()->company(), $request->all(), PurchaseOrderExport::class, $this->filename); + + return response()->json(['message' => 'working...'], 200); + } + // expect a list of visible fields, or use the default + + $export = new PurchaseOrderExport(auth()->user()->company(), $request->all()); + + $csv = $export->run(); + + $headers = [ + 'Content-Disposition' => 'attachment', + 'Content-Type' => 'text/csv', + ]; + + return response()->streamDownload(function () use ($csv) { + echo $csv; + }, $this->filename, $headers); + } +} diff --git a/app/Http/Controllers/Reports/VendorReportController.php b/app/Http/Controllers/Reports/VendorReportController.php new file mode 100644 index 000000000000..2494daadb923 --- /dev/null +++ b/app/Http/Controllers/Reports/VendorReportController.php @@ -0,0 +1,53 @@ +has('send_email') && $request->get('send_email')) { + SendToAdmin::dispatch(auth()->user()->company(), $request->all(), VendorExport::class, $this->filename); + + return response()->json(['message' => 'working...'], 200); + } + // expect a list of visible fields, or use the default + + $export = new VendorExport(auth()->user()->company(), $request->all()); + + $csv = $export->run(); + + $headers = [ + 'Content-Disposition' => 'attachment', + 'Content-Type' => 'text/csv', + ]; + + return response()->streamDownload(function () use ($csv) { + echo $csv; + }, $this->filename, $headers); + } +} From df2e4089a3afc2b82c53287d831a5462540dbc5d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 14 Jul 2023 14:23:01 +1000 Subject: [PATCH 10/22] Add rate limiting for contact reset password urls --- app/Http/Livewire/RequiredClientInfo.php | 3 ++- app/Providers/RouteServiceProvider.php | 7 ++++++- app/Repositories/ActivityRepository.php | 3 +++ routes/client.php | 16 ++++++++-------- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/app/Http/Livewire/RequiredClientInfo.php b/app/Http/Livewire/RequiredClientInfo.php index df184eff52bf..f8777538673a 100644 --- a/app/Http/Livewire/RequiredClientInfo.php +++ b/app/Http/Livewire/RequiredClientInfo.php @@ -257,7 +257,8 @@ class RequiredClientInfo extends Component } if (Str::startsWith($field['name'], 'contact_')) { - if (empty($this->contact->{$_field}) || is_null($this->contact->{$_field})) { + + if (empty($this->contact->{$_field}) || is_null($this->contact->{$_field}) || str_contains($this->contact->{$_field}, '@example.com')) { $this->show_form = true; } else { $this->fields[$index]['filled'] = true; diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index d8a11fba07e5..3d9dc442150b 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -57,7 +57,7 @@ class RouteServiceProvider extends ServiceProvider if (Ninja::isSelfHost()) { return Limit::none(); } else { - return Limit::perMinute(50)->by($request->ip()); + return Limit::perMinute(30)->by($request->ip()); } }); @@ -89,6 +89,11 @@ class RouteServiceProvider extends ServiceProvider return Limit::perMinute(2)->by($request->ip()); }); + RateLimiter::for('portal', function (Request $request) { + return Limit::perMinute(15)->by($request->ip()); + }); + + } /** diff --git a/app/Repositories/ActivityRepository.php b/app/Repositories/ActivityRepository.php index 3fdb6d402608..575a2499ab14 100644 --- a/app/Repositories/ActivityRepository.php +++ b/app/Repositories/ActivityRepository.php @@ -50,6 +50,9 @@ class ActivityRepository extends BaseRepository $activity->{$key} = $value; } + if($entity->company) + $activity->account_id = $entity->company->account_id; + if ($token_id = $this->getTokenId($event_vars)) { $activity->token_id = $token_id; } diff --git a/routes/client.php b/routes/client.php index f708f64113a5..ec0de9acc7e3 100644 --- a/routes/client.php +++ b/routes/client.php @@ -18,18 +18,18 @@ use App\Http\Controllers\ClientPortal\SubscriptionController; use App\Http\Controllers\Auth\ContactForgotPasswordController; use App\Http\Controllers\ClientPortal\PaymentMethodController; -Route::get('client', [ContactLoginController::class, 'showLoginForm'])->name('client.catchall')->middleware(['domain_db', 'contact_account','locale']); //catch all +Route::get('client', [ContactLoginController::class, 'showLoginForm'])->name('client.catchall')->middleware(['domain_db', 'contact_account','locale', 'throttle:portal']); //catch all -Route::get('client/login/{company_key?}', [ContactLoginController::class, 'showLoginForm'])->name('client.login')->middleware(['domain_db', 'contact_account','locale']); +Route::get('client/login/{company_key?}', [ContactLoginController::class, 'showLoginForm'])->name('client.login')->middleware(['domain_db', 'contact_account','locale', 'throttle:portal']); Route::post('client/login/{company_key?}', [ContactLoginController::class, 'login'])->name('client.login.submit'); Route::get('client/register/{company_key?}', [ContactRegisterController::class, 'showRegisterForm'])->name('client.register')->middleware(['domain_db', 'contact_account', 'contact_register','locale']); -Route::post('client/register/{company_key?}', [ContactRegisterController::class, 'register'])->middleware(['domain_db', 'contact_account', 'contact_register', 'locale', 'throttle:10,1']); +Route::post('client/register/{company_key?}', [ContactRegisterController::class, 'register'])->middleware(['domain_db', 'contact_account', 'contact_register', 'locale', 'throttle:portal']); -Route::get('client/password/reset', [ContactForgotPasswordController::class, 'showLinkRequestForm'])->name('client.password.request')->middleware(['domain_db', 'contact_account','locale']); -Route::post('client/password/email', [ContactForgotPasswordController::class, 'sendResetLinkEmail'])->name('client.password.email')->middleware('locale'); -Route::get('client/password/reset/{token}', [ContactResetPasswordController::class, 'showResetForm'])->name('client.password.reset')->middleware(['domain_db', 'contact_account','locale']); -Route::post('client/password/reset', [ContactResetPasswordController::class, 'reset'])->name('client.password.update')->middleware(['domain_db', 'contact_account','locale']); +Route::get('client/password/reset', [ContactForgotPasswordController::class, 'showLinkRequestForm'])->name('client.password.request')->middleware(['domain_db', 'contact_account','locale', 'throttle:portal']); +Route::post('client/password/email', [ContactForgotPasswordController::class, 'sendResetLinkEmail'])->name('client.password.email')->middleware(['locale', 'throttle:portal']); +Route::get('client/password/reset/{token}', [ContactResetPasswordController::class, 'showResetForm'])->name('client.password.reset')->middleware(['domain_db', 'contact_account','locale', 'throttle:portal']); +Route::post('client/password/reset', [ContactResetPasswordController::class, 'reset'])->name('client.password.update')->middleware(['domain_db', 'contact_account','locale', 'throttle:portal']); Route::get('view/{entity_type}/{invitation_key}', [App\Http\Controllers\ClientPortal\EntityViewController::class, 'index'])->name('client.entity_view'); Route::get('view/{entity_type}/{invitation_key}/password', [App\Http\Controllers\ClientPortal\EntityViewController::class ,'password'])->name('client.entity_view.password'); @@ -78,7 +78,7 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'domain_db','check_clie Route::put('profile/{client_contact}/localization', [App\Http\Controllers\ClientPortal\ProfileController::class, 'updateClientLocalization'])->name('profile.edit_localization'); Route::get('payment_methods/{payment_method}/verification', [App\Http\Controllers\ClientPortal\PaymentMethodController::class, 'verify'])->name('payment_methods.verification'); - Route::post('payment_methods/{payment_method}/verification', [App\Http\Controllers\ClientPortal\PaymentMethodController::class, 'processVerification'])->middleware(['throttle:10,1']); + Route::post('payment_methods/{payment_method}/verification', [App\Http\Controllers\ClientPortal\PaymentMethodController::class, 'processVerification'])->middleware(['throttle:portal']); Route::get('payment_methods/confirm', [App\Http\Controllers\ClientPortal\PaymentMethodController::class, 'store'])->name('payment_methods.confirm'); From f82b21343c58a999ec6d14620f1fdef1b02f95cf Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 14 Jul 2023 14:27:32 +1000 Subject: [PATCH 11/22] Disallow updates to cancelled invoices --- app/Http/Requests/Invoice/UpdateInvoiceRequest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php index 9805160ab9e7..0a6992192e93 100644 --- a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php @@ -71,6 +71,7 @@ class UpdateInvoiceRequest extends Request $rules['tax_name1'] = 'bail|sometimes|string|nullable'; $rules['tax_name2'] = 'bail|sometimes|string|nullable'; $rules['tax_name3'] = 'bail|sometimes|string|nullable'; + $rules['status_id'] = 'bail|sometimes|not_in:5'; //do not all cancelled invoices to be modfified. // not needed. // $rules['partial_due_date'] = 'bail|sometimes|required_unless:partial,0,null'; @@ -100,7 +101,8 @@ class UpdateInvoiceRequest extends Request public function messages() { return [ - 'id' => ctrans('text.locked_invoice'), + 'id' => ctrans('texts.locked_invoice'), + 'status_id' => ctrans('texts.locked_invoice'), ]; } } From 1127f3ea5f07a38ea0a680bb080fe9e7a0599dc4 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 14 Jul 2023 17:09:23 +1000 Subject: [PATCH 12/22] Additional payment logic --- app/Filters/QueryFilters.php | 27 +++ .../Credit/ValidCreditsRules.php | 5 +- app/Services/Invoice/HandleRestore.php | 16 +- app/Services/Invoice/MarkInvoiceDeleted.php | 10 +- app/Services/Payment/DeletePayment.php | 2 + tests/Feature/Export/ExportCompanyTest.php | 2 +- tests/Feature/PaymentV2Test.php | 192 ++++++++++++++++++ tests/Unit/PaymentTypeTest.php | 2 +- 8 files changed, 248 insertions(+), 8 deletions(-) create mode 100644 tests/Feature/PaymentV2Test.php diff --git a/app/Filters/QueryFilters.php b/app/Filters/QueryFilters.php index fc33f283a554..036b7a892ed0 100644 --- a/app/Filters/QueryFilters.php +++ b/app/Filters/QueryFilters.php @@ -288,6 +288,33 @@ abstract class QueryFilters return $this->builder; } + /** + * @return Builder + * @throws RuntimeException + */ + public function without_deleted_clients(): Builder + { + return $this->builder->where(function ($query) { + $query->whereHas('client', function ($sub_query) { + $sub_query->where('is_deleted', 0); + })->orWhere('client_id', null); + }); + } + + /** + * @return Builder + * @throws RuntimeException + */ + public function without_deleted_vendors(): Builder + { + return $this->builder->where(function ($query) { + $query->whereHas('vendor', function ($sub_query) { + $sub_query->where('is_deleted', 0); + })->orWhere('vendor_id', null); + }); + } + + public function with(string $value = ''): Builder { if (strlen($value) == 0) { diff --git a/app/Http/ValidationRules/Credit/ValidCreditsRules.php b/app/Http/ValidationRules/Credit/ValidCreditsRules.php index 7b6fc32aed88..b0892da7330e 100644 --- a/app/Http/ValidationRules/Credit/ValidCreditsRules.php +++ b/app/Http/ValidationRules/Credit/ValidCreditsRules.php @@ -69,13 +69,16 @@ class ValidCreditsRules implements Rule if (! $cred) { $this->error_msg = ctrans('texts.credit_not_found'); - return false; } if ($cred->client_id != $this->input['client_id']) { $this->error_msg = ctrans('texts.invoices_dont_match_client'); + return false; + } + if($cred->balance < $credit['amount']) { + $this->error_msg = ctrans('texts.insufficient_credit_balance'); return false; } } diff --git a/app/Services/Invoice/HandleRestore.php b/app/Services/Invoice/HandleRestore.php index 4dc9621f4acf..c3e2947e8732 100644 --- a/app/Services/Invoice/HandleRestore.php +++ b/app/Services/Invoice/HandleRestore.php @@ -44,7 +44,6 @@ class HandleRestore extends AbstractService //cannot restore an invoice with a deleted payment foreach ($this->invoice->payments as $payment) { if (($this->invoice->paid_to_date == 0) && $payment->is_deleted) { - $this->invoice->delete(); //set it back to deleted so that it can be restored from repository return $this->invoice; } } @@ -81,8 +80,8 @@ class HandleRestore extends AbstractService Paymentable::query() ->withTrashed() ->where('payment_id', $payment->id) - ->where('paymentable_type', '=', 'invoices') - ->where('paymentable_id', $this->invoice->id) + // ->where('paymentable_type', '=', 'invoices') + // ->where('paymentable_id', $this->invoice->id) ->update(['deleted_at' => null]); }); @@ -102,6 +101,12 @@ class HandleRestore extends AbstractService ->where('paymentable_type', '=', 'invoices') ->where('paymentable_id', $this->invoice->id) ->sum(DB::raw('refunded')); + + //14/07/2023 - do not include credits in the payment amount + $this->adjustment_amount -= $payment->paymentables + ->where('paymentable_type', '=', 'App\Models\Credit') + ->sum(DB::raw('amount')); + } $this->total_payments = $this->invoice->payments->sum('amount') - $this->invoice->payments->sum('refunded'); @@ -130,11 +135,16 @@ class HandleRestore extends AbstractService ->where('paymentable_id', $this->invoice->id) ->sum(DB::raw('refunded')); + $payment_adjustment -= $payment->paymentables + ->where('paymentable_type', '=', 'App\Models\Credit') + ->sum(DB::raw('amount')); + $payment->amount += $payment_adjustment; $payment->applied += $payment_adjustment; $payment->is_deleted = false; $payment->restore(); $payment->saveQuietly(); + }); return $this; diff --git a/app/Services/Invoice/MarkInvoiceDeleted.php b/app/Services/Invoice/MarkInvoiceDeleted.php index 27291123d56e..72cfe0136de3 100644 --- a/app/Services/Invoice/MarkInvoiceDeleted.php +++ b/app/Services/Invoice/MarkInvoiceDeleted.php @@ -11,11 +11,12 @@ namespace App\Services\Invoice; -use App\Jobs\Inventory\AdjustProductInventory; +use App\Models\Credit; use App\Models\Invoice; use App\Services\AbstractService; -use App\Utils\Traits\GeneratesCounter; use Illuminate\Support\Facades\DB; +use App\Utils\Traits\GeneratesCounter; +use App\Jobs\Inventory\AdjustProductInventory; class MarkInvoiceDeleted extends AbstractService { @@ -94,6 +95,11 @@ class MarkInvoiceDeleted extends AbstractService ->where('paymentable_id', $this->invoice->id) ->sum(DB::raw('refunded')); + //14-07-2023 - Do not include credits in the payment adjustment. + $payment_adjustment -= $payment->paymentables + ->where('paymentable_type', '=', 'App\Models\Credit') + ->sum(DB::raw('amount')); + $payment->amount -= $payment_adjustment; $payment->applied -= $payment_adjustment; $payment->save(); diff --git a/app/Services/Payment/DeletePayment.php b/app/Services/Payment/DeletePayment.php index f5c02d1e86e5..fb50797b64f5 100644 --- a/app/Services/Payment/DeletePayment.php +++ b/app/Services/Payment/DeletePayment.php @@ -109,6 +109,8 @@ class DeletePayment $paymentable_invoice->service() ->updatePaidToDate($net_deletable * -1) ->save(); + $paymentable_invoice->delete(); + } }); } diff --git a/tests/Feature/Export/ExportCompanyTest.php b/tests/Feature/Export/ExportCompanyTest.php index 88696330ff63..c96c7fb347fc 100644 --- a/tests/Feature/Export/ExportCompanyTest.php +++ b/tests/Feature/Export/ExportCompanyTest.php @@ -46,7 +46,7 @@ class ExportCompanyTest extends TestCase public function testCompanyExport() { - $res = (new CompanyExport($this->company, $this->company->users->first()))->handle(); + $res = (new CompanyExport($this->company, $this->company->users->first(), '123'))->handle(); $this->assertTrue($res); } diff --git a/tests/Feature/PaymentV2Test.php b/tests/Feature/PaymentV2Test.php new file mode 100644 index 000000000000..761c00b03361 --- /dev/null +++ b/tests/Feature/PaymentV2Test.php @@ -0,0 +1,192 @@ +faker = \Faker\Factory::create(); + + Model::reguard(); + + $this->makeTestData(); + $this->withoutExceptionHandling(); + + $this->withoutMiddleware( + ThrottleRequests::class + ); + } + + public function testStorePaymentWithCreditsThenDeletingInvoices() + { + $client = Client::factory()->create(['company_id' =>$this->company->id, 'user_id' => $this->user->id, 'balance' => 20, 'paid_to_date' => 0]); + ClientContact::factory()->create([ + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + ]); + + $invoice = Invoice::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'status_id' => Invoice::STATUS_SENT, + 'uses_inclusive_taxes' => false, + 'amount' => 20, + 'balance' => 20, + 'discount' => 0, + 'number' => uniqid("st", true), + 'line_items' => [] + ]); + + $this->assertEquals(20, $client->balance); + $this->assertEquals(0, $client->paid_to_date); + $this->assertEquals(20, $invoice->amount); + $this->assertEquals(20, $invoice->balance); + + $credit = Credit::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'status_id' => Invoice::STATUS_SENT, + 'uses_inclusive_taxes' => false, + 'amount' => 20, + 'balance' => 20, + 'discount' => 0, + 'number' => uniqid("st", true), + 'line_items' => [] + ]); + + $this->assertEquals(20, $credit->amount); + $this->assertEquals(20, $credit->balance); + + $data = [ + 'client_id' => $client->hashed_id, + 'invoices' => [ + [ + 'invoice_id' => $invoice->hashed_id, + 'amount' => 20, + ], + ], + 'credits' => [ + [ + 'credit_id' => $credit->hashed_id, + 'amount' => 20, + ], + ], + 'date' => '2020/12/12', + + ]; + + $response = null; + + try { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/payments?include=invoices', $data); + } catch (ValidationException $e) { + $message = json_decode($e->validator->getMessageBag(), 1); + nlog($message); + $this->assertNotNull($message); + } + + $arr = $response->json(); + $response->assertStatus(200); + + $payment_id = $arr['data']['id']; + + $payment = Payment::find($this->decodePrimaryKey($payment_id)); + + $this->assertNotNull($payment); + $this->assertNotNull($payment->invoices()); + $this->assertEquals(1, $payment->invoices()->count()); + $this->assertEquals(0, $payment->amount); + $this->assertEquals(0, $client->fresh()->balance); + $this->assertEquals(20, $client->fresh()->paid_to_date); + + $data = [ + 'action' => 'delete', + 'ids' => [ + $invoice->hashed_id, + ], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/invoices/bulk', $data); + + $response->assertStatus(200); + + $invoice = $invoice->fresh(); + $payment = $payment->fresh(); + + $this->assertEquals(true, $invoice->is_deleted); + $this->assertEquals(0, $payment->amount); + $this->assertEquals(0, $client->fresh()->balance); + $this->assertEquals(0, $client->fresh()->paid_to_date); + + $data = [ + 'action' => 'restore', + 'ids' => [ + $invoice->hashed_id, + ], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/invoices/bulk', $data); + + $invoice = $invoice->fresh(); + $this->assertEquals(false, $invoice->is_deleted); + + $payment = $payment->fresh(); + + $this->assertEquals(0, $payment->amount); + $this->assertEquals(20, $client->fresh()->paid_to_date); + + } +} \ No newline at end of file diff --git a/tests/Unit/PaymentTypeTest.php b/tests/Unit/PaymentTypeTest.php index 92440ad31efd..326e972627ad 100644 --- a/tests/Unit/PaymentTypeTest.php +++ b/tests/Unit/PaymentTypeTest.php @@ -33,7 +33,7 @@ class PaymentTypeTest extends TestCase $payment_type_class = new PaymentType; foreach($payment_type_class->type_names as $type) - {nlog($type); + { $this->assertTrue(Lang::has("texts.{$type}")); } } From a4a03cc3ed9ba13ddef07bb8bfa251a3cf722c25 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 14 Jul 2023 19:08:55 +1000 Subject: [PATCH 13/22] Client Export --- app/Export/CSV/BaseExport.php | 4 +++- app/Export/CSV/ClientExport.php | 17 +++++++++++------ app/Filters/InvoiceFilters.php | 13 ------------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index ca5e9c1a0953..d15e51b477e5 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -75,7 +75,8 @@ class BaseExport protected array $client_report_keys = [ "name" => "client.name", - "user" => "client.user_id", + "user" => "client.user", + "assigned_user" => "client.assigned_user", "balance" => "client.balance", "paid_to_date" => "client.paid_to_date", "currency" => "client.currency_id", @@ -796,6 +797,7 @@ class BaseExport $header[] = "{$prefix} " . ctrans("texts.{$key}"); } +nlog($header); return $header; } diff --git a/app/Export/CSV/ClientExport.php b/app/Export/CSV/ClientExport.php index c41dcc7a63df..21a862ec9d90 100644 --- a/app/Export/CSV/ClientExport.php +++ b/app/Export/CSV/ClientExport.php @@ -82,7 +82,6 @@ class ClientExport extends BaseExport ]; public array $forced_keys = [ - 'status', ]; public function __construct(Company $company, array $input) @@ -144,11 +143,11 @@ class ClientExport extends BaseExport $keyval = array_search($key, $this->entity_keys); if (is_array($parts) && $parts[0] == 'client' && array_key_exists($parts[1], $transformed_client)) { - $entity[$keyval] = $transformed_client[$parts[1]]; + $entity[$key] = $transformed_client[$parts[1]]; } elseif (is_array($parts) && $parts[0] == 'contact' && array_key_exists($parts[1], $transformed_contact)) { - $entity[$keyval] = $transformed_contact[$parts[1]]; + $entity[$key] = $transformed_contact[$parts[1]]; } else { - $entity[$keyval] = ''; + $entity[$key] = ''; } } @@ -157,6 +156,14 @@ class ClientExport extends BaseExport private function decorateAdvancedFields(Client $client, array $entity) :array { + if (in_array('client.user', $this->input['report_keys'])) { + $entity['client.user'] = $client->user->present()->name(); + } + + if (in_array('client.assigned_user', $this->input['report_keys'])) { + $entity['client.assigned_user'] = $client->assigned_user ? $client->user->present()->name() : ''; + } + if (in_array('client.country_id', $this->input['report_keys'])) { $entity['country'] = $client->country ? ctrans("texts.country_{$client->country->name}") : ''; } @@ -173,8 +180,6 @@ class ClientExport extends BaseExport $entity['industry_id'] = $client->industry ? ctrans("texts.industry_{$client->industry->name}") : ''; } - $entity['status'] = $this->calculateStatus($client); - return $entity; } diff --git a/app/Filters/InvoiceFilters.php b/app/Filters/InvoiceFilters.php index 3f1feccc3610..1e07412eda4c 100644 --- a/app/Filters/InvoiceFilters.php +++ b/app/Filters/InvoiceFilters.php @@ -135,19 +135,6 @@ class InvoiceFilters extends QueryFilters } - - - /** - * @return Builder - * @throws RuntimeException - */ - public function without_deleted_clients(): Builder - { - return $this->builder->whereHas('client', function ($query) { - $query->where('is_deleted', 0); - }); - } - /** * @return Builder * @return Builder From b8bc92deb0e529ccff75c73c74591949cada3a46 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 15 Jul 2023 09:17:38 +1000 Subject: [PATCH 14/22] Improve the performance of PDF viewing when PDF file size is very large --- .../ClientPortal/InvoiceController.php | 58 +++- .../VendorPortal/PurchaseOrderController.php | 49 +++- app/Http/Livewire/PdfSlot.php | 22 +- app/Jobs/Vendor/CreatePurchaseOrderPdf.php | 2 +- .../Designs/Utilities/DesignHelpers.php | 4 + app/Utils/VendorHtmlEngine.php | 12 +- composer.json | 3 +- composer.lock | 255 ++++-------------- .../components/livewire/pdf-slot.blade.php | 3 +- .../ninja2020/components/pdf-viewer.blade.php | 6 +- routes/client.php | 3 +- routes/vendor.php | 6 +- 12 files changed, 184 insertions(+), 239 deletions(-) diff --git a/app/Http/Controllers/ClientPortal/InvoiceController.php b/app/Http/Controllers/ClientPortal/InvoiceController.php index 8a0013c6d8f9..6e04f4a7e938 100644 --- a/app/Http/Controllers/ClientPortal/InvoiceController.php +++ b/app/Http/Controllers/ClientPortal/InvoiceController.php @@ -11,22 +11,29 @@ namespace App\Http\Controllers\ClientPortal; -use App\Events\Invoice\InvoiceWasViewed; -use App\Events\Misc\InvitationWasViewed; -use App\Http\Controllers\Controller; -use App\Http\Requests\ClientPortal\Invoices\ProcessInvoicesInBulkRequest; -use App\Http\Requests\ClientPortal\Invoices\ShowInvoiceRequest; -use App\Http\Requests\ClientPortal\Invoices\ShowInvoicesRequest; -use App\Models\Invoice; use App\Utils\Ninja; use App\Utils\Number; -use App\Utils\Traits\MakesDates; -use App\Utils\Traits\MakesHash; -use Illuminate\Contracts\View\Factory; -use Illuminate\Http\RedirectResponse; -use Illuminate\Http\Request; -use Illuminate\Support\Facades\Storage; +use App\Models\Invoice; use Illuminate\View\View; +use Illuminate\Http\Request; +use App\Models\QuoteInvitation; +use App\Utils\Traits\MakesHash; +use App\Models\CreditInvitation; +use App\Utils\Traits\MakesDates; +use App\Models\InvoiceInvitation; +use App\Http\Controllers\Controller; +use Illuminate\Http\RedirectResponse; +use Illuminate\Support\Facades\Cache; +use Illuminate\Contracts\View\Factory; +use App\Models\PurchaseOrderInvitation; +use Illuminate\Support\Facades\Storage; +use App\Events\Invoice\InvoiceWasViewed; +use App\Events\Misc\InvitationWasViewed; +use App\Models\RecurringInvoiceInvitation; +use App\Jobs\Vendor\CreatePurchaseOrderPdf; +use App\Http\Requests\ClientPortal\Invoices\ShowInvoiceRequest; +use App\Http\Requests\ClientPortal\Invoices\ShowInvoicesRequest; +use App\Http\Requests\ClientPortal\Invoices\ProcessInvoicesInBulkRequest; class InvoiceController extends Controller { @@ -77,6 +84,31 @@ class InvoiceController extends Controller return $this->render('invoices.show', $data); } + public function showBlob($hash) + { + $data = Cache::pull($hash); + + match($data['entity_type']){ + 'invoice' => $invitation = InvoiceInvitation::withTrashed()->find($data['invitation_id']), + 'quote' => $invitation = QuoteInvitation::withTrashed()->find($data['invitation_id']), + 'credit' => $invitation = CreditInvitation::withTrashed()->find($data['invitation_id']), + 'recurring_invoice' => $invitation = RecurringInvoiceInvitation::withTrashed()->find($data['invitation_id']), + }; + + $file = (new \App\Jobs\Entity\CreateRawPdf($invitation, $invitation->company->db))->handle(); + + // $headers = ['Content-Type' => 'application/pdf']; + // $entity_string = $data['entity_type']; + // $file_name = $invitation->{$entity_string}->numberFormatter().'.pdf'; + // return response()->streamDownload(function () use ($file) { + // echo $file; + // }, $file_name, $headers); + + $headers = ['Content-Type' => 'application/pdf']; + return response()->make($file, 200, $headers); + + } + /** * Pay one or more invoices. * diff --git a/app/Http/Controllers/VendorPortal/PurchaseOrderController.php b/app/Http/Controllers/VendorPortal/PurchaseOrderController.php index 9cf21be66421..65ce601a7c6a 100644 --- a/app/Http/Controllers/VendorPortal/PurchaseOrderController.php +++ b/app/Http/Controllers/VendorPortal/PurchaseOrderController.php @@ -11,21 +11,24 @@ namespace App\Http\Controllers\VendorPortal; -use App\Events\Misc\InvitationWasViewed; -use App\Events\PurchaseOrder\PurchaseOrderWasAccepted; -use App\Events\PurchaseOrder\PurchaseOrderWasViewed; +use App\Utils\Ninja; +use Illuminate\View\View; +use App\Models\PurchaseOrder; +use App\Utils\Traits\MakesHash; +use App\Utils\Traits\MakesDates; use App\Http\Controllers\Controller; -use App\Http\Requests\VendorPortal\PurchaseOrders\ProcessPurchaseOrdersInBulkRequest; +use App\Jobs\Invoice\InjectSignature; +use Illuminate\Support\Facades\Cache; +use Illuminate\Contracts\View\Factory; +use App\Models\PurchaseOrderInvitation; +use Illuminate\Support\Facades\Storage; +use App\Events\Misc\InvitationWasViewed; +use App\Jobs\Vendor\CreatePurchaseOrderPdf; +use App\Events\PurchaseOrder\PurchaseOrderWasViewed; +use App\Events\PurchaseOrder\PurchaseOrderWasAccepted; use App\Http\Requests\VendorPortal\PurchaseOrders\ShowPurchaseOrderRequest; use App\Http\Requests\VendorPortal\PurchaseOrders\ShowPurchaseOrdersRequest; -use App\Jobs\Invoice\InjectSignature; -use App\Models\PurchaseOrder; -use App\Utils\Ninja; -use App\Utils\Traits\MakesDates; -use App\Utils\Traits\MakesHash; -use Illuminate\Contracts\View\Factory; -use Illuminate\Support\Facades\Storage; -use Illuminate\View\View; +use App\Http\Requests\VendorPortal\PurchaseOrders\ProcessPurchaseOrdersInBulkRequest; class PurchaseOrderController extends Controller { @@ -108,6 +111,28 @@ class PurchaseOrderController extends Controller return $this->render('purchase_orders.show', $data); } + public function showBlob($hash) + { + $data = Cache::pull($hash); + + $invitation = PurchaseOrderInvitation::withTrashed()->find($data['invitation_id']); + + $file = (new CreatePurchaseOrderPdf($invitation, $invitation->company->db))->rawPdf(); + + // $headers = ['Content-Type' => 'application/pdf']; + // $entity_string = $data['entity_type']; + // $file_name = $invitation->{$entity_string}->numberFormatter().'.pdf'; + // return response()->streamDownload(function () use ($file) { + // echo $file; + // }, $file_name, $headers); + + $headers = ['Content-Type' => 'application/pdf']; + return response()->make($file, 200, $headers); + + } + + + private function sidebarMenu() :array { $enabled_modules = auth()->guard('vendor')->user()->company->enabled_modules; diff --git a/app/Http/Livewire/PdfSlot.php b/app/Http/Livewire/PdfSlot.php index b28379338550..52736fb4e90b 100644 --- a/app/Http/Livewire/PdfSlot.php +++ b/app/Http/Livewire/PdfSlot.php @@ -16,6 +16,7 @@ use App\Utils\Number; use Livewire\Component; use App\Utils\HtmlEngine; use App\Libraries\MultiDB; +use Illuminate\Support\Str; use App\Models\QuoteInvitation; use App\Utils\VendorHtmlEngine; use App\Models\CreditInvitation; @@ -23,6 +24,7 @@ use App\Services\Pdf\PdfBuilder; use App\Services\Pdf\PdfService; use App\Models\InvoiceInvitation; use App\Services\Pdf\PdfDesigner; +use Illuminate\Support\Facades\Cache; use App\Services\Pdf\PdfConfiguration; use App\Models\PurchaseOrderInvitation; use App\Models\RecurringInvoiceInvitation; @@ -52,6 +54,8 @@ class PdfSlot extends Component public $show_quantity = true; + public $route_entity = 'client'; + public function mount() { MultiDB::setDb($this->db); @@ -59,7 +63,21 @@ class PdfSlot extends Component public function getPdf() { - $this->pdf = $this->entity->fullscreenPdfViewer($this->invitation); + // $this->pdf = $this->entity->fullscreenPdfViewer($this->invitation); + + $blob = [ + 'entity_type' => $this->resolveEntityType(), + 'entity_id' => $this->entity->id, + 'invitation_id' => $this->invitation->id, + 'download' => false, + ]; + + $hash = Str::random(64); + + Cache::put($hash, $blob, now()->addMinutes(2)); + + $this->pdf = $hash; + } public function downloadPdf() @@ -82,6 +100,7 @@ class PdfSlot extends Component public function render() { + $this->entity_type = $this->resolveEntityType(); $this->settings = $this->entity->client ? $this->entity->client->getMergedSettings() : $this->entity->company->settings; @@ -254,6 +273,7 @@ class PdfSlot extends Component } elseif ($this->invitation instanceof RecurringInvoiceInvitation) { return 'recurring_invoice'; } elseif ($this->invitation instanceof PurchaseOrderInvitation) { + $this->route_entity = 'vendor'; return 'purchase_order'; } diff --git a/app/Jobs/Vendor/CreatePurchaseOrderPdf.php b/app/Jobs/Vendor/CreatePurchaseOrderPdf.php index 5961c3c8564e..607f78d0b97a 100644 --- a/app/Jobs/Vendor/CreatePurchaseOrderPdf.php +++ b/app/Jobs/Vendor/CreatePurchaseOrderPdf.php @@ -188,7 +188,7 @@ class CreatePurchaseOrderPdf implements ShouldQueue } if (config('ninja.log_pdf_html')) { - info($maker->getCompiledHTML()); + nlog($maker->getCompiledHTML()); } $maker = null; diff --git a/app/Services/PdfMaker/Designs/Utilities/DesignHelpers.php b/app/Services/PdfMaker/Designs/Utilities/DesignHelpers.php index 8f41938b4540..89aa0ea21420 100644 --- a/app/Services/PdfMaker/Designs/Utilities/DesignHelpers.php +++ b/app/Services/PdfMaker/Designs/Utilities/DesignHelpers.php @@ -274,6 +274,10 @@ trait DesignHelpers // Some variables don't map 1:1 to table columns. This gives us support for such cases. $aliases = [ '$quote.balance_due' => 'partial', + '$purchase_order.po_number' => 'number', + '$purchase_order.total' => 'amount', + '$purchase_order.due_date' => 'due_date', + '$purchase_order.balance_due' => 'balance_due', ]; try { diff --git a/app/Utils/VendorHtmlEngine.php b/app/Utils/VendorHtmlEngine.php index 0f1df93278a7..fa0bff0b8deb 100644 --- a/app/Utils/VendorHtmlEngine.php +++ b/app/Utils/VendorHtmlEngine.php @@ -133,16 +133,18 @@ class VendorHtmlEngine $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['$purchase_order.due_date'] = &$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['$purchase_order.po_number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.po_number')]; + + $data['$poNumber'] = &$data['$purchase_order.po_number']; $data['$entity.datetime'] = ['value' => $this->formatDatetime($this->entity->created_at, $this->company->date_format()), 'label' => ctrans('texts.date')]; - $data['$po_number'] = &$data['$poNumber']; $data['$status_logo'] = ['value' => ' ', 'label' => ' ']; $data['$entity'] = ['value' => '', 'label' => ctrans('texts.purchase_order')]; - $data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.purchase_order_number')]; + $data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.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']; @@ -155,7 +157,6 @@ class VendorHtmlEngine $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['$entity_issued_to'] = ['value' => '', 'label' => ctrans("texts.purchase_order_issued_to")]; @@ -189,8 +190,9 @@ class VendorHtmlEngine } } - // $data['$balance_due'] = $data['$balance_due']; $data['$outstanding'] = &$data['$balance_due']; + $data['$purchase_order.balance_due'] = &$data['$balance_due']; + $data['$partial_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->vendor) ?: ' ', 'label' => ctrans('texts.partial_due')]; $data['$partial'] = &$data['$partial_due']; diff --git a/composer.json b/composer.json index c5907c5a1803..7e1bf4f4ba1d 100644 --- a/composer.json +++ b/composer.json @@ -95,7 +95,8 @@ "turbo124/predis": "1.1.11", "twilio/sdk": "^6.40", "webpatser/laravel-countries": "dev-master#75992ad", - "wepay/php-sdk": "^0.3" + "wepay/php-sdk": "^0.3", + "psr/http-message": "^1.0" }, "require-dev": { "php": "^8.1", diff --git a/composer.lock b/composer.lock index 3f353f7a4788..294a1edc5334 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "135eec9ab7a1e8c0ab3820ff27cf1488", + "content-hash": "be16996524279f340c44e2f6c9a0ba53", "packages": [ { "name": "adrienrn/php-mimetyper", @@ -424,16 +424,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.269.0", + "version": "3.275.7", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "6d759ef9f24f0c7f271baf8014f41fc0cfdfbf78" + "reference": "54dcef3349c81b46c0f5f6e54b5f9bfb5db19903" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6d759ef9f24f0c7f271baf8014f41fc0cfdfbf78", - "reference": "6d759ef9f24f0c7f271baf8014f41fc0cfdfbf78", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/54dcef3349c81b46c0f5f6e54b5f9bfb5db19903", + "reference": "54dcef3349c81b46c0f5f6e54b5f9bfb5db19903", "shasum": "" }, "require": { @@ -445,7 +445,8 @@ "guzzlehttp/promises": "^1.4.0", "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", "mtdowling/jmespath.php": "^2.6", - "php": ">=5.5" + "php": ">=5.5", + "psr/http-message": "^1.0" }, "require-dev": { "andrewsville/php-token-reflection": "^1.4", @@ -462,7 +463,6 @@ "paragonie/random_compat": ">= 2", "phpunit/phpunit": "^4.8.35 || ^5.6.3 || ^9.5", "psr/cache": "^1.0", - "psr/http-message": "^1.0", "psr/simple-cache": "^1.0", "sebastian/comparator": "^1.2.3 || ^4.0", "yoast/phpunit-polyfills": "^1.0" @@ -513,9 +513,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.269.0" + "source": "https://github.com/aws/aws-sdk-php/tree/3.275.7" }, - "time": "2023-04-26T18:21:04+00:00" + "time": "2023-07-13T18:21:04+00:00" }, { "name": "bacon/bacon-qr-code", @@ -2485,16 +2485,16 @@ }, { "name": "google/apiclient-services", - "version": "v0.307.0", + "version": "v0.308.0", "source": { "type": "git", "url": "https://github.com/googleapis/google-api-php-client-services.git", - "reference": "5f7d451aa912355dda61aa29ac3a4afa8b252467" + "reference": "85cf00383e6bf6eca131bd3261b7859ea418a578" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/5f7d451aa912355dda61aa29ac3a4afa8b252467", - "reference": "5f7d451aa912355dda61aa29ac3a4afa8b252467", + "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/85cf00383e6bf6eca131bd3261b7859ea418a578", + "reference": "85cf00383e6bf6eca131bd3261b7859ea418a578", "shasum": "" }, "require": { @@ -2523,9 +2523,9 @@ ], "support": { "issues": "https://github.com/googleapis/google-api-php-client-services/issues", - "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.307.0" + "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.308.0" }, - "time": "2023-07-01T01:02:13+00:00" + "time": "2023-07-09T01:06:13+00:00" }, { "name": "google/auth", @@ -4554,16 +4554,16 @@ }, { "name": "laravel/socialite", - "version": "v5.6.3", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "00ea7f8630673ea49304fc8a9fca5a64eb838c7e" + "reference": "f5996f499e14db15407201a6bfbaba3ce6ce736c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/00ea7f8630673ea49304fc8a9fca5a64eb838c7e", - "reference": "00ea7f8630673ea49304fc8a9fca5a64eb838c7e", + "url": "https://api.github.com/repos/laravel/socialite/zipball/f5996f499e14db15407201a6bfbaba3ce6ce736c", + "reference": "f5996f499e14db15407201a6bfbaba3ce6ce736c", "shasum": "" }, "require": { @@ -4620,7 +4620,7 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2023-06-06T13:42:43+00:00" + "time": "2023-07-08T20:51:43+00:00" }, { "name": "laravel/tinker", @@ -5768,16 +5768,16 @@ }, { "name": "mollie/mollie-api-php", - "version": "v2.57.0", + "version": "v2.58.0", "source": { "type": "git", "url": "https://github.com/mollie/mollie-api-php.git", - "reference": "be201657b00a197e5238bbec81fad2a0fc0b83e4" + "reference": "5120e5b3e4622a290b64acf87266ea47d10d7301" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mollie/mollie-api-php/zipball/be201657b00a197e5238bbec81fad2a0fc0b83e4", - "reference": "be201657b00a197e5238bbec81fad2a0fc0b83e4", + "url": "https://api.github.com/repos/mollie/mollie-api-php/zipball/5120e5b3e4622a290b64acf87266ea47d10d7301", + "reference": "5120e5b3e4622a290b64acf87266ea47d10d7301", "shasum": "" }, "require": { @@ -5854,9 +5854,9 @@ ], "support": { "issues": "https://github.com/mollie/mollie-api-php/issues", - "source": "https://github.com/mollie/mollie-api-php/tree/v2.57.0" + "source": "https://github.com/mollie/mollie-api-php/tree/v2.58.0" }, - "time": "2023-06-23T09:09:15+00:00" + "time": "2023-07-11T12:01:27+00:00" }, { "name": "moneyphp/money", @@ -7280,16 +7280,16 @@ }, { "name": "php-http/discovery", - "version": "1.19.0", + "version": "1.19.1", "source": { "type": "git", "url": "https://github.com/php-http/discovery.git", - "reference": "1856a119a0b0ba8da8b5c33c080aa7af8fac25b4" + "reference": "57f3de01d32085fea20865f9b16fb0e69347c39e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/discovery/zipball/1856a119a0b0ba8da8b5c33c080aa7af8fac25b4", - "reference": "1856a119a0b0ba8da8b5c33c080aa7af8fac25b4", + "url": "https://api.github.com/repos/php-http/discovery/zipball/57f3de01d32085fea20865f9b16fb0e69347c39e", + "reference": "57f3de01d32085fea20865f9b16fb0e69347c39e", "shasum": "" }, "require": { @@ -7352,9 +7352,9 @@ ], "support": { "issues": "https://github.com/php-http/discovery/issues", - "source": "https://github.com/php-http/discovery/tree/1.19.0" + "source": "https://github.com/php-http/discovery/tree/1.19.1" }, - "time": "2023-06-19T08:45:36+00:00" + "time": "2023-07-11T07:02:26+00:00" }, { "name": "php-http/guzzle7-adapter", @@ -7844,16 +7844,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.20", + "version": "3.0.21", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "543a1da81111a0bfd6ae7bbc2865c5e89ed3fc67" + "reference": "4580645d3fc05c189024eb3b834c6c1e4f0f30a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/543a1da81111a0bfd6ae7bbc2865c5e89ed3fc67", - "reference": "543a1da81111a0bfd6ae7bbc2865c5e89ed3fc67", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/4580645d3fc05c189024eb3b834c6c1e4f0f30a1", + "reference": "4580645d3fc05c189024eb3b834c6c1e4f0f30a1", "shasum": "" }, "require": { @@ -7934,7 +7934,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.20" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.21" }, "funding": [ { @@ -7950,7 +7950,7 @@ "type": "tidelift" } ], - "time": "2023-06-13T06:30:34+00:00" + "time": "2023-07-09T15:24:48+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -8360,16 +8360,16 @@ }, { "name": "psr/http-message", - "version": "2.0", + "version": "1.1", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", "shasum": "" }, "require": { @@ -8378,7 +8378,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -8393,7 +8393,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "homepage": "http://www.php-fig.org/" } ], "description": "Common interface for HTTP messages", @@ -8407,9 +8407,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" + "source": "https://github.com/php-fig/http-message/tree/1.1" }, - "time": "2023-04-04T09:54:51+00:00" + "time": "2023-04-04T09:50:52+00:00" }, { "name": "psr/log", @@ -15117,16 +15117,16 @@ }, { "name": "filp/whoops", - "version": "2.15.2", + "version": "2.15.3", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "aac9304c5ed61bf7b1b7a6064bf9806ab842ce73" + "reference": "c83e88a30524f9360b11f585f71e6b17313b7187" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/aac9304c5ed61bf7b1b7a6064bf9806ab842ce73", - "reference": "aac9304c5ed61bf7b1b7a6064bf9806ab842ce73", + "url": "https://api.github.com/repos/filp/whoops/zipball/c83e88a30524f9360b11f585f71e6b17313b7187", + "reference": "c83e88a30524f9360b11f585f71e6b17313b7187", "shasum": "" }, "require": { @@ -15176,7 +15176,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.15.2" + "source": "https://github.com/filp/whoops/tree/2.15.3" }, "funding": [ { @@ -15184,7 +15184,7 @@ "type": "github" } ], - "time": "2023-04-12T12:00:00+00:00" + "time": "2023-07-13T12:00:00+00:00" }, { "name": "friendsofphp/php-cs-fixer", @@ -15392,79 +15392,6 @@ }, "time": "2023-02-16T20:00:16+00:00" }, - { - "name": "laravel/dusk", - "version": "v6.25.2", - "source": { - "type": "git", - "url": "https://github.com/laravel/dusk.git", - "reference": "25a595ac3dc82089a91af10dd23b0d58fd3f6d0b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel/dusk/zipball/25a595ac3dc82089a91af10dd23b0d58fd3f6d0b", - "reference": "25a595ac3dc82089a91af10dd23b0d58fd3f6d0b", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-zip": "*", - "illuminate/console": "^6.0|^7.0|^8.0|^9.0", - "illuminate/support": "^6.0|^7.0|^8.0|^9.0", - "nesbot/carbon": "^2.0", - "php": "^7.2|^8.0", - "php-webdriver/webdriver": "^1.9.0", - "symfony/console": "^4.3|^5.0|^6.0", - "symfony/finder": "^4.3|^5.0|^6.0", - "symfony/process": "^4.3|^5.0|^6.0", - "vlucas/phpdotenv": "^3.0|^4.0|^5.2" - }, - "require-dev": { - "mockery/mockery": "^1.0", - "orchestra/testbench": "^4.16|^5.17.1|^6.12.1|^7.0", - "phpunit/phpunit": "^7.5.15|^8.4|^9.0" - }, - "suggest": { - "ext-pcntl": "Used to gracefully terminate Dusk when tests are running." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "6.x-dev" - }, - "laravel": { - "providers": [ - "Laravel\\Dusk\\DuskServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "Laravel\\Dusk\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "Laravel Dusk provides simple end-to-end testing and browser automation.", - "keywords": [ - "laravel", - "testing", - "webdriver" - ], - "support": { - "issues": "https://github.com/laravel/dusk/issues", - "source": "https://github.com/laravel/dusk/tree/v6.25.2" - }, - "time": "2022-09-29T09:37:07+00:00" - }, { "name": "maximebf/debugbar", "version": "v1.18.2", @@ -16067,72 +15994,6 @@ }, "time": "2022-02-21T01:04:05+00:00" }, - { - "name": "php-webdriver/webdriver", - "version": "1.14.0", - "source": { - "type": "git", - "url": "https://github.com/php-webdriver/php-webdriver.git", - "reference": "3ea4f924afb43056bf9c630509e657d951608563" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/3ea4f924afb43056bf9c630509e657d951608563", - "reference": "3ea4f924afb43056bf9c630509e657d951608563", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "ext-json": "*", - "ext-zip": "*", - "php": "^7.3 || ^8.0", - "symfony/polyfill-mbstring": "^1.12", - "symfony/process": "^5.0 || ^6.0" - }, - "replace": { - "facebook/webdriver": "*" - }, - "require-dev": { - "ergebnis/composer-normalize": "^2.20.0", - "ondram/ci-detector": "^4.0", - "php-coveralls/php-coveralls": "^2.4", - "php-mock/php-mock-phpunit": "^2.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpunit/phpunit": "^9.3", - "squizlabs/php_codesniffer": "^3.5", - "symfony/var-dumper": "^5.0 || ^6.0" - }, - "suggest": { - "ext-SimpleXML": "For Firefox profile creation" - }, - "type": "library", - "autoload": { - "files": [ - "lib/Exception/TimeoutException.php" - ], - "psr-4": { - "Facebook\\WebDriver\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.", - "homepage": "https://github.com/php-webdriver/php-webdriver", - "keywords": [ - "Chromedriver", - "geckodriver", - "php", - "selenium", - "webdriver" - ], - "support": { - "issues": "https://github.com/php-webdriver/php-webdriver/issues", - "source": "https://github.com/php-webdriver/php-webdriver/tree/1.14.0" - }, - "time": "2023-02-09T12:12:19+00:00" - }, { "name": "phpdocumentor/reflection-docblock", "version": "5.3.0", @@ -16659,16 +16520,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.9", + "version": "9.6.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a9aceaf20a682aeacf28d582654a1670d8826778" + "reference": "a6d351645c3fe5a30f5e86be6577d946af65a328" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a9aceaf20a682aeacf28d582654a1670d8826778", - "reference": "a9aceaf20a682aeacf28d582654a1670d8826778", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a6d351645c3fe5a30f5e86be6577d946af65a328", + "reference": "a6d351645c3fe5a30f5e86be6577d946af65a328", "shasum": "" }, "require": { @@ -16742,7 +16603,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.9" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.10" }, "funding": [ { @@ -16758,7 +16619,7 @@ "type": "tidelift" } ], - "time": "2023-06-11T06:13:56+00:00" + "time": "2023-07-10T04:04:23+00:00" }, { "name": "sebastian/cli-parser", diff --git a/resources/views/portal/ninja2020/components/livewire/pdf-slot.blade.php b/resources/views/portal/ninja2020/components/livewire/pdf-slot.blade.php index 553b96d9bf8c..0a0852abd984 100644 --- a/resources/views/portal/ninja2020/components/livewire/pdf-slot.blade.php +++ b/resources/views/portal/ninja2020/components/livewire/pdf-slot.blade.php @@ -14,7 +14,8 @@ -
-
+
+
-
-
diff --git a/resources/views/portal/ninja2020/invoices/includes/terms.blade.php b/resources/views/portal/ninja2020/invoices/includes/terms.blade.php index a963926e9de9..299bf9fc3f1d 100644 --- a/resources/views/portal/ninja2020/invoices/includes/terms.blade.php +++ b/resources/views/portal/ninja2020/invoices/includes/terms.blade.php @@ -1,4 +1,4 @@ -
-
+
-
-
diff --git a/resources/views/portal/ninja2020/invoices/show.blade.php b/resources/views/portal/ninja2020/invoices/show.blade.php index e62ef6e12c34..7f090a4f7ecc 100644 --- a/resources/views/portal/ninja2020/invoices/show.blade.php +++ b/resources/views/portal/ninja2020/invoices/show.blade.php @@ -5,7 +5,6 @@ @include('portal.ninja2020.components.no-cache') - @endpush @@ -96,18 +95,26 @@ @endif @include('portal.ninja2020.components.entity-documents', ['entity' => $invoice]) - @include('portal.ninja2020.components.pdf-viewer', ['entity' => $invoice, 'invitation' => $invitation]) - @include('portal.ninja2020.invoices.includes.terms', ['entities' => [$invoice], 'entity_type' => ctrans('texts.invoice')]) - @include('portal.ninja2020.invoices.includes.signature') + @livewire('pdf-slot', ['entity' => $invoice, 'invitation' => $invitation, 'db' => $invitation->company->db]) + @endsection @section('footer') - - + @include('portal.ninja2020.invoices.includes.signature') + @include('portal.ninja2020.invoices.includes.terms', ['entities' => [$invoice], 'entity_type' => ctrans('texts.invoice')]) +@endsection + +@push('head') + + -@endsection +@endpush diff --git a/resources/views/portal/ninja2020/purchase_orders/show.blade.php b/resources/views/portal/ninja2020/purchase_orders/show.blade.php index c95648b5d875..172beb0291fc 100644 --- a/resources/views/portal/ninja2020/purchase_orders/show.blade.php +++ b/resources/views/portal/ninja2020/purchase_orders/show.blade.php @@ -46,19 +46,27 @@ @endif @include('portal.ninja2020.components.entity-documents', ['entity' => $purchase_order]) - @include('portal.ninja2020.components.pdf-viewer', ['entity' => $purchase_order, 'invitation' => $invitation]) + @livewire('pdf-slot', ['entity' => $purchase_order, 'invitation' => $invitation, 'db' => $invitation->company->db]) + +@endsection + +@section('footer') @include('portal.ninja2020.invoices.includes.terms', ['entities' => [$purchase_order], 'entity_type' => ctrans('texts.purchase_order')]) @include('portal.ninja2020.invoices.includes.signature') @endsection -@section('footer') - - +@push('head') + + -@endsection +@endpush diff --git a/resources/views/portal/ninja2020/quotes/includes/signature.blade.php b/resources/views/portal/ninja2020/quotes/includes/signature.blade.php index ae3e73d4e5eb..5f39e58b3997 100644 --- a/resources/views/portal/ninja2020/quotes/includes/signature.blade.php +++ b/resources/views/portal/ninja2020/quotes/includes/signature.blade.php @@ -1,5 +1,5 @@