From 9595267ee7f4b36dd95f421e06a94408081d8109 Mon Sep 17 00:00:00 2001 From: WuYongJie <549727955@qq.com> Date: Mon, 20 Jul 2026 18:20:25 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=AF=BC=E5=85=A5=20Akaunting=20?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E6=BA=90=E7=A0=81=E4=B8=8E=E5=9B=A2=E9=98=9F?= =?UTF-8?q?=E5=8D=8F=E4=BD=9C=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 16 + .editorconfig | 18 + .gitattributes | 26 + .github/FUNDING.yml | 1 + .github/ISSUE_TEMPLATE.md | 21 + .github/ISSUE_TEMPLATE/bug.yml | 63 + .github/ISSUE_TEMPLATE/config.yml | 11 + .github/workflows/tests.yml | 51 + .gitignore | 98 + .htaccess | 49 + AGENTS.md | 72 + LICENSE.txt | 78 + LocalValetDriver.php | 23 + README.md | 62 + SECURITY.md | 19 + app/Abstracts/BulkAction.php | 328 + app/Abstracts/Commands/Module.php | 86 + app/Abstracts/Event.php | 12 + app/Abstracts/Export.php | 301 + app/Abstracts/Factory.php | 76 + app/Abstracts/Http/ApiController.php | 186 + app/Abstracts/Http/Controller.php | 272 + app/Abstracts/Http/FormRequest.php | 20 + app/Abstracts/Http/PaymentController.php | 209 + app/Abstracts/Http/Response.php | 55 + app/Abstracts/Http/SettingController.php | 174 + app/Abstracts/Import.php | 276 + app/Abstracts/ImportMultipleSheets.php | 31 + app/Abstracts/Job.php | 136 + app/Abstracts/JobShouldQueue.php | 161 + app/Abstracts/Listeners/Report.php | 363 + app/Abstracts/Listeners/Update.php | 36 + app/Abstracts/Model.php | 269 + app/Abstracts/Notification.php | 179 + app/Abstracts/Observer.php | 10 + app/Abstracts/Report.php | 741 + app/Abstracts/View/Component.php | 13 + .../View/Components/Contacts/Form.php | 568 + .../View/Components/Contacts/Index.php | 497 + .../View/Components/Contacts/Show.php | 319 + .../View/Components/Documents/Form.php | 1395 + .../View/Components/Documents/Index.php | 907 + .../View/Components/Documents/Show.php | 1434 + .../View/Components/Documents/Template.php | 767 + app/Abstracts/View/Components/Form.php | 362 + .../View/Components/Transactions/Index.php | 760 + .../View/Components/Transactions/Show.php | 1142 + .../View/Components/Transactions/Template.php | 660 + .../View/Components/Transfers/Show.php | 50 + .../View/Components/Transfers/Template.php | 50 + app/Abstracts/Widget.php | 143 + app/Builders/Category.php | 74 + app/BulkActions/Auth/Users.php | 123 + app/BulkActions/Banking/Accounts.php | 100 + app/BulkActions/Banking/Reconciliations.php | 96 + app/BulkActions/Banking/Transactions.php | 122 + app/BulkActions/Banking/Transfers.php | 57 + app/BulkActions/Common/Companies.php | 122 + app/BulkActions/Common/Dashboards.php | 67 + app/BulkActions/Common/Items.php | 100 + app/BulkActions/Purchases/Bills.php | 156 + app/BulkActions/Purchases/Vendors.php | 97 + app/BulkActions/Sales/Customers.php | 97 + app/BulkActions/Sales/Invoices.php | 172 + app/BulkActions/Settings/Categories.php | 117 + app/BulkActions/Settings/Currencies.php | 108 + app/BulkActions/Settings/Taxes.php | 109 + app/Classifiers/BulkAction.php | 29 + app/Classifiers/Event.php | 29 + app/Classifiers/Export.php | 29 + app/Classifiers/Import.php | 29 + app/Classifiers/Job.php | 29 + app/Classifiers/Observer.php | 29 + app/Classifiers/Report.php | 29 + app/Classifiers/Scope.php | 29 + app/Classifiers/Widget.php | 29 + app/Console/Commands/BillReminder.php | 99 + app/Console/Commands/CompanySeed.php | 50 + app/Console/Commands/DownloadModule.php | 130 + app/Console/Commands/FinishUpdate.php | 68 + app/Console/Commands/Install.php | 201 + app/Console/Commands/InstallRefresh.php | 57 + app/Console/Commands/InvoiceReminder.php | 99 + app/Console/Commands/RecurringCheck.php | 284 + app/Console/Commands/SampleData.php | 37 + app/Console/Commands/StorageTempClear.php | 42 + app/Console/Commands/UninstallModule.php | 54 + app/Console/Commands/Update.php | 199 + app/Console/Commands/UpdateAll.php | 74 + app/Console/Commands/UpdateDb.php | 43 + app/Console/Commands/UserSeed.php | 34 + app/Console/Kernel.php | 60 + app/Console/Stubs/Modules/assets/js/app.stub | 0 .../Stubs/Modules/assets/sass/app.stub | 0 app/Console/Stubs/Modules/command.stub | 32 + app/Console/Stubs/Modules/component.stub | 28 + app/Console/Stubs/Modules/composer.stub | 17 + app/Console/Stubs/Modules/controller-api.stub | 61 + .../Stubs/Modules/controller-plain.stub | 9 + app/Console/Stubs/Modules/controller.stub | 86 + app/Console/Stubs/Modules/event.stub | 28 + app/Console/Stubs/Modules/factory.stub | 28 + app/Console/Stubs/Modules/job-queued.stub | 34 + app/Console/Stubs/Modules/job.stub | 28 + app/Console/Stubs/Modules/json.stub | 17 + app/Console/Stubs/Modules/lang/general.stub | 8 + app/Console/Stubs/Modules/listener-duck.stub | 30 + .../Stubs/Modules/listener-queued-duck.stub | 32 + .../Stubs/Modules/listener-queued.stub | 23 + app/Console/Stubs/Modules/listener.stub | 19 + .../Stubs/Modules/listeners/install.stub | 47 + app/Console/Stubs/Modules/mail.stub | 33 + app/Console/Stubs/Modules/middleware.stub | 21 + app/Console/Stubs/Modules/migration/add.stub | 32 + .../Stubs/Modules/migration/create.stub | 32 + .../Stubs/Modules/migration/delete.stub | 32 + app/Console/Stubs/Modules/migration/drop.stub | 32 + .../Stubs/Modules/migration/plain.stub | 28 + app/Console/Stubs/Modules/model.stub | 23 + app/Console/Stubs/Modules/notification.stub | 61 + app/Console/Stubs/Modules/package.stub | 17 + app/Console/Stubs/Modules/policy.plain.stub | 20 + app/Console/Stubs/Modules/provider.stub | 28 + .../Stubs/Modules/providers/event.stub | 30 + app/Console/Stubs/Modules/request.stub | 20 + .../Stubs/Modules/resource-collection.stub | 19 + app/Console/Stubs/Modules/resource.stub | 19 + app/Console/Stubs/Modules/routes/admin.stub | 13 + app/Console/Stubs/Modules/routes/portal.stub | 14 + app/Console/Stubs/Modules/rule.stub | 40 + .../Stubs/Modules/scaffold/config.stub | 7 + .../Stubs/Modules/scaffold/provider.stub | 103 + app/Console/Stubs/Modules/seeder.stub | 23 + app/Console/Stubs/Modules/unit-test.stub | 19 + app/Console/Stubs/Modules/views/index.stub | 9 + app/Console/Stubs/Modules/webpack.stub | 20 + app/Events/Auth/Attempting.php | 28 + app/Events/Auth/Authenticated.php | 28 + app/Events/Auth/Failed.php | 28 + app/Events/Auth/LandingPageShowing.php | 20 + app/Events/Auth/RoleCreated.php | 24 + app/Events/Auth/RoleCreating.php | 22 + app/Events/Auth/RoleDeleted.php | 20 + app/Events/Auth/RoleDeleting.php | 20 + app/Events/Auth/RoleUpdated.php | 24 + app/Events/Auth/RoleUpdating.php | 24 + app/Events/Auth/UserCreated.php | 24 + app/Events/Auth/UserCreating.php | 22 + app/Events/Auth/UserDeleted.php | 20 + app/Events/Auth/UserDeleting.php | 20 + app/Events/Auth/UserUpdated.php | 24 + app/Events/Auth/UserUpdating.php | 24 + app/Events/Banking/AccountCreated.php | 21 + app/Events/Banking/AccountCreating.php | 20 + app/Events/Banking/AccountDeleted.php | 20 + app/Events/Banking/AccountDeleting.php | 20 + app/Events/Banking/AccountUpdated.php | 25 + app/Events/Banking/AccountUpdating.php | 25 + .../Banking/DocumentTransactionCreated.php | 26 + .../Banking/DocumentTransactionCreating.php | 25 + .../Banking/DocumentTransactionUpdated.php | 30 + .../Banking/DocumentTransactionUpdating.php | 30 + app/Events/Banking/TransactionCreated.php | 21 + app/Events/Banking/TransactionCreating.php | 20 + app/Events/Banking/TransactionDeleted.php | 20 + app/Events/Banking/TransactionDeleting.php | 20 + app/Events/Banking/TransactionPrinting.php | 22 + app/Events/Banking/TransactionRecurring.php | 20 + app/Events/Banking/TransactionSending.php | 20 + app/Events/Banking/TransactionSent.php | 20 + app/Events/Banking/TransactionSplitted.php | 23 + app/Events/Banking/TransactionSplitting.php | 23 + app/Events/Banking/TransactionUpdated.php | 25 + app/Events/Banking/TransactionUpdating.php | 25 + app/Events/Banking/TransferCreated.php | 21 + app/Events/Banking/TransferCreating.php | 20 + app/Events/Banking/TransferDeleted.php | 20 + app/Events/Banking/TransferDeleting.php | 20 + app/Events/Banking/TransferPrinting.php | 22 + app/Events/Banking/TransferUpdated.php | 25 + app/Events/Banking/TransferUpdating.php | 25 + app/Events/Common/BulkActionsAdding.php | 20 + app/Events/Common/CompanyCreated.php | 23 + app/Events/Common/CompanyCreating.php | 20 + app/Events/Common/CompanyDeleted.php | 24 + app/Events/Common/CompanyDeleting.php | 24 + .../Common/CompanyForgettingCurrent.php | 20 + app/Events/Common/CompanyForgotCurrent.php | 20 + app/Events/Common/CompanyMadeCurrent.php | 20 + app/Events/Common/CompanyMakingCurrent.php | 20 + app/Events/Common/CompanySwitched.php | 24 + app/Events/Common/CompanyUpdated.php | 24 + app/Events/Common/CompanyUpdating.php | 24 + app/Events/Common/ContactCreated.php | 21 + app/Events/Common/ContactCreating.php | 20 + app/Events/Common/ContactDeleted.php | 20 + app/Events/Common/ContactDeleting.php | 20 + app/Events/Common/ContactUpdated.php | 25 + app/Events/Common/ContactUpdating.php | 25 + app/Events/Common/DatesFormating.php | 23 + app/Events/Common/GlobalSearched.php | 20 + app/Events/Common/ImportViewCreated.php | 21 + app/Events/Common/ImportViewCreating.php | 21 + app/Events/Common/ItemCreated.php | 25 + app/Events/Common/ItemCreating.php | 20 + app/Events/Common/ItemDeleted.php | 20 + app/Events/Common/ItemDeleting.php | 20 + app/Events/Common/ItemUpdated.php | 25 + app/Events/Common/ItemUpdating.php | 25 + app/Events/Common/RelationshipCounting.php | 20 + app/Events/Common/RelationshipDeleting.php | 20 + app/Events/Common/SearchStringApplied.php | 20 + app/Events/Common/SearchStringApplying.php | 20 + app/Events/Document/DocumentCancelled.php | 22 + app/Events/Document/DocumentCreated.php | 24 + app/Events/Document/DocumentCreating.php | 22 + app/Events/Document/DocumentDeleted.php | 20 + app/Events/Document/DocumentDeleting.php | 20 + app/Events/Document/DocumentDuplicating.php | 20 + app/Events/Document/DocumentMarkedSent.php | 20 + app/Events/Document/DocumentPrinting.php | 22 + app/Events/Document/DocumentReceived.php | 22 + app/Events/Document/DocumentRecurring.php | 21 + app/Events/Document/DocumentReminded.php | 24 + .../Document/DocumentResourceShowing.php | 34 + app/Events/Document/DocumentRestored.php | 22 + app/Events/Document/DocumentSending.php | 20 + app/Events/Document/DocumentSent.php | 20 + app/Events/Document/DocumentTemplates.php | 26 + app/Events/Document/DocumentUpdated.php | 24 + app/Events/Document/DocumentUpdating.php | 24 + app/Events/Document/DocumentViewed.php | 20 + app/Events/Document/PaidAmountCalculated.php | 20 + app/Events/Document/PaymentReceived.php | 31 + app/Events/Document/TransactionsCounted.php | 20 + app/Events/Email/InvalidEmailDetected.php | 50 + app/Events/Email/TooManyEmailsSent.php | 15 + app/Events/Export/HeadingsPreparing.php | 20 + app/Events/Export/RowsPreparing.php | 24 + app/Events/Import/RowPreparing.php | 24 + app/Events/Install/UpdateCacheCleared.php | 20 + app/Events/Install/UpdateCopied.php | 28 + app/Events/Install/UpdateDownloaded.php | 28 + app/Events/Install/UpdateFailed.php | 35 + app/Events/Install/UpdateFinished.php | 28 + app/Events/Install/UpdateUnzipped.php | 28 + app/Events/Menu/AdminCreated.php | 20 + app/Events/Menu/AdminCreating.php | 20 + app/Events/Menu/ItemAuthorizing.php | 20 + app/Events/Menu/NewwCreated.php | 20 + app/Events/Menu/NewwCreating.php | 20 + app/Events/Menu/NotificationsCreated.php | 20 + app/Events/Menu/NotificationsCreating.php | 20 + app/Events/Menu/PortalCreated.php | 20 + app/Events/Menu/PortalCreating.php | 20 + app/Events/Menu/ProfileCreated.php | 20 + app/Events/Menu/ProfileCreating.php | 20 + app/Events/Menu/SettingsCreated.php | 20 + app/Events/Menu/SettingsCreating.php | 20 + app/Events/Menu/SettingsFinished.php | 20 + app/Events/Module/Copied.php | 24 + app/Events/Module/Disabled.php | 24 + app/Events/Module/Disabling.php | 24 + app/Events/Module/Enabled.php | 24 + app/Events/Module/Installed.php | 28 + app/Events/Module/Installing.php | 24 + app/Events/Module/PaymentMethodShowing.php | 20 + app/Events/Module/Uninstalled.php | 24 + app/Events/Module/Uninstalling.php | 24 + app/Events/Report/ClassesCreated.php | 20 + app/Events/Report/DataLoaded.php | 20 + app/Events/Report/DataLoading.php | 20 + app/Events/Report/FilterApplying.php | 28 + app/Events/Report/FilterShowing.php | 20 + app/Events/Report/GroupApplying.php | 28 + app/Events/Report/GroupShowing.php | 20 + app/Events/Report/RowsShowing.php | 20 + app/Events/Report/SkipClass.php | 20 + app/Events/Report/SkipRowsShowing.php | 20 + app/Events/Report/TotalCalculated.php | 40 + app/Events/Report/TotalCalculating.php | 40 + app/Events/Setting/CategoryCreated.php | 24 + app/Events/Setting/CategoryCreating.php | 22 + app/Events/Setting/CategoryDeleted.php | 20 + app/Events/Setting/CategoryDeleting.php | 20 + app/Events/Setting/CategoryUpdated.php | 24 + app/Events/Setting/CategoryUpdating.php | 24 + app/Events/Setting/CurrencyCreated.php | 24 + app/Events/Setting/CurrencyCreating.php | 20 + app/Events/Setting/CurrencyDeleted.php | 20 + app/Events/Setting/CurrencyDeleting.php | 20 + app/Events/Setting/CurrencyUpdated.php | 24 + app/Events/Setting/CurrencyUpdating.php | 24 + app/Events/Setting/SettingUpdated.php | 20 + app/Events/Setting/SettingUpdating.php | 20 + app/Events/Setting/TaxCreated.php | 24 + app/Events/Setting/TaxCreating.php | 20 + app/Events/Setting/TaxDeleted.php | 20 + app/Events/Setting/TaxDeleting.php | 20 + app/Events/Setting/TaxUpdated.php | 24 + app/Events/Setting/TaxUpdating.php | 24 + app/Events/Widget/ClassesCreated.php | 20 + app/Exceptions/Common/LastDashboard.php | 10 + app/Exceptions/Common/NotUserDashboard.php | 10 + app/Exceptions/Common/TooManyEmailsSent.php | 14 + app/Exceptions/Handler.php | 401 + app/Exceptions/Http/Resource.php | 59 + .../Settings/LastCategoryDelete.php | 22 + app/Exceptions/Trackers/Bugsnag.php | 25 + app/Exceptions/Trackers/Sentry.php | 66 + app/Exports/Banking/RecurringTransactions.php | 28 + app/Exports/Banking/Sheets/Recurring.php | 37 + .../Banking/Sheets/RecurringTransactions.php | 54 + app/Exports/Banking/Transactions.php | 77 + app/Exports/Banking/Transfers.php | 66 + app/Exports/Common/Items.php | 28 + app/Exports/Common/Reports.php | 34 + app/Exports/Common/Sheets/ItemTaxes.php | 40 + app/Exports/Common/Sheets/Items.php | 37 + app/Exports/Purchases/Bills/Bills.php | 36 + .../Purchases/Bills/Sheets/BillHistories.php | 38 + .../Purchases/Bills/Sheets/BillItemTaxes.php | 42 + .../Purchases/Bills/Sheets/BillItems.php | 47 + .../Purchases/Bills/Sheets/BillTotals.php | 39 + .../Bills/Sheets/BillTransactions.php | 60 + app/Exports/Purchases/Bills/Sheets/Bills.php | 73 + .../RecurringBills/RecurringBills.php | 36 + .../RecurringBills/Sheets/Recurring.php | 37 + .../Sheets/RecurringBillHistories.php | 38 + .../Sheets/RecurringBillItemTaxes.php | 42 + .../Sheets/RecurringBillItems.php | 47 + .../Sheets/RecurringBillTotals.php | 39 + .../RecurringBills/Sheets/RecurringBills.php | 72 + app/Exports/Purchases/Vendors.php | 48 + app/Exports/Sales/Customers.php | 51 + app/Exports/Sales/Invoices/Invoices.php | 36 + .../Invoices/Sheets/InvoiceHistories.php | 38 + .../Invoices/Sheets/InvoiceItemTaxes.php | 42 + .../Sales/Invoices/Sheets/InvoiceItems.php | 47 + .../Sales/Invoices/Sheets/InvoiceTotals.php | 40 + .../Invoices/Sheets/InvoiceTransactions.php | 60 + .../Sales/Invoices/Sheets/Invoices.php | 74 + .../RecurringInvoices/RecurringInvoices.php | 36 + .../RecurringInvoices/Sheets/Recurring.php | 37 + .../Sheets/RecurringInvoiceHistories.php | 38 + .../Sheets/RecurringInvoiceItemTaxes.php | 42 + .../Sheets/RecurringInvoiceItems.php | 47 + .../Sheets/RecurringInvoiceTotals.php | 39 + .../Sheets/RecurringInvoices.php | 73 + app/Exports/Settings/Categories.php | 37 + app/Exports/Settings/Taxes.php | 27 + app/Http/Controllers/Api/Auth/Users.php | 134 + app/Http/Controllers/Api/Banking/Accounts.php | 126 + .../Api/Banking/Reconciliations.php | 81 + .../Controllers/Api/Banking/Transactions.php | 93 + .../Controllers/Api/Banking/Transfers.php | 111 + app/Http/Controllers/Api/Common/Companies.php | 148 + app/Http/Controllers/Api/Common/Contacts.php | 126 + .../Controllers/Api/Common/Dashboards.php | 154 + app/Http/Controllers/Api/Common/Items.php | 113 + app/Http/Controllers/Api/Common/Ping.php | 30 + app/Http/Controllers/Api/Common/Reports.php | 81 + .../Controllers/Api/Common/Translations.php | 91 + .../Api/Document/DocumentTransactions.php | 109 + .../Controllers/Api/Document/Documents.php | 119 + .../Controllers/Api/Settings/Categories.php | 115 + .../Controllers/Api/Settings/Currencies.php | 126 + .../Controllers/Api/Settings/Settings.php | 105 + app/Http/Controllers/Api/Settings/Taxes.php | 115 + app/Http/Controllers/Auth/Forgot.php | 102 + app/Http/Controllers/Auth/Login.php | 150 + app/Http/Controllers/Auth/Register.php | 88 + app/Http/Controllers/Auth/Reset.php | 146 + app/Http/Controllers/Auth/Users.php | 448 + app/Http/Controllers/Banking/Accounts.php | 307 + app/Http/Controllers/Banking/FundAccounts.php | 182 + .../Controllers/Banking/Reconciliations.php | 256 + .../Banking/RecurringTransactions.php | 271 + app/Http/Controllers/Banking/Transactions.php | 462 + app/Http/Controllers/Banking/Transfers.php | 252 + app/Http/Controllers/Business/Pages.php | 99 + app/Http/Controllers/Common/BulkActions.php | 167 + app/Http/Controllers/Common/Companies.php | 264 + app/Http/Controllers/Common/Contacts.php | 33 + app/Http/Controllers/Common/Dashboards.php | 261 + app/Http/Controllers/Common/Import.php | 27 + app/Http/Controllers/Common/Items.php | 324 + app/Http/Controllers/Common/Plans.php | 23 + app/Http/Controllers/Common/Reports.php | 316 + app/Http/Controllers/Common/Uploads.php | 317 + app/Http/Controllers/Common/Widgets.php | 200 + app/Http/Controllers/Install/Database.php | 62 + app/Http/Controllers/Install/Language.php | 59 + app/Http/Controllers/Install/Requirements.php | 36 + app/Http/Controllers/Install/Settings.php | 49 + app/Http/Controllers/Install/Updates.php | 323 + app/Http/Controllers/Modals/Accounts.php | 62 + app/Http/Controllers/Modals/Categories.php | 89 + app/Http/Controllers/Modals/Companies.php | 38 + app/Http/Controllers/Modals/Currencies.php | 86 + app/Http/Controllers/Modals/Customers.php | 113 + .../Modals/DocumentItemColumns.php | 138 + .../Modals/DocumentTransactions.php | 332 + app/Http/Controllers/Modals/InvoiceEmails.php | 84 + app/Http/Controllers/Modals/InvoiceShare.php | 81 + .../Controllers/Modals/InvoiceTemplates.php | 67 + app/Http/Controllers/Modals/Items.php | 76 + app/Http/Controllers/Modals/Taxes.php | 82 + .../Controllers/Modals/TransactionEmails.php | 101 + .../Controllers/Modals/TransactionShare.php | 80 + .../Controllers/Modals/TransferTemplates.php | 67 + app/Http/Controllers/Modals/Vendors.php | 113 + app/Http/Controllers/Modules/ApiKey.php | 46 + app/Http/Controllers/Modules/Home.php | 28 + app/Http/Controllers/Modules/Item.php | 401 + app/Http/Controllers/Modules/My.php | 24 + app/Http/Controllers/Modules/Tiles.php | 242 + app/Http/Controllers/Portal/Dashboard.php | 101 + app/Http/Controllers/Portal/Invoices.php | 203 + app/Http/Controllers/Portal/Payments.php | 132 + app/Http/Controllers/Portal/Profile.php | 107 + app/Http/Controllers/Purchases/Bills.php | 353 + .../Controllers/Purchases/RecurringBills.php | 235 + app/Http/Controllers/Purchases/Vendors.php | 269 + app/Http/Controllers/Sales/Customers.php | 269 + app/Http/Controllers/Sales/Invoices.php | 378 + .../Controllers/Sales/RecurringInvoices.php | 235 + app/Http/Controllers/Settings/Categories.php | 311 + app/Http/Controllers/Settings/Company.php | 13 + app/Http/Controllers/Settings/Currencies.php | 248 + app/Http/Controllers/Settings/Defaults.php | 57 + app/Http/Controllers/Settings/Email.php | 23 + .../Controllers/Settings/EmailTemplates.php | 84 + app/Http/Controllers/Settings/Invoice.php | 38 + .../Controllers/Settings/Localisation.php | 58 + app/Http/Controllers/Settings/Modules.php | 82 + app/Http/Controllers/Settings/Navigation.php | 77 + app/Http/Controllers/Settings/Schedule.php | 13 + app/Http/Controllers/Settings/Taxes.php | 249 + .../Controllers/SystemSettings/Databases.php | 195 + .../Controllers/SystemSettings/Sidebar.php | 102 + app/Http/Controllers/SystemSettings/Users.php | 182 + app/Http/Controllers/Wizard/Companies.php | 130 + app/Http/Controllers/Wizard/Currencies.php | 131 + app/Http/Controllers/Wizard/Finish.php | 58 + app/Http/Kernel.php | 217 + app/Http/Livewire/Common/Search.php | 273 + app/Http/Livewire/Menu/Favorite.php | 99 + app/Http/Livewire/Menu/Favorites.php | 101 + app/Http/Livewire/Menu/Neww.php | 83 + app/Http/Livewire/Menu/Notifications.php | 163 + app/Http/Livewire/Menu/Profile.php | 32 + app/Http/Livewire/Menu/Settings.php | 55 + app/Http/Livewire/Notification/Browser.php | 49 + app/Http/Livewire/Report/Pin.php | 99 + app/Http/Livewire/Report/Pins.php | 69 + app/Http/Livewire/Tab/Pin.php | 96 + .../Middleware/AddWWWAuthenticateHeader.php | 118 + app/Http/Middleware/AddXHeader.php | 32 + app/Http/Middleware/AdminMenu.php | 35 + app/Http/Middleware/Authenticate.php | 48 + .../AuthenticateOnceWithBasicAuth.php | 20 + .../AuthenticateOnceWithDynamicApi.php | 88 + .../Middleware/AuthenticateOnceWithOAuth.php | 107 + app/Http/Middleware/CanInstall.php | 26 + app/Http/Middleware/CheckForReadOnlyMode.php | 68 + app/Http/Middleware/DateFormat.php | 57 + app/Http/Middleware/DisableQuic.php | 24 + app/Http/Middleware/Dropzone.php | 77 + app/Http/Middleware/EncryptCookies.php | 17 + app/Http/Middleware/EnforceLocalOnly.php | 35 + app/Http/Middleware/IdentifyCompany.php | 67 + app/Http/Middleware/LogoutIfUserDisabled.php | 28 + app/Http/Middleware/Money.php | 121 + app/Http/Middleware/PortalMenu.php | 35 + .../Middleware/RedirectIfAuthenticated.php | 32 + .../RedirectIfHitModuleSubscription.php | 40 + .../Middleware/RedirectIfHitPlanLimits.php | 50 + app/Http/Middleware/RedirectIfNoApiKey.php | 28 + .../Middleware/RedirectIfNotInstalled.php | 31 + .../RedirectIfWizardNotCompleted.php | 31 + app/Http/Middleware/TrimStrings.php | 18 + app/Http/Middleware/TrustHosts.php | 22 + app/Http/Middleware/TrustProxies.php | 23 + app/Http/Middleware/ValidateSignature.php | 86 + app/Http/Middleware/VerifyCsrfToken.php | 24 + app/Http/Requests/Auth/Forgot.php | 30 + app/Http/Requests/Auth/Login.php | 31 + app/Http/Requests/Auth/Register.php | 31 + app/Http/Requests/Auth/Reset.php | 32 + app/Http/Requests/Auth/User.php | 96 + app/Http/Requests/Banking/Account.php | 32 + app/Http/Requests/Banking/FundAccount.php | 25 + app/Http/Requests/Banking/Reconciliation.php | 23 + .../Banking/ReconciliationCalculate.php | 22 + app/Http/Requests/Banking/Transaction.php | 101 + .../Requests/Banking/TransactionConnect.php | 22 + app/Http/Requests/Banking/Transfer.php | 24 + app/Http/Requests/Common/BulkAction.php | 21 + app/Http/Requests/Common/Company.php | 46 + app/Http/Requests/Common/Contact.php | 89 + app/Http/Requests/Common/ContactPerson.php | 24 + app/Http/Requests/Common/CustomMail.php | 23 + app/Http/Requests/Common/Dashboard.php | 22 + app/Http/Requests/Common/Import.php | 20 + app/Http/Requests/Common/Item.php | 84 + app/Http/Requests/Common/ItemTax.php | 21 + app/Http/Requests/Common/Notification.php | 21 + app/Http/Requests/Common/Report.php | 22 + app/Http/Requests/Common/ReportShow.php | 51 + app/Http/Requests/Common/TotalItem.php | 32 + app/Http/Requests/Common/Widget.php | 31 + app/Http/Requests/Document/Document.php | 172 + .../Requests/Document/DocumentAddItem.php | 21 + .../Requests/Document/DocumentHistory.php | 23 + app/Http/Requests/Document/DocumentItem.php | 41 + .../Requests/Document/DocumentItemTax.php | 25 + app/Http/Requests/Document/DocumentTotal.php | 24 + app/Http/Requests/Install/Database.php | 22 + app/Http/Requests/Install/Setting.php | 23 + app/Http/Requests/Module/Install.php | 35 + app/Http/Requests/Module/Module.php | 35 + app/Http/Requests/Portal/InvoiceConfirm.php | 20 + app/Http/Requests/Portal/InvoicePayment.php | 20 + app/Http/Requests/Portal/InvoiceShow.php | 39 + app/Http/Requests/Portal/PaymentShow.php | 39 + app/Http/Requests/Portal/Profile.php | 70 + app/Http/Requests/Setting/Category.php | 30 + app/Http/Requests/Setting/Currency.php | 37 + app/Http/Requests/Setting/EmailTemplate.php | 21 + app/Http/Requests/Setting/Module.php | 34 + app/Http/Requests/Setting/Setting.php | 75 + app/Http/Requests/Setting/Tax.php | 40 + app/Http/Requests/Wizard/Company.php | 67 + app/Http/Resources/Auth/Owner.php | 31 + app/Http/Resources/Auth/Role.php | 28 + app/Http/Resources/Auth/User.php | 36 + app/Http/Resources/Banking/Account.php | 39 + app/Http/Resources/Banking/Reconciliation.php | 35 + app/Http/Resources/Banking/Transaction.php | 54 + app/Http/Resources/Banking/TransactionTax.php | 34 + app/Http/Resources/Banking/Transfer.php | 38 + app/Http/Resources/Common/Company.php | 33 + app/Http/Resources/Common/Contact.php | 40 + app/Http/Resources/Common/ContactPerson.php | 32 + app/Http/Resources/Common/Dashboard.php | 36 + app/Http/Resources/Common/Item.php | 41 + app/Http/Resources/Common/ItemTax.php | 31 + app/Http/Resources/Common/Report.php | 54 + app/Http/Resources/Common/Widget.php | 33 + app/Http/Resources/Document/Document.php | 77 + .../Resources/Document/DocumentHistory.php | 32 + app/Http/Resources/Document/DocumentItem.php | 38 + .../Resources/Document/DocumentItemTax.php | 36 + app/Http/Resources/Document/DocumentTotal.php | 34 + app/Http/Resources/Setting/Category.php | 34 + app/Http/Resources/Setting/Currency.php | 36 + app/Http/Resources/Setting/Setting.php | 24 + app/Http/Resources/Setting/Tax.php | 30 + app/Http/Responses/Common/Items.php | 10 + app/Http/ViewComposers/ContactType.php | 30 + app/Http/ViewComposers/DocumentRecurring.php | 40 + app/Http/ViewComposers/DocumentType.php | 31 + app/Http/ViewComposers/PlanLimits.php | 77 + .../ViewComposers/ReadOnlyNotification.php | 23 + app/Imports/Banking/RecurringTransactions.php | 18 + app/Imports/Banking/Sheets/Recurring.php | 37 + .../Banking/Sheets/RecurringTransactions.php | 52 + app/Imports/Banking/Transactions.php | 61 + app/Imports/Banking/Transfers.php | 170 + app/Imports/Common/Items.php | 18 + app/Imports/Common/Sheets/ItemTaxes.php | 43 + app/Imports/Common/Sheets/Items.php | 43 + app/Imports/Purchases/Bills/Bills.php | 26 + .../Purchases/Bills/Sheets/BillHistories.php | 59 + .../Purchases/Bills/Sheets/BillItemTaxes.php | 86 + .../Purchases/Bills/Sheets/BillItems.php | 73 + .../Purchases/Bills/Sheets/BillTotals.php | 58 + .../Bills/Sheets/BillTransactions.php | 57 + app/Imports/Purchases/Bills/Sheets/Bills.php | 70 + .../RecurringBills/RecurringBills.php | 26 + .../RecurringBills/Sheets/Recurring.php | 40 + .../Sheets/RecurringBillHistories.php | 62 + .../Sheets/RecurringBillItemTaxes.php | 84 + .../Sheets/RecurringBillItems.php | 76 + .../Sheets/RecurringBillTotals.php | 62 + .../RecurringBills/Sheets/RecurringBills.php | 87 + app/Imports/Purchases/Vendors.php | 44 + app/Imports/Sales/Customers.php | 49 + app/Imports/Sales/Invoices/Invoices.php | 26 + .../Invoices/Sheets/InvoiceHistories.php | 59 + .../Invoices/Sheets/InvoiceItemTaxes.php | 86 + .../Sales/Invoices/Sheets/InvoiceItems.php | 73 + .../Sales/Invoices/Sheets/InvoiceTotals.php | 58 + .../Invoices/Sheets/InvoiceTransactions.php | 58 + .../Sales/Invoices/Sheets/Invoices.php | 70 + .../RecurringInvoices/RecurringInvoices.php | 26 + .../RecurringInvoices/Sheets/Recurring.php | 40 + .../Sheets/RecurringInvoiceHistories.php | 62 + .../Sheets/RecurringInvoiceItemTaxes.php | 86 + .../Sheets/RecurringInvoiceItems.php | 76 + .../Sheets/RecurringInvoiceTotals.php | 62 + .../Sheets/RecurringInvoices.php | 87 + app/Imports/Settings/Categories.php | 60 + app/Imports/Settings/Taxes.php | 53 + app/Interfaces/Export/WithParentSheet.php | 8 + app/Interfaces/Job/HasOwner.php | 8 + app/Interfaces/Job/HasSource.php | 8 + app/Interfaces/Job/ShouldCreate.php | 8 + app/Interfaces/Job/ShouldDelete.php | 8 + app/Interfaces/Job/ShouldUpdate.php | 8 + .../Listener/ShouldUpdateAllCompanies.php | 8 + app/Interfaces/Utility/DocumentNumber.php | 12 + app/Interfaces/Utility/TransactionNumber.php | 12 + app/Jobs/Auth/CreateInvitation.php | 55 + app/Jobs/Auth/CreatePermission.php | 19 + app/Jobs/Auth/CreateRole.php | 31 + app/Jobs/Auth/CreateUser.php | 117 + app/Jobs/Auth/DeleteInvitation.php | 18 + app/Jobs/Auth/DeletePermission.php | 18 + app/Jobs/Auth/DeleteRole.php | 26 + app/Jobs/Auth/DeleteUser.php | 43 + app/Jobs/Auth/NotifyUser.php | 36 + app/Jobs/Auth/UpdatePermission.php | 19 + app/Jobs/Auth/UpdateRole.php | 35 + app/Jobs/Auth/UpdateUser.php | 137 + app/Jobs/Banking/CreateAccount.php | 33 + .../CreateBankingDocumentTransaction.php | 131 + app/Jobs/Banking/CreateReconciliation.php | 49 + app/Jobs/Banking/CreateTransaction.php | 53 + app/Jobs/Banking/CreateTransactionTaxes.php | 156 + app/Jobs/Banking/CreateTransfer.php | 134 + app/Jobs/Banking/DeleteAccount.php | 54 + app/Jobs/Banking/DeleteReconciliation.php | 26 + app/Jobs/Banking/DeleteTransaction.php | 44 + app/Jobs/Banking/DeleteTransfer.php | 26 + app/Jobs/Banking/DuplicateTransaction.php | 30 + .../MatchBankingDocumentTransaction.php | 94 + app/Jobs/Banking/SendTransaction.php | 27 + .../Banking/SendTransactionAsCustomMail.php | 66 + app/Jobs/Banking/SplitTransaction.php | 85 + app/Jobs/Banking/UpdateAccount.php | 68 + .../UpdateBankingDocumentTransaction.php | 127 + app/Jobs/Banking/UpdateReconciliation.php | 41 + app/Jobs/Banking/UpdateTransaction.php | 77 + app/Jobs/Banking/UpdateTransfer.php | 131 + app/Jobs/Common/CreateCompany.php | 170 + app/Jobs/Common/CreateContact.php | 67 + app/Jobs/Common/CreateContactPerson.php | 21 + app/Jobs/Common/CreateContactPersons.php | 59 + app/Jobs/Common/CreateDashboard.php | 145 + app/Jobs/Common/CreateItem.php | 47 + app/Jobs/Common/CreateItemTaxes.php | 57 + app/Jobs/Common/CreateMediableForDownload.php | 116 + app/Jobs/Common/CreateMediableForExport.php | 101 + app/Jobs/Common/CreateReport.php | 30 + app/Jobs/Common/CreateWidget.php | 21 + app/Jobs/Common/CreateZipForDownload.php | 110 + app/Jobs/Common/DeleteCompany.php | 98 + app/Jobs/Common/DeleteContact.php | 63 + app/Jobs/Common/DeleteContactPerson.php | 18 + app/Jobs/Common/DeleteDashboard.php | 51 + app/Jobs/Common/DeleteItem.php | 50 + app/Jobs/Common/DeleteReport.php | 71 + app/Jobs/Common/DeleteWidget.php | 18 + app/Jobs/Common/DuplicateContact.php | 27 + app/Jobs/Common/UpdateCompany.php | 157 + app/Jobs/Common/UpdateContact.php | 127 + app/Jobs/Common/UpdateContactPerson.php | 19 + app/Jobs/Common/UpdateDashboard.php | 64 + app/Jobs/Common/UpdateItem.php | 52 + app/Jobs/Common/UpdateReport.php | 26 + app/Jobs/Common/UpdateWidget.php | 19 + app/Jobs/Document/CancelDocument.php | 50 + app/Jobs/Document/CreateDocument.php | 69 + app/Jobs/Document/CreateDocumentHistory.php | 67 + app/Jobs/Document/CreateDocumentItem.php | 203 + .../Document/CreateDocumentItemsAndTotals.php | 290 + app/Jobs/Document/DeleteDocument.php | 51 + app/Jobs/Document/DownloadDocument.php | 68 + app/Jobs/Document/DuplicateDocument.php | 37 + app/Jobs/Document/RestoreDocument.php | 28 + app/Jobs/Document/SendDocument.php | 28 + .../Document/SendDocumentAsCustomMail.php | 67 + app/Jobs/Document/UpdateDocument.php | 113 + app/Jobs/Install/CopyFiles.php | 102 + app/Jobs/Install/DisableModule.php | 66 + app/Jobs/Install/DownloadFile.php | 81 + app/Jobs/Install/DownloadModule.php | 41 + app/Jobs/Install/EnableModule.php | 64 + app/Jobs/Install/FinishUpdate.php | 135 + app/Jobs/Install/InstallModule.php | 66 + app/Jobs/Install/UninstallModule.php | 66 + app/Jobs/Install/UnzipFile.php | 79 + app/Jobs/Setting/CreateCategory.php | 41 + app/Jobs/Setting/CreateCurrency.php | 38 + app/Jobs/Setting/CreateEmailTemplate.php | 21 + app/Jobs/Setting/CreateTax.php | 27 + app/Jobs/Setting/DeleteCategory.php | 113 + app/Jobs/Setting/DeleteCurrency.php | 57 + app/Jobs/Setting/DeleteEmailTemplate.php | 18 + app/Jobs/Setting/DeleteTax.php | 49 + app/Jobs/Setting/UpdateCategory.php | 61 + app/Jobs/Setting/UpdateCurrency.php | 79 + app/Jobs/Setting/UpdateEmailTemplate.php | 19 + app/Jobs/Setting/UpdateTax.php | 60 + app/Listeners/Auth/AddLandingPages.php | 93 + app/Listeners/Auth/Login.php | 23 + app/Listeners/Auth/Logout.php | 19 + .../Banking/IncreaseNextTransactionNumber.php | 25 + app/Listeners/Common/ClearPlansCache.php | 26 + app/Listeners/Common/PreparingResponse.php | 34 + .../Common/SkipScheduleInReadOnlyMode.php | 30 + .../Document/CreateDocumentCreatedHistory.php | 25 + .../Document/CreateDocumentTransaction.php | 67 + .../Document/IncreaseNextDocumentNumber.php | 23 + .../Document/MarkDocumentCancelled.php | 42 + .../Document/MarkDocumentReceived.php | 50 + app/Listeners/Document/MarkDocumentSent.php | 46 + app/Listeners/Document/MarkDocumentViewed.php | 50 + app/Listeners/Document/RestoreDocument.php | 36 + .../SendDocumentPaymentNotification.php | 43 + .../SendDocumentRecurringNotification.php | 58 + .../SendDocumentReminderNotification.php | 37 + .../Document/SendDocumentViewNotification.php | 51 + .../Document/SettingFieldCreated.php | 83 + .../Document/SettingFieldUpdated.php | 83 + .../Email/DisablePersonDueToInvalidEmail.php | 50 + .../Email/ReportTooManyEmailsSent.php | 14 + .../Email/SendInvalidEmailNotification.php | 65 + .../Email/TellFirewallTooManyEmailsSent.php | 40 + app/Listeners/Menu/ShowInAdmin.php | 65 + app/Listeners/Menu/ShowInNeww.php | 57 + app/Listeners/Menu/ShowInNotifications.php | 108 + app/Listeners/Menu/ShowInPortal.php | 36 + app/Listeners/Menu/ShowInProfile.php | 39 + app/Listeners/Menu/ShowInSettings.php | 79 + app/Listeners/Module/ClearCache.php | 45 + app/Listeners/Module/FinishInstallation.php | 27 + app/Listeners/Module/FinishUninstallation.php | 146 + app/Listeners/Module/InstallExtraModules.php | 63 + app/Listeners/Module/UpdateExtraModules.php | 68 + app/Listeners/Report/AddAccounts.php | 93 + app/Listeners/Report/AddBasis.php | 39 + app/Listeners/Report/AddContacts.php | 77 + app/Listeners/Report/AddCustomers.php | 117 + app/Listeners/Report/AddDate.php | 54 + app/Listeners/Report/AddDiscount.php | 52 + app/Listeners/Report/AddExpenseCategories.php | 81 + app/Listeners/Report/AddGroup.php | 39 + app/Listeners/Report/AddIncomeCategories.php | 81 + .../Report/AddIncomeExpenseCategories.php | 122 + app/Listeners/Report/AddPeriod.php | 40 + app/Listeners/Report/AddRowsToTax.php | 33 + app/Listeners/Report/AddSearchString.php | 70 + app/Listeners/Report/AddVendors.php | 117 + .../DeleteCategoryDeletedSubCategories.php | 31 + .../Update/CreateModuleUpdatedHistory.php | 42 + .../Update/SendNotificationOnFailure.php | 51 + app/Listeners/Update/V30/Version300.php | 656 + app/Listeners/Update/V30/Version3013.php | 34 + app/Listeners/Update/V30/Version3014.php | 71 + app/Listeners/Update/V30/Version3015.php | 43 + app/Listeners/Update/V30/Version3016.php | 43 + app/Listeners/Update/V30/Version3017.php | 43 + app/Listeners/Update/V30/Version303.php | 107 + app/Listeners/Update/V30/Version304.php | 111 + app/Listeners/Update/V30/Version305.php | 95 + app/Listeners/Update/V30/Version307.php | 43 + app/Listeners/Update/V30/Version309.php | 63 + app/Listeners/Update/V31/Version310.php | 43 + app/Listeners/Update/V31/Version3112.php | 43 + app/Listeners/Update/V31/Version3115.php | 43 + app/Listeners/Update/V31/Version3119.php | 58 + app/Listeners/Update/V31/Version315.php | 93 + app/Listeners/Update/V31/Version317.php | 78 + app/Listeners/Update/V31/Version318.php | 91 + app/Listeners/Update/V32/Version320.php | 61 + app/Models/Auth/Permission.php | 81 + app/Models/Auth/Role.php | 103 + app/Models/Auth/User.php | 420 + app/Models/Auth/UserCompany.php | 36 + app/Models/Auth/UserDashboard.php | 41 + app/Models/Auth/UserInvitation.php | 40 + app/Models/Auth/UserRole.php | 36 + app/Models/Banking/Account.php | 223 + app/Models/Banking/FundAccount.php | 43 + app/Models/Banking/Reconciliation.php | 89 + app/Models/Banking/Transaction.php | 784 + app/Models/Banking/TransactionTax.php | 123 + app/Models/Banking/Transfer.php | 306 + app/Models/Common/Company.php | 707 + app/Models/Common/Contact.php | 423 + app/Models/Common/ContactPerson.php | 70 + app/Models/Common/Dashboard.php | 162 + app/Models/Common/Item.php | 237 + app/Models/Common/ItemTax.php | 30 + app/Models/Common/Media.php | 33 + app/Models/Common/Notification.php | 20 + app/Models/Common/Recurring.php | 187 + app/Models/Common/Report.php | 99 + app/Models/Common/Widget.php | 138 + app/Models/Document/Document.php | 794 + app/Models/Document/DocumentHistory.php | 57 + app/Models/Document/DocumentItem.php | 157 + app/Models/Document/DocumentItemTax.php | 73 + app/Models/Document/DocumentTotal.php | 100 + app/Models/Module/Module.php | 29 + app/Models/Module/ModuleHistory.php | 22 + app/Models/Setting/Category.php | 452 + app/Models/Setting/Currency.php | 239 + app/Models/Setting/EmailTemplate.php | 114 + app/Models/Setting/Setting.php | 37 + app/Models/Setting/Tax.php | 182 + app/Models/Settings/DatabaseConnection.php | 30 + app/Notifications/Auth/Invitation.php | 53 + app/Notifications/Auth/Reset.php | 61 + app/Notifications/Banking/Transaction.php | 159 + .../Common/DownloadCompleted.php | 81 + app/Notifications/Common/DownloadFailed.php | 77 + app/Notifications/Common/ExportCompleted.php | 81 + app/Notifications/Common/ExportFailed.php | 77 + app/Notifications/Common/ImportCompleted.php | 77 + app/Notifications/Common/ImportFailed.php | 83 + app/Notifications/Email/InvalidEmail.php | 80 + app/Notifications/Install/UpdateFailed.php | 125 + app/Notifications/Portal/PaymentReceived.php | 152 + app/Notifications/Purchase/Bill.php | 113 + app/Notifications/Sale/Invoice.php | 166 + app/Observers/Transaction.php | 80 + app/Providers/App.php | 55 + app/Providers/Auth.php | 27 + app/Providers/Binding.php | 22 + app/Providers/Blade.php | 55 + app/Providers/Broadcast.php | 20 + app/Providers/Event.php | 158 + app/Providers/Macro.php | 157 + app/Providers/Observer.php | 29 + app/Providers/Queue.php | 129 + app/Providers/Route.php | 330 + app/Providers/Validation.php | 158 + app/Providers/ViewComposer.php | 55 + app/Relations/HasMany/Category.php | 25 + app/Reports/DiscountSummary.php | 131 + app/Reports/ExpenseSummary.php | 67 + app/Reports/IncomeExpenseSummary.php | 101 + app/Reports/IncomeSummary.php | 67 + app/Reports/ProfitLoss.php | 316 + app/Reports/TaxSummary.php | 227 + app/Scopes/Category.php | 26 + app/Scopes/Company.php | 51 + app/Scopes/Contact.php | 28 + app/Scopes/Document.php | 25 + app/Scopes/Transaction.php | 27 + app/Traits/Categories.php | 342 + app/Traits/Charts.php | 235 + app/Traits/Cloud.php | 47 + app/Traits/Companies.php | 208 + app/Traits/Contacts.php | 103 + app/Traits/Currencies.php | 85 + app/Traits/Database.php | 13 + app/Traits/DateTime.php | 373 + app/Traits/Documents.php | 316 + app/Traits/Emails.php | 51 + app/Traits/HasApiTokens.php | 83 + app/Traits/Import.php | 583 + app/Traits/Jobs.php | 83 + app/Traits/Media.php | 63 + app/Traits/Modules.php | 659 + app/Traits/Omnipay.php | 210 + app/Traits/Owners.php | 18 + app/Traits/Permissions.php | 514 + app/Traits/Plans.php | 104 + app/Traits/Recurring.php | 233 + app/Traits/Relationships.php | 70 + app/Traits/Scopes.php | 114 + app/Traits/SearchString.php | 196 + app/Traits/Settings.php | 30 + app/Traits/SiteApi.php | 86 + app/Traits/Sources.php | 72 + app/Traits/Tailwind.php | 113 + app/Traits/Tenants.php | 30 + app/Traits/Trackers.php | 43 + app/Traits/Transactions.php | 262 + app/Traits/Translations.php | 41 + app/Traits/Updates.php | 83 + app/Traits/Uploads.php | 216 + app/Traits/Users.php | 138 + app/Traits/ViewComponents.php | 746 + app/Utilities/BusinessNavigation.php | 39 + app/Utilities/Console.php | 56 + app/Utilities/Date.php | 140 + app/Utilities/DocumentNumber.php | 39 + app/Utilities/Export.php | 57 + app/Utilities/FundAccountSpreadsheet.php | 297 + app/Utilities/Import.php | 104 + app/Utilities/Info.php | 104 + app/Utilities/Installer.php | 324 + app/Utilities/ModuleActivator.php | 179 + app/Utilities/Modules.php | 92 + app/Utilities/Overrider.php | 140 + app/Utilities/QueueCollection.php | 10 + app/Utilities/Recurring.php | 51 + app/Utilities/Reports.php | 164 + app/Utilities/SidebarPermissions.php | 88 + app/Utilities/Str.php | 57 + app/Utilities/SystemAccess.php | 73 + app/Utilities/TransactionNumber.php | 62 + app/Utilities/Versions.php | 248 + app/Utilities/Widgets.php | 222 + app/Utilities/helpers.php | 483 + app/View/Components/Alert.php | 142 + app/View/Components/Alert/Border.php | 18 + app/View/Components/Alert/Danger.php | 18 + app/View/Components/Alert/Info.php | 18 + app/View/Components/Alert/Success.php | 18 + app/View/Components/Alert/Warning.php | 18 + app/View/Components/Button.php | 72 + app/View/Components/Button/Hover.php | 36 + app/View/Components/Contacts/Form/Address.php | 18 + app/View/Components/Contacts/Form/Billing.php | 18 + app/View/Components/Contacts/Form/Buttons.php | 18 + app/View/Components/Contacts/Form/Content.php | 18 + app/View/Components/Contacts/Form/General.php | 18 + app/View/Components/Contacts/Form/Persons.php | 18 + .../Components/Contacts/Index/Buttons.php | 18 + .../Components/Contacts/Index/Content.php | 18 + .../Components/Contacts/Index/MoreButtons.php | 18 + app/View/Components/Contacts/Script.php | 75 + app/View/Components/Contacts/Show/Buttons.php | 18 + app/View/Components/Contacts/Show/Content.php | 121 + .../Components/Contacts/Show/MoreButtons.php | 18 + app/View/Components/Date.php | 68 + app/View/Components/DeleteButton.php | 288 + app/View/Components/DeleteLink.php | 301 + .../Components/Documents/Form/Advanced.php | 18 + .../Components/Documents/Form/Buttons.php | 18 + .../Components/Documents/Form/Company.php | 22 + .../Components/Documents/Form/Contact.php | 230 + .../Components/Documents/Form/Content.php | 24 + app/View/Components/Documents/Form/Footer.php | 18 + .../Components/Documents/Form/ItemButton.php | 105 + .../Components/Documents/Form/ItemColumns.php | 31 + app/View/Components/Documents/Form/Items.php | 21 + .../Components/Documents/Form/LineItem.php | 18 + app/View/Components/Documents/Form/Main.php | 18 + .../Components/Documents/Form/Metadata.php | 18 + app/View/Components/Documents/Form/Note.php | 18 + .../Components/Documents/Form/Recurring.php | 18 + .../Documents/Form/RecurringMetadata.php | 18 + app/View/Components/Documents/Form/Totals.php | 18 + .../Components/Documents/Index/BulkAction.php | 18 + .../Components/Documents/Index/Buttons.php | 18 + .../Components/Documents/Index/Content.php | 18 + .../Components/Documents/Index/Document.php | 18 + .../Documents/Index/Information.php | 63 + .../Documents/Index/MoreButtons.php | 18 + .../Documents/Index/RecurringTemplates.php | 18 + app/View/Components/Documents/Script.php | 88 + .../Components/Documents/Show/Attachment.php | 39 + .../Components/Documents/Show/Buttons.php | 18 + .../Components/Documents/Show/Children.php | 28 + .../Components/Documents/Show/Content.php | 18 + app/View/Components/Documents/Show/Create.php | 35 + .../Components/Documents/Show/GetPaid.php | 32 + .../Components/Documents/Show/MakePayment.php | 32 + .../Components/Documents/Show/Message.php | 44 + .../Components/Documents/Show/MoreButtons.php | 18 + .../Components/Documents/Show/Receive.php | 41 + .../Components/Documents/Show/Restore.php | 39 + .../Components/Documents/Show/Schedule.php | 36 + app/View/Components/Documents/Show/Send.php | 45 + .../Components/Documents/Show/Template.php | 18 + .../Components/Documents/Template/Classic.php | 18 + .../Documents/Template/Ddefault.php | 18 + .../Documents/Template/LineItem.php | 18 + .../Components/Documents/Template/Modern.php | 18 + app/View/Components/Dropdown.php | 34 + app/View/Components/Dropdown/Button.php | 18 + app/View/Components/Dropdown/Link.php | 33 + app/View/Components/EmptyPage.php | 370 + app/View/Components/Form.php | 132 + app/View/Components/Form/Accordion.php | 46 + app/View/Components/Form/Buttons.php | 139 + app/View/Components/Form/Group/Account.php | 73 + app/View/Components/Form/Group/Attachment.php | 80 + app/View/Components/Form/Group/Category.php | 184 + app/View/Components/Form/Group/Checkbox.php | 27 + app/View/Components/Form/Group/Color.php | 20 + app/View/Components/Form/Group/Contact.php | 83 + app/View/Components/Form/Group/Country.php | 28 + app/View/Components/Form/Group/Currency.php | 60 + app/View/Components/Form/Group/Customer.php | 10 + app/View/Components/Form/Group/Date.php | 20 + app/View/Components/Form/Group/DateTime.php | 20 + app/View/Components/Form/Group/Editor.php | 20 + app/View/Components/Form/Group/Email.php | 20 + app/View/Components/Form/Group/File.php | 38 + app/View/Components/Form/Group/Import.php | 38 + .../Components/Form/Group/InvoiceText.php | 20 + app/View/Components/Form/Group/Locale.php | 26 + app/View/Components/Form/Group/Money.php | 20 + app/View/Components/Form/Group/Number.php | 20 + .../Components/Form/Group/NumberDigit.php | 53 + app/View/Components/Form/Group/Password.php | 20 + .../Components/Form/Group/PaymentMethod.php | 33 + .../Components/Form/Group/PaymentTerm.php | 42 + app/View/Components/Form/Group/Radio.php | 30 + app/View/Components/Form/Group/Recurring.php | 225 + app/View/Components/Form/Group/Select.php | 20 + app/View/Components/Form/Group/Sswitch.php | 20 + app/View/Components/Form/Group/Tax.php | 73 + app/View/Components/Form/Group/Text.php | 20 + app/View/Components/Form/Group/Textarea.php | 22 + app/View/Components/Form/Group/Time.php | 20 + app/View/Components/Form/Group/Toggle.php | 20 + app/View/Components/Form/Group/Vendor.php | 10 + app/View/Components/Form/Input/Checkbox.php | 20 + app/View/Components/Form/Input/Color.php | 20 + app/View/Components/Form/Input/Editor.php | 20 + app/View/Components/Form/Input/Email.php | 20 + app/View/Components/Form/Input/Ffile.php | 30 + app/View/Components/Form/Input/Hidden.php | 20 + app/View/Components/Form/Input/Import.php | 30 + app/View/Components/Form/Input/Input.php | 20 + app/View/Components/Form/Input/Money.php | 20 + app/View/Components/Form/Input/Number.php | 20 + app/View/Components/Form/Input/Password.php | 20 + app/View/Components/Form/Input/Radio.php | 20 + app/View/Components/Form/Input/Select.php | 20 + app/View/Components/Form/Input/Text.php | 20 + app/View/Components/Form/Input/Textarea.php | 20 + app/View/Components/Form/Section.php | 38 + app/View/Components/Icon.php | 86 + app/View/Components/Index/Balance.php | 71 + app/View/Components/Index/Bulkaction.php | 108 + app/View/Components/Index/Category.php | 120 + app/View/Components/Index/Country.php | 46 + app/View/Components/Index/Currency.php | 45 + app/View/Components/Index/Ddefault.php | 63 + app/View/Components/Index/Disable.php | 80 + app/View/Components/Index/Search.php | 121 + app/View/Components/Index/Status.php | 56 + app/View/Components/Index/Summary.php | 31 + app/View/Components/Layouts/Admin.php | 18 + app/View/Components/Layouts/Admin/Head.php | 18 + app/View/Components/Layouts/Admin/Menu.php | 51 + .../Layouts/Admin/Notifications.php | 57 + app/View/Components/Layouts/Modules.php | 23 + app/View/Components/Layouts/Modules/Bar.php | 60 + .../Components/Layouts/Modules/Releases.php | 26 + .../Components/Layouts/Modules/Reviews.php | 26 + app/View/Components/Layouts/Portal/Menu.php | 34 + app/View/Components/Layouts/Wizard.php | 18 + .../Components/Layouts/Wizard/Scripts.php | 160 + app/View/Components/Link.php | 66 + app/View/Components/Link/Hover.php | 36 + app/View/Components/Media/Ffile.php | 36 + app/View/Components/Menu/Favorite.php | 41 + app/View/Components/Modules/Banners.php | 12 + app/View/Components/Modules/Free.php | 12 + app/View/Components/Modules/Installed.php | 12 + app/View/Components/Modules/Item.php | 31 + app/View/Components/Modules/Items.php | 257 + app/View/Components/Modules/Nnew.php | 12 + app/View/Components/Modules/NoApps.php | 12 + app/View/Components/Modules/Paid.php | 12 + app/View/Components/Modules/PreSale.php | 12 + app/View/Components/Modules/Purchased.php | 12 + app/View/Components/Pagination.php | 53 + app/View/Components/PaymentMethod.php | 81 + app/View/Components/Script.php | 72 + app/View/Components/Script/Declaration.php | 65 + .../Components/Script/Exceptions/Trackers.php | 144 + app/View/Components/SearchString.php | 292 + app/View/Components/Show/Accordion.php | 46 + app/View/Components/Show/NoRecords.php | 144 + app/View/Components/Show/Right.php | 21 + app/View/Components/Show/Status.php | 56 + app/View/Components/Suggestions.php | 81 + app/View/Components/Table/Actions.php | 114 + app/View/Components/Table/Tbody.php | 18 + app/View/Components/Table/Td.php | 80 + app/View/Components/Table/Th.php | 75 + app/View/Components/Table/Thead.php | 18 + app/View/Components/Table/Tr.php | 61 + app/View/Components/Tips.php | 104 + app/View/Components/Title.php | 46 + app/View/Components/Tooltip.php | 163 + .../Transactions/Index/BulkAction.php | 18 + .../Components/Transactions/Index/Buttons.php | 18 + .../Components/Transactions/Index/Content.php | 18 + .../Transactions/Index/MoreButtons.php | 18 + .../Transactions/Index/RecurringTemplates.php | 18 + .../Transactions/Index/Transaction.php | 18 + app/View/Components/Transactions/Script.php | 56 + .../Transactions/Show/Attachment.php | 18 + .../Components/Transactions/Show/Buttons.php | 18 + .../Components/Transactions/Show/Children.php | 18 + .../Components/Transactions/Show/Connect.php | 18 + .../Components/Transactions/Show/Content.php | 18 + .../Components/Transactions/Show/Create.php | 18 + .../Transactions/Show/MoreButtons.php | 18 + .../Components/Transactions/Show/Schedule.php | 18 + .../Components/Transactions/Show/Template.php | 18 + .../Components/Transactions/Show/Transfer.php | 18 + .../Transactions/Template/Ddefault.php | 18 + app/View/Components/Transfers/Script.php | 55 + .../Components/Transfers/Show/Attachment.php | 18 + .../Components/Transfers/Show/Buttons.php | 18 + .../Components/Transfers/Show/Content.php | 18 + app/View/Components/Transfers/Show/Create.php | 18 + .../Components/Transfers/Show/MoreButtons.php | 18 + .../Components/Transfers/Show/Template.php | 18 + .../Transfers/Show/Transactions.php | 18 + .../Transfers/Template/Ddefault.php | 18 + .../Components/Transfers/Template/Second.php | 18 + .../Components/Transfers/Template/Third.php | 18 + app/View/Components/UpdateAlert.php | 73 + app/View/Components/Widgets/Contact.php | 52 + .../Components/Widgets/InvoiceHistory.php | 50 + app/View/Components/Widgets/LastPayment.php | 50 + .../Components/Widgets/LatestInvoices.php | 50 + .../Components/Widgets/OutstandingBalance.php | 32 + .../Components/Widgets/PaymentHistory.php | 50 + app/View/Presenters/Menu.php | 280 + app/Widgets/AccountBalance.php | 46 + app/Widgets/BankFeeds.php | 24 + app/Widgets/CashFlow.php | 217 + app/Widgets/Currencies.php | 27 + app/Widgets/ExpensesByCategory.php | 44 + app/Widgets/Payables.php | 86 + app/Widgets/ProfitLoss.php | 196 + app/Widgets/Receivables.php | 86 + artisan | 29 + bootstrap/app.php | 55 + bootstrap/autoload.php | 20 + bootstrap/cache/.gitignore | 2 + compose.jiangling.yml | 22 + composer.json | 223 + composer.lock | 15900 +++++ config/apexcharts.php | 80 + config/api.php | 159 + config/app.php | 225 + config/auth.php | 126 + config/broadcasting.php | 70 + config/bugsnag.php | 364 + config/business_navigation.php | 277 + config/cache.php | 110 + config/charts.php | 15 + config/cors.php | 34 + config/database.php | 219 + config/debugbar.php | 278 + config/dompdf.php | 284 + config/excel.php | 355 + config/filesystems.php | 136 + config/firewall.php | 466 + config/hashing.php | 52 + config/ide-helper.php | 246 + config/image.php | 20 + config/imagecache.php | 71 + config/import.php | 36 + config/language.php | 231 + config/laratrust.php | 360 + config/laravel-model-caching.php | 13 + config/livewire.php | 159 + config/logging.php | 161 + config/mail.php | 133 + config/mediable.php | 236 + config/menu.php | 16 + config/module.php | 200 + config/money.php | 1843 + config/queue.php | 94 + config/read-only.php | 50 + config/sanctum.php | 67 + config/search-string.php | 592 + config/sentry.php | 112 + config/services.php | 37 + config/session.php | 198 + config/setting.php | 223 + config/sortable.php | 79 + config/stats.php | 96 + config/trustedproxy.php | 7 + config/type.php | 619 + config/update.php | 24 + config/version.php | 25 + config/view.php | 36 + database/.gitignore | 1 + database/factories/Account.php | 76 + database/factories/Category.php | 125 + database/factories/Company.php | 56 + database/factories/Contact.php | 95 + database/factories/Currency.php | 77 + database/factories/Dashboard.php | 79 + database/factories/Document.php | 370 + database/factories/Item.php | 62 + database/factories/Reconciliation.php | 64 + database/factories/Tax.php | 119 + database/factories/Transaction.php | 147 + database/factories/Transfer.php | 41 + database/factories/User.php | 76 + database/factories/Widget.php | 36 + .../migrations/2017_09_14_000000_core_v1.php | 535 + .../migrations/2019_11_16_000000_core_v2.php | 460 + .../2022_05_10_000000_core_v300.php | 105 + .../2022_06_28_000000_core_v304.php | 30 + .../2022_07_21_000000_core_v305.php | 34 + .../2022_08_29_000000_core_v3015.php | 53 + .../2022_08_29_000000_core_v307.php | 42 + .../2023_06_22_000000_core_v3016.php | 64 + .../2023_08_14_000000_core_v3017.php | 34 + .../2023_10_03_000000_core_v310.php | 62 + .../2024_09_26_000000_core_v3112.php | 34 + .../2024_12_29_000000_core_v3115.php | 34 + .../2026_02_17_000000_core_v3122.php | 56 + ...7_18_000000_create_fund_accounts_table.php | 36 + ...0_000000_create_system_settings_tables.php | 123 + database/seeds/Accounts.php | 44 + database/seeds/Categories.php | 104 + database/seeds/Company.php | 24 + database/seeds/Currencies.php | 53 + database/seeds/Dashboards.php | 49 + database/seeds/DatabaseSeeder.php | 16 + database/seeds/EmailTemplates.php | 107 + database/seeds/Modules.php | 41 + database/seeds/Permissions.php | 174 + database/seeds/Reports.php | 76 + database/seeds/SampleData.php | 67 + database/seeds/Settings.php | 51 + database/seeds/TestCompany.php | 135 + database/seeds/User.php | 18 + deploy/Dockerfile | 52 + deploy/apache-vhost.conf | 16 + deploy/entrypoint.sh | 29 + deploy/host-nginx.conf | 19 + deploy/php.ini | 10 + docs/ecommerce-finance-development.md | 320 + docs/team-collaboration.md | 77 + index.php | 38 + manifest.json | 125 + modules/.gitkeep | 0 nginx.example.com.conf | 54 + .../Translation/MessageSelector.php | 412 + .../View/Concerns/ManagesStacks.php | 201 + .../laravel-module/Commands/DeleteCommand.php | 22 + .../Commands/DisableCommand.php | 59 + .../laravel-module/Commands/EnableCommand.php | 59 + .../Commands/InstallCommand.php | 59 + .../HandleRequests/HandleRequests.php | 107 + .../maatwebsite/excel/Fakes/ExcelFake.php | 358 + overrides/maatwebsite/excel/Fakes/fake_file | 0 overrides/maatwebsite/excel/QueuedWriter.php | 215 + .../symfony/process/PhpExecutableFinder.php | 177 + package-lock.json | 21322 ++++++ package.json | 105 + phpunit.xml | 34 + presets.js | 537 + public/.htaccess | 21 + public/akaunting-js/generalAction.js | 514 + public/akaunting-js/hotkeys.js | 62 + public/akaunting-js/popper.js | 6 + public/akaunting-js/swiper-bundle.min.js | 14 + public/css/app.css | 5 + public/css/custom_loading.css | 99 + public/css/element.css | 15726 +++++ public/css/fonts/NotoSansArabic.ttf | Bin 0 -> 844676 bytes public/css/fonts/element-icons.ttf | Bin 0 -> 55956 bytes public/css/fonts/element-icons.woff | Bin 0 -> 28200 bytes public/css/fonts/firefly_sung_normal.ttf | Bin 0 -> 15443432 bytes .../MaterialIconsOutlined-Regular.eot | Bin 0 -> 357942 bytes .../MaterialIconsOutlined-Regular.otf | Bin 0 -> 314916 bytes .../MaterialIconsOutlined-Regular.ttf | Bin 0 -> 357668 bytes .../MaterialIconsOutlined-Regular.woff | Bin 0 -> 172364 bytes .../MaterialIconsOutlined-Regular.woff2 | Bin 0 -> 130748 bytes .../Regular/MaterialIcons-Regular.eot | Bin 0 -> 327556 bytes .../Regular/MaterialIcons-Regular.otf | Bin 0 -> 702804 bytes .../Regular/MaterialIcons-Regular.ttf | Bin 0 -> 327360 bytes .../Regular/MaterialIcons-Regular.woff | Bin 0 -> 166412 bytes .../Regular/MaterialIcons-Regular.woff2 | Bin 0 -> 118032 bytes .../Rounded/MaterialIconsRound-Regular.eot | Bin 0 -> 424658 bytes .../Rounded/MaterialIconsRound-Regular.otf | Bin 0 -> 373784 bytes .../Rounded/MaterialIconsRound-Regular.ttf | Bin 0 -> 424396 bytes .../Rounded/MaterialIconsRound-Regular.woff | Bin 0 -> 196788 bytes .../Rounded/MaterialIconsRound-Regular.woff2 | Bin 0 -> 146364 bytes .../Sharp/MaterialIconsSharp-Regular.eot | Bin 0 -> 304070 bytes .../Sharp/MaterialIconsSharp-Regular.otf | Bin 0 -> 266260 bytes .../Sharp/MaterialIconsSharp-Regular.ttf | Bin 0 -> 303808 bytes .../Sharp/MaterialIconsSharp-Regular.woff | Bin 0 -> 149932 bytes .../Sharp/MaterialIconsSharp-Regular.woff2 | Bin 0 -> 114500 bytes public/css/fonts/material-icons/style.css | 141 + public/css/print.css | 1137 + public/css/third_party/dropzone_custom.css | 138 + public/css/third_party/swiper-bundle.min.css | 1 + public/css/third_party/vue-html-editor.css | 945 + .../third_party/vue-html-editor_custom.css | 288 + public/files/import/bills.xlsx | Bin 0 -> 15282 bytes public/files/import/categories.xlsx | Bin 0 -> 9666 bytes public/files/import/customers.xlsx | Bin 0 -> 9475 bytes public/files/import/invoices.xlsx | Bin 0 -> 15402 bytes public/files/import/items.xlsx | Bin 0 -> 10532 bytes public/files/import/recurring-bills.xlsx | Bin 0 -> 14619 bytes public/files/import/recurring-invoices.xlsx | Bin 0 -> 14169 bytes .../files/import/recurring-transactions.xlsx | Bin 0 -> 10231 bytes public/files/import/taxes.xlsx | Bin 0 -> 9598 bytes public/files/import/transactions.xlsx | Bin 0 -> 10206 bytes public/files/import/transfers.xlsx | Bin 0 -> 10356 bytes public/files/import/vendors.xlsx | Bin 0 -> 9611 bytes public/fonts/MaterialIcons-Regular.eot | Bin 0 -> 356347 bytes public/fonts/MaterialIcons-Regular.ttf | Bin 0 -> 327360 bytes public/fonts/MaterialIcons-Regular.woff | Bin 0 -> 166412 bytes public/fonts/MaterialIcons-Regular.woff2 | Bin 0 -> 356348 bytes public/img/akaunting-loading.gif | Bin 0 -> 104457 bytes public/img/akaunting-logo-gold.png | Bin 0 -> 7763 bytes public/img/akaunting-logo-green.svg | 1 + public/img/akaunting-logo-horizontal.svg | 61 + public/img/akaunting-logo-purple.svg | 6 + public/img/akaunting-logo-white.svg | 1 + public/img/akaunting-logo-wild-blue.png | Bin 0 -> 3929 bytes public/img/auth/bullet_active.png | Bin 0 -> 200 bytes public/img/auth/bullet_inactive.png | Bin 0 -> 212 bytes public/img/auth/chart.png | Bin 0 -> 165877 bytes public/img/auth/client.png | Bin 0 -> 71910 bytes public/img/auth/folder.png | Bin 0 -> 125125 bytes public/img/auth/layout.png | Bin 0 -> 160811 bytes public/img/auth/login-bg.png | Bin 0 -> 198921 bytes public/img/auth/logo.png | Bin 0 -> 1546 bytes public/img/auth/small-logo.png | Bin 0 -> 1629 bytes public/img/company.png | Bin 0 -> 13098 bytes public/img/credit_card/amex.png | Bin 0 -> 3232 bytes public/img/credit_card/chip.png | Bin 0 -> 8992 bytes public/img/credit_card/dinersclub.png | Bin 0 -> 2292 bytes public/img/credit_card/discover.png | Bin 0 -> 3372 bytes public/img/credit_card/jcb.png | Bin 0 -> 3587 bytes public/img/credit_card/mastercard.png | Bin 0 -> 5693 bytes public/img/credit_card/troy.png | Bin 0 -> 2618 bytes public/img/credit_card/unionpay.png | Bin 0 -> 6697 bytes public/img/credit_card/visa.png | Bin 0 -> 3580 bytes public/img/dashboard.png | Bin 0 -> 258828 bytes public/img/empty_pages/bills.png | Bin 0 -> 110926 bytes public/img/empty_pages/companies.png | Bin 0 -> 101712 bytes public/img/empty_pages/contacs.png | Bin 0 -> 115862 bytes public/img/empty_pages/customers.png | Bin 0 -> 68929 bytes public/img/empty_pages/default.png | Bin 0 -> 86016 bytes public/img/empty_pages/invoices.png | Bin 0 -> 62276 bytes public/img/empty_pages/items.png | Bin 0 -> 93852 bytes public/img/empty_pages/no-apps.png | Bin 0 -> 383332 bytes public/img/empty_pages/payments.png | Bin 0 -> 77376 bytes public/img/empty_pages/reconciliations.png | Bin 0 -> 88797 bytes .../img/empty_pages/recurring_templates.png | Bin 0 -> 115169 bytes public/img/empty_pages/revenues.png | Bin 0 -> 76728 bytes public/img/empty_pages/subscriptions.png | Bin 0 -> 75953 bytes public/img/empty_pages/taxes.png | Bin 0 -> 76693 bytes public/img/empty_pages/tickets.png | Bin 0 -> 72006 bytes public/img/empty_pages/transactions.png | Bin 0 -> 103418 bytes public/img/empty_pages/transfers.png | Bin 0 -> 90627 bytes public/img/empty_pages/users.png | Bin 0 -> 1083035 bytes public/img/empty_pages/vendors.png | Bin 0 -> 59446 bytes public/img/errors/403.png | Bin 0 -> 175499 bytes public/img/errors/404.png | Bin 0 -> 142162 bytes public/img/errors/500.png | Bin 0 -> 169358 bytes public/img/favicon.ico | Bin 0 -> 99678 bytes public/img/icons/.gitkeep | 0 public/img/import.png | Bin 0 -> 158858 bytes public/img/invoice_templates/classic.png | Bin 0 -> 37157 bytes public/img/invoice_templates/default.png | Bin 0 -> 36208 bytes public/img/invoice_templates/modern.png | Bin 0 -> 38307 bytes .../img/no_records/accounts_transactions.png | Bin 0 -> 19970 bytes public/img/no_records/accounts_transfers.png | Bin 0 -> 16268 bytes public/img/no_records/customers_invoices.png | Bin 0 -> 37289 bytes .../img/no_records/customers_transactions.png | Bin 0 -> 22302 bytes public/img/no_records/vendors_bills.png | Bin 0 -> 17408 bytes .../img/no_records/vendors_transactions.png | Bin 0 -> 27421 bytes public/img/pwa/icon-192x192-maskable.png | Bin 0 -> 5254 bytes public/img/pwa/icon-192x192.png | Bin 0 -> 7919 bytes public/img/pwa/icon-512x512-maskable.png | Bin 0 -> 13504 bytes public/img/pwa/icon-512x512.png | Bin 0 -> 21439 bytes public/img/pwa/screenshot-dashboard.png | Bin 0 -> 374201 bytes public/img/pwa/screenshot-invoice.png | Bin 0 -> 479388 bytes public/img/pwa/splash-1242x2208.png | Bin 0 -> 15478 bytes public/img/pwa/splash-1242x2688.png | Bin 0 -> 16284 bytes public/img/pwa/splash-1536x2048.png | Bin 0 -> 16123 bytes public/img/pwa/splash-1668x2224.png | Bin 0 -> 16983 bytes public/img/pwa/splash-1668x2388.png | Bin 0 -> 17311 bytes public/img/pwa/splash-2048x2732.png | Bin 0 -> 19157 bytes public/img/pwa/splash-640x1136.png | Bin 0 -> 8499 bytes public/img/pwa/splash-750x1334.png | Bin 0 -> 10198 bytes public/img/pwa/splash-828x1792.png | Bin 0 -> 11775 bytes public/img/tailwind_icons/not-equal.svg | 5 + public/img/transfer_templates/default.png | Bin 0 -> 24272 bytes public/img/transfer_templates/second.png | Bin 0 -> 23845 bytes public/img/transfer_templates/third.png | Bin 0 -> 22133 bytes public/img/user.svg | 1 + public/img/wizard-modules.png | Bin 0 -> 78154 bytes public/img/wizard-rocket.gif | Bin 0 -> 1181401 bytes public/index.php | 76 + public/money.json | 58961 ++++++++++++++++ public/shortcuts-config.json | 30 + resources/assets/css/creditcard/style.scss | 685 + resources/assets/js/Install.vue | 8 + resources/assets/js/Wizard.vue | 48 + resources/assets/js/bootstrap.js | 69 + .../assets/js/components/AkauntingColor.vue | 289 + .../js/components/AkauntingCompanyEdit.vue | 244 + .../AkauntingConnectTransactions.vue | 542 + .../js/components/AkauntingContactCard.vue | 689 + .../js/components/AkauntingCountdown.vue | 141 + .../AkauntingCurrencyConversion.vue | 117 + .../assets/js/components/AkauntingDate.vue | 221 + .../js/components/AkauntingDocumentButton.vue | 321 + .../AkauntingDropzoneFileUpload.vue | 292 + .../components/AkauntingEditItemColumns.vue | 172 + .../js/components/AkauntingHtmlEditor.vue | 112 + .../assets/js/components/AkauntingImport.vue | 254 + .../js/components/AkauntingItemButton.vue | 669 + .../assets/js/components/AkauntingModal.vue | 180 + .../js/components/AkauntingModalAddNew.vue | 381 + .../assets/js/components/AkauntingMoney.vue | 250 + .../js/components/AkauntingRadioGroup.vue | 87 + .../js/components/AkauntingRecurring.vue | 394 + .../assets/js/components/AkauntingSearch.vue | 1363 + .../assets/js/components/AkauntingSelect.vue | 1358 + .../js/components/AkauntingSelectRemote.vue | 1689 + .../assets/js/components/AkauntingSlider.vue | 125 + .../assets/js/components/AkauntingSwitch.vue | 77 + .../assets/js/components/AkauntingWidget.vue | 282 + resources/assets/js/components/Badge.vue | 51 + resources/assets/js/components/BaseAlert.vue | 74 + resources/assets/js/components/BaseButton.vue | 71 + resources/assets/js/components/Cards/Card.vue | 70 + .../assets/js/components/Cards/StatsCard.vue | 48 + .../assets/js/components/CreditCard/Card.vue | 269 + .../js/components/CreditCard/CardForm.vue | 719 + .../assets/js/components/Inputs/BaseInput.vue | 189 + resources/assets/js/components/Modal.vue | 125 + .../NotificationPlugin/Notification.vue | 259 + .../NotificationPlugin/Notifications.vue | 63 + .../js/components/NotificationPlugin/index.js | 81 + .../js/components/SearchString/FilterMain.vue | 301 + .../components/SearchString/FilteredList.vue | 125 + .../js/components/SearchString/Filters.vue | 389 + .../components/SearchString/SearchString.vue | 802 + .../assets/js/directives/click-ouside.js | 15 + .../assets/js/exceptions/trackers/bugsnag.js | 1 + .../assets/js/exceptions/trackers/sentry.js | 43 + resources/assets/js/install.js | 89 + resources/assets/js/mixins/global.js | 1651 + resources/assets/js/mixins/wizardAction.js | 210 + resources/assets/js/plugins/bulk-action.js | 252 + resources/assets/js/plugins/currency.js | 114 + .../assets/js/plugins/dashboard-plugin.js | 35 + resources/assets/js/plugins/error.js | 38 + resources/assets/js/plugins/form.js | 509 + resources/assets/js/plugins/functions.js | 43 + .../assets/js/plugins/globalComponents.js | 29 + .../assets/js/plugins/globalDirectives.js | 13 + resources/assets/js/plugins/money.js | 138 + .../assets/js/plugins/nprogress-axios.js | 28 + resources/assets/js/polyfills.js | 96 + resources/assets/js/views/auth/common.js | 142 + resources/assets/js/views/auth/users.js | 76 + resources/assets/js/views/banking/accounts.js | 34 + .../js/views/banking/reconciliations.js | 129 + .../assets/js/views/banking/transactions.js | 165 + .../assets/js/views/banking/transfers.js | 156 + resources/assets/js/views/common/companies.js | 87 + resources/assets/js/views/common/contacts.js | 166 + .../assets/js/views/common/dashboards.js | 227 + resources/assets/js/views/common/documents.js | 1087 + resources/assets/js/views/common/imports.js | 31 + resources/assets/js/views/common/items.js | 134 + resources/assets/js/views/common/reports.js | 93 + .../assets/js/views/install/Database.vue | 192 + .../assets/js/views/install/Language.vue | 79 + .../assets/js/views/install/Requirements.vue | 50 + .../assets/js/views/install/Settings.vue | 179 + resources/assets/js/views/install/Steps.vue | 48 + resources/assets/js/views/install/update.js | 152 + resources/assets/js/views/modules/apps.js | 334 + resources/assets/js/views/portal/apps.js | 288 + .../assets/js/views/settings/categories.js | 60 + .../assets/js/views/settings/currencies.js | 88 + .../assets/js/views/settings/settings.js | 181 + resources/assets/js/views/settings/taxes.js | 40 + resources/assets/js/views/wizard/Company.vue | 385 + .../assets/js/views/wizard/Currencies.vue | 421 + resources/assets/js/views/wizard/Finish.vue | 175 + resources/assets/js/views/wizard/Steps.vue | 89 + resources/assets/js/wizard.js | 65 + resources/assets/sass/app.css | 873 + resources/lang/ar-SA/accounts.php | 31 + resources/lang/ar-SA/auth.php | 93 + resources/lang/ar-SA/bills.php | 63 + resources/lang/ar-SA/bulk_actions.php | 27 + resources/lang/ar-SA/categories.php | 11 + resources/lang/ar-SA/companies.php | 22 + resources/lang/ar-SA/countries.php | 256 + resources/lang/ar-SA/currencies.php | 27 + resources/lang/ar-SA/customers.php | 31 + resources/lang/ar-SA/dashboards.php | 15 + resources/lang/ar-SA/demo.php | 31 + resources/lang/ar-SA/documents.php | 104 + resources/lang/ar-SA/email_templates.php | 65 + resources/lang/ar-SA/errors.php | 25 + resources/lang/ar-SA/footer.php | 13 + resources/lang/ar-SA/general.php | 353 + resources/lang/ar-SA/header.php | 33 + resources/lang/ar-SA/import.php | 18 + resources/lang/ar-SA/install.php | 51 + resources/lang/ar-SA/invoices.php | 82 + resources/lang/ar-SA/items.php | 16 + resources/lang/ar-SA/maintenance.php | 11 + resources/lang/ar-SA/messages.php | 53 + resources/lang/ar-SA/modules.php | 133 + resources/lang/ar-SA/notifications.php | 250 + resources/lang/ar-SA/pagination.php | 9 + resources/lang/ar-SA/passwords.php | 22 + resources/lang/ar-SA/portal.php | 53 + resources/lang/ar-SA/reconciliations.php | 22 + resources/lang/ar-SA/recurring.php | 43 + resources/lang/ar-SA/reports.php | 45 + resources/lang/ar-SA/search_string.php | 21 + resources/lang/ar-SA/settings.php | 209 + resources/lang/ar-SA/taxes.php | 20 + resources/lang/ar-SA/transactions.php | 58 + resources/lang/ar-SA/transfers.php | 34 + resources/lang/ar-SA/updates.php | 15 + resources/lang/ar-SA/validation.php | 146 + resources/lang/ar-SA/vendors.php | 16 + resources/lang/ar-SA/widgets.php | 34 + resources/lang/az-AZ/accounts.php | 14 + resources/lang/az-AZ/auth.php | 41 + resources/lang/az-AZ/bills.php | 57 + resources/lang/az-AZ/bulk_actions.php | 23 + resources/lang/az-AZ/companies.php | 14 + resources/lang/az-AZ/countries.php | 253 + resources/lang/az-AZ/currencies.php | 19 + resources/lang/az-AZ/customers.php | 12 + resources/lang/az-AZ/dashboards.php | 11 + resources/lang/az-AZ/demo.php | 31 + resources/lang/az-AZ/documents.php | 54 + resources/lang/az-AZ/email_templates.php | 50 + resources/lang/az-AZ/errors.php | 23 + resources/lang/az-AZ/footer.php | 10 + resources/lang/az-AZ/general.php | 231 + resources/lang/az-AZ/header.php | 16 + resources/lang/az-AZ/import.php | 9 + resources/lang/az-AZ/install.php | 46 + resources/lang/az-AZ/invoices.php | 60 + resources/lang/az-AZ/items.php | 9 + resources/lang/az-AZ/maintenance.php | 9 + resources/lang/az-AZ/messages.php | 37 + resources/lang/az-AZ/modules.php | 83 + resources/lang/az-AZ/notifications.php | 10 + resources/lang/az-AZ/pagination.php | 9 + resources/lang/az-AZ/passwords.php | 22 + resources/lang/az-AZ/payments.php | 10 + resources/lang/az-AZ/reconciliations.php | 18 + resources/lang/az-AZ/recurring.php | 20 + resources/lang/az-AZ/reports.php | 28 + resources/lang/az-AZ/search_string.php | 20 + resources/lang/az-AZ/settings.php | 144 + resources/lang/az-AZ/taxes.php | 12 + resources/lang/az-AZ/transfers.php | 12 + resources/lang/az-AZ/updates.php | 15 + resources/lang/az-AZ/validation.php | 123 + resources/lang/az-AZ/widgets.php | 24 + resources/lang/bg-BG/accounts.php | 31 + resources/lang/bg-BG/auth.php | 93 + resources/lang/bg-BG/bills.php | 63 + resources/lang/bg-BG/bulk_actions.php | 27 + resources/lang/bg-BG/categories.php | 11 + resources/lang/bg-BG/companies.php | 22 + resources/lang/bg-BG/countries.php | 256 + resources/lang/bg-BG/currencies.php | 27 + resources/lang/bg-BG/customers.php | 31 + resources/lang/bg-BG/dashboards.php | 15 + resources/lang/bg-BG/demo.php | 31 + resources/lang/bg-BG/documents.php | 105 + resources/lang/bg-BG/email_templates.php | 64 + resources/lang/bg-BG/errors.php | 25 + resources/lang/bg-BG/footer.php | 13 + resources/lang/bg-BG/general.php | 355 + resources/lang/bg-BG/header.php | 34 + resources/lang/bg-BG/import.php | 18 + resources/lang/bg-BG/install.php | 51 + resources/lang/bg-BG/invoices.php | 83 + resources/lang/bg-BG/items.php | 17 + resources/lang/bg-BG/maintenance.php | 11 + resources/lang/bg-BG/messages.php | 51 + resources/lang/bg-BG/modules.php | 137 + resources/lang/bg-BG/notifications.php | 250 + resources/lang/bg-BG/pagination.php | 9 + resources/lang/bg-BG/passwords.php | 22 + resources/lang/bg-BG/portal.php | 53 + resources/lang/bg-BG/reconciliations.php | 22 + resources/lang/bg-BG/recurring.php | 44 + resources/lang/bg-BG/reports.php | 39 + resources/lang/bg-BG/search_string.php | 21 + resources/lang/bg-BG/settings.php | 210 + resources/lang/bg-BG/taxes.php | 20 + resources/lang/bg-BG/transactions.php | 51 + resources/lang/bg-BG/transfers.php | 35 + resources/lang/bg-BG/updates.php | 15 + resources/lang/bg-BG/validation.php | 177 + resources/lang/bg-BG/vendors.php | 16 + resources/lang/bg-BG/widgets.php | 34 + resources/lang/bn-BD/accounts.php | 18 + resources/lang/bn-BD/auth.php | 43 + resources/lang/bn-BD/bills.php | 56 + resources/lang/bn-BD/bulk_actions.php | 21 + resources/lang/bn-BD/companies.php | 14 + resources/lang/bn-BD/countries.php | 253 + resources/lang/bn-BD/currencies.php | 18 + resources/lang/bn-BD/customers.php | 12 + resources/lang/bn-BD/dashboards.php | 11 + resources/lang/bn-BD/demo.php | 31 + resources/lang/bn-BD/email_templates.php | 50 + resources/lang/bn-BD/errors.php | 23 + resources/lang/bn-BD/footer.php | 10 + resources/lang/bn-BD/general.php | 209 + resources/lang/bn-BD/header.php | 16 + resources/lang/bn-BD/import.php | 9 + resources/lang/bn-BD/install.php | 46 + resources/lang/bn-BD/invoices.php | 60 + resources/lang/bn-BD/items.php | 8 + resources/lang/bn-BD/maintenance.php | 11 + resources/lang/bn-BD/messages.php | 37 + resources/lang/bn-BD/modules.php | 83 + resources/lang/bn-BD/notifications.php | 10 + resources/lang/bn-BD/pagination.php | 9 + resources/lang/bn-BD/passwords.php | 23 + resources/lang/bn-BD/reconciliations.php | 16 + resources/lang/bn-BD/recurring.php | 20 + resources/lang/bn-BD/reports.php | 27 + resources/lang/bn-BD/settings.php | 129 + resources/lang/bn-BD/taxes.php | 12 + resources/lang/bn-BD/transfers.php | 12 + resources/lang/bn-BD/updates.php | 15 + resources/lang/bn-BD/validation.php | 124 + resources/lang/bn-BD/widgets.php | 23 + resources/lang/bs-BA/accounts.php | 31 + resources/lang/bs-BA/auth.php | 92 + resources/lang/bs-BA/bills.php | 63 + resources/lang/bs-BA/bulk_actions.php | 26 + resources/lang/bs-BA/categories.php | 11 + resources/lang/bs-BA/companies.php | 22 + resources/lang/bs-BA/countries.php | 256 + resources/lang/bs-BA/currencies.php | 27 + resources/lang/bs-BA/customers.php | 30 + resources/lang/bs-BA/dashboards.php | 15 + resources/lang/bs-BA/demo.php | 31 + resources/lang/bs-BA/documents.php | 86 + resources/lang/bs-BA/email_templates.php | 65 + resources/lang/bs-BA/errors.php | 23 + resources/lang/bs-BA/footer.php | 13 + resources/lang/bs-BA/general.php | 311 + resources/lang/bs-BA/header.php | 33 + resources/lang/bs-BA/import.php | 10 + resources/lang/bs-BA/install.php | 50 + resources/lang/bs-BA/invoices.php | 85 + resources/lang/bs-BA/items.php | 17 + resources/lang/bs-BA/maintenance.php | 11 + resources/lang/bs-BA/messages.php | 46 + resources/lang/bs-BA/modules.php | 126 + resources/lang/bs-BA/notifications.php | 181 + resources/lang/bs-BA/pagination.php | 9 + resources/lang/bs-BA/passwords.php | 22 + resources/lang/bs-BA/payments.php | 10 + resources/lang/bs-BA/portal.php | 52 + resources/lang/bs-BA/reconciliations.php | 22 + resources/lang/bs-BA/recurring.php | 41 + resources/lang/bs-BA/reports.php | 43 + resources/lang/bs-BA/revenues.php | 10 + resources/lang/bs-BA/search_string.php | 21 + resources/lang/bs-BA/settings.php | 203 + resources/lang/bs-BA/taxes.php | 20 + resources/lang/bs-BA/transactions.php | 45 + resources/lang/bs-BA/transfers.php | 30 + resources/lang/bs-BA/updates.php | 15 + resources/lang/bs-BA/validation.php | 159 + resources/lang/bs-BA/vendors.php | 16 + resources/lang/bs-BA/widgets.php | 34 + resources/lang/ca-ES/accounts.php | 31 + resources/lang/ca-ES/auth.php | 93 + resources/lang/ca-ES/bills.php | 63 + resources/lang/ca-ES/bulk_actions.php | 27 + resources/lang/ca-ES/categories.php | 11 + resources/lang/ca-ES/companies.php | 22 + resources/lang/ca-ES/countries.php | 256 + resources/lang/ca-ES/currencies.php | 27 + resources/lang/ca-ES/customers.php | 31 + resources/lang/ca-ES/dashboards.php | 15 + resources/lang/ca-ES/demo.php | 31 + resources/lang/ca-ES/documents.php | 103 + resources/lang/ca-ES/email_templates.php | 64 + resources/lang/ca-ES/errors.php | 25 + resources/lang/ca-ES/footer.php | 13 + resources/lang/ca-ES/general.php | 350 + resources/lang/ca-ES/header.php | 33 + resources/lang/ca-ES/import.php | 18 + resources/lang/ca-ES/install.php | 51 + resources/lang/ca-ES/invoices.php | 83 + resources/lang/ca-ES/items.php | 17 + resources/lang/ca-ES/maintenance.php | 11 + resources/lang/ca-ES/messages.php | 52 + resources/lang/ca-ES/modules.php | 136 + resources/lang/ca-ES/notifications.php | 251 + resources/lang/ca-ES/pagination.php | 9 + resources/lang/ca-ES/passwords.php | 22 + resources/lang/ca-ES/payments.php | 10 + resources/lang/ca-ES/portal.php | 53 + resources/lang/ca-ES/reconciliations.php | 22 + resources/lang/ca-ES/recurring.php | 43 + resources/lang/ca-ES/reports.php | 40 + resources/lang/ca-ES/revenues.php | 10 + resources/lang/ca-ES/search_string.php | 21 + resources/lang/ca-ES/settings.php | 209 + resources/lang/ca-ES/taxes.php | 20 + resources/lang/ca-ES/transactions.php | 58 + resources/lang/ca-ES/transfers.php | 34 + resources/lang/ca-ES/updates.php | 15 + resources/lang/ca-ES/validation.php | 177 + resources/lang/ca-ES/vendors.php | 16 + resources/lang/ca-ES/widgets.php | 34 + resources/lang/cs-CZ/accounts.php | 31 + resources/lang/cs-CZ/auth.php | 93 + resources/lang/cs-CZ/bills.php | 63 + resources/lang/cs-CZ/bulk_actions.php | 26 + resources/lang/cs-CZ/categories.php | 11 + resources/lang/cs-CZ/companies.php | 22 + resources/lang/cs-CZ/countries.php | 256 + resources/lang/cs-CZ/currencies.php | 27 + resources/lang/cs-CZ/customers.php | 31 + resources/lang/cs-CZ/dashboard.php | 23 + resources/lang/cs-CZ/dashboards.php | 15 + resources/lang/cs-CZ/demo.php | 31 + resources/lang/cs-CZ/documents.php | 103 + resources/lang/cs-CZ/email_templates.php | 64 + resources/lang/cs-CZ/errors.php | 25 + resources/lang/cs-CZ/footer.php | 13 + resources/lang/cs-CZ/general.php | 339 + resources/lang/cs-CZ/header.php | 33 + resources/lang/cs-CZ/import.php | 13 + resources/lang/cs-CZ/install.php | 51 + resources/lang/cs-CZ/invoices.php | 83 + resources/lang/cs-CZ/items.php | 17 + resources/lang/cs-CZ/maintenance.php | 11 + resources/lang/cs-CZ/messages.php | 50 + resources/lang/cs-CZ/modules.php | 136 + resources/lang/cs-CZ/notifications.php | 218 + resources/lang/cs-CZ/pagination.php | 9 + resources/lang/cs-CZ/passwords.php | 22 + resources/lang/cs-CZ/portal.php | 53 + resources/lang/cs-CZ/reconciliations.php | 22 + resources/lang/cs-CZ/recurring.php | 43 + resources/lang/cs-CZ/reports.php | 39 + resources/lang/cs-CZ/search_string.php | 21 + resources/lang/cs-CZ/settings.php | 203 + resources/lang/cs-CZ/taxes.php | 20 + resources/lang/cs-CZ/transactions.php | 50 + resources/lang/cs-CZ/transfers.php | 34 + resources/lang/cs-CZ/updates.php | 15 + resources/lang/cs-CZ/validation.php | 175 + resources/lang/cs-CZ/vendors.php | 16 + resources/lang/cs-CZ/widgets.php | 34 + resources/lang/da-DK/accounts.php | 31 + resources/lang/da-DK/auth.php | 93 + resources/lang/da-DK/bills.php | 63 + resources/lang/da-DK/bulk_actions.php | 26 + resources/lang/da-DK/categories.php | 11 + resources/lang/da-DK/companies.php | 22 + resources/lang/da-DK/countries.php | 256 + resources/lang/da-DK/currencies.php | 27 + resources/lang/da-DK/customers.php | 30 + resources/lang/da-DK/dashboards.php | 15 + resources/lang/da-DK/demo.php | 31 + resources/lang/da-DK/documents.php | 93 + resources/lang/da-DK/email_templates.php | 67 + resources/lang/da-DK/errors.php | 25 + resources/lang/da-DK/footer.php | 13 + resources/lang/da-DK/general.php | 317 + resources/lang/da-DK/header.php | 33 + resources/lang/da-DK/import.php | 14 + resources/lang/da-DK/install.php | 51 + resources/lang/da-DK/invoices.php | 82 + resources/lang/da-DK/items.php | 17 + resources/lang/da-DK/maintenance.php | 11 + resources/lang/da-DK/messages.php | 48 + resources/lang/da-DK/modules.php | 136 + resources/lang/da-DK/notifications.php | 219 + resources/lang/da-DK/pagination.php | 9 + resources/lang/da-DK/passwords.php | 22 + resources/lang/da-DK/payments.php | 10 + resources/lang/da-DK/portal.php | 53 + resources/lang/da-DK/reconciliations.php | 22 + resources/lang/da-DK/recurring.php | 43 + resources/lang/da-DK/reports.php | 43 + resources/lang/da-DK/revenues.php | 10 + resources/lang/da-DK/search_string.php | 21 + resources/lang/da-DK/settings.php | 151 + resources/lang/da-DK/taxes.php | 12 + resources/lang/da-DK/transfers.php | 15 + resources/lang/da-DK/updates.php | 15 + resources/lang/da-DK/validation.php | 165 + resources/lang/da-DK/widgets.php | 24 + resources/lang/de-DE/accounts.php | 31 + resources/lang/de-DE/auth.php | 93 + resources/lang/de-DE/bills.php | 63 + resources/lang/de-DE/bulk_actions.php | 27 + resources/lang/de-DE/categories.php | 11 + resources/lang/de-DE/companies.php | 22 + resources/lang/de-DE/countries.php | 256 + resources/lang/de-DE/currencies.php | 27 + resources/lang/de-DE/customers.php | 31 + resources/lang/de-DE/dashboards.php | 15 + resources/lang/de-DE/demo.php | 31 + resources/lang/de-DE/documents.php | 104 + resources/lang/de-DE/email_templates.php | 64 + resources/lang/de-DE/errors.php | 25 + resources/lang/de-DE/footer.php | 13 + resources/lang/de-DE/general.php | 353 + resources/lang/de-DE/header.php | 33 + resources/lang/de-DE/import.php | 18 + resources/lang/de-DE/install.php | 51 + resources/lang/de-DE/invoices.php | 83 + resources/lang/de-DE/items.php | 17 + resources/lang/de-DE/maintenance.php | 11 + resources/lang/de-DE/messages.php | 53 + resources/lang/de-DE/modules.php | 133 + resources/lang/de-DE/notifications.php | 250 + resources/lang/de-DE/pagination.php | 9 + resources/lang/de-DE/passwords.php | 22 + resources/lang/de-DE/portal.php | 53 + resources/lang/de-DE/reconciliations.php | 22 + resources/lang/de-DE/recurring.php | 43 + resources/lang/de-DE/reports.php | 45 + resources/lang/de-DE/search_string.php | 21 + resources/lang/de-DE/settings.php | 209 + resources/lang/de-DE/taxes.php | 20 + resources/lang/de-DE/transactions.php | 58 + resources/lang/de-DE/transfers.php | 34 + resources/lang/de-DE/updates.php | 15 + resources/lang/de-DE/validation.php | 179 + resources/lang/de-DE/vendors.php | 16 + resources/lang/de-DE/widgets.php | 34 + resources/lang/el-GR/accounts.php | 31 + resources/lang/el-GR/auth.php | 93 + resources/lang/el-GR/bills.php | 63 + resources/lang/el-GR/bulk_actions.php | 27 + resources/lang/el-GR/categories.php | 11 + resources/lang/el-GR/companies.php | 22 + resources/lang/el-GR/countries.php | 256 + resources/lang/el-GR/currencies.php | 27 + resources/lang/el-GR/customers.php | 31 + resources/lang/el-GR/dashboards.php | 15 + resources/lang/el-GR/demo.php | 31 + resources/lang/el-GR/documents.php | 104 + resources/lang/el-GR/email_templates.php | 64 + resources/lang/el-GR/errors.php | 25 + resources/lang/el-GR/footer.php | 13 + resources/lang/el-GR/general.php | 353 + resources/lang/el-GR/header.php | 33 + resources/lang/el-GR/import.php | 18 + resources/lang/el-GR/install.php | 51 + resources/lang/el-GR/invoices.php | 83 + resources/lang/el-GR/items.php | 17 + resources/lang/el-GR/maintenance.php | 11 + resources/lang/el-GR/messages.php | 52 + resources/lang/el-GR/modules.php | 136 + resources/lang/el-GR/notifications.php | 250 + resources/lang/el-GR/pagination.php | 9 + resources/lang/el-GR/passwords.php | 22 + resources/lang/el-GR/portal.php | 53 + resources/lang/el-GR/reconciliations.php | 22 + resources/lang/el-GR/recurring.php | 43 + resources/lang/el-GR/reports.php | 44 + resources/lang/el-GR/search_string.php | 21 + resources/lang/el-GR/settings.php | 209 + resources/lang/el-GR/taxes.php | 20 + resources/lang/el-GR/transactions.php | 58 + resources/lang/el-GR/transfers.php | 34 + resources/lang/el-GR/updates.php | 15 + resources/lang/el-GR/validation.php | 179 + resources/lang/el-GR/vendors.php | 16 + resources/lang/el-GR/widgets.php | 34 + resources/lang/en-AU/accounts.php | 31 + resources/lang/en-AU/auth.php | 93 + resources/lang/en-AU/bills.php | 63 + resources/lang/en-AU/bulk_actions.php | 27 + resources/lang/en-AU/categories.php | 11 + resources/lang/en-AU/companies.php | 22 + resources/lang/en-AU/countries.php | 256 + resources/lang/en-AU/currencies.php | 27 + resources/lang/en-AU/customers.php | 31 + resources/lang/en-AU/dashboards.php | 15 + resources/lang/en-AU/demo.php | 31 + resources/lang/en-AU/documents.php | 104 + resources/lang/en-AU/email_templates.php | 64 + resources/lang/en-AU/errors.php | 25 + resources/lang/en-AU/footer.php | 13 + resources/lang/en-AU/general.php | 353 + resources/lang/en-AU/header.php | 33 + resources/lang/en-AU/import.php | 18 + resources/lang/en-AU/install.php | 51 + resources/lang/en-AU/invoices.php | 83 + resources/lang/en-AU/items.php | 17 + resources/lang/en-AU/maintenance.php | 11 + resources/lang/en-AU/messages.php | 53 + resources/lang/en-AU/modules.php | 133 + resources/lang/en-AU/notifications.php | 250 + resources/lang/en-AU/pagination.php | 9 + resources/lang/en-AU/passwords.php | 22 + resources/lang/en-AU/portal.php | 53 + resources/lang/en-AU/reconciliations.php | 22 + resources/lang/en-AU/recurring.php | 43 + resources/lang/en-AU/reports.php | 45 + resources/lang/en-AU/search_string.php | 21 + resources/lang/en-AU/settings.php | 209 + resources/lang/en-AU/taxes.php | 20 + resources/lang/en-AU/transactions.php | 58 + resources/lang/en-AU/transfers.php | 34 + resources/lang/en-AU/updates.php | 15 + resources/lang/en-AU/validation.php | 179 + resources/lang/en-AU/vendors.php | 16 + resources/lang/en-AU/widgets.php | 34 + resources/lang/en-GB/accounts.php | 31 + resources/lang/en-GB/auth.php | 93 + resources/lang/en-GB/bills.php | 63 + resources/lang/en-GB/bulk_actions.php | 27 + resources/lang/en-GB/categories.php | 11 + resources/lang/en-GB/companies.php | 22 + resources/lang/en-GB/countries.php | 256 + resources/lang/en-GB/currencies.php | 27 + resources/lang/en-GB/customers.php | 31 + resources/lang/en-GB/dashboards.php | 15 + resources/lang/en-GB/demo.php | 31 + resources/lang/en-GB/documents.php | 104 + resources/lang/en-GB/email_templates.php | 64 + resources/lang/en-GB/errors.php | 25 + resources/lang/en-GB/footer.php | 13 + resources/lang/en-GB/general.php | 353 + resources/lang/en-GB/header.php | 33 + resources/lang/en-GB/import.php | 18 + resources/lang/en-GB/install.php | 51 + resources/lang/en-GB/invoices.php | 83 + resources/lang/en-GB/items.php | 17 + resources/lang/en-GB/maintenance.php | 11 + resources/lang/en-GB/messages.php | 53 + resources/lang/en-GB/modules.php | 133 + resources/lang/en-GB/notifications.php | 250 + resources/lang/en-GB/pagination.php | 9 + resources/lang/en-GB/passwords.php | 22 + resources/lang/en-GB/portal.php | 53 + resources/lang/en-GB/reconciliations.php | 22 + resources/lang/en-GB/recurring.php | 43 + resources/lang/en-GB/reports.php | 45 + resources/lang/en-GB/search_string.php | 21 + resources/lang/en-GB/settings.php | 209 + resources/lang/en-GB/taxes.php | 20 + resources/lang/en-GB/transactions.php | 58 + resources/lang/en-GB/transfers.php | 34 + resources/lang/en-GB/updates.php | 15 + resources/lang/en-GB/validation.php | 179 + resources/lang/en-GB/vendors.php | 16 + resources/lang/en-GB/widgets.php | 34 + resources/lang/en-US/accounts.php | 31 + resources/lang/en-US/auth.php | 93 + resources/lang/en-US/bills.php | 63 + resources/lang/en-US/bulk_actions.php | 27 + resources/lang/en-US/categories.php | 11 + resources/lang/en-US/companies.php | 22 + resources/lang/en-US/countries.php | 256 + resources/lang/en-US/currencies.php | 27 + resources/lang/en-US/customers.php | 31 + resources/lang/en-US/dashboards.php | 15 + resources/lang/en-US/demo.php | 31 + resources/lang/en-US/documents.php | 104 + resources/lang/en-US/email_templates.php | 64 + resources/lang/en-US/errors.php | 25 + resources/lang/en-US/footer.php | 13 + resources/lang/en-US/general.php | 353 + resources/lang/en-US/header.php | 33 + resources/lang/en-US/import.php | 18 + resources/lang/en-US/install.php | 51 + resources/lang/en-US/invoices.php | 83 + resources/lang/en-US/items.php | 17 + resources/lang/en-US/maintenance.php | 11 + resources/lang/en-US/messages.php | 53 + resources/lang/en-US/modules.php | 136 + resources/lang/en-US/notifications.php | 250 + resources/lang/en-US/pagination.php | 9 + resources/lang/en-US/passwords.php | 22 + resources/lang/en-US/portal.php | 53 + resources/lang/en-US/reconciliations.php | 22 + resources/lang/en-US/recurring.php | 43 + resources/lang/en-US/reports.php | 45 + resources/lang/en-US/search_string.php | 21 + resources/lang/en-US/settings.php | 209 + resources/lang/en-US/taxes.php | 20 + resources/lang/en-US/transactions.php | 58 + resources/lang/en-US/transfers.php | 34 + resources/lang/en-US/updates.php | 15 + resources/lang/en-US/validation.php | 179 + resources/lang/en-US/vendors.php | 16 + resources/lang/en-US/widgets.php | 34 + resources/lang/es-AR/accounts.php | 31 + resources/lang/es-AR/auth.php | 41 + resources/lang/es-AR/bills.php | 63 + resources/lang/es-AR/bulk_actions.php | 26 + resources/lang/es-AR/categories.php | 11 + resources/lang/es-AR/companies.php | 22 + resources/lang/es-AR/countries.php | 256 + resources/lang/es-AR/currencies.php | 27 + resources/lang/es-AR/customers.php | 30 + resources/lang/es-AR/dashboards.php | 15 + resources/lang/es-AR/demo.php | 31 + resources/lang/es-AR/documents.php | 12 + resources/lang/es-AR/email_templates.php | 64 + resources/lang/es-AR/errors.php | 25 + resources/lang/es-AR/footer.php | 13 + resources/lang/es-AR/general.php | 206 + resources/lang/es-AR/header.php | 16 + resources/lang/es-AR/import.php | 9 + resources/lang/es-AR/install.php | 46 + resources/lang/es-AR/invoices.php | 85 + resources/lang/es-AR/items.php | 17 + resources/lang/es-AR/maintenance.php | 9 + resources/lang/es-AR/messages.php | 37 + resources/lang/es-AR/modules.php | 83 + resources/lang/es-AR/notifications.php | 10 + resources/lang/es-AR/pagination.php | 9 + resources/lang/es-AR/passwords.php | 23 + resources/lang/es-AR/reconciliations.php | 18 + resources/lang/es-AR/recurring.php | 41 + resources/lang/es-AR/reports.php | 43 + resources/lang/es-AR/search_string.php | 21 + resources/lang/es-AR/settings.php | 144 + resources/lang/es-AR/taxes.php | 12 + resources/lang/es-AR/transfers.php | 12 + resources/lang/es-AR/updates.php | 15 + resources/lang/es-AR/validation.php | 123 + resources/lang/es-AR/widgets.php | 23 + resources/lang/es-CO/accounts.php | 31 + resources/lang/es-CO/auth.php | 93 + resources/lang/es-CO/bills.php | 63 + resources/lang/es-CO/bulk_actions.php | 26 + resources/lang/es-CO/categories.php | 11 + resources/lang/es-CO/companies.php | 22 + resources/lang/es-CO/countries.php | 256 + resources/lang/es-CO/currencies.php | 27 + resources/lang/es-CO/customers.php | 30 + resources/lang/es-CO/dashboards.php | 15 + resources/lang/es-CO/demo.php | 31 + resources/lang/es-CO/documents.php | 86 + resources/lang/es-CO/errors.php | 25 + resources/lang/es-CO/footer.php | 13 + resources/lang/es-CO/pagination.php | 9 + resources/lang/es-CO/passwords.php | 24 + resources/lang/es-CO/search_string.php | 21 + resources/lang/es-CO/taxes.php | 20 + resources/lang/es-CO/vendors.php | 16 + resources/lang/es-CO/widgets.php | 34 + resources/lang/es-ES/accounts.php | 31 + resources/lang/es-ES/auth.php | 93 + resources/lang/es-ES/bills.php | 63 + resources/lang/es-ES/bulk_actions.php | 27 + resources/lang/es-ES/categories.php | 11 + resources/lang/es-ES/companies.php | 22 + resources/lang/es-ES/countries.php | 256 + resources/lang/es-ES/currencies.php | 27 + resources/lang/es-ES/customers.php | 31 + resources/lang/es-ES/dashboards.php | 15 + resources/lang/es-ES/demo.php | 31 + resources/lang/es-ES/documents.php | 104 + resources/lang/es-ES/email_templates.php | 64 + resources/lang/es-ES/errors.php | 24 + resources/lang/es-ES/footer.php | 13 + resources/lang/es-ES/general.php | 354 + resources/lang/es-ES/header.php | 33 + resources/lang/es-ES/import.php | 18 + resources/lang/es-ES/install.php | 51 + resources/lang/es-ES/invoices.php | 83 + resources/lang/es-ES/items.php | 17 + resources/lang/es-ES/maintenance.php | 11 + resources/lang/es-ES/messages.php | 53 + resources/lang/es-ES/modules.php | 133 + resources/lang/es-ES/notifications.php | 250 + resources/lang/es-ES/pagination.php | 9 + resources/lang/es-ES/passwords.php | 22 + resources/lang/es-ES/portal.php | 53 + resources/lang/es-ES/reconciliations.php | 22 + resources/lang/es-ES/recurring.php | 43 + resources/lang/es-ES/reports.php | 45 + resources/lang/es-ES/search_string.php | 21 + resources/lang/es-ES/settings.php | 209 + resources/lang/es-ES/taxes.php | 20 + resources/lang/es-ES/transactions.php | 58 + resources/lang/es-ES/transfers.php | 33 + resources/lang/es-ES/updates.php | 15 + resources/lang/es-ES/validation.php | 152 + resources/lang/es-ES/vendors.php | 16 + resources/lang/es-ES/widgets.php | 34 + resources/lang/es-MX/accounts.php | 31 + resources/lang/es-MX/auth.php | 93 + resources/lang/es-MX/bills.php | 63 + resources/lang/es-MX/bulk_actions.php | 27 + resources/lang/es-MX/categories.php | 11 + resources/lang/es-MX/companies.php | 22 + resources/lang/es-MX/countries.php | 256 + resources/lang/es-MX/currencies.php | 27 + resources/lang/es-MX/customers.php | 31 + resources/lang/es-MX/dashboards.php | 15 + resources/lang/es-MX/demo.php | 31 + resources/lang/es-MX/documents.php | 104 + resources/lang/es-MX/email_templates.php | 64 + resources/lang/es-MX/errors.php | 24 + resources/lang/es-MX/footer.php | 13 + resources/lang/es-MX/general.php | 353 + resources/lang/es-MX/header.php | 33 + resources/lang/es-MX/import.php | 18 + resources/lang/es-MX/install.php | 51 + resources/lang/es-MX/invoices.php | 83 + resources/lang/es-MX/items.php | 17 + resources/lang/es-MX/maintenance.php | 11 + resources/lang/es-MX/messages.php | 53 + resources/lang/es-MX/modules.php | 133 + resources/lang/es-MX/notifications.php | 250 + resources/lang/es-MX/pagination.php | 9 + resources/lang/es-MX/passwords.php | 22 + resources/lang/es-MX/portal.php | 53 + resources/lang/es-MX/reconciliations.php | 22 + resources/lang/es-MX/recurring.php | 43 + resources/lang/es-MX/reports.php | 45 + resources/lang/es-MX/search_string.php | 21 + resources/lang/es-MX/settings.php | 209 + resources/lang/es-MX/taxes.php | 20 + resources/lang/es-MX/transactions.php | 58 + resources/lang/es-MX/transfers.php | 33 + resources/lang/es-MX/updates.php | 15 + resources/lang/es-MX/validation.php | 152 + resources/lang/es-MX/vendors.php | 16 + resources/lang/es-MX/widgets.php | 34 + resources/lang/et-EE/accounts.php | 14 + resources/lang/et-EE/auth.php | 41 + resources/lang/et-EE/bills.php | 56 + resources/lang/et-EE/bulk_actions.php | 23 + resources/lang/et-EE/companies.php | 14 + resources/lang/et-EE/countries.php | 253 + resources/lang/et-EE/currencies.php | 19 + resources/lang/et-EE/customers.php | 12 + resources/lang/et-EE/dashboards.php | 11 + resources/lang/et-EE/demo.php | 31 + resources/lang/et-EE/documents.php | 54 + resources/lang/et-EE/email_templates.php | 50 + resources/lang/et-EE/errors.php | 23 + resources/lang/et-EE/footer.php | 10 + resources/lang/et-EE/general.php | 232 + resources/lang/et-EE/header.php | 16 + resources/lang/et-EE/import.php | 11 + resources/lang/et-EE/install.php | 50 + resources/lang/et-EE/invoices.php | 60 + resources/lang/et-EE/items.php | 9 + resources/lang/et-EE/maintenance.php | 9 + resources/lang/et-EE/messages.php | 39 + resources/lang/et-EE/modules.php | 83 + resources/lang/et-EE/notifications.php | 54 + resources/lang/et-EE/pagination.php | 9 + resources/lang/et-EE/passwords.php | 23 + resources/lang/et-EE/reconciliations.php | 18 + resources/lang/et-EE/recurring.php | 20 + resources/lang/et-EE/reports.php | 28 + resources/lang/et-EE/search_string.php | 20 + resources/lang/et-EE/settings.php | 144 + resources/lang/et-EE/taxes.php | 12 + resources/lang/et-EE/transfers.php | 14 + resources/lang/et-EE/updates.php | 15 + resources/lang/et-EE/validation.php | 123 + resources/lang/et-EE/widgets.php | 24 + resources/lang/fa-IR/accounts.php | 31 + resources/lang/fa-IR/auth.php | 41 + resources/lang/fa-IR/bills.php | 57 + resources/lang/fa-IR/bulk_actions.php | 23 + resources/lang/fa-IR/categories.php | 11 + resources/lang/fa-IR/companies.php | 14 + resources/lang/fa-IR/countries.php | 256 + resources/lang/fa-IR/currencies.php | 19 + resources/lang/fa-IR/customers.php | 12 + resources/lang/fa-IR/dashboards.php | 11 + resources/lang/fa-IR/demo.php | 31 + resources/lang/fa-IR/email_templates.php | 52 + resources/lang/fa-IR/errors.php | 23 + resources/lang/fa-IR/footer.php | 10 + resources/lang/fa-IR/general.php | 228 + resources/lang/fa-IR/header.php | 16 + resources/lang/fa-IR/import.php | 9 + resources/lang/fa-IR/install.php | 50 + resources/lang/fa-IR/invoices.php | 61 + resources/lang/fa-IR/items.php | 9 + resources/lang/fa-IR/maintenance.php | 11 + resources/lang/fa-IR/messages.php | 37 + resources/lang/fa-IR/modules.php | 83 + resources/lang/fa-IR/notifications.php | 10 + resources/lang/fa-IR/pagination.php | 9 + resources/lang/fa-IR/passwords.php | 22 + resources/lang/fa-IR/payments.php | 10 + resources/lang/fa-IR/reconciliations.php | 18 + resources/lang/fa-IR/recurring.php | 20 + resources/lang/fa-IR/reports.php | 28 + resources/lang/fa-IR/revenues.php | 10 + resources/lang/fa-IR/settings.php | 138 + resources/lang/fa-IR/taxes.php | 12 + resources/lang/fa-IR/transfers.php | 12 + resources/lang/fa-IR/updates.php | 15 + resources/lang/fa-IR/validation.php | 123 + resources/lang/fa-IR/widgets.php | 24 + resources/lang/fi-FI/accounts.php | 31 + resources/lang/fi-FI/auth.php | 95 + resources/lang/fi-FI/bills.php | 56 + resources/lang/fi-FI/bulk_actions.php | 26 + resources/lang/fi-FI/categories.php | 11 + resources/lang/fi-FI/companies.php | 14 + resources/lang/fi-FI/countries.php | 253 + resources/lang/fi-FI/currencies.php | 19 + resources/lang/fi-FI/customers.php | 12 + resources/lang/fi-FI/dashboards.php | 11 + resources/lang/fi-FI/demo.php | 31 + resources/lang/fi-FI/documents.php | 54 + resources/lang/fi-FI/email_templates.php | 50 + resources/lang/fi-FI/errors.php | 23 + resources/lang/fi-FI/footer.php | 10 + resources/lang/fi-FI/general.php | 231 + resources/lang/fi-FI/header.php | 16 + resources/lang/fi-FI/import.php | 9 + resources/lang/fi-FI/install.php | 46 + resources/lang/fi-FI/invoices.php | 60 + resources/lang/fi-FI/items.php | 8 + resources/lang/fi-FI/maintenance.php | 9 + resources/lang/fi-FI/messages.php | 37 + resources/lang/fi-FI/modules.php | 83 + resources/lang/fi-FI/notifications.php | 10 + resources/lang/fi-FI/pagination.php | 9 + resources/lang/fi-FI/passwords.php | 22 + resources/lang/fi-FI/reconciliations.php | 18 + resources/lang/fi-FI/recurring.php | 20 + resources/lang/fi-FI/reports.php | 28 + resources/lang/fi-FI/search_string.php | 21 + resources/lang/fi-FI/settings.php | 138 + resources/lang/fi-FI/taxes.php | 12 + resources/lang/fi-FI/transfers.php | 12 + resources/lang/fi-FI/updates.php | 15 + resources/lang/fi-FI/validation.php | 123 + resources/lang/fi-FI/widgets.php | 23 + resources/lang/fr-FR/accounts.php | 31 + resources/lang/fr-FR/auth.php | 93 + resources/lang/fr-FR/bills.php | 63 + resources/lang/fr-FR/bulk_actions.php | 27 + resources/lang/fr-FR/categories.php | 11 + resources/lang/fr-FR/companies.php | 22 + resources/lang/fr-FR/countries.php | 256 + resources/lang/fr-FR/currencies.php | 27 + resources/lang/fr-FR/customers.php | 31 + resources/lang/fr-FR/dashboards.php | 15 + resources/lang/fr-FR/demo.php | 31 + resources/lang/fr-FR/documents.php | 104 + resources/lang/fr-FR/email_templates.php | 64 + resources/lang/fr-FR/errors.php | 25 + resources/lang/fr-FR/footer.php | 13 + resources/lang/fr-FR/general.php | 354 + resources/lang/fr-FR/header.php | 33 + resources/lang/fr-FR/import.php | 18 + resources/lang/fr-FR/install.php | 51 + resources/lang/fr-FR/invoices.php | 83 + resources/lang/fr-FR/items.php | 17 + resources/lang/fr-FR/maintenance.php | 11 + resources/lang/fr-FR/messages.php | 53 + resources/lang/fr-FR/modules.php | 133 + resources/lang/fr-FR/notifications.php | 250 + resources/lang/fr-FR/pagination.php | 9 + resources/lang/fr-FR/passwords.php | 22 + resources/lang/fr-FR/portal.php | 53 + resources/lang/fr-FR/reconciliations.php | 22 + resources/lang/fr-FR/recurring.php | 43 + resources/lang/fr-FR/reports.php | 45 + resources/lang/fr-FR/search_string.php | 21 + resources/lang/fr-FR/settings.php | 209 + resources/lang/fr-FR/taxes.php | 20 + resources/lang/fr-FR/transactions.php | 58 + resources/lang/fr-FR/transfers.php | 34 + resources/lang/fr-FR/updates.php | 15 + resources/lang/fr-FR/validation.php | 179 + resources/lang/fr-FR/vendors.php | 16 + resources/lang/fr-FR/widgets.php | 34 + resources/lang/he-IL/accounts.php | 14 + resources/lang/he-IL/auth.php | 39 + resources/lang/he-IL/bills.php | 63 + resources/lang/he-IL/categories.php | 11 + resources/lang/he-IL/companies.php | 22 + resources/lang/he-IL/countries.php | 256 + resources/lang/he-IL/currencies.php | 18 + resources/lang/he-IL/customers.php | 16 + resources/lang/he-IL/dashboard.php | 24 + resources/lang/he-IL/dashboards.php | 15 + resources/lang/he-IL/demo.php | 31 + resources/lang/he-IL/errors.php | 23 + resources/lang/he-IL/footer.php | 10 + resources/lang/he-IL/general.php | 148 + resources/lang/he-IL/header.php | 16 + resources/lang/he-IL/import.php | 9 + resources/lang/he-IL/install.php | 44 + resources/lang/he-IL/invoices.php | 68 + resources/lang/he-IL/items.php | 8 + resources/lang/he-IL/messages.php | 29 + resources/lang/he-IL/modules.php | 80 + resources/lang/he-IL/notifications.php | 10 + resources/lang/he-IL/pagination.php | 9 + resources/lang/he-IL/passwords.php | 22 + resources/lang/he-IL/reconciliations.php | 14 + resources/lang/he-IL/recurring.php | 20 + resources/lang/he-IL/reports.php | 27 + resources/lang/he-IL/settings.php | 102 + resources/lang/he-IL/taxes.php | 20 + resources/lang/he-IL/transfers.php | 12 + resources/lang/he-IL/updates.php | 15 + resources/lang/he-IL/validation.php | 121 + resources/lang/hi-IN/accounts.php | 31 + resources/lang/hi-IN/auth.php | 93 + resources/lang/hi-IN/bills.php | 63 + resources/lang/hi-IN/bulk_actions.php | 27 + resources/lang/hi-IN/categories.php | 11 + resources/lang/hi-IN/companies.php | 22 + resources/lang/hi-IN/countries.php | 256 + resources/lang/hi-IN/currencies.php | 27 + resources/lang/hi-IN/customers.php | 31 + resources/lang/hi-IN/dashboards.php | 15 + resources/lang/hi-IN/demo.php | 31 + resources/lang/hi-IN/documents.php | 104 + resources/lang/hi-IN/email_templates.php | 64 + resources/lang/hi-IN/errors.php | 25 + resources/lang/hi-IN/footer.php | 13 + resources/lang/hi-IN/general.php | 353 + resources/lang/hi-IN/header.php | 33 + resources/lang/hi-IN/import.php | 18 + resources/lang/hi-IN/install.php | 51 + resources/lang/hi-IN/invoices.php | 83 + resources/lang/hi-IN/items.php | 17 + resources/lang/hi-IN/maintenance.php | 11 + resources/lang/hi-IN/messages.php | 53 + resources/lang/hi-IN/modules.php | 136 + resources/lang/hi-IN/notifications.php | 250 + resources/lang/hi-IN/pagination.php | 9 + resources/lang/hi-IN/passwords.php | 22 + resources/lang/hi-IN/portal.php | 53 + resources/lang/hi-IN/reconciliations.php | 22 + resources/lang/hi-IN/recurring.php | 43 + resources/lang/hi-IN/reports.php | 46 + resources/lang/hi-IN/search_string.php | 21 + resources/lang/hi-IN/settings.php | 210 + resources/lang/hi-IN/taxes.php | 20 + resources/lang/hi-IN/transactions.php | 58 + resources/lang/hi-IN/transfers.php | 34 + resources/lang/hi-IN/updates.php | 15 + resources/lang/hi-IN/validation.php | 179 + resources/lang/hi-IN/vendors.php | 16 + resources/lang/hi-IN/widgets.php | 34 + resources/lang/hr-HR/accounts.php | 14 + resources/lang/hr-HR/auth.php | 41 + resources/lang/hr-HR/bills.php | 69 + resources/lang/hr-HR/bulk_actions.php | 21 + resources/lang/hr-HR/companies.php | 14 + resources/lang/hr-HR/countries.php | 253 + resources/lang/hr-HR/currencies.php | 18 + resources/lang/hr-HR/customers.php | 12 + resources/lang/hr-HR/dashboards.php | 11 + resources/lang/hr-HR/demo.php | 31 + resources/lang/hr-HR/email_templates.php | 51 + resources/lang/hr-HR/errors.php | 23 + resources/lang/hr-HR/footer.php | 10 + resources/lang/hr-HR/general.php | 206 + resources/lang/hr-HR/header.php | 16 + resources/lang/hr-HR/import.php | 9 + resources/lang/hr-HR/install.php | 45 + resources/lang/hr-HR/invoices.php | 77 + resources/lang/hr-HR/items.php | 8 + resources/lang/hr-HR/maintenance.php | 9 + resources/lang/hr-HR/messages.php | 37 + resources/lang/hr-HR/modules.php | 83 + resources/lang/hr-HR/notifications.php | 10 + resources/lang/hr-HR/pagination.php | 9 + resources/lang/hr-HR/passwords.php | 23 + resources/lang/hr-HR/reconciliations.php | 16 + resources/lang/hr-HR/recurring.php | 20 + resources/lang/hr-HR/reports.php | 27 + resources/lang/hr-HR/settings.php | 129 + resources/lang/hr-HR/taxes.php | 11 + resources/lang/hr-HR/transfers.php | 12 + resources/lang/hr-HR/updates.php | 15 + resources/lang/hr-HR/validation.php | 123 + resources/lang/hr-HR/widgets.php | 23 + resources/lang/hu-HU/accounts.php | 14 + resources/lang/hu-HU/auth.php | 42 + resources/lang/hu-HU/bills.php | 59 + resources/lang/hu-HU/bulk_actions.php | 36 + resources/lang/hu-HU/companies.php | 14 + resources/lang/hu-HU/countries.php | 253 + resources/lang/hu-HU/currencies.php | 18 + resources/lang/hu-HU/customers.php | 13 + resources/lang/hu-HU/dashboards.php | 13 + resources/lang/hu-HU/demo.php | 32 + resources/lang/hu-HU/email_templates.php | 61 + resources/lang/hu-HU/errors.php | 23 + resources/lang/hu-HU/footer.php | 10 + resources/lang/hu-HU/header.php | 16 + resources/lang/hu-HU/import.php | 9 + resources/lang/hu-HU/install.php | 51 + resources/lang/hu-HU/invoices.php | 65 + resources/lang/hu-HU/items.php | 17 + resources/lang/hu-HU/maintenance.php | 10 + resources/lang/hu-HU/messages.php | 42 + resources/lang/hu-HU/modules.php | 93 + resources/lang/hu-HU/notifications.php | 10 + resources/lang/hu-HU/pagination.php | 9 + resources/lang/hu-HU/passwords.php | 22 + resources/lang/hu-HU/reconciliations.php | 18 + resources/lang/hu-HU/recurring.php | 20 + resources/lang/hu-HU/reports.php | 27 + resources/lang/hu-HU/taxes.php | 12 + resources/lang/hu-HU/transfers.php | 12 + resources/lang/hu-HU/updates.php | 15 + resources/lang/hu-HU/validation.php | 123 + resources/lang/hu-HU/vendors.php | 16 + resources/lang/hu-HU/widgets.php | 23 + resources/lang/id-ID/accounts.php | 31 + resources/lang/id-ID/auth.php | 93 + resources/lang/id-ID/bills.php | 63 + resources/lang/id-ID/bulk_actions.php | 27 + resources/lang/id-ID/categories.php | 11 + resources/lang/id-ID/companies.php | 22 + resources/lang/id-ID/countries.php | 256 + resources/lang/id-ID/currencies.php | 27 + resources/lang/id-ID/customers.php | 31 + resources/lang/id-ID/dashboards.php | 15 + resources/lang/id-ID/demo.php | 31 + resources/lang/id-ID/documents.php | 104 + resources/lang/id-ID/email_templates.php | 64 + resources/lang/id-ID/errors.php | 25 + resources/lang/id-ID/footer.php | 13 + resources/lang/id-ID/general.php | 353 + resources/lang/id-ID/header.php | 33 + resources/lang/id-ID/import.php | 18 + resources/lang/id-ID/install.php | 51 + resources/lang/id-ID/invoices.php | 83 + resources/lang/id-ID/items.php | 17 + resources/lang/id-ID/maintenance.php | 11 + resources/lang/id-ID/messages.php | 53 + resources/lang/id-ID/modules.php | 133 + resources/lang/id-ID/notifications.php | 249 + resources/lang/id-ID/pagination.php | 9 + resources/lang/id-ID/passwords.php | 22 + resources/lang/id-ID/portal.php | 53 + resources/lang/id-ID/reconciliations.php | 22 + resources/lang/id-ID/recurring.php | 43 + resources/lang/id-ID/reports.php | 51 + resources/lang/id-ID/search_string.php | 21 + resources/lang/id-ID/settings.php | 209 + resources/lang/id-ID/taxes.php | 20 + resources/lang/id-ID/transactions.php | 58 + resources/lang/id-ID/transfers.php | 34 + resources/lang/id-ID/updates.php | 15 + resources/lang/id-ID/validation.php | 179 + resources/lang/id-ID/vendors.php | 16 + resources/lang/id-ID/widgets.php | 34 + resources/lang/is-IS/accounts.php | 14 + resources/lang/is-IS/auth.php | 41 + resources/lang/is-IS/bills.php | 56 + resources/lang/is-IS/bulk_actions.php | 21 + resources/lang/is-IS/companies.php | 14 + resources/lang/is-IS/countries.php | 253 + resources/lang/is-IS/currencies.php | 18 + resources/lang/is-IS/customers.php | 12 + resources/lang/is-IS/dashboards.php | 11 + resources/lang/is-IS/demo.php | 34 + resources/lang/is-IS/email_templates.php | 50 + resources/lang/is-IS/errors.php | 23 + resources/lang/is-IS/footer.php | 10 + resources/lang/is-IS/general.php | 206 + resources/lang/is-IS/header.php | 16 + resources/lang/is-IS/import.php | 9 + resources/lang/is-IS/install.php | 45 + resources/lang/is-IS/invoices.php | 77 + resources/lang/is-IS/items.php | 8 + resources/lang/is-IS/maintenance.php | 11 + resources/lang/is-IS/messages.php | 37 + resources/lang/is-IS/modules.php | 83 + resources/lang/is-IS/notifications.php | 10 + resources/lang/is-IS/pagination.php | 10 + resources/lang/is-IS/passwords.php | 23 + resources/lang/is-IS/reconciliations.php | 16 + resources/lang/is-IS/recurring.php | 20 + resources/lang/is-IS/reports.php | 27 + resources/lang/is-IS/settings.php | 129 + resources/lang/is-IS/taxes.php | 11 + resources/lang/is-IS/transfers.php | 12 + resources/lang/is-IS/updates.php | 15 + resources/lang/is-IS/validation.php | 123 + resources/lang/is-IS/widgets.php | 23 + resources/lang/it-IT/accounts.php | 31 + resources/lang/it-IT/auth.php | 93 + resources/lang/it-IT/bills.php | 63 + resources/lang/it-IT/bulk_actions.php | 27 + resources/lang/it-IT/categories.php | 11 + resources/lang/it-IT/companies.php | 22 + resources/lang/it-IT/countries.php | 256 + resources/lang/it-IT/currencies.php | 27 + resources/lang/it-IT/customers.php | 31 + resources/lang/it-IT/dashboards.php | 15 + resources/lang/it-IT/demo.php | 31 + resources/lang/it-IT/documents.php | 104 + resources/lang/it-IT/email_templates.php | 64 + resources/lang/it-IT/errors.php | 25 + resources/lang/it-IT/footer.php | 13 + resources/lang/it-IT/general.php | 353 + resources/lang/it-IT/header.php | 33 + resources/lang/it-IT/import.php | 18 + resources/lang/it-IT/install.php | 51 + resources/lang/it-IT/invoices.php | 83 + resources/lang/it-IT/items.php | 17 + resources/lang/it-IT/maintenance.php | 11 + resources/lang/it-IT/messages.php | 53 + resources/lang/it-IT/modules.php | 134 + resources/lang/it-IT/notifications.php | 250 + resources/lang/it-IT/pagination.php | 9 + resources/lang/it-IT/passwords.php | 22 + resources/lang/it-IT/portal.php | 53 + resources/lang/it-IT/reconciliations.php | 22 + resources/lang/it-IT/recurring.php | 43 + resources/lang/it-IT/reports.php | 45 + resources/lang/it-IT/search_string.php | 21 + resources/lang/it-IT/settings.php | 209 + resources/lang/it-IT/taxes.php | 20 + resources/lang/it-IT/transactions.php | 58 + resources/lang/it-IT/transfers.php | 34 + resources/lang/it-IT/updates.php | 15 + resources/lang/it-IT/validation.php | 179 + resources/lang/it-IT/vendors.php | 16 + resources/lang/it-IT/widgets.php | 34 + resources/lang/ja-JP/accounts.php | 14 + resources/lang/ja-JP/auth.php | 41 + resources/lang/ja-JP/bills.php | 56 + resources/lang/ja-JP/bulk_actions.php | 22 + resources/lang/ja-JP/companies.php | 14 + resources/lang/ja-JP/countries.php | 253 + resources/lang/ja-JP/currencies.php | 18 + resources/lang/ja-JP/customers.php | 12 + resources/lang/ja-JP/dashboards.php | 11 + resources/lang/ja-JP/demo.php | 31 + resources/lang/ja-JP/email_templates.php | 50 + resources/lang/ja-JP/errors.php | 23 + resources/lang/ja-JP/footer.php | 10 + resources/lang/ja-JP/general.php | 206 + resources/lang/ja-JP/header.php | 16 + resources/lang/ja-JP/import.php | 9 + resources/lang/ja-JP/install.php | 45 + resources/lang/ja-JP/invoices.php | 60 + resources/lang/ja-JP/items.php | 8 + resources/lang/ja-JP/maintenance.php | 9 + resources/lang/ja-JP/messages.php | 37 + resources/lang/ja-JP/modules.php | 83 + resources/lang/ja-JP/notifications.php | 10 + resources/lang/ja-JP/pagination.php | 9 + resources/lang/ja-JP/passwords.php | 23 + resources/lang/ja-JP/reconciliations.php | 16 + resources/lang/ja-JP/recurring.php | 20 + resources/lang/ja-JP/reports.php | 27 + resources/lang/ja-JP/settings.php | 129 + resources/lang/ja-JP/taxes.php | 11 + resources/lang/ja-JP/transfers.php | 12 + resources/lang/ja-JP/updates.php | 15 + resources/lang/ja-JP/validation.php | 123 + resources/lang/ja-JP/widgets.php | 23 + resources/lang/ka-GE/accounts.php | 14 + resources/lang/ka-GE/auth.php | 41 + resources/lang/ka-GE/bills.php | 61 + resources/lang/ka-GE/companies.php | 14 + resources/lang/ka-GE/countries.php | 253 + resources/lang/ka-GE/currencies.php | 18 + resources/lang/ka-GE/customers.php | 12 + resources/lang/ka-GE/dashboard.php | 24 + resources/lang/ka-GE/demo.php | 31 + resources/lang/ka-GE/email_templates.php | 50 + resources/lang/ka-GE/errors.php | 21 + resources/lang/ka-GE/footer.php | 10 + resources/lang/ka-GE/general.php | 147 + resources/lang/ka-GE/header.php | 16 + resources/lang/ka-GE/import.php | 9 + resources/lang/ka-GE/install.php | 44 + resources/lang/ka-GE/invoices.php | 72 + resources/lang/ka-GE/items.php | 8 + resources/lang/ka-GE/maintenance.php | 9 + resources/lang/ka-GE/messages.php | 32 + resources/lang/ka-GE/modules.php | 85 + resources/lang/ka-GE/notifications.php | 10 + resources/lang/ka-GE/pagination.php | 9 + resources/lang/ka-GE/passwords.php | 22 + resources/lang/ka-GE/reconciliations.php | 16 + resources/lang/ka-GE/recurring.php | 20 + resources/lang/ka-GE/reports.php | 27 + resources/lang/ka-GE/settings.php | 103 + resources/lang/ka-GE/taxes.php | 11 + resources/lang/ka-GE/transfers.php | 12 + resources/lang/ka-GE/updates.php | 15 + resources/lang/ka-GE/validation.php | 123 + resources/lang/ko-KR/accounts.php | 14 + resources/lang/ko-KR/auth.php | 39 + resources/lang/ko-KR/bills.php | 61 + resources/lang/ko-KR/companies.php | 13 + resources/lang/ko-KR/countries.php | 253 + resources/lang/ko-KR/currencies.php | 18 + resources/lang/ko-KR/customers.php | 16 + resources/lang/ko-KR/dashboard.php | 24 + resources/lang/ko-KR/demo.php | 16 + resources/lang/ko-KR/footer.php | 10 + resources/lang/ko-KR/general.php | 148 + resources/lang/ko-KR/header.php | 16 + resources/lang/ko-KR/import.php | 9 + resources/lang/ko-KR/install.php | 44 + resources/lang/ko-KR/invoices.php | 71 + resources/lang/ko-KR/items.php | 8 + resources/lang/ko-KR/messages.php | 32 + resources/lang/ko-KR/modules.php | 83 + resources/lang/ko-KR/notifications.php | 10 + resources/lang/ko-KR/pagination.php | 9 + resources/lang/ko-KR/passwords.php | 22 + resources/lang/ko-KR/reconciliations.php | 16 + resources/lang/ko-KR/recurring.php | 20 + resources/lang/ko-KR/reports.php | 27 + resources/lang/ko-KR/settings.php | 103 + resources/lang/ko-KR/taxes.php | 11 + resources/lang/ko-KR/transfers.php | 12 + resources/lang/ko-KR/updates.php | 15 + resources/lang/ko-KR/validation.php | 121 + resources/lang/lt-LT/accounts.php | 14 + resources/lang/lt-LT/auth.php | 41 + resources/lang/lt-LT/bills.php | 56 + resources/lang/lt-LT/bulk_actions.php | 21 + resources/lang/lt-LT/companies.php | 14 + resources/lang/lt-LT/countries.php | 253 + resources/lang/lt-LT/currencies.php | 18 + resources/lang/lt-LT/customers.php | 12 + resources/lang/lt-LT/dashboards.php | 11 + resources/lang/lt-LT/demo.php | 31 + resources/lang/lt-LT/email_templates.php | 50 + resources/lang/lt-LT/errors.php | 23 + resources/lang/lt-LT/footer.php | 10 + resources/lang/lt-LT/general.php | 206 + resources/lang/lt-LT/header.php | 16 + resources/lang/lt-LT/import.php | 9 + resources/lang/lt-LT/install.php | 45 + resources/lang/lt-LT/invoices.php | 85 + resources/lang/lt-LT/items.php | 8 + resources/lang/lt-LT/maintenance.php | 9 + resources/lang/lt-LT/messages.php | 37 + resources/lang/lt-LT/modules.php | 83 + resources/lang/lt-LT/notifications.php | 10 + resources/lang/lt-LT/pagination.php | 9 + resources/lang/lt-LT/passwords.php | 23 + resources/lang/lt-LT/reconciliations.php | 16 + resources/lang/lt-LT/recurring.php | 20 + resources/lang/lt-LT/reports.php | 27 + resources/lang/lt-LT/settings.php | 129 + resources/lang/lt-LT/taxes.php | 11 + resources/lang/lt-LT/transfers.php | 12 + resources/lang/lt-LT/updates.php | 15 + resources/lang/lt-LT/validation.php | 123 + resources/lang/lt-LT/widgets.php | 23 + resources/lang/lv-LV/accounts.php | 31 + resources/lang/lv-LV/auth.php | 93 + resources/lang/lv-LV/bills.php | 63 + resources/lang/lv-LV/bulk_actions.php | 26 + resources/lang/lv-LV/categories.php | 11 + resources/lang/lv-LV/companies.php | 22 + resources/lang/lv-LV/countries.php | 256 + resources/lang/lv-LV/currencies.php | 27 + resources/lang/lv-LV/customers.php | 30 + resources/lang/lv-LV/dashboards.php | 15 + resources/lang/lv-LV/demo.php | 31 + resources/lang/lv-LV/documents.php | 92 + resources/lang/lv-LV/email_templates.php | 65 + resources/lang/lv-LV/errors.php | 25 + resources/lang/lv-LV/footer.php | 13 + resources/lang/lv-LV/general.php | 312 + resources/lang/lv-LV/header.php | 33 + resources/lang/lv-LV/import.php | 10 + resources/lang/lv-LV/install.php | 51 + resources/lang/lv-LV/invoices.php | 85 + resources/lang/lv-LV/items.php | 17 + resources/lang/lv-LV/maintenance.php | 11 + resources/lang/lv-LV/messages.php | 48 + resources/lang/lv-LV/modules.php | 136 + resources/lang/lv-LV/notifications.php | 200 + resources/lang/lv-LV/pagination.php | 9 + resources/lang/lv-LV/passwords.php | 22 + resources/lang/lv-LV/portal.php | 53 + resources/lang/lv-LV/reconciliations.php | 22 + resources/lang/lv-LV/recurring.php | 42 + resources/lang/lv-LV/reports.php | 43 + resources/lang/lv-LV/search_string.php | 21 + resources/lang/lv-LV/settings.php | 203 + resources/lang/lv-LV/taxes.php | 20 + resources/lang/lv-LV/transactions.php | 47 + resources/lang/lv-LV/transfers.php | 34 + resources/lang/lv-LV/updates.php | 15 + resources/lang/lv-LV/validation.php | 174 + resources/lang/lv-LV/vendors.php | 16 + resources/lang/lv-LV/widgets.php | 34 + resources/lang/mk-MK/accounts.php | 31 + resources/lang/mk-MK/auth.php | 93 + resources/lang/mk-MK/bills.php | 63 + resources/lang/mk-MK/categories.php | 11 + resources/lang/mk-MK/companies.php | 22 + resources/lang/mk-MK/countries.php | 253 + resources/lang/mk-MK/currencies.php | 27 + resources/lang/mk-MK/customers.php | 30 + resources/lang/mk-MK/dashboard.php | 24 + resources/lang/mk-MK/dashboards.php | 15 + resources/lang/mk-MK/demo.php | 31 + resources/lang/mk-MK/documents.php | 86 + resources/lang/mk-MK/email_templates.php | 64 + resources/lang/mk-MK/errors.php | 25 + resources/lang/mk-MK/footer.php | 13 + resources/lang/mk-MK/general.php | 312 + resources/lang/mk-MK/header.php | 33 + resources/lang/mk-MK/import.php | 10 + resources/lang/mk-MK/install.php | 44 + resources/lang/mk-MK/invoices.php | 85 + resources/lang/mk-MK/items.php | 17 + resources/lang/mk-MK/maintenance.php | 11 + resources/lang/mk-MK/messages.php | 49 + resources/lang/mk-MK/modules.php | 82 + resources/lang/mk-MK/notifications.php | 27 + resources/lang/mk-MK/pagination.php | 9 + resources/lang/mk-MK/passwords.php | 22 + resources/lang/mk-MK/payments.php | 10 + resources/lang/mk-MK/portal.php | 53 + resources/lang/mk-MK/reconciliations.php | 22 + resources/lang/mk-MK/recurring.php | 41 + resources/lang/mk-MK/reports.php | 43 + resources/lang/mk-MK/revenues.php | 10 + resources/lang/mk-MK/search_string.php | 21 + resources/lang/mk-MK/settings.php | 203 + resources/lang/mk-MK/taxes.php | 20 + resources/lang/mk-MK/transactions.php | 47 + resources/lang/mk-MK/transfers.php | 34 + resources/lang/mk-MK/updates.php | 15 + resources/lang/mk-MK/validation.php | 174 + resources/lang/mk-MK/vendors.php | 16 + resources/lang/mk-MK/widgets.php | 34 + resources/lang/ms-MY/accounts.php | 31 + resources/lang/ms-MY/auth.php | 93 + resources/lang/ms-MY/bills.php | 63 + resources/lang/ms-MY/bulk_actions.php | 27 + resources/lang/ms-MY/categories.php | 11 + resources/lang/ms-MY/companies.php | 22 + resources/lang/ms-MY/countries.php | 256 + resources/lang/ms-MY/currencies.php | 27 + resources/lang/ms-MY/customers.php | 31 + resources/lang/ms-MY/dashboards.php | 15 + resources/lang/ms-MY/demo.php | 31 + resources/lang/ms-MY/documents.php | 104 + resources/lang/ms-MY/email_templates.php | 64 + resources/lang/ms-MY/errors.php | 25 + resources/lang/ms-MY/footer.php | 13 + resources/lang/ms-MY/general.php | 353 + resources/lang/ms-MY/header.php | 33 + resources/lang/ms-MY/import.php | 18 + resources/lang/ms-MY/install.php | 51 + resources/lang/ms-MY/invoices.php | 83 + resources/lang/ms-MY/items.php | 17 + resources/lang/ms-MY/maintenance.php | 11 + resources/lang/ms-MY/messages.php | 53 + resources/lang/ms-MY/modules.php | 133 + resources/lang/ms-MY/notifications.php | 250 + resources/lang/ms-MY/pagination.php | 9 + resources/lang/ms-MY/passwords.php | 22 + resources/lang/ms-MY/portal.php | 53 + resources/lang/ms-MY/reconciliations.php | 22 + resources/lang/ms-MY/recurring.php | 43 + resources/lang/ms-MY/reports.php | 45 + resources/lang/ms-MY/search_string.php | 21 + resources/lang/ms-MY/settings.php | 209 + resources/lang/ms-MY/taxes.php | 20 + resources/lang/ms-MY/transactions.php | 58 + resources/lang/ms-MY/transfers.php | 34 + resources/lang/ms-MY/updates.php | 15 + resources/lang/ms-MY/validation.php | 179 + resources/lang/ms-MY/vendors.php | 16 + resources/lang/ms-MY/widgets.php | 34 + resources/lang/nb-NO/accounts.php | 31 + resources/lang/nb-NO/auth.php | 41 + resources/lang/nb-NO/bills.php | 63 + resources/lang/nb-NO/bulk_actions.php | 26 + resources/lang/nb-NO/categories.php | 11 + resources/lang/nb-NO/companies.php | 22 + resources/lang/nb-NO/countries.php | 256 + resources/lang/nb-NO/currencies.php | 27 + resources/lang/nb-NO/customers.php | 30 + resources/lang/nb-NO/dashboards.php | 15 + resources/lang/nb-NO/demo.php | 31 + resources/lang/nb-NO/documents.php | 86 + resources/lang/nb-NO/email_templates.php | 59 + resources/lang/nb-NO/errors.php | 23 + resources/lang/nb-NO/footer.php | 13 + resources/lang/nb-NO/general.php | 239 + resources/lang/nb-NO/header.php | 28 + resources/lang/nb-NO/import.php | 10 + resources/lang/nb-NO/install.php | 46 + resources/lang/nb-NO/invoices.php | 61 + resources/lang/nb-NO/items.php | 17 + resources/lang/nb-NO/maintenance.php | 11 + resources/lang/nb-NO/messages.php | 41 + resources/lang/nb-NO/modules.php | 83 + resources/lang/nb-NO/notifications.php | 188 + resources/lang/nb-NO/pagination.php | 9 + resources/lang/nb-NO/passwords.php | 22 + resources/lang/nb-NO/payments.php | 10 + resources/lang/nb-NO/portal.php | 52 + resources/lang/nb-NO/reconciliations.php | 22 + resources/lang/nb-NO/recurring.php | 20 + resources/lang/nb-NO/reports.php | 28 + resources/lang/nb-NO/revenues.php | 10 + resources/lang/nb-NO/search_string.php | 21 + resources/lang/nb-NO/settings.php | 203 + resources/lang/nb-NO/taxes.php | 12 + resources/lang/nb-NO/transactions.php | 47 + resources/lang/nb-NO/transfers.php | 15 + resources/lang/nb-NO/updates.php | 15 + resources/lang/nb-NO/validation.php | 159 + resources/lang/nb-NO/vendors.php | 16 + resources/lang/nb-NO/widgets.php | 34 + resources/lang/ne-NP/accounts.php | 14 + resources/lang/ne-NP/auth.php | 41 + resources/lang/ne-NP/bills.php | 57 + resources/lang/ne-NP/bulk_actions.php | 23 + resources/lang/ne-NP/companies.php | 14 + resources/lang/ne-NP/countries.php | 253 + resources/lang/ne-NP/currencies.php | 19 + resources/lang/ne-NP/customers.php | 12 + resources/lang/ne-NP/dashboards.php | 11 + resources/lang/ne-NP/demo.php | 31 + resources/lang/ne-NP/documents.php | 12 + resources/lang/ne-NP/email_templates.php | 50 + resources/lang/ne-NP/errors.php | 23 + resources/lang/ne-NP/footer.php | 10 + resources/lang/ne-NP/general.php | 239 + resources/lang/ne-NP/header.php | 28 + resources/lang/ne-NP/import.php | 10 + resources/lang/ne-NP/install.php | 46 + resources/lang/ne-NP/invoices.php | 61 + resources/lang/ne-NP/items.php | 9 + resources/lang/ne-NP/maintenance.php | 9 + resources/lang/ne-NP/messages.php | 41 + resources/lang/ne-NP/modules.php | 83 + resources/lang/ne-NP/notifications.php | 79 + resources/lang/ne-NP/pagination.php | 9 + resources/lang/ne-NP/passwords.php | 22 + resources/lang/ne-NP/reconciliations.php | 18 + resources/lang/ne-NP/recurring.php | 20 + resources/lang/ne-NP/reports.php | 28 + resources/lang/ne-NP/search_string.php | 15 + resources/lang/ne-NP/settings.php | 144 + resources/lang/ne-NP/taxes.php | 12 + resources/lang/ne-NP/transfers.php | 15 + resources/lang/ne-NP/updates.php | 15 + resources/lang/ne-NP/validation.php | 123 + resources/lang/ne-NP/widgets.php | 24 + resources/lang/nl-NL/accounts.php | 31 + resources/lang/nl-NL/auth.php | 93 + resources/lang/nl-NL/bills.php | 63 + resources/lang/nl-NL/bulk_actions.php | 27 + resources/lang/nl-NL/categories.php | 11 + resources/lang/nl-NL/companies.php | 22 + resources/lang/nl-NL/countries.php | 256 + resources/lang/nl-NL/currencies.php | 27 + resources/lang/nl-NL/customers.php | 31 + resources/lang/nl-NL/dashboards.php | 15 + resources/lang/nl-NL/demo.php | 31 + resources/lang/nl-NL/documents.php | 104 + resources/lang/nl-NL/email_templates.php | 64 + resources/lang/nl-NL/errors.php | 25 + resources/lang/nl-NL/footer.php | 13 + resources/lang/nl-NL/general.php | 353 + resources/lang/nl-NL/header.php | 33 + resources/lang/nl-NL/import.php | 18 + resources/lang/nl-NL/install.php | 51 + resources/lang/nl-NL/invoices.php | 83 + resources/lang/nl-NL/items.php | 17 + resources/lang/nl-NL/maintenance.php | 11 + resources/lang/nl-NL/messages.php | 53 + resources/lang/nl-NL/modules.php | 136 + resources/lang/nl-NL/notifications.php | 250 + resources/lang/nl-NL/pagination.php | 9 + resources/lang/nl-NL/passwords.php | 22 + resources/lang/nl-NL/portal.php | 53 + resources/lang/nl-NL/reconciliations.php | 22 + resources/lang/nl-NL/recurring.php | 43 + resources/lang/nl-NL/reports.php | 45 + resources/lang/nl-NL/search_string.php | 21 + resources/lang/nl-NL/settings.php | 209 + resources/lang/nl-NL/taxes.php | 20 + resources/lang/nl-NL/transactions.php | 58 + resources/lang/nl-NL/transfers.php | 34 + resources/lang/nl-NL/updates.php | 15 + resources/lang/nl-NL/validation.php | 179 + resources/lang/nl-NL/vendors.php | 16 + resources/lang/nl-NL/widgets.php | 34 + resources/lang/pl-PL/accounts.php | 31 + resources/lang/pl-PL/auth.php | 93 + resources/lang/pl-PL/bills.php | 63 + resources/lang/pl-PL/bulk_actions.php | 27 + resources/lang/pl-PL/categories.php | 11 + resources/lang/pl-PL/companies.php | 22 + resources/lang/pl-PL/countries.php | 256 + resources/lang/pl-PL/currencies.php | 27 + resources/lang/pl-PL/customers.php | 31 + resources/lang/pl-PL/dashboards.php | 15 + resources/lang/pl-PL/demo.php | 31 + resources/lang/pl-PL/documents.php | 104 + resources/lang/pl-PL/email_templates.php | 64 + resources/lang/pl-PL/errors.php | 25 + resources/lang/pl-PL/footer.php | 13 + resources/lang/pl-PL/general.php | 353 + resources/lang/pl-PL/header.php | 33 + resources/lang/pl-PL/import.php | 18 + resources/lang/pl-PL/install.php | 51 + resources/lang/pl-PL/invoices.php | 83 + resources/lang/pl-PL/items.php | 17 + resources/lang/pl-PL/maintenance.php | 11 + resources/lang/pl-PL/messages.php | 53 + resources/lang/pl-PL/modules.php | 129 + resources/lang/pl-PL/notifications.php | 155 + resources/lang/pl-PL/pagination.php | 9 + resources/lang/pl-PL/passwords.php | 22 + resources/lang/pl-PL/portal.php | 53 + resources/lang/pl-PL/reconciliations.php | 22 + resources/lang/pl-PL/recurring.php | 43 + resources/lang/pl-PL/reports.php | 45 + resources/lang/pl-PL/search_string.php | 21 + resources/lang/pl-PL/settings.php | 209 + resources/lang/pl-PL/taxes.php | 20 + resources/lang/pl-PL/transactions.php | 58 + resources/lang/pl-PL/transfers.php | 34 + resources/lang/pl-PL/updates.php | 15 + resources/lang/pl-PL/validation.php | 146 + resources/lang/pl-PL/vendors.php | 16 + resources/lang/pl-PL/widgets.php | 34 + resources/lang/pt-BR/accounts.php | 31 + resources/lang/pt-BR/auth.php | 93 + resources/lang/pt-BR/bills.php | 63 + resources/lang/pt-BR/bulk_actions.php | 27 + resources/lang/pt-BR/categories.php | 11 + resources/lang/pt-BR/companies.php | 22 + resources/lang/pt-BR/countries.php | 256 + resources/lang/pt-BR/currencies.php | 27 + resources/lang/pt-BR/customers.php | 31 + resources/lang/pt-BR/dashboards.php | 15 + resources/lang/pt-BR/demo.php | 31 + resources/lang/pt-BR/documents.php | 104 + resources/lang/pt-BR/email_templates.php | 64 + resources/lang/pt-BR/errors.php | 25 + resources/lang/pt-BR/footer.php | 13 + resources/lang/pt-BR/general.php | 353 + resources/lang/pt-BR/header.php | 33 + resources/lang/pt-BR/import.php | 18 + resources/lang/pt-BR/install.php | 51 + resources/lang/pt-BR/invoices.php | 82 + resources/lang/pt-BR/items.php | 17 + resources/lang/pt-BR/maintenance.php | 11 + resources/lang/pt-BR/messages.php | 53 + resources/lang/pt-BR/modules.php | 133 + resources/lang/pt-BR/notifications.php | 247 + resources/lang/pt-BR/pagination.php | 9 + resources/lang/pt-BR/passwords.php | 22 + resources/lang/pt-BR/portal.php | 53 + resources/lang/pt-BR/reconciliations.php | 22 + resources/lang/pt-BR/recurring.php | 43 + resources/lang/pt-BR/reports.php | 45 + resources/lang/pt-BR/search_string.php | 21 + resources/lang/pt-BR/settings.php | 209 + resources/lang/pt-BR/taxes.php | 20 + resources/lang/pt-BR/transactions.php | 58 + resources/lang/pt-BR/transfers.php | 34 + resources/lang/pt-BR/updates.php | 15 + resources/lang/pt-BR/validation.php | 179 + resources/lang/pt-BR/vendors.php | 16 + resources/lang/pt-BR/widgets.php | 34 + resources/lang/pt-PT/accounts.php | 14 + resources/lang/pt-PT/auth.php | 41 + resources/lang/pt-PT/bills.php | 69 + resources/lang/pt-PT/bulk_actions.php | 21 + resources/lang/pt-PT/companies.php | 14 + resources/lang/pt-PT/countries.php | 253 + resources/lang/pt-PT/currencies.php | 18 + resources/lang/pt-PT/customers.php | 12 + resources/lang/pt-PT/dashboards.php | 11 + resources/lang/pt-PT/demo.php | 31 + resources/lang/pt-PT/email_templates.php | 50 + resources/lang/pt-PT/errors.php | 23 + resources/lang/pt-PT/footer.php | 10 + resources/lang/pt-PT/general.php | 206 + resources/lang/pt-PT/header.php | 16 + resources/lang/pt-PT/import.php | 9 + resources/lang/pt-PT/install.php | 46 + resources/lang/pt-PT/invoices.php | 77 + resources/lang/pt-PT/items.php | 8 + resources/lang/pt-PT/maintenance.php | 9 + resources/lang/pt-PT/messages.php | 37 + resources/lang/pt-PT/modules.php | 83 + resources/lang/pt-PT/notifications.php | 10 + resources/lang/pt-PT/pagination.php | 9 + resources/lang/pt-PT/passwords.php | 23 + resources/lang/pt-PT/reconciliations.php | 17 + resources/lang/pt-PT/recurring.php | 20 + resources/lang/pt-PT/reports.php | 27 + resources/lang/pt-PT/settings.php | 129 + resources/lang/pt-PT/taxes.php | 12 + resources/lang/pt-PT/transfers.php | 12 + resources/lang/pt-PT/updates.php | 15 + resources/lang/pt-PT/validation.php | 123 + resources/lang/pt-PT/widgets.php | 23 + resources/lang/ro-RO/accounts.php | 31 + resources/lang/ro-RO/auth.php | 93 + resources/lang/ro-RO/bills.php | 64 + resources/lang/ro-RO/bulk_actions.php | 23 + resources/lang/ro-RO/categories.php | 11 + resources/lang/ro-RO/companies.php | 22 + resources/lang/ro-RO/countries.php | 256 + resources/lang/ro-RO/currencies.php | 27 + resources/lang/ro-RO/customers.php | 30 + resources/lang/ro-RO/dashboard.php | 24 + resources/lang/ro-RO/dashboards.php | 15 + resources/lang/ro-RO/demo.php | 31 + resources/lang/ro-RO/documents.php | 56 + resources/lang/ro-RO/email_templates.php | 64 + resources/lang/ro-RO/errors.php | 25 + resources/lang/ro-RO/footer.php | 13 + resources/lang/ro-RO/general.php | 311 + resources/lang/ro-RO/header.php | 16 + resources/lang/ro-RO/import.php | 9 + resources/lang/ro-RO/install.php | 51 + resources/lang/ro-RO/invoices.php | 86 + resources/lang/ro-RO/items.php | 17 + resources/lang/ro-RO/maintenance.php | 11 + resources/lang/ro-RO/messages.php | 47 + resources/lang/ro-RO/modules.php | 83 + resources/lang/ro-RO/notifications.php | 54 + resources/lang/ro-RO/pagination.php | 9 + resources/lang/ro-RO/passwords.php | 22 + resources/lang/ro-RO/portal.php | 52 + resources/lang/ro-RO/reconciliations.php | 22 + resources/lang/ro-RO/recurring.php | 42 + resources/lang/ro-RO/reports.php | 43 + resources/lang/ro-RO/search_string.php | 21 + resources/lang/ro-RO/settings.php | 203 + resources/lang/ro-RO/taxes.php | 20 + resources/lang/ro-RO/transfers.php | 34 + resources/lang/ro-RO/updates.php | 15 + resources/lang/ro-RO/validation.php | 123 + resources/lang/ro-RO/vendors.php | 16 + resources/lang/ro-RO/widgets.php | 34 + resources/lang/ru-RU/accounts.php | 31 + resources/lang/ru-RU/auth.php | 93 + resources/lang/ru-RU/bills.php | 63 + resources/lang/ru-RU/bulk_actions.php | 27 + resources/lang/ru-RU/categories.php | 11 + resources/lang/ru-RU/companies.php | 22 + resources/lang/ru-RU/countries.php | 254 + resources/lang/ru-RU/currencies.php | 27 + resources/lang/ru-RU/customers.php | 31 + resources/lang/ru-RU/dashboards.php | 15 + resources/lang/ru-RU/demo.php | 31 + resources/lang/ru-RU/documents.php | 104 + resources/lang/ru-RU/email_templates.php | 65 + resources/lang/ru-RU/errors.php | 25 + resources/lang/ru-RU/footer.php | 13 + resources/lang/ru-RU/general.php | 353 + resources/lang/ru-RU/header.php | 33 + resources/lang/ru-RU/import.php | 18 + resources/lang/ru-RU/install.php | 51 + resources/lang/ru-RU/invoices.php | 83 + resources/lang/ru-RU/items.php | 17 + resources/lang/ru-RU/maintenance.php | 11 + resources/lang/ru-RU/messages.php | 53 + resources/lang/ru-RU/modules.php | 133 + resources/lang/ru-RU/notifications.php | 250 + resources/lang/ru-RU/pagination.php | 9 + resources/lang/ru-RU/passwords.php | 22 + resources/lang/ru-RU/portal.php | 53 + resources/lang/ru-RU/reconciliations.php | 22 + resources/lang/ru-RU/recurring.php | 43 + resources/lang/ru-RU/reports.php | 45 + resources/lang/ru-RU/search_string.php | 21 + resources/lang/ru-RU/settings.php | 209 + resources/lang/ru-RU/taxes.php | 20 + resources/lang/ru-RU/transactions.php | 58 + resources/lang/ru-RU/transfers.php | 34 + resources/lang/ru-RU/updates.php | 15 + resources/lang/ru-RU/validation.php | 179 + resources/lang/ru-RU/vendors.php | 16 + resources/lang/ru-RU/widgets.php | 34 + resources/lang/si-LK/passwords.php | 22 + resources/lang/sk-SK/accounts.php | 31 + resources/lang/sk-SK/auth.php | 93 + resources/lang/sk-SK/bills.php | 58 + resources/lang/sk-SK/bulk_actions.php | 26 + resources/lang/sk-SK/categories.php | 11 + resources/lang/sk-SK/companies.php | 22 + resources/lang/sk-SK/countries.php | 256 + resources/lang/sk-SK/currencies.php | 27 + resources/lang/sk-SK/customers.php | 30 + resources/lang/sk-SK/dashboard.php | 24 + resources/lang/sk-SK/dashboards.php | 15 + resources/lang/sk-SK/demo.php | 16 + resources/lang/sk-SK/errors.php | 25 + resources/lang/sk-SK/footer.php | 10 + resources/lang/sk-SK/general.php | 148 + resources/lang/sk-SK/header.php | 16 + resources/lang/sk-SK/import.php | 10 + resources/lang/sk-SK/install.php | 44 + resources/lang/sk-SK/invoices.php | 68 + resources/lang/sk-SK/items.php | 8 + resources/lang/sk-SK/messages.php | 29 + resources/lang/sk-SK/modules.php | 80 + resources/lang/sk-SK/notifications.php | 10 + resources/lang/sk-SK/pagination.php | 9 + resources/lang/sk-SK/passwords.php | 22 + resources/lang/sk-SK/reconciliations.php | 14 + resources/lang/sk-SK/recurring.php | 20 + resources/lang/sk-SK/reports.php | 27 + resources/lang/sk-SK/search_string.php | 20 + resources/lang/sk-SK/settings.php | 102 + resources/lang/sk-SK/taxes.php | 8 + resources/lang/sk-SK/transfers.php | 12 + resources/lang/sk-SK/updates.php | 15 + resources/lang/sk-SK/validation.php | 121 + resources/lang/sl-SI/accounts.php | 32 + resources/lang/sl-SI/auth.php | 97 + resources/lang/sl-SI/bills.php | 63 + resources/lang/sl-SI/bulk_actions.php | 28 + resources/lang/sl-SI/categories.php | 11 + resources/lang/sl-SI/companies.php | 22 + resources/lang/sl-SI/countries.php | 256 + resources/lang/sl-SI/currencies.php | 27 + resources/lang/sl-SI/customers.php | 39 + resources/lang/sl-SI/dashboards.php | 17 + resources/lang/sl-SI/demo.php | 34 + resources/lang/sl-SI/documents.php | 103 + resources/lang/sl-SI/email_templates.php | 66 + resources/lang/sl-SI/errors.php | 26 + resources/lang/sl-SI/footer.php | 13 + resources/lang/sl-SI/general.php | 340 + resources/lang/sl-SI/header.php | 33 + resources/lang/sl-SI/import.php | 13 + resources/lang/sl-SI/install.php | 51 + resources/lang/sl-SI/invoices.php | 83 + resources/lang/sl-SI/items.php | 17 + resources/lang/sl-SI/maintenance.php | 11 + resources/lang/sl-SI/messages.php | 50 + resources/lang/sl-SI/modules.php | 136 + resources/lang/sl-SI/notifications.php | 218 + resources/lang/sl-SI/pagination.php | 9 + resources/lang/sl-SI/passwords.php | 22 + resources/lang/sl-SI/payments.php | 10 + resources/lang/sl-SI/portal.php | 53 + resources/lang/sl-SI/reconciliations.php | 22 + resources/lang/sl-SI/recurring.php | 43 + resources/lang/sl-SI/reports.php | 40 + resources/lang/sl-SI/revenues.php | 10 + resources/lang/sl-SI/search_string.php | 21 + resources/lang/sl-SI/settings.php | 212 + resources/lang/sl-SI/taxes.php | 20 + resources/lang/sl-SI/transactions.php | 53 + resources/lang/sl-SI/transfers.php | 34 + resources/lang/sl-SI/updates.php | 15 + resources/lang/sl-SI/validation.php | 174 + resources/lang/sl-SI/vendors.php | 16 + resources/lang/sl-SI/widgets.php | 34 + resources/lang/sq-AL/accounts.php | 14 + resources/lang/sq-AL/auth.php | 41 + resources/lang/sq-AL/bills.php | 56 + resources/lang/sq-AL/bulk_actions.php | 23 + resources/lang/sq-AL/companies.php | 14 + resources/lang/sq-AL/countries.php | 253 + resources/lang/sq-AL/currencies.php | 19 + resources/lang/sq-AL/customers.php | 12 + resources/lang/sq-AL/dashboards.php | 11 + resources/lang/sq-AL/demo.php | 31 + resources/lang/sq-AL/documents.php | 54 + resources/lang/sq-AL/email_templates.php | 50 + resources/lang/sq-AL/errors.php | 23 + resources/lang/sq-AL/footer.php | 10 + resources/lang/sq-AL/general.php | 232 + resources/lang/sq-AL/header.php | 16 + resources/lang/sq-AL/import.php | 9 + resources/lang/sq-AL/install.php | 46 + resources/lang/sq-AL/invoices.php | 60 + resources/lang/sq-AL/items.php | 9 + resources/lang/sq-AL/maintenance.php | 9 + resources/lang/sq-AL/messages.php | 39 + resources/lang/sq-AL/modules.php | 83 + resources/lang/sq-AL/notifications.php | 54 + resources/lang/sq-AL/pagination.php | 9 + resources/lang/sq-AL/passwords.php | 23 + resources/lang/sq-AL/reconciliations.php | 18 + resources/lang/sq-AL/recurring.php | 20 + resources/lang/sq-AL/reports.php | 28 + resources/lang/sq-AL/search_string.php | 20 + resources/lang/sq-AL/settings.php | 144 + resources/lang/sq-AL/taxes.php | 12 + resources/lang/sq-AL/transfers.php | 14 + resources/lang/sq-AL/updates.php | 15 + resources/lang/sq-AL/validation.php | 123 + resources/lang/sq-AL/widgets.php | 24 + resources/lang/sr-CS/accounts.php | 14 + resources/lang/sr-CS/auth.php | 41 + resources/lang/sr-CS/bills.php | 56 + resources/lang/sr-CS/bulk_actions.php | 23 + resources/lang/sr-CS/companies.php | 14 + resources/lang/sr-CS/countries.php | 253 + resources/lang/sr-CS/currencies.php | 19 + resources/lang/sr-CS/customers.php | 12 + resources/lang/sr-CS/dashboards.php | 11 + resources/lang/sr-CS/demo.php | 31 + resources/lang/sr-CS/documents.php | 12 + resources/lang/sr-CS/errors.php | 23 + resources/lang/sr-CS/footer.php | 10 + resources/lang/sr-CS/import.php | 9 + resources/lang/sr-CS/items.php | 8 + resources/lang/sr-CS/pagination.php | 9 + resources/lang/sr-CS/passwords.php | 23 + resources/lang/sr-CS/search_string.php | 15 + resources/lang/sr-CS/taxes.php | 12 + resources/lang/sr-CS/transfers.php | 12 + resources/lang/sr-CS/updates.php | 15 + resources/lang/sr-CS/widgets.php | 23 + resources/lang/sr-RS/accounts.php | 14 + resources/lang/sr-RS/auth.php | 39 + resources/lang/sr-RS/bills.php | 58 + resources/lang/sr-RS/companies.php | 13 + resources/lang/sr-RS/countries.php | 253 + resources/lang/sr-RS/currencies.php | 18 + resources/lang/sr-RS/customers.php | 16 + resources/lang/sr-RS/dashboard.php | 24 + resources/lang/sr-RS/demo.php | 16 + resources/lang/sr-RS/footer.php | 10 + resources/lang/sr-RS/general.php | 146 + resources/lang/sr-RS/header.php | 16 + resources/lang/sr-RS/import.php | 9 + resources/lang/sr-RS/install.php | 44 + resources/lang/sr-RS/invoices.php | 68 + resources/lang/sr-RS/items.php | 18 + resources/lang/sr-RS/messages.php | 32 + resources/lang/sr-RS/modules.php | 83 + resources/lang/sr-RS/notifications.php | 10 + resources/lang/sr-RS/pagination.php | 9 + resources/lang/sr-RS/passwords.php | 22 + resources/lang/sr-RS/reconciliations.php | 16 + resources/lang/sr-RS/recurring.php | 20 + resources/lang/sr-RS/reports.php | 27 + resources/lang/sr-RS/settings.php | 102 + resources/lang/sr-RS/taxes.php | 11 + resources/lang/sr-RS/transfers.php | 12 + resources/lang/sr-RS/updates.php | 15 + resources/lang/sr-RS/validation.php | 121 + resources/lang/sr-SP/accounts.php | 14 + resources/lang/sr-SP/auth.php | 41 + resources/lang/sr-SP/bills.php | 56 + resources/lang/sr-SP/countries.php | 253 + resources/lang/sr-SP/currencies.php | 18 + resources/lang/sr-SP/footer.php | 10 + resources/lang/sr-SP/header.php | 16 + resources/lang/sr-SP/import.php | 9 + resources/lang/sr-SP/items.php | 8 + resources/lang/sr-SP/notifications.php | 10 + resources/lang/sr-SP/recurring.php | 20 + resources/lang/sr-SP/transfers.php | 12 + resources/lang/sr-SP/updates.php | 15 + resources/lang/sv-SE/accounts.php | 31 + resources/lang/sv-SE/auth.php | 93 + resources/lang/sv-SE/bills.php | 63 + resources/lang/sv-SE/bulk_actions.php | 26 + resources/lang/sv-SE/categories.php | 11 + resources/lang/sv-SE/companies.php | 22 + resources/lang/sv-SE/countries.php | 256 + resources/lang/sv-SE/currencies.php | 27 + resources/lang/sv-SE/customers.php | 30 + resources/lang/sv-SE/dashboards.php | 15 + resources/lang/sv-SE/demo.php | 31 + resources/lang/sv-SE/documents.php | 86 + resources/lang/sv-SE/email_templates.php | 64 + resources/lang/sv-SE/errors.php | 25 + resources/lang/sv-SE/footer.php | 13 + resources/lang/sv-SE/general.php | 312 + resources/lang/sv-SE/header.php | 33 + resources/lang/sv-SE/import.php | 10 + resources/lang/sv-SE/install.php | 51 + resources/lang/sv-SE/invoices.php | 85 + resources/lang/sv-SE/items.php | 17 + resources/lang/sv-SE/maintenance.php | 11 + resources/lang/sv-SE/messages.php | 47 + resources/lang/sv-SE/modules.php | 135 + resources/lang/sv-SE/notifications.php | 188 + resources/lang/sv-SE/pagination.php | 9 + resources/lang/sv-SE/passwords.php | 22 + resources/lang/sv-SE/payments.php | 10 + resources/lang/sv-SE/portal.php | 53 + resources/lang/sv-SE/reconciliations.php | 22 + resources/lang/sv-SE/recurring.php | 41 + resources/lang/sv-SE/reports.php | 43 + resources/lang/sv-SE/revenues.php | 10 + resources/lang/sv-SE/search_string.php | 21 + resources/lang/sv-SE/settings.php | 204 + resources/lang/sv-SE/taxes.php | 20 + resources/lang/sv-SE/transactions.php | 47 + resources/lang/sv-SE/transfers.php | 34 + resources/lang/sv-SE/updates.php | 15 + resources/lang/sv-SE/validation.php | 174 + resources/lang/sv-SE/vendors.php | 16 + resources/lang/sv-SE/widgets.php | 34 + resources/lang/sw-KE/passwords.php | 22 + resources/lang/ta-IN/accounts.php | 14 + resources/lang/ta-IN/auth.php | 41 + resources/lang/ta-IN/countries.php | 253 + resources/lang/th-TH/accounts.php | 14 + resources/lang/th-TH/auth.php | 41 + resources/lang/th-TH/bills.php | 61 + resources/lang/th-TH/companies.php | 14 + resources/lang/th-TH/countries.php | 253 + resources/lang/th-TH/currencies.php | 19 + resources/lang/th-TH/customers.php | 12 + resources/lang/th-TH/dashboard.php | 24 + resources/lang/th-TH/dashboards.php | 11 + resources/lang/th-TH/demo.php | 31 + resources/lang/th-TH/errors.php | 23 + resources/lang/th-TH/footer.php | 10 + resources/lang/th-TH/general.php | 147 + resources/lang/th-TH/header.php | 16 + resources/lang/th-TH/import.php | 9 + resources/lang/th-TH/install.php | 44 + resources/lang/th-TH/invoices.php | 71 + resources/lang/th-TH/items.php | 8 + resources/lang/th-TH/maintenance.php | 9 + resources/lang/th-TH/messages.php | 32 + resources/lang/th-TH/modules.php | 83 + resources/lang/th-TH/notifications.php | 10 + resources/lang/th-TH/pagination.php | 9 + resources/lang/th-TH/passwords.php | 23 + resources/lang/th-TH/reconciliations.php | 16 + resources/lang/th-TH/recurring.php | 20 + resources/lang/th-TH/reports.php | 27 + resources/lang/th-TH/settings.php | 103 + resources/lang/th-TH/taxes.php | 12 + resources/lang/th-TH/transfers.php | 12 + resources/lang/th-TH/updates.php | 15 + resources/lang/th-TH/validation.php | 123 + resources/lang/tr-TR/accounts.php | 31 + resources/lang/tr-TR/auth.php | 93 + resources/lang/tr-TR/bills.php | 63 + resources/lang/tr-TR/bulk_actions.php | 27 + resources/lang/tr-TR/categories.php | 11 + resources/lang/tr-TR/companies.php | 22 + resources/lang/tr-TR/countries.php | 256 + resources/lang/tr-TR/currencies.php | 27 + resources/lang/tr-TR/customers.php | 31 + resources/lang/tr-TR/dashboards.php | 15 + resources/lang/tr-TR/demo.php | 31 + resources/lang/tr-TR/documents.php | 104 + resources/lang/tr-TR/email_templates.php | 64 + resources/lang/tr-TR/errors.php | 25 + resources/lang/tr-TR/footer.php | 13 + resources/lang/tr-TR/general.php | 353 + resources/lang/tr-TR/header.php | 33 + resources/lang/tr-TR/import.php | 18 + resources/lang/tr-TR/install.php | 51 + resources/lang/tr-TR/invoices.php | 83 + resources/lang/tr-TR/items.php | 17 + resources/lang/tr-TR/maintenance.php | 11 + resources/lang/tr-TR/messages.php | 53 + resources/lang/tr-TR/modules.php | 134 + resources/lang/tr-TR/notifications.php | 250 + resources/lang/tr-TR/pagination.php | 9 + resources/lang/tr-TR/passwords.php | 22 + resources/lang/tr-TR/portal.php | 53 + resources/lang/tr-TR/reconciliations.php | 22 + resources/lang/tr-TR/recurring.php | 43 + resources/lang/tr-TR/reports.php | 45 + resources/lang/tr-TR/search_string.php | 21 + resources/lang/tr-TR/settings.php | 209 + resources/lang/tr-TR/taxes.php | 20 + resources/lang/tr-TR/transactions.php | 58 + resources/lang/tr-TR/transfers.php | 34 + resources/lang/tr-TR/updates.php | 15 + resources/lang/tr-TR/validation.php | 179 + resources/lang/tr-TR/vendors.php | 16 + resources/lang/tr-TR/widgets.php | 34 + resources/lang/uk-UA/accounts.php | 14 + resources/lang/uk-UA/auth.php | 41 + resources/lang/uk-UA/bills.php | 63 + resources/lang/uk-UA/bulk_actions.php | 19 + resources/lang/uk-UA/companies.php | 14 + resources/lang/uk-UA/countries.php | 253 + resources/lang/uk-UA/currencies.php | 18 + resources/lang/uk-UA/customers.php | 12 + resources/lang/uk-UA/dashboards.php | 11 + resources/lang/uk-UA/demo.php | 31 + resources/lang/uk-UA/email_templates.php | 50 + resources/lang/uk-UA/errors.php | 23 + resources/lang/uk-UA/footer.php | 10 + resources/lang/uk-UA/general.php | 206 + resources/lang/uk-UA/header.php | 16 + resources/lang/uk-UA/import.php | 9 + resources/lang/uk-UA/install.php | 44 + resources/lang/uk-UA/invoices.php | 72 + resources/lang/uk-UA/items.php | 8 + resources/lang/uk-UA/maintenance.php | 9 + resources/lang/uk-UA/messages.php | 35 + resources/lang/uk-UA/modules.php | 83 + resources/lang/uk-UA/notifications.php | 10 + resources/lang/uk-UA/pagination.php | 9 + resources/lang/uk-UA/passwords.php | 22 + resources/lang/uk-UA/reconciliations.php | 16 + resources/lang/uk-UA/recurring.php | 21 + resources/lang/uk-UA/reports.php | 27 + resources/lang/uk-UA/settings.php | 123 + resources/lang/uk-UA/taxes.php | 11 + resources/lang/uk-UA/transfers.php | 12 + resources/lang/uk-UA/updates.php | 15 + resources/lang/uk-UA/validation.php | 122 + resources/lang/uk-UA/widgets.php | 23 + resources/lang/ur-PK/accounts.php | 31 + resources/lang/ur-PK/auth.php | 39 + resources/lang/ur-PK/bills.php | 61 + resources/lang/ur-PK/companies.php | 13 + resources/lang/ur-PK/countries.php | 253 + resources/lang/ur-PK/currencies.php | 18 + resources/lang/ur-PK/customers.php | 16 + resources/lang/ur-PK/dashboard.php | 24 + resources/lang/ur-PK/demo.php | 13 + resources/lang/ur-PK/errors.php | 24 + resources/lang/ur-PK/footer.php | 10 + resources/lang/ur-PK/general.php | 148 + resources/lang/ur-PK/header.php | 16 + resources/lang/ur-PK/import.php | 9 + resources/lang/ur-PK/install.php | 44 + resources/lang/ur-PK/invoices.php | 72 + resources/lang/ur-PK/items.php | 8 + resources/lang/ur-PK/messages.php | 32 + resources/lang/ur-PK/modules.php | 85 + resources/lang/ur-PK/notifications.php | 10 + resources/lang/ur-PK/pagination.php | 9 + resources/lang/ur-PK/passwords.php | 22 + resources/lang/ur-PK/reconciliations.php | 16 + resources/lang/ur-PK/recurring.php | 20 + resources/lang/ur-PK/reports.php | 27 + resources/lang/ur-PK/settings.php | 103 + resources/lang/ur-PK/taxes.php | 11 + resources/lang/ur-PK/transfers.php | 12 + resources/lang/ur-PK/updates.php | 15 + resources/lang/ur-PK/validation.php | 121 + resources/lang/uz-UZ/accounts.php | 14 + resources/lang/uz-UZ/auth.php | 41 + resources/lang/uz-UZ/bills.php | 61 + resources/lang/uz-UZ/bulk_actions.php | 19 + resources/lang/uz-UZ/companies.php | 14 + resources/lang/uz-UZ/countries.php | 253 + resources/lang/uz-UZ/currencies.php | 18 + resources/lang/uz-UZ/customers.php | 12 + resources/lang/uz-UZ/dashboard.php | 23 + resources/lang/uz-UZ/dashboards.php | 11 + resources/lang/uz-UZ/demo.php | 31 + resources/lang/uz-UZ/email_templates.php | 50 + resources/lang/uz-UZ/errors.php | 23 + resources/lang/uz-UZ/footer.php | 10 + resources/lang/uz-UZ/general.php | 181 + resources/lang/uz-UZ/header.php | 16 + resources/lang/uz-UZ/import.php | 9 + resources/lang/uz-UZ/install.php | 44 + resources/lang/uz-UZ/invoices.php | 69 + resources/lang/uz-UZ/items.php | 8 + resources/lang/uz-UZ/maintenance.php | 9 + resources/lang/uz-UZ/messages.php | 34 + resources/lang/uz-UZ/modules.php | 83 + resources/lang/uz-UZ/notifications.php | 10 + resources/lang/uz-UZ/pagination.php | 9 + resources/lang/uz-UZ/passwords.php | 22 + resources/lang/uz-UZ/reconciliations.php | 16 + resources/lang/uz-UZ/recurring.php | 20 + resources/lang/uz-UZ/reports.php | 27 + resources/lang/uz-UZ/settings.php | 119 + resources/lang/uz-UZ/taxes.php | 11 + resources/lang/uz-UZ/transfers.php | 12 + resources/lang/uz-UZ/updates.php | 15 + resources/lang/uz-UZ/validation.php | 122 + resources/lang/uz-UZ/widgets.php | 23 + resources/lang/vi-VN/accounts.php | 31 + resources/lang/vi-VN/auth.php | 39 + resources/lang/vi-VN/bills.php | 58 + resources/lang/vi-VN/companies.php | 13 + resources/lang/vi-VN/countries.php | 253 + resources/lang/vi-VN/currencies.php | 18 + resources/lang/vi-VN/customers.php | 16 + resources/lang/vi-VN/dashboard.php | 24 + resources/lang/vi-VN/demo.php | 16 + resources/lang/vi-VN/footer.php | 10 + resources/lang/vi-VN/general.php | 148 + resources/lang/vi-VN/header.php | 16 + resources/lang/vi-VN/import.php | 10 + resources/lang/vi-VN/install.php | 44 + resources/lang/vi-VN/invoices.php | 68 + resources/lang/vi-VN/items.php | 8 + resources/lang/vi-VN/messages.php | 29 + resources/lang/vi-VN/modules.php | 80 + resources/lang/vi-VN/notifications.php | 10 + resources/lang/vi-VN/pagination.php | 9 + resources/lang/vi-VN/passwords.php | 22 + resources/lang/vi-VN/reconciliations.php | 14 + resources/lang/vi-VN/recurring.php | 21 + resources/lang/vi-VN/reports.php | 27 + resources/lang/vi-VN/settings.php | 102 + resources/lang/vi-VN/taxes.php | 8 + resources/lang/vi-VN/transfers.php | 12 + resources/lang/vi-VN/updates.php | 15 + resources/lang/vi-VN/validation.php | 121 + resources/lang/zh-CN/accounts.php | 31 + resources/lang/zh-CN/auth.php | 93 + resources/lang/zh-CN/bills.php | 63 + resources/lang/zh-CN/bulk_actions.php | 27 + resources/lang/zh-CN/categories.php | 11 + resources/lang/zh-CN/companies.php | 22 + resources/lang/zh-CN/countries.php | 256 + resources/lang/zh-CN/currencies.php | 27 + resources/lang/zh-CN/customers.php | 31 + resources/lang/zh-CN/dashboards.php | 15 + resources/lang/zh-CN/demo.php | 31 + resources/lang/zh-CN/documents.php | 104 + resources/lang/zh-CN/email_templates.php | 64 + resources/lang/zh-CN/errors.php | 25 + resources/lang/zh-CN/footer.php | 13 + resources/lang/zh-CN/fund_accounts.php | 35 + resources/lang/zh-CN/general.php | 353 + resources/lang/zh-CN/header.php | 33 + resources/lang/zh-CN/import.php | 18 + resources/lang/zh-CN/install.php | 51 + resources/lang/zh-CN/invoices.php | 83 + resources/lang/zh-CN/items.php | 17 + resources/lang/zh-CN/maintenance.php | 11 + resources/lang/zh-CN/messages.php | 53 + resources/lang/zh-CN/modules.php | 133 + resources/lang/zh-CN/notifications.php | 250 + resources/lang/zh-CN/pagination.php | 9 + resources/lang/zh-CN/passwords.php | 21 + resources/lang/zh-CN/portal.php | 53 + resources/lang/zh-CN/reconciliations.php | 22 + resources/lang/zh-CN/recurring.php | 43 + resources/lang/zh-CN/reports.php | 45 + resources/lang/zh-CN/search_string.php | 21 + resources/lang/zh-CN/settings.php | 209 + resources/lang/zh-CN/taxes.php | 19 + resources/lang/zh-CN/transactions.php | 58 + resources/lang/zh-CN/transfers.php | 34 + resources/lang/zh-CN/updates.php | 15 + resources/lang/zh-CN/validation.php | 179 + resources/lang/zh-CN/vendors.php | 16 + resources/lang/zh-CN/widgets.php | 34 + resources/lang/zh-TW/accounts.php | 14 + resources/lang/zh-TW/auth.php | 41 + resources/lang/zh-TW/bills.php | 56 + resources/lang/zh-TW/bulk_actions.php | 23 + resources/lang/zh-TW/companies.php | 14 + resources/lang/zh-TW/countries.php | 253 + resources/lang/zh-TW/currencies.php | 19 + resources/lang/zh-TW/customers.php | 12 + resources/lang/zh-TW/dashboards.php | 11 + resources/lang/zh-TW/demo.php | 31 + resources/lang/zh-TW/email_templates.php | 50 + resources/lang/zh-TW/errors.php | 23 + resources/lang/zh-TW/footer.php | 10 + resources/lang/zh-TW/general.php | 231 + resources/lang/zh-TW/header.php | 16 + resources/lang/zh-TW/import.php | 9 + resources/lang/zh-TW/install.php | 46 + resources/lang/zh-TW/invoices.php | 60 + resources/lang/zh-TW/items.php | 9 + resources/lang/zh-TW/maintenance.php | 9 + resources/lang/zh-TW/messages.php | 37 + resources/lang/zh-TW/modules.php | 83 + resources/lang/zh-TW/notifications.php | 27 + resources/lang/zh-TW/pagination.php | 9 + resources/lang/zh-TW/passwords.php | 23 + resources/lang/zh-TW/reconciliations.php | 18 + resources/lang/zh-TW/recurring.php | 20 + resources/lang/zh-TW/reports.php | 27 + resources/lang/zh-TW/settings.php | 144 + resources/lang/zh-TW/taxes.php | 12 + resources/lang/zh-TW/transfers.php | 12 + resources/lang/zh-TW/updates.php | 15 + resources/lang/zh-TW/validation.php | 123 + resources/lang/zh-TW/widgets.php | 24 + resources/views/auth/forgot/create.blade.php | 53 + resources/views/auth/login/create.blade.php | 81 + .../views/auth/register/create.blade.php | 64 + resources/views/auth/reset/create.blade.php | 71 + resources/views/auth/users/create.blade.php | 77 + resources/views/auth/users/edit.blade.php | 91 + resources/views/auth/users/index.blade.php | 110 + resources/views/auth/users/show.blade.php | 273 + .../views/banking/accounts/create.blade.php | 77 + .../views/banking/accounts/edit.blade.php | 75 + .../views/banking/accounts/index.blade.php | 116 + .../views/banking/accounts/show.blade.php | 472 + .../banking/fund_accounts/form.blade.php | 69 + .../banking/fund_accounts/index.blade.php | 174 + .../fund_accounts/store-index.blade.php | 14 + .../banking/reconciliations/create.blade.php | 315 + .../banking/reconciliations/edit.blade.php | 265 + .../banking/reconciliations/index.blade.php | 123 + .../recurring_transactions/create.blade.php | 124 + .../recurring_transactions/edit.blade.php | 129 + .../recurring_transactions/index.blade.php | 32 + .../recurring_transactions/show.blade.php | 38 + .../banking/transactions/create.blade.php | 89 + .../views/banking/transactions/edit.blade.php | 112 + .../banking/transactions/import.blade.php | 151 + .../banking/transactions/index.blade.php | 30 + .../transactions/print_default.blade.php | 9 + .../views/banking/transactions/show.blade.php | 23 + .../views/banking/transfers/create.blade.php | 82 + .../views/banking/transfers/edit.blade.php | 94 + .../views/banking/transfers/index.blade.php | 166 + .../banking/transfers/print_default.blade.php | 9 + .../banking/transfers/print_second.blade.php | 9 + .../banking/transfers/print_third.blade.php | 9 + .../views/banking/transfers/show.blade.php | 58 + .../bulk-actions/auth/users/edit.blade.php | 43 + .../banking/accounts/edit.blade.php | 41 + .../banking/transactions/edit.blade.php | 7 + .../common/companies/edit.blade.php | 16 + .../bulk-actions/common/items/edit.blade.php | 30 + .../purchases/bills/edit.blade.php | 1 + .../purchases/vendors/edit.blade.php | 14 + .../sales/customers/edit.blade.php | 14 + .../sales/invoices/edit.blade.php | 1 + .../settings/categories/edit.blade.php | 11 + .../settings/currencies/edit.blade.php | 33 + .../settings/taxes/edit.blade.php | 11 + resources/views/business/pages/show.blade.php | 102 + .../views/common/companies/create.blade.php | 41 + .../views/common/companies/edit.blade.php | 43 + .../views/common/companies/index.blade.php | 148 + .../views/common/dashboards/create.blade.php | 31 + .../views/common/dashboards/edit.blade.php | 35 + .../views/common/dashboards/index.blade.php | 74 + .../views/common/dashboards/show.blade.php | 173 + .../views/common/import/create.blade.php | 77 + resources/views/common/items/create.blade.php | 65 + resources/views/common/items/edit.blade.php | 63 + resources/views/common/items/index.blade.php | 150 + .../views/common/reports/create.blade.php | 43 + resources/views/common/reports/edit.blade.php | 158 + .../views/common/reports/index.blade.php | 94 + .../views/components/alert/border.blade.php | 15 + .../views/components/alert/call.blade.php | 8 + .../views/components/alert/danger.blade.php | 15 + .../views/components/alert/index.blade.php | 116 + .../views/components/alert/info.blade.php | 15 + .../views/components/alert/success.blade.php | 15 + .../views/components/alert/warning.blade.php | 15 + .../views/components/button/hover.blade.php | 9 + .../views/components/button/index.blade.php | 3 + .../views/components/button/loading.blade.php | 21 + .../contacts/form/address.blade.php | 30 + .../contacts/form/billing.blade.php | 22 + .../contacts/form/buttons.blade.php | 5 + .../contacts/form/content.blade.php | 32 + .../contacts/form/general.blade.php | 73 + .../contacts/form/persons.blade.php | 88 + .../contacts/index/buttons.blade.php | 15 + .../contacts/index/content.blade.php | 268 + .../contacts/index/more-buttons.blade.php | 27 + .../components/contacts/script.blade.php | 15 + .../contacts/show/buttons.blade.php | 44 + .../contacts/show/content.blade.php | 410 + .../contacts/show/more-buttons.blade.php | 33 + resources/views/components/date.blade.php | 1 + .../views/components/delete-button.blade.php | 45 + .../views/components/delete-link.blade.php | 27 + .../documents/form/advanced.blade.php | 63 + .../documents/form/buttons.blade.php | 22 + .../documents/form/company.blade.php | 66 + .../documents/form/contact.blade.php | 33 + .../documents/form/content.blade.php | 32 + .../documents/form/item-button.blade.php | 21 + .../documents/form/item-columns.blade.php | 18 + .../components/documents/form/items.blade.php | 145 + .../documents/form/line-item.blade.php | 497 + .../components/documents/form/main.blade.php | 15 + .../documents/form/metadata.blade.php | 90 + .../components/documents/form/note.blade.php | 13 + .../documents/form/recurring.blade.php | 24 + .../form/recurring_metadata.blade.php | 94 + .../documents/form/totals.blade.php | 247 + .../documents/index/bulk-action.blade.php | 32 + .../documents/index/buttons.blade.php | 15 + .../documents/index/content.blade.php | 159 + .../documents/index/document.blade.php | 180 + .../documents/index/information.blade.php | 120 + .../documents/index/more-buttons.blade.php | 27 + .../index/recurring_templates.blade.php | 238 + .../components/documents/script.blade.php | 28 + .../documents/show/attachment.blade.php | 38 + .../documents/show/buttons.blade.php | 23 + .../documents/show/children.blade.php | 31 + .../documents/show/content.blade.php | 129 + .../documents/show/create.blade.php | 38 + .../documents/show/get-paid.blade.php | 162 + .../documents/show/make-payment.blade.php | 141 + .../documents/show/message.blade.php | 17 + .../documents/show/more-buttons.blade.php | 147 + .../documents/show/receive.blade.php | 40 + .../documents/show/restore.blade.php | 28 + .../documents/show/schedule.blade.php | 36 + .../components/documents/show/send.blade.php | 96 + .../documents/show/template.blade.php | 142 + .../documents/template/classic.blade.php | 421 + .../documents/template/default.blade.php | 392 + .../documents/template/line-item.blade.php | 80 + .../documents/template/modern.blade.php | 388 + .../components/dropdown/button.blade.php | 5 + .../components/dropdown/divider.blade.php | 3 + .../views/components/dropdown/index.blade.php | 27 + .../views/components/dropdown/link.blade.php | 5 + .../views/components/email/body.blade.php | 1 + .../views/components/email/footer.blade.php | 33 + .../views/components/empty-data.blade.php | 1 + .../views/components/empty-page.blade.php | 83 + .../components/form/accordion/head.blade.php | 11 + .../components/form/accordion/index.blade.php | 26 + .../views/components/form/buttons.blade.php | 25 + .../views/components/form/container.blade.php | 19 + .../views/components/form/error.blade.php | 4 + .../components/form/group/account.blade.php | 60 + .../form/group/attachment.blade.php | 10 + .../components/form/group/category.blade.php | 142 + .../components/form/group/checkbox.blade.php | 73 + .../components/form/group/color.blade.php | 79 + .../components/form/group/contact.blade.php | 221 + .../components/form/group/country.blade.php | 10 + .../components/form/group/currency.blade.php | 39 + .../components/form/group/customer.blade.php | 19 + .../components/form/group/date.blade.php | 109 + .../components/form/group/date_time.blade.php | 95 + .../components/form/group/editor.blade.php | 47 + .../components/form/group/email.blade.php | 59 + .../components/form/group/file.blade.php | 59 + .../components/form/group/import.blade.php | 59 + .../form/group/invoice_text.blade.php | 180 + .../components/form/group/locale.blade.php | 11 + .../components/form/group/money.blade.php | 88 + .../components/form/group/number.blade.php | 58 + .../form/group/number_digit.blade.php | 13 + .../components/form/group/password.blade.php | 58 + .../form/group/payment_method.blade.php | 12 + .../form/group/payment_term.blade.php | 185 + .../components/form/group/radio.blade.php | 60 + .../components/form/group/recurring.blade.php | 86 + .../components/form/group/select.blade.php | 220 + .../components/form/group/switch.blade.php | 26 + .../views/components/form/group/tax.blade.php | 251 + .../components/form/group/text.blade.php | 65 + .../components/form/group/textarea.blade.php | 61 + .../components/form/group/time.blade.php | 93 + .../components/form/group/toggle.blade.php | 48 + .../components/form/group/vendor.blade.php | 19 + .../views/components/form/icon.blade.php | 5 + .../views/components/form/index.blade.php | 26 + .../components/form/input/checkbox.blade.php | 25 + .../components/form/input/color.blade.php | 77 + .../components/form/input/editor.blade.php | 25 + .../components/form/input/email.blade.php | 19 + .../components/form/input/file.blade.php | 83 + .../components/form/input/hidden.blade.php | 16 + .../components/form/input/import.blade.php | 80 + .../components/form/input/input.blade.php | 18 + .../components/form/input/money.blade.php | 82 + .../components/form/input/number.blade.php | 19 + .../components/form/input/password.blade.php | 34 + .../components/form/input/radio.blade.php | 28 + .../components/form/input/select.blade.php | 180 + .../components/form/input/text.blade.php | 17 + .../components/form/input/textarea.blade.php | 26 + .../views/components/form/label.blade.php | 7 + .../components/form/section/head.blade.php | 11 + .../components/form/section/index.blade.php | 22 + resources/views/components/icon.blade.php | 20 + .../views/components/index/balance.blade.php | 3 + .../components/index/bulkaction/all.blade.php | 13 + .../index/bulkaction/index.blade.php | 96 + .../index/bulkaction/modal.blade.php | 13 + .../index/bulkaction/single.blade.php | 21 + .../views/components/index/category.blade.php | 11 + .../components/index/container.blade.php | 5 + .../views/components/index/country.blade.php | 1 + .../views/components/index/currency.blade.php | 1 + .../views/components/index/default.blade.php | 7 + .../views/components/index/disable.blade.php | 7 + .../views/components/index/search.blade.php | 25 + .../views/components/index/status.blade.php | 8 + .../views/components/index/summary.blade.php | 247 + .../views/components/layouts/admin.blade.php | 99 + .../layouts/admin/content.blade.php | 15 + .../components/layouts/admin/footer.blade.php | 16 + .../components/layouts/admin/head.blade.php | 66 + .../components/layouts/admin/header.blade.php | 45 + .../components/layouts/admin/menu.blade.php | 262 + .../layouts/admin/notifications.blade.php | 3 + .../layouts/admin/scripts.blade.php | 313 + .../views/components/layouts/auth.blade.php | 35 + .../components/layouts/auth/content.blade.php | 7 + .../components/layouts/auth/footer.blade.php | 12 + .../components/layouts/auth/head.blade.php | 61 + .../components/layouts/auth/scripts.blade.php | 15 + .../components/layouts/auth/slider.blade.php | 51 + .../views/components/layouts/error.blade.php | 32 + .../layouts/error/content.blade.php | 15 + .../components/layouts/error/head.blade.php | 57 + .../layouts/error/scripts.blade.php | 11 + .../components/layouts/install.blade.php | 52 + .../layouts/install/content.blade.php | 50 + .../components/layouts/install/head.blade.php | 57 + .../layouts/install/scripts.blade.php | 13 + .../components/layouts/maintenance.blade.php | 6 + .../layouts/maintenance/body.blade.php | 15 + .../layouts/maintenance/head.blade.php | 32 + .../components/layouts/modules.blade.php | 83 + .../components/layouts/modules/bar.blade.php | 137 + .../components/layouts/modules/head.blade.php | 85 + .../components/layouts/modules/item.blade.php | 63 + .../layouts/modules/my_apps_item.blade.php | 59 + .../layouts/modules/releases.blade.php | 9 + .../layouts/modules/reviews.blade.php | 41 + .../layouts/modules/show/buttons.blade.php | 184 + .../layouts/modules/show/changelog.blade.php | 5 + .../modules/show/description.blade.php | 7 + .../modules/show/documentation.blade.php | 7 + .../layouts/modules/show/features.blade.php | 29 + .../modules/show/information.blade.php | 44 + .../modules/show/installation.blade.php | 5 + .../layouts/modules/show/price.blade.php | 69 + .../layouts/modules/show/releases.blade.php | 35 + .../layouts/modules/show/reviews.blade.php | 35 + .../modules/show/screenshots.blade.php | 5 + .../layouts/modules/show/toggle.blade.php | 31 + .../views/components/layouts/portal.blade.php | 89 + .../layouts/portal/content.blade.php | 15 + .../layouts/portal/finish/buttons.blade.php | 19 + .../layouts/portal/finish/content.blade.php | 10 + .../layouts/portal/footer.blade.php | 15 + .../components/layouts/portal/head.blade.php | 65 + .../layouts/portal/header.blade.php | 39 + .../components/layouts/portal/menu.blade.php | 191 + .../layouts/portal/scripts.blade.php | 271 + .../components/layouts/preview.blade.php | 98 + .../layouts/preview/content.blade.php | 15 + .../layouts/preview/footer.blade.php | 18 + .../components/layouts/preview/head.blade.php | 65 + .../layouts/preview/header.blade.php | 39 + .../layouts/preview/scripts.blade.php | 13 + .../views/components/layouts/print.blade.php | 20 + .../layouts/print/content.blade.php | 3 + .../components/layouts/print/head.blade.php | 71 + .../layouts/print/scripts.blade.php | 27 + .../components/layouts/pwa/head.blade.php | 36 + .../views/components/layouts/signed.blade.php | 78 + .../layouts/signed/content.blade.php | 15 + .../layouts/signed/footer.blade.php | 18 + .../components/layouts/signed/head.blade.php | 65 + .../layouts/signed/header.blade.php | 39 + .../layouts/signed/scripts.blade.php | 13 + .../views/components/layouts/wizard.blade.php | 24 + .../layouts/wizard/background.blade.php | 2 + .../layouts/wizard/content.blade.php | 11 + .../components/layouts/wizard/head.blade.php | 60 + .../layouts/wizard/scripts.blade.php | 29 + .../views/components/link/hover.blade.php | 9 + .../views/components/link/index.blade.php | 3 + .../views/components/link/loading.blade.php | 11 + .../components/loading/absolute.blade.php | 18 + .../components/loading/content.blade.php | 18 + .../views/components/loading/menu.blade.php | 89 + .../views/components/media/file.blade.php | 43 + .../views/components/menu/favorite.blade.php | 6 + .../components/modules/banners.blade.php | 33 + .../views/components/modules/free.blade.php | 7 + .../components/modules/installed.blade.php | 6 + .../views/components/modules/item.blade.php | 101 + .../views/components/modules/items.blade.php | 52 + .../views/components/modules/nnew.blade.php | 7 + .../components/modules/no_apps.blade.php | 44 + .../views/components/modules/paid.blade.php | 7 + .../components/modules/pre_sale.blade.php | 7 + .../components/modules/purchased.blade.php | 6 + .../components/modules/raw_items.blade.php | 3 + resources/views/components/nl2br.blade.php | 1 + .../views/components/pagination.blade.php | 43 + .../views/components/payment_method.blade.php | 5 + .../payment_method/hosted.blade.php | 19 + .../payment_method/redirect.blade.php | 17 + .../views/components/read-only.blade.php | 3 + .../views/components/reports/detail.blade.php | 7 + .../reports/detail/content/footer.blade.php | 0 .../reports/detail/content/header.blade.php | 0 .../components/reports/detail/table.blade.php | 7 + .../reports/detail/table/body.blade.php | 13 + .../reports/detail/table/footer.blade.php | 11 + .../reports/detail/table/header.blade.php | 14 + .../reports/detail/table/row.blade.php | 90 + .../views/components/reports/fields.blade.php | 103 + .../views/components/reports/filter.blade.php | 117 + .../views/components/reports/print.blade.php | 13 + .../views/components/reports/show.blade.php | 77 + .../components/reports/summary.blade.php | 17 + .../reports/summary/chart.blade.php | 43 + .../reports/summary/content/footer.blade.php | 0 .../reports/summary/content/header.blade.php | 0 .../reports/summary/table.blade.php | 7 + .../reports/summary/table/body.blade.php | 29 + .../reports/summary/table/footer.blade.php | 0 .../reports/summary/table/header.blade.php | 0 .../reports/summary/table/row.blade.php | 110 + resources/views/components/script.blade.php | 3 + .../components/script/declaration.blade.php | 5 + .../script/exceptions/trackers.blade.php | 16 + .../views/components/search-string.blade.php | 29 + .../components/show/accordion/head.blade.php | 13 + .../components/show/accordion/index.blade.php | 26 + .../views/components/show/container.blade.php | 9 + .../components/show/content/index.blade.php | 3 + .../components/show/content/left.blade.php | 3 + .../components/show/content/right.blade.php | 7 + .../components/show/no-records.blade.php | 31 + .../views/components/show/status.blade.php | 8 + .../components/show/summary/index.blade.php | 3 + .../components/show/summary/left.blade.php | 11 + .../components/show/summary/right.blade.php | 243 + .../views/components/suggestions.blade.php | 11 + .../components/table/actions-mobile.blade.php | 153 + .../views/components/table/actions.blade.php | 142 + .../views/components/table/index.blade.php | 7 + .../views/components/table/tbody.blade.php | 3 + resources/views/components/table/td.blade.php | 44 + resources/views/components/table/th.blade.php | 43 + .../views/components/table/thead.blade.php | 3 + resources/views/components/table/tr.blade.php | 3 + .../views/components/tabs/index.blade.php | 26 + .../views/components/tabs/nav-link.blade.php | 19 + .../views/components/tabs/nav-pin.blade.php | 21 + resources/views/components/tabs/nav.blade.php | 18 + resources/views/components/tabs/tab.blade.php | 10 + .../views/components/tips/fixed.blade.php | 38 + .../views/components/tips/relative.blade.php | 37 + resources/views/components/title.blade.php | 11 + resources/views/components/tooltip.blade.php | 38 + .../transactions/index/bulk-action.blade.php | 47 + .../transactions/index/buttons.blade.php | 27 + .../transactions/index/content.blade.php | 190 + .../transactions/index/more-buttons.blade.php | 27 + .../index/recurring_templates.blade.php | 225 + .../transactions/index/transaction.blade.php | 213 + .../components/transactions/script.blade.php | 1 + .../transactions/show/attachment.blade.php | 16 + .../transactions/show/buttons.blade.php | 23 + .../transactions/show/children.blade.php | 22 + .../transactions/show/connect.blade.php | 22 + .../transactions/show/content.blade.php | 162 + .../transactions/show/create.blade.php | 27 + .../transactions/show/more-buttons.blade.php | 147 + .../transactions/show/schedule.blade.php | 33 + .../transactions/show/template.blade.php | 75 + .../transactions/show/transfer.blade.php | 41 + .../transactions/template/default.blade.php | 417 + .../components/transfers/script.blade.php | 1 + .../transfers/show/attachment.blade.php | 16 + .../transfers/show/buttons.blade.php | 9 + .../transfers/show/content.blade.php | 17 + .../transfers/show/create.blade.php | 18 + .../transfers/show/more-buttons.blade.php | 67 + .../transfers/show/template.blade.php | 10 + .../transfers/show/transactions.blade.php | 24 + .../transfers/template/default.blade.php | 273 + .../transfers/template/second.blade.php | 268 + .../transfers/template/third.blade.php | 282 + .../views/components/update-alert.blade.php | 5 + .../components/widgets/contact.blade.php | 51 + .../views/components/widgets/header.blade.php | 64 + .../widgets/invoice_history.blade.php | 23 + .../components/widgets/last_payment.blade.php | 19 + .../widgets/latest_invoices.blade.php | 23 + .../widgets/outstanding_balance.blade.php | 25 + .../widgets/payment_history.blade.php | 34 + resources/views/errors/403.blade.php | 36 + resources/views/errors/404.blade.php | 36 + resources/views/errors/500.blade.php | 36 + .../views/install/database/create.blade.php | 17 + .../views/install/language/create.blade.php | 15 + .../views/install/requirements/show.blade.php | 13 + .../views/install/settings/create.blade.php | 17 + .../views/install/updates/edit.blade.php | 35 + .../views/install/updates/index.blade.php | 142 + .../views/livewire/common/search.blade.php | 46 + .../views/livewire/menu/favorite.blade.php | 13 + .../views/livewire/menu/favorites.blade.php | 20 + resources/views/livewire/menu/neww.blade.php | 25 + .../livewire/menu/notifications.blade.php | 83 + .../views/livewire/menu/profile.blade.php | 9 + .../views/livewire/menu/settings.blade.php | 31 + .../notification/browser/firefox.blade.php | 55 + .../notification/browser/index.blade.php | 2 + resources/views/livewire/report/pin.blade.php | 26 + .../views/livewire/report/pins.blade.php | 32 + resources/views/livewire/tab/pin.blade.php | 35 + .../views/modals/accounts/create.blade.php | 32 + .../views/modals/categories/create.blade.php | 26 + .../views/modals/companies/edit.blade.php | 17 + .../views/modals/currencies/create.blade.php | 13 + .../views/modals/customers/create.blade.php | 71 + .../views/modals/customers/edit.blade.php | 71 + .../modals/documents/item_columns.blade.php | 43 + .../views/modals/documents/payment.blade.php | 143 + .../views/modals/invoices/email.blade.php | 128 + .../views/modals/invoices/share.blade.php | 35 + resources/views/modals/items/create.blade.php | 17 + .../settings/transfer_template.blade.php | 42 + resources/views/modals/taxes/create.blade.php | 11 + .../modals/transactions/connect.blade.php | 27 + .../views/modals/transactions/email.blade.php | 128 + .../views/modals/transactions/share.blade.php | 36 + .../views/modals/vendors/create.blade.php | 70 + resources/views/modals/vendors/edit.blade.php | 70 + .../views/modules/api_key/create.blade.php | 21 + resources/views/modules/home/index.blade.php | 29 + .../modules/item/documentation.blade.php | 35 + .../views/modules/item/pre_sale.blade.php | 174 + resources/views/modules/item/show.blade.php | 276 + resources/views/modules/my/index.blade.php | 35 + resources/views/modules/tiles/index.blade.php | 21 + .../views/portal/dashboard/index.blade.php | 33 + .../views/portal/invoices/finish.blade.php | 31 + .../views/portal/invoices/index.blade.php | 174 + .../views/portal/invoices/preview.blade.php | 153 + .../views/portal/invoices/show.blade.php | 145 + .../views/portal/invoices/signed.blade.php | 161 + .../views/portal/payments/index.blade.php | 82 + .../views/portal/payments/preview.blade.php | 71 + .../views/portal/payments/show.blade.php | 68 + .../views/portal/payments/signed.blade.php | 79 + resources/views/portal/profile/edit.blade.php | 75 + .../views/purchases/bills/create.blade.php | 25 + .../views/purchases/bills/edit.blade.php | 19 + .../views/purchases/bills/index.blade.php | 25 + .../views/purchases/bills/print.blade.php | 14 + .../views/purchases/bills/show.blade.php | 45 + .../recurring_bills/create.blade.php | 25 + .../purchases/recurring_bills/edit.blade.php | 20 + .../purchases/recurring_bills/index.blade.php | 31 + .../purchases/recurring_bills/show.blade.php | 40 + .../views/purchases/vendors/create.blade.php | 17 + .../views/purchases/vendors/edit.blade.php | 11 + .../views/purchases/vendors/index.blade.php | 23 + .../views/purchases/vendors/show.blade.php | 31 + .../profit_loss/content/footer.blade.php | 30 + .../profit_loss/content/header.blade.php | 19 + .../reports/profit_loss/detail.blade.php | 38 + .../profit_loss/table/footer.blade.php | 26 + .../profit_loss/table/header.blade.php | 7 + .../reports/profit_loss/table/row.blade.php | 120 + .../tax_summary/content/footer.blade.php | 0 .../tax_summary/content/header.blade.php | 19 + .../tax_summary/table/footer.blade.php | 19 + .../tax_summary/table/header.blade.php | 7 + .../views/sales/customers/create.blade.php | 17 + .../views/sales/customers/edit.blade.php | 11 + .../views/sales/customers/index.blade.php | 23 + .../views/sales/customers/show.blade.php | 31 + .../views/sales/invoices/create.blade.php | 17 + resources/views/sales/invoices/edit.blade.php | 11 + .../views/sales/invoices/index.blade.php | 25 + .../sales/invoices/print_classic.blade.php | 12 + .../sales/invoices/print_default.blade.php | 12 + .../sales/invoices/print_modern.blade.php | 12 + resources/views/sales/invoices/show.blade.php | 27 + .../sales/recurring_invoices/create.blade.php | 17 + .../sales/recurring_invoices/edit.blade.php | 11 + .../sales/recurring_invoices/index.blade.php | 31 + .../sales/recurring_invoices/show.blade.php | 40 + .../settings/categories/create.blade.php | 52 + .../views/settings/categories/edit.blade.php | 60 + .../views/settings/categories/index.blade.php | 165 + .../categories/sub_category.blade.php | 126 + .../views/settings/company/edit.blade.php | 61 + .../settings/currencies/create.blade.php | 51 + .../views/settings/currencies/edit.blade.php | 49 + .../views/settings/currencies/index.blade.php | 100 + .../views/settings/default/edit.blade.php | 82 + .../settings/email-templates/edit.blade.php | 92 + resources/views/settings/email/edit.blade.php | 43 + .../views/settings/invoice/edit.blade.php | 162 + .../settings/localisation/edit.blade.php | 59 + .../views/settings/modules/edit.blade.php | 138 + .../views/settings/navigation/edit.blade.php | 61 + .../views/settings/schedule/edit.blade.php | 62 + .../views/settings/taxes/create.blade.php | 39 + resources/views/settings/taxes/edit.blade.php | 37 + .../views/settings/taxes/index.blade.php | 110 + .../system_settings/databases/form.blade.php | 72 + .../system_settings/databases/index.blade.php | 85 + .../partials/navigation.blade.php | 20 + .../system_settings/sidebar/edit.blade.php | 107 + .../system_settings/users/form.blade.php | 60 + .../system_settings/users/index.blade.php | 82 + .../views/widgets/account_balance.blade.php | 15 + resources/views/widgets/bank_feeds.blade.php | 23 + resources/views/widgets/bar_chart.blade.php | 13 + resources/views/widgets/cash_flow.blade.php | 53 + resources/views/widgets/currencies.blade.php | 15 + resources/views/widgets/donut_chart.blade.php | 13 + resources/views/widgets/line_chart.blade.php | 13 + .../widgets/receivables_payables.blade.php | 57 + .../views/wizard/companies/edit.blade.php | 7 + .../views/wizard/currencies/index.blade.php | 7 + resources/views/wizard/finish/index.blade.php | 7 + routes/admin.php | 316 + routes/api.php | 84 + routes/channels.php | 18 + routes/common.php | 34 + routes/console.php | 19 + routes/guest.php | 29 + routes/install.php | 22 + routes/portal.php | 27 + routes/preview.php | 23 + routes/signed.php | 21 + routes/web.php | 16 + routes/wizard.php | 21 + safelist.txt | 116 + serviceworker.js | 45 + storage/app/.gitignore | 5 + storage/app/public/.gitignore | 2 + storage/app/temp/.gitignore | 2 + storage/app/uploads/.gitignore | 2 + storage/debugbar/.gitignore | 2 + storage/fonts/.gitignore | 2 + storage/framework/.gitignore | 8 + storage/framework/testing/.gitignore | 2 + tailwind.config.js | 23 + tests/CreatesApplication.php | 22 + tests/Feature/Auth/UsersTest.php | 244 + tests/Feature/Banking/AccountsTest.php | 142 + .../Banking/DocumentTransactionsTest.php | 168 + tests/Feature/Banking/ReconciliationsTest.php | 82 + .../Feature/Banking/SplitTransactionTest.php | 122 + .../Feature/Banking/TransactionTaxesTest.php | 97 + tests/Feature/Banking/TransactionsTest.php | 295 + tests/Feature/Banking/TransfersTest.php | 175 + tests/Feature/Commands/BillReminderTest.php | 48 + .../Feature/Commands/InvoiceReminderTest.php | 48 + tests/Feature/Commands/RecurringCheckTest.php | 160 + tests/Feature/Common/CompaniesTest.php | 126 + tests/Feature/Common/DashboardsTest.php | 171 + tests/Feature/Common/ItemsTest.php | 163 + tests/Feature/Common/ReportsTest.php | 65 + tests/Feature/Common/SourcesTest.php | 46 + tests/Feature/Document/CancelDocumentTest.php | 88 + tests/Feature/Email/TooManyEmailsSentTest.php | 51 + tests/Feature/FeatureTestCase.php | 63 + tests/Feature/PaymentTestCase.php | 159 + .../Performance/N1QueryOptimizationTest.php | 178 + .../Feature/Performance/VendorN1QueryTest.php | 255 + tests/Feature/Purchases/BillsTest.php | 310 + tests/Feature/Purchases/VendorsTest.php | 188 + tests/Feature/Sales/CustomersTest.php | 220 + tests/Feature/Sales/InvoicesTest.php | 370 + tests/Feature/Settings/CategoriesTest.php | 93 + tests/Feature/Settings/CurrenciesTest.php | 95 + tests/Feature/Settings/NavigationTest.php | 34 + tests/Feature/Settings/TaxesTest.php | 89 + tests/Feature/Wizard/CompaniesTest.php | 42 + tests/Feature/Wizard/CurrenciesTest.php | 86 + tests/TestCase.php | 19 + tests/Unit/ExampleTest.php | 18 + .../Imports/Banking/TransfersImportTest.php | 26 + tests/Unit/UpdatesTest.php | 53 + .../Utilities/CalculationToQuantityTest.php | 21 + .../Unit/Utilities/DateImportParsingTest.php | 25 + web.config | 23 + webpack.mix.js | 82 + 4202 files changed, 395334 insertions(+) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 .htaccess create mode 100644 AGENTS.md create mode 100644 LICENSE.txt create mode 100644 LocalValetDriver.php create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 app/Abstracts/BulkAction.php create mode 100644 app/Abstracts/Commands/Module.php create mode 100644 app/Abstracts/Event.php create mode 100644 app/Abstracts/Export.php create mode 100644 app/Abstracts/Factory.php create mode 100644 app/Abstracts/Http/ApiController.php create mode 100644 app/Abstracts/Http/Controller.php create mode 100644 app/Abstracts/Http/FormRequest.php create mode 100644 app/Abstracts/Http/PaymentController.php create mode 100644 app/Abstracts/Http/Response.php create mode 100644 app/Abstracts/Http/SettingController.php create mode 100644 app/Abstracts/Import.php create mode 100644 app/Abstracts/ImportMultipleSheets.php create mode 100644 app/Abstracts/Job.php create mode 100644 app/Abstracts/JobShouldQueue.php create mode 100644 app/Abstracts/Listeners/Report.php create mode 100644 app/Abstracts/Listeners/Update.php create mode 100644 app/Abstracts/Model.php create mode 100644 app/Abstracts/Notification.php create mode 100644 app/Abstracts/Observer.php create mode 100644 app/Abstracts/Report.php create mode 100644 app/Abstracts/View/Component.php create mode 100644 app/Abstracts/View/Components/Contacts/Form.php create mode 100644 app/Abstracts/View/Components/Contacts/Index.php create mode 100644 app/Abstracts/View/Components/Contacts/Show.php create mode 100644 app/Abstracts/View/Components/Documents/Form.php create mode 100644 app/Abstracts/View/Components/Documents/Index.php create mode 100644 app/Abstracts/View/Components/Documents/Show.php create mode 100644 app/Abstracts/View/Components/Documents/Template.php create mode 100644 app/Abstracts/View/Components/Form.php create mode 100644 app/Abstracts/View/Components/Transactions/Index.php create mode 100644 app/Abstracts/View/Components/Transactions/Show.php create mode 100644 app/Abstracts/View/Components/Transactions/Template.php create mode 100644 app/Abstracts/View/Components/Transfers/Show.php create mode 100644 app/Abstracts/View/Components/Transfers/Template.php create mode 100644 app/Abstracts/Widget.php create mode 100644 app/Builders/Category.php create mode 100644 app/BulkActions/Auth/Users.php create mode 100644 app/BulkActions/Banking/Accounts.php create mode 100644 app/BulkActions/Banking/Reconciliations.php create mode 100644 app/BulkActions/Banking/Transactions.php create mode 100644 app/BulkActions/Banking/Transfers.php create mode 100644 app/BulkActions/Common/Companies.php create mode 100644 app/BulkActions/Common/Dashboards.php create mode 100644 app/BulkActions/Common/Items.php create mode 100644 app/BulkActions/Purchases/Bills.php create mode 100644 app/BulkActions/Purchases/Vendors.php create mode 100644 app/BulkActions/Sales/Customers.php create mode 100644 app/BulkActions/Sales/Invoices.php create mode 100644 app/BulkActions/Settings/Categories.php create mode 100644 app/BulkActions/Settings/Currencies.php create mode 100644 app/BulkActions/Settings/Taxes.php create mode 100644 app/Classifiers/BulkAction.php create mode 100644 app/Classifiers/Event.php create mode 100644 app/Classifiers/Export.php create mode 100644 app/Classifiers/Import.php create mode 100644 app/Classifiers/Job.php create mode 100644 app/Classifiers/Observer.php create mode 100644 app/Classifiers/Report.php create mode 100644 app/Classifiers/Scope.php create mode 100644 app/Classifiers/Widget.php create mode 100644 app/Console/Commands/BillReminder.php create mode 100644 app/Console/Commands/CompanySeed.php create mode 100644 app/Console/Commands/DownloadModule.php create mode 100644 app/Console/Commands/FinishUpdate.php create mode 100644 app/Console/Commands/Install.php create mode 100644 app/Console/Commands/InstallRefresh.php create mode 100644 app/Console/Commands/InvoiceReminder.php create mode 100644 app/Console/Commands/RecurringCheck.php create mode 100644 app/Console/Commands/SampleData.php create mode 100644 app/Console/Commands/StorageTempClear.php create mode 100644 app/Console/Commands/UninstallModule.php create mode 100644 app/Console/Commands/Update.php create mode 100644 app/Console/Commands/UpdateAll.php create mode 100644 app/Console/Commands/UpdateDb.php create mode 100644 app/Console/Commands/UserSeed.php create mode 100644 app/Console/Kernel.php create mode 100644 app/Console/Stubs/Modules/assets/js/app.stub create mode 100644 app/Console/Stubs/Modules/assets/sass/app.stub create mode 100644 app/Console/Stubs/Modules/command.stub create mode 100644 app/Console/Stubs/Modules/component.stub create mode 100644 app/Console/Stubs/Modules/composer.stub create mode 100644 app/Console/Stubs/Modules/controller-api.stub create mode 100644 app/Console/Stubs/Modules/controller-plain.stub create mode 100644 app/Console/Stubs/Modules/controller.stub create mode 100644 app/Console/Stubs/Modules/event.stub create mode 100644 app/Console/Stubs/Modules/factory.stub create mode 100644 app/Console/Stubs/Modules/job-queued.stub create mode 100644 app/Console/Stubs/Modules/job.stub create mode 100644 app/Console/Stubs/Modules/json.stub create mode 100644 app/Console/Stubs/Modules/lang/general.stub create mode 100644 app/Console/Stubs/Modules/listener-duck.stub create mode 100644 app/Console/Stubs/Modules/listener-queued-duck.stub create mode 100644 app/Console/Stubs/Modules/listener-queued.stub create mode 100644 app/Console/Stubs/Modules/listener.stub create mode 100644 app/Console/Stubs/Modules/listeners/install.stub create mode 100644 app/Console/Stubs/Modules/mail.stub create mode 100644 app/Console/Stubs/Modules/middleware.stub create mode 100644 app/Console/Stubs/Modules/migration/add.stub create mode 100644 app/Console/Stubs/Modules/migration/create.stub create mode 100644 app/Console/Stubs/Modules/migration/delete.stub create mode 100644 app/Console/Stubs/Modules/migration/drop.stub create mode 100644 app/Console/Stubs/Modules/migration/plain.stub create mode 100644 app/Console/Stubs/Modules/model.stub create mode 100644 app/Console/Stubs/Modules/notification.stub create mode 100644 app/Console/Stubs/Modules/package.stub create mode 100644 app/Console/Stubs/Modules/policy.plain.stub create mode 100644 app/Console/Stubs/Modules/provider.stub create mode 100644 app/Console/Stubs/Modules/providers/event.stub create mode 100644 app/Console/Stubs/Modules/request.stub create mode 100644 app/Console/Stubs/Modules/resource-collection.stub create mode 100644 app/Console/Stubs/Modules/resource.stub create mode 100644 app/Console/Stubs/Modules/routes/admin.stub create mode 100644 app/Console/Stubs/Modules/routes/portal.stub create mode 100644 app/Console/Stubs/Modules/rule.stub create mode 100644 app/Console/Stubs/Modules/scaffold/config.stub create mode 100644 app/Console/Stubs/Modules/scaffold/provider.stub create mode 100644 app/Console/Stubs/Modules/seeder.stub create mode 100644 app/Console/Stubs/Modules/unit-test.stub create mode 100644 app/Console/Stubs/Modules/views/index.stub create mode 100644 app/Console/Stubs/Modules/webpack.stub create mode 100644 app/Events/Auth/Attempting.php create mode 100644 app/Events/Auth/Authenticated.php create mode 100644 app/Events/Auth/Failed.php create mode 100644 app/Events/Auth/LandingPageShowing.php create mode 100644 app/Events/Auth/RoleCreated.php create mode 100644 app/Events/Auth/RoleCreating.php create mode 100644 app/Events/Auth/RoleDeleted.php create mode 100644 app/Events/Auth/RoleDeleting.php create mode 100644 app/Events/Auth/RoleUpdated.php create mode 100644 app/Events/Auth/RoleUpdating.php create mode 100644 app/Events/Auth/UserCreated.php create mode 100644 app/Events/Auth/UserCreating.php create mode 100644 app/Events/Auth/UserDeleted.php create mode 100644 app/Events/Auth/UserDeleting.php create mode 100644 app/Events/Auth/UserUpdated.php create mode 100644 app/Events/Auth/UserUpdating.php create mode 100644 app/Events/Banking/AccountCreated.php create mode 100644 app/Events/Banking/AccountCreating.php create mode 100644 app/Events/Banking/AccountDeleted.php create mode 100644 app/Events/Banking/AccountDeleting.php create mode 100644 app/Events/Banking/AccountUpdated.php create mode 100644 app/Events/Banking/AccountUpdating.php create mode 100644 app/Events/Banking/DocumentTransactionCreated.php create mode 100644 app/Events/Banking/DocumentTransactionCreating.php create mode 100644 app/Events/Banking/DocumentTransactionUpdated.php create mode 100644 app/Events/Banking/DocumentTransactionUpdating.php create mode 100644 app/Events/Banking/TransactionCreated.php create mode 100644 app/Events/Banking/TransactionCreating.php create mode 100644 app/Events/Banking/TransactionDeleted.php create mode 100644 app/Events/Banking/TransactionDeleting.php create mode 100644 app/Events/Banking/TransactionPrinting.php create mode 100644 app/Events/Banking/TransactionRecurring.php create mode 100644 app/Events/Banking/TransactionSending.php create mode 100644 app/Events/Banking/TransactionSent.php create mode 100644 app/Events/Banking/TransactionSplitted.php create mode 100644 app/Events/Banking/TransactionSplitting.php create mode 100644 app/Events/Banking/TransactionUpdated.php create mode 100644 app/Events/Banking/TransactionUpdating.php create mode 100644 app/Events/Banking/TransferCreated.php create mode 100644 app/Events/Banking/TransferCreating.php create mode 100644 app/Events/Banking/TransferDeleted.php create mode 100644 app/Events/Banking/TransferDeleting.php create mode 100644 app/Events/Banking/TransferPrinting.php create mode 100644 app/Events/Banking/TransferUpdated.php create mode 100644 app/Events/Banking/TransferUpdating.php create mode 100644 app/Events/Common/BulkActionsAdding.php create mode 100644 app/Events/Common/CompanyCreated.php create mode 100644 app/Events/Common/CompanyCreating.php create mode 100644 app/Events/Common/CompanyDeleted.php create mode 100644 app/Events/Common/CompanyDeleting.php create mode 100644 app/Events/Common/CompanyForgettingCurrent.php create mode 100644 app/Events/Common/CompanyForgotCurrent.php create mode 100644 app/Events/Common/CompanyMadeCurrent.php create mode 100644 app/Events/Common/CompanyMakingCurrent.php create mode 100644 app/Events/Common/CompanySwitched.php create mode 100644 app/Events/Common/CompanyUpdated.php create mode 100644 app/Events/Common/CompanyUpdating.php create mode 100644 app/Events/Common/ContactCreated.php create mode 100644 app/Events/Common/ContactCreating.php create mode 100644 app/Events/Common/ContactDeleted.php create mode 100644 app/Events/Common/ContactDeleting.php create mode 100644 app/Events/Common/ContactUpdated.php create mode 100644 app/Events/Common/ContactUpdating.php create mode 100644 app/Events/Common/DatesFormating.php create mode 100644 app/Events/Common/GlobalSearched.php create mode 100644 app/Events/Common/ImportViewCreated.php create mode 100644 app/Events/Common/ImportViewCreating.php create mode 100644 app/Events/Common/ItemCreated.php create mode 100644 app/Events/Common/ItemCreating.php create mode 100644 app/Events/Common/ItemDeleted.php create mode 100644 app/Events/Common/ItemDeleting.php create mode 100644 app/Events/Common/ItemUpdated.php create mode 100644 app/Events/Common/ItemUpdating.php create mode 100644 app/Events/Common/RelationshipCounting.php create mode 100644 app/Events/Common/RelationshipDeleting.php create mode 100644 app/Events/Common/SearchStringApplied.php create mode 100644 app/Events/Common/SearchStringApplying.php create mode 100644 app/Events/Document/DocumentCancelled.php create mode 100644 app/Events/Document/DocumentCreated.php create mode 100644 app/Events/Document/DocumentCreating.php create mode 100644 app/Events/Document/DocumentDeleted.php create mode 100644 app/Events/Document/DocumentDeleting.php create mode 100644 app/Events/Document/DocumentDuplicating.php create mode 100644 app/Events/Document/DocumentMarkedSent.php create mode 100644 app/Events/Document/DocumentPrinting.php create mode 100644 app/Events/Document/DocumentReceived.php create mode 100644 app/Events/Document/DocumentRecurring.php create mode 100644 app/Events/Document/DocumentReminded.php create mode 100644 app/Events/Document/DocumentResourceShowing.php create mode 100644 app/Events/Document/DocumentRestored.php create mode 100644 app/Events/Document/DocumentSending.php create mode 100644 app/Events/Document/DocumentSent.php create mode 100644 app/Events/Document/DocumentTemplates.php create mode 100644 app/Events/Document/DocumentUpdated.php create mode 100644 app/Events/Document/DocumentUpdating.php create mode 100644 app/Events/Document/DocumentViewed.php create mode 100644 app/Events/Document/PaidAmountCalculated.php create mode 100644 app/Events/Document/PaymentReceived.php create mode 100644 app/Events/Document/TransactionsCounted.php create mode 100644 app/Events/Email/InvalidEmailDetected.php create mode 100644 app/Events/Email/TooManyEmailsSent.php create mode 100644 app/Events/Export/HeadingsPreparing.php create mode 100644 app/Events/Export/RowsPreparing.php create mode 100644 app/Events/Import/RowPreparing.php create mode 100644 app/Events/Install/UpdateCacheCleared.php create mode 100644 app/Events/Install/UpdateCopied.php create mode 100644 app/Events/Install/UpdateDownloaded.php create mode 100644 app/Events/Install/UpdateFailed.php create mode 100644 app/Events/Install/UpdateFinished.php create mode 100644 app/Events/Install/UpdateUnzipped.php create mode 100644 app/Events/Menu/AdminCreated.php create mode 100644 app/Events/Menu/AdminCreating.php create mode 100644 app/Events/Menu/ItemAuthorizing.php create mode 100644 app/Events/Menu/NewwCreated.php create mode 100644 app/Events/Menu/NewwCreating.php create mode 100644 app/Events/Menu/NotificationsCreated.php create mode 100644 app/Events/Menu/NotificationsCreating.php create mode 100644 app/Events/Menu/PortalCreated.php create mode 100644 app/Events/Menu/PortalCreating.php create mode 100644 app/Events/Menu/ProfileCreated.php create mode 100644 app/Events/Menu/ProfileCreating.php create mode 100644 app/Events/Menu/SettingsCreated.php create mode 100644 app/Events/Menu/SettingsCreating.php create mode 100644 app/Events/Menu/SettingsFinished.php create mode 100644 app/Events/Module/Copied.php create mode 100644 app/Events/Module/Disabled.php create mode 100644 app/Events/Module/Disabling.php create mode 100644 app/Events/Module/Enabled.php create mode 100644 app/Events/Module/Installed.php create mode 100644 app/Events/Module/Installing.php create mode 100644 app/Events/Module/PaymentMethodShowing.php create mode 100644 app/Events/Module/Uninstalled.php create mode 100644 app/Events/Module/Uninstalling.php create mode 100644 app/Events/Report/ClassesCreated.php create mode 100644 app/Events/Report/DataLoaded.php create mode 100644 app/Events/Report/DataLoading.php create mode 100644 app/Events/Report/FilterApplying.php create mode 100644 app/Events/Report/FilterShowing.php create mode 100644 app/Events/Report/GroupApplying.php create mode 100644 app/Events/Report/GroupShowing.php create mode 100644 app/Events/Report/RowsShowing.php create mode 100644 app/Events/Report/SkipClass.php create mode 100644 app/Events/Report/SkipRowsShowing.php create mode 100644 app/Events/Report/TotalCalculated.php create mode 100644 app/Events/Report/TotalCalculating.php create mode 100644 app/Events/Setting/CategoryCreated.php create mode 100644 app/Events/Setting/CategoryCreating.php create mode 100644 app/Events/Setting/CategoryDeleted.php create mode 100644 app/Events/Setting/CategoryDeleting.php create mode 100644 app/Events/Setting/CategoryUpdated.php create mode 100644 app/Events/Setting/CategoryUpdating.php create mode 100644 app/Events/Setting/CurrencyCreated.php create mode 100644 app/Events/Setting/CurrencyCreating.php create mode 100644 app/Events/Setting/CurrencyDeleted.php create mode 100644 app/Events/Setting/CurrencyDeleting.php create mode 100644 app/Events/Setting/CurrencyUpdated.php create mode 100644 app/Events/Setting/CurrencyUpdating.php create mode 100644 app/Events/Setting/SettingUpdated.php create mode 100644 app/Events/Setting/SettingUpdating.php create mode 100644 app/Events/Setting/TaxCreated.php create mode 100644 app/Events/Setting/TaxCreating.php create mode 100644 app/Events/Setting/TaxDeleted.php create mode 100644 app/Events/Setting/TaxDeleting.php create mode 100644 app/Events/Setting/TaxUpdated.php create mode 100644 app/Events/Setting/TaxUpdating.php create mode 100644 app/Events/Widget/ClassesCreated.php create mode 100644 app/Exceptions/Common/LastDashboard.php create mode 100644 app/Exceptions/Common/NotUserDashboard.php create mode 100644 app/Exceptions/Common/TooManyEmailsSent.php create mode 100644 app/Exceptions/Handler.php create mode 100644 app/Exceptions/Http/Resource.php create mode 100644 app/Exceptions/Settings/LastCategoryDelete.php create mode 100644 app/Exceptions/Trackers/Bugsnag.php create mode 100644 app/Exceptions/Trackers/Sentry.php create mode 100644 app/Exports/Banking/RecurringTransactions.php create mode 100644 app/Exports/Banking/Sheets/Recurring.php create mode 100644 app/Exports/Banking/Sheets/RecurringTransactions.php create mode 100644 app/Exports/Banking/Transactions.php create mode 100644 app/Exports/Banking/Transfers.php create mode 100644 app/Exports/Common/Items.php create mode 100644 app/Exports/Common/Reports.php create mode 100644 app/Exports/Common/Sheets/ItemTaxes.php create mode 100644 app/Exports/Common/Sheets/Items.php create mode 100644 app/Exports/Purchases/Bills/Bills.php create mode 100644 app/Exports/Purchases/Bills/Sheets/BillHistories.php create mode 100644 app/Exports/Purchases/Bills/Sheets/BillItemTaxes.php create mode 100644 app/Exports/Purchases/Bills/Sheets/BillItems.php create mode 100644 app/Exports/Purchases/Bills/Sheets/BillTotals.php create mode 100644 app/Exports/Purchases/Bills/Sheets/BillTransactions.php create mode 100644 app/Exports/Purchases/Bills/Sheets/Bills.php create mode 100644 app/Exports/Purchases/RecurringBills/RecurringBills.php create mode 100644 app/Exports/Purchases/RecurringBills/Sheets/Recurring.php create mode 100644 app/Exports/Purchases/RecurringBills/Sheets/RecurringBillHistories.php create mode 100644 app/Exports/Purchases/RecurringBills/Sheets/RecurringBillItemTaxes.php create mode 100644 app/Exports/Purchases/RecurringBills/Sheets/RecurringBillItems.php create mode 100644 app/Exports/Purchases/RecurringBills/Sheets/RecurringBillTotals.php create mode 100644 app/Exports/Purchases/RecurringBills/Sheets/RecurringBills.php create mode 100644 app/Exports/Purchases/Vendors.php create mode 100644 app/Exports/Sales/Customers.php create mode 100644 app/Exports/Sales/Invoices/Invoices.php create mode 100644 app/Exports/Sales/Invoices/Sheets/InvoiceHistories.php create mode 100644 app/Exports/Sales/Invoices/Sheets/InvoiceItemTaxes.php create mode 100644 app/Exports/Sales/Invoices/Sheets/InvoiceItems.php create mode 100644 app/Exports/Sales/Invoices/Sheets/InvoiceTotals.php create mode 100644 app/Exports/Sales/Invoices/Sheets/InvoiceTransactions.php create mode 100644 app/Exports/Sales/Invoices/Sheets/Invoices.php create mode 100644 app/Exports/Sales/RecurringInvoices/RecurringInvoices.php create mode 100644 app/Exports/Sales/RecurringInvoices/Sheets/Recurring.php create mode 100644 app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoiceHistories.php create mode 100644 app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoiceItemTaxes.php create mode 100644 app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoiceItems.php create mode 100644 app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoiceTotals.php create mode 100644 app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoices.php create mode 100644 app/Exports/Settings/Categories.php create mode 100644 app/Exports/Settings/Taxes.php create mode 100644 app/Http/Controllers/Api/Auth/Users.php create mode 100644 app/Http/Controllers/Api/Banking/Accounts.php create mode 100644 app/Http/Controllers/Api/Banking/Reconciliations.php create mode 100644 app/Http/Controllers/Api/Banking/Transactions.php create mode 100644 app/Http/Controllers/Api/Banking/Transfers.php create mode 100644 app/Http/Controllers/Api/Common/Companies.php create mode 100644 app/Http/Controllers/Api/Common/Contacts.php create mode 100644 app/Http/Controllers/Api/Common/Dashboards.php create mode 100644 app/Http/Controllers/Api/Common/Items.php create mode 100644 app/Http/Controllers/Api/Common/Ping.php create mode 100644 app/Http/Controllers/Api/Common/Reports.php create mode 100644 app/Http/Controllers/Api/Common/Translations.php create mode 100644 app/Http/Controllers/Api/Document/DocumentTransactions.php create mode 100644 app/Http/Controllers/Api/Document/Documents.php create mode 100644 app/Http/Controllers/Api/Settings/Categories.php create mode 100644 app/Http/Controllers/Api/Settings/Currencies.php create mode 100644 app/Http/Controllers/Api/Settings/Settings.php create mode 100644 app/Http/Controllers/Api/Settings/Taxes.php create mode 100644 app/Http/Controllers/Auth/Forgot.php create mode 100644 app/Http/Controllers/Auth/Login.php create mode 100644 app/Http/Controllers/Auth/Register.php create mode 100644 app/Http/Controllers/Auth/Reset.php create mode 100644 app/Http/Controllers/Auth/Users.php create mode 100644 app/Http/Controllers/Banking/Accounts.php create mode 100644 app/Http/Controllers/Banking/FundAccounts.php create mode 100644 app/Http/Controllers/Banking/Reconciliations.php create mode 100644 app/Http/Controllers/Banking/RecurringTransactions.php create mode 100644 app/Http/Controllers/Banking/Transactions.php create mode 100644 app/Http/Controllers/Banking/Transfers.php create mode 100644 app/Http/Controllers/Business/Pages.php create mode 100644 app/Http/Controllers/Common/BulkActions.php create mode 100644 app/Http/Controllers/Common/Companies.php create mode 100644 app/Http/Controllers/Common/Contacts.php create mode 100644 app/Http/Controllers/Common/Dashboards.php create mode 100644 app/Http/Controllers/Common/Import.php create mode 100644 app/Http/Controllers/Common/Items.php create mode 100644 app/Http/Controllers/Common/Plans.php create mode 100644 app/Http/Controllers/Common/Reports.php create mode 100644 app/Http/Controllers/Common/Uploads.php create mode 100644 app/Http/Controllers/Common/Widgets.php create mode 100644 app/Http/Controllers/Install/Database.php create mode 100644 app/Http/Controllers/Install/Language.php create mode 100644 app/Http/Controllers/Install/Requirements.php create mode 100644 app/Http/Controllers/Install/Settings.php create mode 100644 app/Http/Controllers/Install/Updates.php create mode 100644 app/Http/Controllers/Modals/Accounts.php create mode 100644 app/Http/Controllers/Modals/Categories.php create mode 100644 app/Http/Controllers/Modals/Companies.php create mode 100644 app/Http/Controllers/Modals/Currencies.php create mode 100644 app/Http/Controllers/Modals/Customers.php create mode 100644 app/Http/Controllers/Modals/DocumentItemColumns.php create mode 100644 app/Http/Controllers/Modals/DocumentTransactions.php create mode 100644 app/Http/Controllers/Modals/InvoiceEmails.php create mode 100644 app/Http/Controllers/Modals/InvoiceShare.php create mode 100644 app/Http/Controllers/Modals/InvoiceTemplates.php create mode 100644 app/Http/Controllers/Modals/Items.php create mode 100644 app/Http/Controllers/Modals/Taxes.php create mode 100644 app/Http/Controllers/Modals/TransactionEmails.php create mode 100644 app/Http/Controllers/Modals/TransactionShare.php create mode 100644 app/Http/Controllers/Modals/TransferTemplates.php create mode 100644 app/Http/Controllers/Modals/Vendors.php create mode 100644 app/Http/Controllers/Modules/ApiKey.php create mode 100644 app/Http/Controllers/Modules/Home.php create mode 100644 app/Http/Controllers/Modules/Item.php create mode 100644 app/Http/Controllers/Modules/My.php create mode 100644 app/Http/Controllers/Modules/Tiles.php create mode 100644 app/Http/Controllers/Portal/Dashboard.php create mode 100644 app/Http/Controllers/Portal/Invoices.php create mode 100644 app/Http/Controllers/Portal/Payments.php create mode 100644 app/Http/Controllers/Portal/Profile.php create mode 100644 app/Http/Controllers/Purchases/Bills.php create mode 100644 app/Http/Controllers/Purchases/RecurringBills.php create mode 100644 app/Http/Controllers/Purchases/Vendors.php create mode 100644 app/Http/Controllers/Sales/Customers.php create mode 100644 app/Http/Controllers/Sales/Invoices.php create mode 100644 app/Http/Controllers/Sales/RecurringInvoices.php create mode 100644 app/Http/Controllers/Settings/Categories.php create mode 100644 app/Http/Controllers/Settings/Company.php create mode 100644 app/Http/Controllers/Settings/Currencies.php create mode 100644 app/Http/Controllers/Settings/Defaults.php create mode 100644 app/Http/Controllers/Settings/Email.php create mode 100644 app/Http/Controllers/Settings/EmailTemplates.php create mode 100644 app/Http/Controllers/Settings/Invoice.php create mode 100644 app/Http/Controllers/Settings/Localisation.php create mode 100644 app/Http/Controllers/Settings/Modules.php create mode 100644 app/Http/Controllers/Settings/Navigation.php create mode 100644 app/Http/Controllers/Settings/Schedule.php create mode 100644 app/Http/Controllers/Settings/Taxes.php create mode 100644 app/Http/Controllers/SystemSettings/Databases.php create mode 100644 app/Http/Controllers/SystemSettings/Sidebar.php create mode 100644 app/Http/Controllers/SystemSettings/Users.php create mode 100644 app/Http/Controllers/Wizard/Companies.php create mode 100644 app/Http/Controllers/Wizard/Currencies.php create mode 100644 app/Http/Controllers/Wizard/Finish.php create mode 100644 app/Http/Kernel.php create mode 100644 app/Http/Livewire/Common/Search.php create mode 100644 app/Http/Livewire/Menu/Favorite.php create mode 100644 app/Http/Livewire/Menu/Favorites.php create mode 100644 app/Http/Livewire/Menu/Neww.php create mode 100644 app/Http/Livewire/Menu/Notifications.php create mode 100644 app/Http/Livewire/Menu/Profile.php create mode 100644 app/Http/Livewire/Menu/Settings.php create mode 100644 app/Http/Livewire/Notification/Browser.php create mode 100644 app/Http/Livewire/Report/Pin.php create mode 100644 app/Http/Livewire/Report/Pins.php create mode 100644 app/Http/Livewire/Tab/Pin.php create mode 100644 app/Http/Middleware/AddWWWAuthenticateHeader.php create mode 100644 app/Http/Middleware/AddXHeader.php create mode 100644 app/Http/Middleware/AdminMenu.php create mode 100644 app/Http/Middleware/Authenticate.php create mode 100644 app/Http/Middleware/AuthenticateOnceWithBasicAuth.php create mode 100644 app/Http/Middleware/AuthenticateOnceWithDynamicApi.php create mode 100644 app/Http/Middleware/AuthenticateOnceWithOAuth.php create mode 100644 app/Http/Middleware/CanInstall.php create mode 100644 app/Http/Middleware/CheckForReadOnlyMode.php create mode 100644 app/Http/Middleware/DateFormat.php create mode 100644 app/Http/Middleware/DisableQuic.php create mode 100644 app/Http/Middleware/Dropzone.php create mode 100644 app/Http/Middleware/EncryptCookies.php create mode 100644 app/Http/Middleware/EnforceLocalOnly.php create mode 100644 app/Http/Middleware/IdentifyCompany.php create mode 100644 app/Http/Middleware/LogoutIfUserDisabled.php create mode 100644 app/Http/Middleware/Money.php create mode 100644 app/Http/Middleware/PortalMenu.php create mode 100644 app/Http/Middleware/RedirectIfAuthenticated.php create mode 100644 app/Http/Middleware/RedirectIfHitModuleSubscription.php create mode 100644 app/Http/Middleware/RedirectIfHitPlanLimits.php create mode 100644 app/Http/Middleware/RedirectIfNoApiKey.php create mode 100644 app/Http/Middleware/RedirectIfNotInstalled.php create mode 100644 app/Http/Middleware/RedirectIfWizardNotCompleted.php create mode 100644 app/Http/Middleware/TrimStrings.php create mode 100644 app/Http/Middleware/TrustHosts.php create mode 100644 app/Http/Middleware/TrustProxies.php create mode 100644 app/Http/Middleware/ValidateSignature.php create mode 100644 app/Http/Middleware/VerifyCsrfToken.php create mode 100644 app/Http/Requests/Auth/Forgot.php create mode 100644 app/Http/Requests/Auth/Login.php create mode 100644 app/Http/Requests/Auth/Register.php create mode 100644 app/Http/Requests/Auth/Reset.php create mode 100644 app/Http/Requests/Auth/User.php create mode 100644 app/Http/Requests/Banking/Account.php create mode 100644 app/Http/Requests/Banking/FundAccount.php create mode 100644 app/Http/Requests/Banking/Reconciliation.php create mode 100644 app/Http/Requests/Banking/ReconciliationCalculate.php create mode 100644 app/Http/Requests/Banking/Transaction.php create mode 100644 app/Http/Requests/Banking/TransactionConnect.php create mode 100644 app/Http/Requests/Banking/Transfer.php create mode 100644 app/Http/Requests/Common/BulkAction.php create mode 100644 app/Http/Requests/Common/Company.php create mode 100644 app/Http/Requests/Common/Contact.php create mode 100644 app/Http/Requests/Common/ContactPerson.php create mode 100644 app/Http/Requests/Common/CustomMail.php create mode 100644 app/Http/Requests/Common/Dashboard.php create mode 100644 app/Http/Requests/Common/Import.php create mode 100644 app/Http/Requests/Common/Item.php create mode 100644 app/Http/Requests/Common/ItemTax.php create mode 100644 app/Http/Requests/Common/Notification.php create mode 100644 app/Http/Requests/Common/Report.php create mode 100644 app/Http/Requests/Common/ReportShow.php create mode 100644 app/Http/Requests/Common/TotalItem.php create mode 100644 app/Http/Requests/Common/Widget.php create mode 100644 app/Http/Requests/Document/Document.php create mode 100644 app/Http/Requests/Document/DocumentAddItem.php create mode 100644 app/Http/Requests/Document/DocumentHistory.php create mode 100644 app/Http/Requests/Document/DocumentItem.php create mode 100644 app/Http/Requests/Document/DocumentItemTax.php create mode 100644 app/Http/Requests/Document/DocumentTotal.php create mode 100644 app/Http/Requests/Install/Database.php create mode 100644 app/Http/Requests/Install/Setting.php create mode 100644 app/Http/Requests/Module/Install.php create mode 100644 app/Http/Requests/Module/Module.php create mode 100644 app/Http/Requests/Portal/InvoiceConfirm.php create mode 100644 app/Http/Requests/Portal/InvoicePayment.php create mode 100644 app/Http/Requests/Portal/InvoiceShow.php create mode 100644 app/Http/Requests/Portal/PaymentShow.php create mode 100644 app/Http/Requests/Portal/Profile.php create mode 100644 app/Http/Requests/Setting/Category.php create mode 100644 app/Http/Requests/Setting/Currency.php create mode 100644 app/Http/Requests/Setting/EmailTemplate.php create mode 100644 app/Http/Requests/Setting/Module.php create mode 100644 app/Http/Requests/Setting/Setting.php create mode 100644 app/Http/Requests/Setting/Tax.php create mode 100644 app/Http/Requests/Wizard/Company.php create mode 100644 app/Http/Resources/Auth/Owner.php create mode 100644 app/Http/Resources/Auth/Role.php create mode 100644 app/Http/Resources/Auth/User.php create mode 100644 app/Http/Resources/Banking/Account.php create mode 100644 app/Http/Resources/Banking/Reconciliation.php create mode 100644 app/Http/Resources/Banking/Transaction.php create mode 100644 app/Http/Resources/Banking/TransactionTax.php create mode 100644 app/Http/Resources/Banking/Transfer.php create mode 100644 app/Http/Resources/Common/Company.php create mode 100644 app/Http/Resources/Common/Contact.php create mode 100644 app/Http/Resources/Common/ContactPerson.php create mode 100644 app/Http/Resources/Common/Dashboard.php create mode 100644 app/Http/Resources/Common/Item.php create mode 100644 app/Http/Resources/Common/ItemTax.php create mode 100644 app/Http/Resources/Common/Report.php create mode 100644 app/Http/Resources/Common/Widget.php create mode 100644 app/Http/Resources/Document/Document.php create mode 100644 app/Http/Resources/Document/DocumentHistory.php create mode 100644 app/Http/Resources/Document/DocumentItem.php create mode 100644 app/Http/Resources/Document/DocumentItemTax.php create mode 100644 app/Http/Resources/Document/DocumentTotal.php create mode 100644 app/Http/Resources/Setting/Category.php create mode 100644 app/Http/Resources/Setting/Currency.php create mode 100644 app/Http/Resources/Setting/Setting.php create mode 100644 app/Http/Resources/Setting/Tax.php create mode 100644 app/Http/Responses/Common/Items.php create mode 100644 app/Http/ViewComposers/ContactType.php create mode 100644 app/Http/ViewComposers/DocumentRecurring.php create mode 100644 app/Http/ViewComposers/DocumentType.php create mode 100644 app/Http/ViewComposers/PlanLimits.php create mode 100644 app/Http/ViewComposers/ReadOnlyNotification.php create mode 100644 app/Imports/Banking/RecurringTransactions.php create mode 100644 app/Imports/Banking/Sheets/Recurring.php create mode 100644 app/Imports/Banking/Sheets/RecurringTransactions.php create mode 100644 app/Imports/Banking/Transactions.php create mode 100644 app/Imports/Banking/Transfers.php create mode 100644 app/Imports/Common/Items.php create mode 100644 app/Imports/Common/Sheets/ItemTaxes.php create mode 100644 app/Imports/Common/Sheets/Items.php create mode 100644 app/Imports/Purchases/Bills/Bills.php create mode 100644 app/Imports/Purchases/Bills/Sheets/BillHistories.php create mode 100644 app/Imports/Purchases/Bills/Sheets/BillItemTaxes.php create mode 100644 app/Imports/Purchases/Bills/Sheets/BillItems.php create mode 100644 app/Imports/Purchases/Bills/Sheets/BillTotals.php create mode 100644 app/Imports/Purchases/Bills/Sheets/BillTransactions.php create mode 100644 app/Imports/Purchases/Bills/Sheets/Bills.php create mode 100644 app/Imports/Purchases/RecurringBills/RecurringBills.php create mode 100644 app/Imports/Purchases/RecurringBills/Sheets/Recurring.php create mode 100644 app/Imports/Purchases/RecurringBills/Sheets/RecurringBillHistories.php create mode 100644 app/Imports/Purchases/RecurringBills/Sheets/RecurringBillItemTaxes.php create mode 100644 app/Imports/Purchases/RecurringBills/Sheets/RecurringBillItems.php create mode 100644 app/Imports/Purchases/RecurringBills/Sheets/RecurringBillTotals.php create mode 100644 app/Imports/Purchases/RecurringBills/Sheets/RecurringBills.php create mode 100644 app/Imports/Purchases/Vendors.php create mode 100644 app/Imports/Sales/Customers.php create mode 100644 app/Imports/Sales/Invoices/Invoices.php create mode 100644 app/Imports/Sales/Invoices/Sheets/InvoiceHistories.php create mode 100644 app/Imports/Sales/Invoices/Sheets/InvoiceItemTaxes.php create mode 100644 app/Imports/Sales/Invoices/Sheets/InvoiceItems.php create mode 100644 app/Imports/Sales/Invoices/Sheets/InvoiceTotals.php create mode 100644 app/Imports/Sales/Invoices/Sheets/InvoiceTransactions.php create mode 100644 app/Imports/Sales/Invoices/Sheets/Invoices.php create mode 100644 app/Imports/Sales/RecurringInvoices/RecurringInvoices.php create mode 100644 app/Imports/Sales/RecurringInvoices/Sheets/Recurring.php create mode 100644 app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoiceHistories.php create mode 100644 app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoiceItemTaxes.php create mode 100644 app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoiceItems.php create mode 100644 app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoiceTotals.php create mode 100644 app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoices.php create mode 100644 app/Imports/Settings/Categories.php create mode 100644 app/Imports/Settings/Taxes.php create mode 100644 app/Interfaces/Export/WithParentSheet.php create mode 100644 app/Interfaces/Job/HasOwner.php create mode 100644 app/Interfaces/Job/HasSource.php create mode 100644 app/Interfaces/Job/ShouldCreate.php create mode 100644 app/Interfaces/Job/ShouldDelete.php create mode 100644 app/Interfaces/Job/ShouldUpdate.php create mode 100644 app/Interfaces/Listener/ShouldUpdateAllCompanies.php create mode 100644 app/Interfaces/Utility/DocumentNumber.php create mode 100644 app/Interfaces/Utility/TransactionNumber.php create mode 100644 app/Jobs/Auth/CreateInvitation.php create mode 100644 app/Jobs/Auth/CreatePermission.php create mode 100644 app/Jobs/Auth/CreateRole.php create mode 100644 app/Jobs/Auth/CreateUser.php create mode 100644 app/Jobs/Auth/DeleteInvitation.php create mode 100644 app/Jobs/Auth/DeletePermission.php create mode 100644 app/Jobs/Auth/DeleteRole.php create mode 100644 app/Jobs/Auth/DeleteUser.php create mode 100644 app/Jobs/Auth/NotifyUser.php create mode 100644 app/Jobs/Auth/UpdatePermission.php create mode 100644 app/Jobs/Auth/UpdateRole.php create mode 100644 app/Jobs/Auth/UpdateUser.php create mode 100644 app/Jobs/Banking/CreateAccount.php create mode 100644 app/Jobs/Banking/CreateBankingDocumentTransaction.php create mode 100644 app/Jobs/Banking/CreateReconciliation.php create mode 100644 app/Jobs/Banking/CreateTransaction.php create mode 100644 app/Jobs/Banking/CreateTransactionTaxes.php create mode 100644 app/Jobs/Banking/CreateTransfer.php create mode 100644 app/Jobs/Banking/DeleteAccount.php create mode 100644 app/Jobs/Banking/DeleteReconciliation.php create mode 100644 app/Jobs/Banking/DeleteTransaction.php create mode 100644 app/Jobs/Banking/DeleteTransfer.php create mode 100644 app/Jobs/Banking/DuplicateTransaction.php create mode 100644 app/Jobs/Banking/MatchBankingDocumentTransaction.php create mode 100644 app/Jobs/Banking/SendTransaction.php create mode 100644 app/Jobs/Banking/SendTransactionAsCustomMail.php create mode 100644 app/Jobs/Banking/SplitTransaction.php create mode 100644 app/Jobs/Banking/UpdateAccount.php create mode 100644 app/Jobs/Banking/UpdateBankingDocumentTransaction.php create mode 100644 app/Jobs/Banking/UpdateReconciliation.php create mode 100644 app/Jobs/Banking/UpdateTransaction.php create mode 100644 app/Jobs/Banking/UpdateTransfer.php create mode 100644 app/Jobs/Common/CreateCompany.php create mode 100644 app/Jobs/Common/CreateContact.php create mode 100644 app/Jobs/Common/CreateContactPerson.php create mode 100644 app/Jobs/Common/CreateContactPersons.php create mode 100644 app/Jobs/Common/CreateDashboard.php create mode 100644 app/Jobs/Common/CreateItem.php create mode 100644 app/Jobs/Common/CreateItemTaxes.php create mode 100644 app/Jobs/Common/CreateMediableForDownload.php create mode 100644 app/Jobs/Common/CreateMediableForExport.php create mode 100644 app/Jobs/Common/CreateReport.php create mode 100644 app/Jobs/Common/CreateWidget.php create mode 100644 app/Jobs/Common/CreateZipForDownload.php create mode 100644 app/Jobs/Common/DeleteCompany.php create mode 100644 app/Jobs/Common/DeleteContact.php create mode 100644 app/Jobs/Common/DeleteContactPerson.php create mode 100644 app/Jobs/Common/DeleteDashboard.php create mode 100644 app/Jobs/Common/DeleteItem.php create mode 100644 app/Jobs/Common/DeleteReport.php create mode 100644 app/Jobs/Common/DeleteWidget.php create mode 100644 app/Jobs/Common/DuplicateContact.php create mode 100644 app/Jobs/Common/UpdateCompany.php create mode 100644 app/Jobs/Common/UpdateContact.php create mode 100644 app/Jobs/Common/UpdateContactPerson.php create mode 100644 app/Jobs/Common/UpdateDashboard.php create mode 100644 app/Jobs/Common/UpdateItem.php create mode 100644 app/Jobs/Common/UpdateReport.php create mode 100644 app/Jobs/Common/UpdateWidget.php create mode 100644 app/Jobs/Document/CancelDocument.php create mode 100644 app/Jobs/Document/CreateDocument.php create mode 100644 app/Jobs/Document/CreateDocumentHistory.php create mode 100644 app/Jobs/Document/CreateDocumentItem.php create mode 100644 app/Jobs/Document/CreateDocumentItemsAndTotals.php create mode 100644 app/Jobs/Document/DeleteDocument.php create mode 100644 app/Jobs/Document/DownloadDocument.php create mode 100644 app/Jobs/Document/DuplicateDocument.php create mode 100644 app/Jobs/Document/RestoreDocument.php create mode 100644 app/Jobs/Document/SendDocument.php create mode 100644 app/Jobs/Document/SendDocumentAsCustomMail.php create mode 100644 app/Jobs/Document/UpdateDocument.php create mode 100644 app/Jobs/Install/CopyFiles.php create mode 100644 app/Jobs/Install/DisableModule.php create mode 100644 app/Jobs/Install/DownloadFile.php create mode 100644 app/Jobs/Install/DownloadModule.php create mode 100644 app/Jobs/Install/EnableModule.php create mode 100644 app/Jobs/Install/FinishUpdate.php create mode 100644 app/Jobs/Install/InstallModule.php create mode 100644 app/Jobs/Install/UninstallModule.php create mode 100644 app/Jobs/Install/UnzipFile.php create mode 100644 app/Jobs/Setting/CreateCategory.php create mode 100644 app/Jobs/Setting/CreateCurrency.php create mode 100644 app/Jobs/Setting/CreateEmailTemplate.php create mode 100644 app/Jobs/Setting/CreateTax.php create mode 100644 app/Jobs/Setting/DeleteCategory.php create mode 100644 app/Jobs/Setting/DeleteCurrency.php create mode 100644 app/Jobs/Setting/DeleteEmailTemplate.php create mode 100644 app/Jobs/Setting/DeleteTax.php create mode 100644 app/Jobs/Setting/UpdateCategory.php create mode 100644 app/Jobs/Setting/UpdateCurrency.php create mode 100644 app/Jobs/Setting/UpdateEmailTemplate.php create mode 100644 app/Jobs/Setting/UpdateTax.php create mode 100644 app/Listeners/Auth/AddLandingPages.php create mode 100644 app/Listeners/Auth/Login.php create mode 100644 app/Listeners/Auth/Logout.php create mode 100644 app/Listeners/Banking/IncreaseNextTransactionNumber.php create mode 100644 app/Listeners/Common/ClearPlansCache.php create mode 100644 app/Listeners/Common/PreparingResponse.php create mode 100644 app/Listeners/Common/SkipScheduleInReadOnlyMode.php create mode 100644 app/Listeners/Document/CreateDocumentCreatedHistory.php create mode 100644 app/Listeners/Document/CreateDocumentTransaction.php create mode 100644 app/Listeners/Document/IncreaseNextDocumentNumber.php create mode 100644 app/Listeners/Document/MarkDocumentCancelled.php create mode 100644 app/Listeners/Document/MarkDocumentReceived.php create mode 100644 app/Listeners/Document/MarkDocumentSent.php create mode 100644 app/Listeners/Document/MarkDocumentViewed.php create mode 100644 app/Listeners/Document/RestoreDocument.php create mode 100644 app/Listeners/Document/SendDocumentPaymentNotification.php create mode 100644 app/Listeners/Document/SendDocumentRecurringNotification.php create mode 100644 app/Listeners/Document/SendDocumentReminderNotification.php create mode 100644 app/Listeners/Document/SendDocumentViewNotification.php create mode 100644 app/Listeners/Document/SettingFieldCreated.php create mode 100644 app/Listeners/Document/SettingFieldUpdated.php create mode 100644 app/Listeners/Email/DisablePersonDueToInvalidEmail.php create mode 100644 app/Listeners/Email/ReportTooManyEmailsSent.php create mode 100644 app/Listeners/Email/SendInvalidEmailNotification.php create mode 100644 app/Listeners/Email/TellFirewallTooManyEmailsSent.php create mode 100644 app/Listeners/Menu/ShowInAdmin.php create mode 100644 app/Listeners/Menu/ShowInNeww.php create mode 100644 app/Listeners/Menu/ShowInNotifications.php create mode 100644 app/Listeners/Menu/ShowInPortal.php create mode 100644 app/Listeners/Menu/ShowInProfile.php create mode 100644 app/Listeners/Menu/ShowInSettings.php create mode 100644 app/Listeners/Module/ClearCache.php create mode 100644 app/Listeners/Module/FinishInstallation.php create mode 100644 app/Listeners/Module/FinishUninstallation.php create mode 100644 app/Listeners/Module/InstallExtraModules.php create mode 100644 app/Listeners/Module/UpdateExtraModules.php create mode 100644 app/Listeners/Report/AddAccounts.php create mode 100644 app/Listeners/Report/AddBasis.php create mode 100644 app/Listeners/Report/AddContacts.php create mode 100644 app/Listeners/Report/AddCustomers.php create mode 100644 app/Listeners/Report/AddDate.php create mode 100644 app/Listeners/Report/AddDiscount.php create mode 100644 app/Listeners/Report/AddExpenseCategories.php create mode 100644 app/Listeners/Report/AddGroup.php create mode 100644 app/Listeners/Report/AddIncomeCategories.php create mode 100644 app/Listeners/Report/AddIncomeExpenseCategories.php create mode 100644 app/Listeners/Report/AddPeriod.php create mode 100644 app/Listeners/Report/AddRowsToTax.php create mode 100644 app/Listeners/Report/AddSearchString.php create mode 100644 app/Listeners/Report/AddVendors.php create mode 100644 app/Listeners/Setting/DeleteCategoryDeletedSubCategories.php create mode 100644 app/Listeners/Update/CreateModuleUpdatedHistory.php create mode 100644 app/Listeners/Update/SendNotificationOnFailure.php create mode 100644 app/Listeners/Update/V30/Version300.php create mode 100644 app/Listeners/Update/V30/Version3013.php create mode 100644 app/Listeners/Update/V30/Version3014.php create mode 100644 app/Listeners/Update/V30/Version3015.php create mode 100644 app/Listeners/Update/V30/Version3016.php create mode 100644 app/Listeners/Update/V30/Version3017.php create mode 100644 app/Listeners/Update/V30/Version303.php create mode 100644 app/Listeners/Update/V30/Version304.php create mode 100644 app/Listeners/Update/V30/Version305.php create mode 100644 app/Listeners/Update/V30/Version307.php create mode 100644 app/Listeners/Update/V30/Version309.php create mode 100644 app/Listeners/Update/V31/Version310.php create mode 100644 app/Listeners/Update/V31/Version3112.php create mode 100644 app/Listeners/Update/V31/Version3115.php create mode 100644 app/Listeners/Update/V31/Version3119.php create mode 100644 app/Listeners/Update/V31/Version315.php create mode 100644 app/Listeners/Update/V31/Version317.php create mode 100644 app/Listeners/Update/V31/Version318.php create mode 100644 app/Listeners/Update/V32/Version320.php create mode 100644 app/Models/Auth/Permission.php create mode 100644 app/Models/Auth/Role.php create mode 100644 app/Models/Auth/User.php create mode 100644 app/Models/Auth/UserCompany.php create mode 100644 app/Models/Auth/UserDashboard.php create mode 100644 app/Models/Auth/UserInvitation.php create mode 100644 app/Models/Auth/UserRole.php create mode 100644 app/Models/Banking/Account.php create mode 100644 app/Models/Banking/FundAccount.php create mode 100644 app/Models/Banking/Reconciliation.php create mode 100644 app/Models/Banking/Transaction.php create mode 100644 app/Models/Banking/TransactionTax.php create mode 100644 app/Models/Banking/Transfer.php create mode 100644 app/Models/Common/Company.php create mode 100644 app/Models/Common/Contact.php create mode 100644 app/Models/Common/ContactPerson.php create mode 100644 app/Models/Common/Dashboard.php create mode 100644 app/Models/Common/Item.php create mode 100644 app/Models/Common/ItemTax.php create mode 100644 app/Models/Common/Media.php create mode 100644 app/Models/Common/Notification.php create mode 100644 app/Models/Common/Recurring.php create mode 100644 app/Models/Common/Report.php create mode 100644 app/Models/Common/Widget.php create mode 100644 app/Models/Document/Document.php create mode 100644 app/Models/Document/DocumentHistory.php create mode 100644 app/Models/Document/DocumentItem.php create mode 100644 app/Models/Document/DocumentItemTax.php create mode 100644 app/Models/Document/DocumentTotal.php create mode 100644 app/Models/Module/Module.php create mode 100644 app/Models/Module/ModuleHistory.php create mode 100644 app/Models/Setting/Category.php create mode 100644 app/Models/Setting/Currency.php create mode 100644 app/Models/Setting/EmailTemplate.php create mode 100644 app/Models/Setting/Setting.php create mode 100644 app/Models/Setting/Tax.php create mode 100644 app/Models/Settings/DatabaseConnection.php create mode 100644 app/Notifications/Auth/Invitation.php create mode 100644 app/Notifications/Auth/Reset.php create mode 100644 app/Notifications/Banking/Transaction.php create mode 100644 app/Notifications/Common/DownloadCompleted.php create mode 100644 app/Notifications/Common/DownloadFailed.php create mode 100644 app/Notifications/Common/ExportCompleted.php create mode 100644 app/Notifications/Common/ExportFailed.php create mode 100644 app/Notifications/Common/ImportCompleted.php create mode 100644 app/Notifications/Common/ImportFailed.php create mode 100644 app/Notifications/Email/InvalidEmail.php create mode 100644 app/Notifications/Install/UpdateFailed.php create mode 100644 app/Notifications/Portal/PaymentReceived.php create mode 100644 app/Notifications/Purchase/Bill.php create mode 100644 app/Notifications/Sale/Invoice.php create mode 100644 app/Observers/Transaction.php create mode 100644 app/Providers/App.php create mode 100644 app/Providers/Auth.php create mode 100644 app/Providers/Binding.php create mode 100644 app/Providers/Blade.php create mode 100644 app/Providers/Broadcast.php create mode 100644 app/Providers/Event.php create mode 100644 app/Providers/Macro.php create mode 100644 app/Providers/Observer.php create mode 100644 app/Providers/Queue.php create mode 100644 app/Providers/Route.php create mode 100644 app/Providers/Validation.php create mode 100644 app/Providers/ViewComposer.php create mode 100644 app/Relations/HasMany/Category.php create mode 100644 app/Reports/DiscountSummary.php create mode 100644 app/Reports/ExpenseSummary.php create mode 100644 app/Reports/IncomeExpenseSummary.php create mode 100644 app/Reports/IncomeSummary.php create mode 100644 app/Reports/ProfitLoss.php create mode 100644 app/Reports/TaxSummary.php create mode 100644 app/Scopes/Category.php create mode 100644 app/Scopes/Company.php create mode 100644 app/Scopes/Contact.php create mode 100644 app/Scopes/Document.php create mode 100644 app/Scopes/Transaction.php create mode 100644 app/Traits/Categories.php create mode 100644 app/Traits/Charts.php create mode 100644 app/Traits/Cloud.php create mode 100644 app/Traits/Companies.php create mode 100644 app/Traits/Contacts.php create mode 100644 app/Traits/Currencies.php create mode 100644 app/Traits/Database.php create mode 100644 app/Traits/DateTime.php create mode 100644 app/Traits/Documents.php create mode 100644 app/Traits/Emails.php create mode 100644 app/Traits/HasApiTokens.php create mode 100644 app/Traits/Import.php create mode 100644 app/Traits/Jobs.php create mode 100644 app/Traits/Media.php create mode 100644 app/Traits/Modules.php create mode 100644 app/Traits/Omnipay.php create mode 100644 app/Traits/Owners.php create mode 100644 app/Traits/Permissions.php create mode 100644 app/Traits/Plans.php create mode 100644 app/Traits/Recurring.php create mode 100644 app/Traits/Relationships.php create mode 100644 app/Traits/Scopes.php create mode 100644 app/Traits/SearchString.php create mode 100644 app/Traits/Settings.php create mode 100644 app/Traits/SiteApi.php create mode 100644 app/Traits/Sources.php create mode 100644 app/Traits/Tailwind.php create mode 100644 app/Traits/Tenants.php create mode 100644 app/Traits/Trackers.php create mode 100644 app/Traits/Transactions.php create mode 100644 app/Traits/Translations.php create mode 100644 app/Traits/Updates.php create mode 100644 app/Traits/Uploads.php create mode 100644 app/Traits/Users.php create mode 100644 app/Traits/ViewComponents.php create mode 100644 app/Utilities/BusinessNavigation.php create mode 100644 app/Utilities/Console.php create mode 100644 app/Utilities/Date.php create mode 100644 app/Utilities/DocumentNumber.php create mode 100644 app/Utilities/Export.php create mode 100644 app/Utilities/FundAccountSpreadsheet.php create mode 100644 app/Utilities/Import.php create mode 100644 app/Utilities/Info.php create mode 100644 app/Utilities/Installer.php create mode 100644 app/Utilities/ModuleActivator.php create mode 100644 app/Utilities/Modules.php create mode 100644 app/Utilities/Overrider.php create mode 100644 app/Utilities/QueueCollection.php create mode 100644 app/Utilities/Recurring.php create mode 100644 app/Utilities/Reports.php create mode 100644 app/Utilities/SidebarPermissions.php create mode 100644 app/Utilities/Str.php create mode 100644 app/Utilities/SystemAccess.php create mode 100644 app/Utilities/TransactionNumber.php create mode 100644 app/Utilities/Versions.php create mode 100644 app/Utilities/Widgets.php create mode 100644 app/Utilities/helpers.php create mode 100644 app/View/Components/Alert.php create mode 100644 app/View/Components/Alert/Border.php create mode 100644 app/View/Components/Alert/Danger.php create mode 100644 app/View/Components/Alert/Info.php create mode 100644 app/View/Components/Alert/Success.php create mode 100644 app/View/Components/Alert/Warning.php create mode 100644 app/View/Components/Button.php create mode 100644 app/View/Components/Button/Hover.php create mode 100644 app/View/Components/Contacts/Form/Address.php create mode 100644 app/View/Components/Contacts/Form/Billing.php create mode 100644 app/View/Components/Contacts/Form/Buttons.php create mode 100644 app/View/Components/Contacts/Form/Content.php create mode 100644 app/View/Components/Contacts/Form/General.php create mode 100644 app/View/Components/Contacts/Form/Persons.php create mode 100644 app/View/Components/Contacts/Index/Buttons.php create mode 100644 app/View/Components/Contacts/Index/Content.php create mode 100644 app/View/Components/Contacts/Index/MoreButtons.php create mode 100644 app/View/Components/Contacts/Script.php create mode 100644 app/View/Components/Contacts/Show/Buttons.php create mode 100644 app/View/Components/Contacts/Show/Content.php create mode 100644 app/View/Components/Contacts/Show/MoreButtons.php create mode 100644 app/View/Components/Date.php create mode 100644 app/View/Components/DeleteButton.php create mode 100644 app/View/Components/DeleteLink.php create mode 100644 app/View/Components/Documents/Form/Advanced.php create mode 100644 app/View/Components/Documents/Form/Buttons.php create mode 100644 app/View/Components/Documents/Form/Company.php create mode 100644 app/View/Components/Documents/Form/Contact.php create mode 100644 app/View/Components/Documents/Form/Content.php create mode 100644 app/View/Components/Documents/Form/Footer.php create mode 100644 app/View/Components/Documents/Form/ItemButton.php create mode 100644 app/View/Components/Documents/Form/ItemColumns.php create mode 100644 app/View/Components/Documents/Form/Items.php create mode 100644 app/View/Components/Documents/Form/LineItem.php create mode 100644 app/View/Components/Documents/Form/Main.php create mode 100644 app/View/Components/Documents/Form/Metadata.php create mode 100644 app/View/Components/Documents/Form/Note.php create mode 100644 app/View/Components/Documents/Form/Recurring.php create mode 100644 app/View/Components/Documents/Form/RecurringMetadata.php create mode 100644 app/View/Components/Documents/Form/Totals.php create mode 100644 app/View/Components/Documents/Index/BulkAction.php create mode 100644 app/View/Components/Documents/Index/Buttons.php create mode 100644 app/View/Components/Documents/Index/Content.php create mode 100644 app/View/Components/Documents/Index/Document.php create mode 100644 app/View/Components/Documents/Index/Information.php create mode 100644 app/View/Components/Documents/Index/MoreButtons.php create mode 100644 app/View/Components/Documents/Index/RecurringTemplates.php create mode 100644 app/View/Components/Documents/Script.php create mode 100644 app/View/Components/Documents/Show/Attachment.php create mode 100644 app/View/Components/Documents/Show/Buttons.php create mode 100644 app/View/Components/Documents/Show/Children.php create mode 100644 app/View/Components/Documents/Show/Content.php create mode 100644 app/View/Components/Documents/Show/Create.php create mode 100644 app/View/Components/Documents/Show/GetPaid.php create mode 100644 app/View/Components/Documents/Show/MakePayment.php create mode 100644 app/View/Components/Documents/Show/Message.php create mode 100644 app/View/Components/Documents/Show/MoreButtons.php create mode 100644 app/View/Components/Documents/Show/Receive.php create mode 100644 app/View/Components/Documents/Show/Restore.php create mode 100644 app/View/Components/Documents/Show/Schedule.php create mode 100644 app/View/Components/Documents/Show/Send.php create mode 100644 app/View/Components/Documents/Show/Template.php create mode 100644 app/View/Components/Documents/Template/Classic.php create mode 100644 app/View/Components/Documents/Template/Ddefault.php create mode 100644 app/View/Components/Documents/Template/LineItem.php create mode 100644 app/View/Components/Documents/Template/Modern.php create mode 100644 app/View/Components/Dropdown.php create mode 100644 app/View/Components/Dropdown/Button.php create mode 100644 app/View/Components/Dropdown/Link.php create mode 100644 app/View/Components/EmptyPage.php create mode 100644 app/View/Components/Form.php create mode 100644 app/View/Components/Form/Accordion.php create mode 100644 app/View/Components/Form/Buttons.php create mode 100644 app/View/Components/Form/Group/Account.php create mode 100644 app/View/Components/Form/Group/Attachment.php create mode 100644 app/View/Components/Form/Group/Category.php create mode 100644 app/View/Components/Form/Group/Checkbox.php create mode 100644 app/View/Components/Form/Group/Color.php create mode 100644 app/View/Components/Form/Group/Contact.php create mode 100644 app/View/Components/Form/Group/Country.php create mode 100644 app/View/Components/Form/Group/Currency.php create mode 100644 app/View/Components/Form/Group/Customer.php create mode 100644 app/View/Components/Form/Group/Date.php create mode 100644 app/View/Components/Form/Group/DateTime.php create mode 100644 app/View/Components/Form/Group/Editor.php create mode 100644 app/View/Components/Form/Group/Email.php create mode 100644 app/View/Components/Form/Group/File.php create mode 100644 app/View/Components/Form/Group/Import.php create mode 100644 app/View/Components/Form/Group/InvoiceText.php create mode 100644 app/View/Components/Form/Group/Locale.php create mode 100644 app/View/Components/Form/Group/Money.php create mode 100644 app/View/Components/Form/Group/Number.php create mode 100644 app/View/Components/Form/Group/NumberDigit.php create mode 100644 app/View/Components/Form/Group/Password.php create mode 100644 app/View/Components/Form/Group/PaymentMethod.php create mode 100644 app/View/Components/Form/Group/PaymentTerm.php create mode 100644 app/View/Components/Form/Group/Radio.php create mode 100644 app/View/Components/Form/Group/Recurring.php create mode 100644 app/View/Components/Form/Group/Select.php create mode 100644 app/View/Components/Form/Group/Sswitch.php create mode 100644 app/View/Components/Form/Group/Tax.php create mode 100644 app/View/Components/Form/Group/Text.php create mode 100644 app/View/Components/Form/Group/Textarea.php create mode 100644 app/View/Components/Form/Group/Time.php create mode 100644 app/View/Components/Form/Group/Toggle.php create mode 100644 app/View/Components/Form/Group/Vendor.php create mode 100644 app/View/Components/Form/Input/Checkbox.php create mode 100644 app/View/Components/Form/Input/Color.php create mode 100644 app/View/Components/Form/Input/Editor.php create mode 100644 app/View/Components/Form/Input/Email.php create mode 100644 app/View/Components/Form/Input/Ffile.php create mode 100644 app/View/Components/Form/Input/Hidden.php create mode 100644 app/View/Components/Form/Input/Import.php create mode 100644 app/View/Components/Form/Input/Input.php create mode 100644 app/View/Components/Form/Input/Money.php create mode 100644 app/View/Components/Form/Input/Number.php create mode 100644 app/View/Components/Form/Input/Password.php create mode 100644 app/View/Components/Form/Input/Radio.php create mode 100644 app/View/Components/Form/Input/Select.php create mode 100644 app/View/Components/Form/Input/Text.php create mode 100644 app/View/Components/Form/Input/Textarea.php create mode 100644 app/View/Components/Form/Section.php create mode 100644 app/View/Components/Icon.php create mode 100644 app/View/Components/Index/Balance.php create mode 100644 app/View/Components/Index/Bulkaction.php create mode 100644 app/View/Components/Index/Category.php create mode 100644 app/View/Components/Index/Country.php create mode 100644 app/View/Components/Index/Currency.php create mode 100644 app/View/Components/Index/Ddefault.php create mode 100644 app/View/Components/Index/Disable.php create mode 100644 app/View/Components/Index/Search.php create mode 100644 app/View/Components/Index/Status.php create mode 100644 app/View/Components/Index/Summary.php create mode 100644 app/View/Components/Layouts/Admin.php create mode 100644 app/View/Components/Layouts/Admin/Head.php create mode 100644 app/View/Components/Layouts/Admin/Menu.php create mode 100644 app/View/Components/Layouts/Admin/Notifications.php create mode 100644 app/View/Components/Layouts/Modules.php create mode 100644 app/View/Components/Layouts/Modules/Bar.php create mode 100644 app/View/Components/Layouts/Modules/Releases.php create mode 100644 app/View/Components/Layouts/Modules/Reviews.php create mode 100644 app/View/Components/Layouts/Portal/Menu.php create mode 100644 app/View/Components/Layouts/Wizard.php create mode 100644 app/View/Components/Layouts/Wizard/Scripts.php create mode 100644 app/View/Components/Link.php create mode 100644 app/View/Components/Link/Hover.php create mode 100644 app/View/Components/Media/Ffile.php create mode 100644 app/View/Components/Menu/Favorite.php create mode 100644 app/View/Components/Modules/Banners.php create mode 100644 app/View/Components/Modules/Free.php create mode 100644 app/View/Components/Modules/Installed.php create mode 100644 app/View/Components/Modules/Item.php create mode 100644 app/View/Components/Modules/Items.php create mode 100644 app/View/Components/Modules/Nnew.php create mode 100644 app/View/Components/Modules/NoApps.php create mode 100644 app/View/Components/Modules/Paid.php create mode 100644 app/View/Components/Modules/PreSale.php create mode 100644 app/View/Components/Modules/Purchased.php create mode 100644 app/View/Components/Pagination.php create mode 100644 app/View/Components/PaymentMethod.php create mode 100644 app/View/Components/Script.php create mode 100644 app/View/Components/Script/Declaration.php create mode 100644 app/View/Components/Script/Exceptions/Trackers.php create mode 100644 app/View/Components/SearchString.php create mode 100644 app/View/Components/Show/Accordion.php create mode 100644 app/View/Components/Show/NoRecords.php create mode 100644 app/View/Components/Show/Right.php create mode 100644 app/View/Components/Show/Status.php create mode 100644 app/View/Components/Suggestions.php create mode 100644 app/View/Components/Table/Actions.php create mode 100644 app/View/Components/Table/Tbody.php create mode 100644 app/View/Components/Table/Td.php create mode 100644 app/View/Components/Table/Th.php create mode 100644 app/View/Components/Table/Thead.php create mode 100644 app/View/Components/Table/Tr.php create mode 100644 app/View/Components/Tips.php create mode 100644 app/View/Components/Title.php create mode 100644 app/View/Components/Tooltip.php create mode 100644 app/View/Components/Transactions/Index/BulkAction.php create mode 100644 app/View/Components/Transactions/Index/Buttons.php create mode 100644 app/View/Components/Transactions/Index/Content.php create mode 100644 app/View/Components/Transactions/Index/MoreButtons.php create mode 100644 app/View/Components/Transactions/Index/RecurringTemplates.php create mode 100644 app/View/Components/Transactions/Index/Transaction.php create mode 100644 app/View/Components/Transactions/Script.php create mode 100644 app/View/Components/Transactions/Show/Attachment.php create mode 100644 app/View/Components/Transactions/Show/Buttons.php create mode 100644 app/View/Components/Transactions/Show/Children.php create mode 100644 app/View/Components/Transactions/Show/Connect.php create mode 100644 app/View/Components/Transactions/Show/Content.php create mode 100644 app/View/Components/Transactions/Show/Create.php create mode 100644 app/View/Components/Transactions/Show/MoreButtons.php create mode 100644 app/View/Components/Transactions/Show/Schedule.php create mode 100644 app/View/Components/Transactions/Show/Template.php create mode 100644 app/View/Components/Transactions/Show/Transfer.php create mode 100644 app/View/Components/Transactions/Template/Ddefault.php create mode 100644 app/View/Components/Transfers/Script.php create mode 100644 app/View/Components/Transfers/Show/Attachment.php create mode 100644 app/View/Components/Transfers/Show/Buttons.php create mode 100644 app/View/Components/Transfers/Show/Content.php create mode 100644 app/View/Components/Transfers/Show/Create.php create mode 100644 app/View/Components/Transfers/Show/MoreButtons.php create mode 100644 app/View/Components/Transfers/Show/Template.php create mode 100644 app/View/Components/Transfers/Show/Transactions.php create mode 100644 app/View/Components/Transfers/Template/Ddefault.php create mode 100644 app/View/Components/Transfers/Template/Second.php create mode 100644 app/View/Components/Transfers/Template/Third.php create mode 100644 app/View/Components/UpdateAlert.php create mode 100644 app/View/Components/Widgets/Contact.php create mode 100644 app/View/Components/Widgets/InvoiceHistory.php create mode 100644 app/View/Components/Widgets/LastPayment.php create mode 100644 app/View/Components/Widgets/LatestInvoices.php create mode 100644 app/View/Components/Widgets/OutstandingBalance.php create mode 100644 app/View/Components/Widgets/PaymentHistory.php create mode 100644 app/View/Presenters/Menu.php create mode 100644 app/Widgets/AccountBalance.php create mode 100644 app/Widgets/BankFeeds.php create mode 100644 app/Widgets/CashFlow.php create mode 100644 app/Widgets/Currencies.php create mode 100644 app/Widgets/ExpensesByCategory.php create mode 100644 app/Widgets/Payables.php create mode 100644 app/Widgets/ProfitLoss.php create mode 100644 app/Widgets/Receivables.php create mode 100644 artisan create mode 100644 bootstrap/app.php create mode 100644 bootstrap/autoload.php create mode 100644 bootstrap/cache/.gitignore create mode 100644 compose.jiangling.yml create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 config/apexcharts.php create mode 100644 config/api.php create mode 100644 config/app.php create mode 100644 config/auth.php create mode 100644 config/broadcasting.php create mode 100644 config/bugsnag.php create mode 100644 config/business_navigation.php create mode 100644 config/cache.php create mode 100644 config/charts.php create mode 100644 config/cors.php create mode 100644 config/database.php create mode 100644 config/debugbar.php create mode 100644 config/dompdf.php create mode 100644 config/excel.php create mode 100644 config/filesystems.php create mode 100644 config/firewall.php create mode 100644 config/hashing.php create mode 100644 config/ide-helper.php create mode 100644 config/image.php create mode 100644 config/imagecache.php create mode 100644 config/import.php create mode 100644 config/language.php create mode 100644 config/laratrust.php create mode 100644 config/laravel-model-caching.php create mode 100644 config/livewire.php create mode 100644 config/logging.php create mode 100644 config/mail.php create mode 100644 config/mediable.php create mode 100644 config/menu.php create mode 100644 config/module.php create mode 100644 config/money.php create mode 100644 config/queue.php create mode 100644 config/read-only.php create mode 100644 config/sanctum.php create mode 100644 config/search-string.php create mode 100644 config/sentry.php create mode 100644 config/services.php create mode 100644 config/session.php create mode 100644 config/setting.php create mode 100644 config/sortable.php create mode 100644 config/stats.php create mode 100644 config/trustedproxy.php create mode 100644 config/type.php create mode 100644 config/update.php create mode 100644 config/version.php create mode 100644 config/view.php create mode 100644 database/.gitignore create mode 100644 database/factories/Account.php create mode 100644 database/factories/Category.php create mode 100644 database/factories/Company.php create mode 100644 database/factories/Contact.php create mode 100644 database/factories/Currency.php create mode 100644 database/factories/Dashboard.php create mode 100644 database/factories/Document.php create mode 100644 database/factories/Item.php create mode 100644 database/factories/Reconciliation.php create mode 100644 database/factories/Tax.php create mode 100644 database/factories/Transaction.php create mode 100644 database/factories/Transfer.php create mode 100644 database/factories/User.php create mode 100644 database/factories/Widget.php create mode 100644 database/migrations/2017_09_14_000000_core_v1.php create mode 100644 database/migrations/2019_11_16_000000_core_v2.php create mode 100644 database/migrations/2022_05_10_000000_core_v300.php create mode 100644 database/migrations/2022_06_28_000000_core_v304.php create mode 100644 database/migrations/2022_07_21_000000_core_v305.php create mode 100644 database/migrations/2022_08_29_000000_core_v3015.php create mode 100644 database/migrations/2022_08_29_000000_core_v307.php create mode 100644 database/migrations/2023_06_22_000000_core_v3016.php create mode 100644 database/migrations/2023_08_14_000000_core_v3017.php create mode 100644 database/migrations/2023_10_03_000000_core_v310.php create mode 100644 database/migrations/2024_09_26_000000_core_v3112.php create mode 100644 database/migrations/2024_12_29_000000_core_v3115.php create mode 100644 database/migrations/2026_02_17_000000_core_v3122.php create mode 100644 database/migrations/2026_07_18_000000_create_fund_accounts_table.php create mode 100644 database/migrations/2026_07_20_000000_create_system_settings_tables.php create mode 100644 database/seeds/Accounts.php create mode 100644 database/seeds/Categories.php create mode 100644 database/seeds/Company.php create mode 100644 database/seeds/Currencies.php create mode 100644 database/seeds/Dashboards.php create mode 100644 database/seeds/DatabaseSeeder.php create mode 100644 database/seeds/EmailTemplates.php create mode 100644 database/seeds/Modules.php create mode 100644 database/seeds/Permissions.php create mode 100644 database/seeds/Reports.php create mode 100644 database/seeds/SampleData.php create mode 100644 database/seeds/Settings.php create mode 100644 database/seeds/TestCompany.php create mode 100644 database/seeds/User.php create mode 100644 deploy/Dockerfile create mode 100644 deploy/apache-vhost.conf create mode 100644 deploy/entrypoint.sh create mode 100644 deploy/host-nginx.conf create mode 100644 deploy/php.ini create mode 100644 docs/ecommerce-finance-development.md create mode 100644 docs/team-collaboration.md create mode 100644 index.php create mode 100644 manifest.json create mode 100644 modules/.gitkeep create mode 100644 nginx.example.com.conf create mode 100644 overrides/Illuminate/Translation/MessageSelector.php create mode 100644 overrides/Illuminate/View/Concerns/ManagesStacks.php create mode 100644 overrides/akaunting/laravel-module/Commands/DeleteCommand.php create mode 100644 overrides/akaunting/laravel-module/Commands/DisableCommand.php create mode 100644 overrides/akaunting/laravel-module/Commands/EnableCommand.php create mode 100644 overrides/akaunting/laravel-module/Commands/InstallCommand.php create mode 100644 overrides/livewire/livewire/Mechanisms/HandleRequests/HandleRequests.php create mode 100644 overrides/maatwebsite/excel/Fakes/ExcelFake.php create mode 100644 overrides/maatwebsite/excel/Fakes/fake_file create mode 100644 overrides/maatwebsite/excel/QueuedWriter.php create mode 100644 overrides/symfony/process/PhpExecutableFinder.php create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 phpunit.xml create mode 100644 presets.js create mode 100644 public/.htaccess create mode 100644 public/akaunting-js/generalAction.js create mode 100644 public/akaunting-js/hotkeys.js create mode 100644 public/akaunting-js/popper.js create mode 100644 public/akaunting-js/swiper-bundle.min.js create mode 100644 public/css/app.css create mode 100644 public/css/custom_loading.css create mode 100644 public/css/element.css create mode 100644 public/css/fonts/NotoSansArabic.ttf create mode 100644 public/css/fonts/element-icons.ttf create mode 100644 public/css/fonts/element-icons.woff create mode 100644 public/css/fonts/firefly_sung_normal.ttf create mode 100644 public/css/fonts/material-icons/Outlined/MaterialIconsOutlined-Regular.eot create mode 100644 public/css/fonts/material-icons/Outlined/MaterialIconsOutlined-Regular.otf create mode 100644 public/css/fonts/material-icons/Outlined/MaterialIconsOutlined-Regular.ttf create mode 100644 public/css/fonts/material-icons/Outlined/MaterialIconsOutlined-Regular.woff create mode 100644 public/css/fonts/material-icons/Outlined/MaterialIconsOutlined-Regular.woff2 create mode 100644 public/css/fonts/material-icons/Regular/MaterialIcons-Regular.eot create mode 100644 public/css/fonts/material-icons/Regular/MaterialIcons-Regular.otf create mode 100644 public/css/fonts/material-icons/Regular/MaterialIcons-Regular.ttf create mode 100644 public/css/fonts/material-icons/Regular/MaterialIcons-Regular.woff create mode 100644 public/css/fonts/material-icons/Regular/MaterialIcons-Regular.woff2 create mode 100644 public/css/fonts/material-icons/Rounded/MaterialIconsRound-Regular.eot create mode 100644 public/css/fonts/material-icons/Rounded/MaterialIconsRound-Regular.otf create mode 100644 public/css/fonts/material-icons/Rounded/MaterialIconsRound-Regular.ttf create mode 100644 public/css/fonts/material-icons/Rounded/MaterialIconsRound-Regular.woff create mode 100644 public/css/fonts/material-icons/Rounded/MaterialIconsRound-Regular.woff2 create mode 100644 public/css/fonts/material-icons/Sharp/MaterialIconsSharp-Regular.eot create mode 100644 public/css/fonts/material-icons/Sharp/MaterialIconsSharp-Regular.otf create mode 100644 public/css/fonts/material-icons/Sharp/MaterialIconsSharp-Regular.ttf create mode 100644 public/css/fonts/material-icons/Sharp/MaterialIconsSharp-Regular.woff create mode 100644 public/css/fonts/material-icons/Sharp/MaterialIconsSharp-Regular.woff2 create mode 100644 public/css/fonts/material-icons/style.css create mode 100644 public/css/print.css create mode 100644 public/css/third_party/dropzone_custom.css create mode 100644 public/css/third_party/swiper-bundle.min.css create mode 100644 public/css/third_party/vue-html-editor.css create mode 100644 public/css/third_party/vue-html-editor_custom.css create mode 100644 public/files/import/bills.xlsx create mode 100644 public/files/import/categories.xlsx create mode 100644 public/files/import/customers.xlsx create mode 100644 public/files/import/invoices.xlsx create mode 100644 public/files/import/items.xlsx create mode 100644 public/files/import/recurring-bills.xlsx create mode 100644 public/files/import/recurring-invoices.xlsx create mode 100644 public/files/import/recurring-transactions.xlsx create mode 100644 public/files/import/taxes.xlsx create mode 100644 public/files/import/transactions.xlsx create mode 100644 public/files/import/transfers.xlsx create mode 100644 public/files/import/vendors.xlsx create mode 100644 public/fonts/MaterialIcons-Regular.eot create mode 100644 public/fonts/MaterialIcons-Regular.ttf create mode 100644 public/fonts/MaterialIcons-Regular.woff create mode 100644 public/fonts/MaterialIcons-Regular.woff2 create mode 100644 public/img/akaunting-loading.gif create mode 100644 public/img/akaunting-logo-gold.png create mode 100644 public/img/akaunting-logo-green.svg create mode 100644 public/img/akaunting-logo-horizontal.svg create mode 100644 public/img/akaunting-logo-purple.svg create mode 100644 public/img/akaunting-logo-white.svg create mode 100644 public/img/akaunting-logo-wild-blue.png create mode 100644 public/img/auth/bullet_active.png create mode 100644 public/img/auth/bullet_inactive.png create mode 100644 public/img/auth/chart.png create mode 100644 public/img/auth/client.png create mode 100644 public/img/auth/folder.png create mode 100644 public/img/auth/layout.png create mode 100644 public/img/auth/login-bg.png create mode 100644 public/img/auth/logo.png create mode 100644 public/img/auth/small-logo.png create mode 100644 public/img/company.png create mode 100644 public/img/credit_card/amex.png create mode 100644 public/img/credit_card/chip.png create mode 100644 public/img/credit_card/dinersclub.png create mode 100644 public/img/credit_card/discover.png create mode 100644 public/img/credit_card/jcb.png create mode 100644 public/img/credit_card/mastercard.png create mode 100644 public/img/credit_card/troy.png create mode 100644 public/img/credit_card/unionpay.png create mode 100644 public/img/credit_card/visa.png create mode 100644 public/img/dashboard.png create mode 100644 public/img/empty_pages/bills.png create mode 100644 public/img/empty_pages/companies.png create mode 100644 public/img/empty_pages/contacs.png create mode 100644 public/img/empty_pages/customers.png create mode 100644 public/img/empty_pages/default.png create mode 100644 public/img/empty_pages/invoices.png create mode 100644 public/img/empty_pages/items.png create mode 100644 public/img/empty_pages/no-apps.png create mode 100644 public/img/empty_pages/payments.png create mode 100644 public/img/empty_pages/reconciliations.png create mode 100644 public/img/empty_pages/recurring_templates.png create mode 100644 public/img/empty_pages/revenues.png create mode 100644 public/img/empty_pages/subscriptions.png create mode 100644 public/img/empty_pages/taxes.png create mode 100644 public/img/empty_pages/tickets.png create mode 100644 public/img/empty_pages/transactions.png create mode 100644 public/img/empty_pages/transfers.png create mode 100644 public/img/empty_pages/users.png create mode 100644 public/img/empty_pages/vendors.png create mode 100644 public/img/errors/403.png create mode 100644 public/img/errors/404.png create mode 100644 public/img/errors/500.png create mode 100644 public/img/favicon.ico create mode 100644 public/img/icons/.gitkeep create mode 100644 public/img/import.png create mode 100644 public/img/invoice_templates/classic.png create mode 100644 public/img/invoice_templates/default.png create mode 100644 public/img/invoice_templates/modern.png create mode 100644 public/img/no_records/accounts_transactions.png create mode 100644 public/img/no_records/accounts_transfers.png create mode 100644 public/img/no_records/customers_invoices.png create mode 100644 public/img/no_records/customers_transactions.png create mode 100644 public/img/no_records/vendors_bills.png create mode 100644 public/img/no_records/vendors_transactions.png create mode 100644 public/img/pwa/icon-192x192-maskable.png create mode 100644 public/img/pwa/icon-192x192.png create mode 100644 public/img/pwa/icon-512x512-maskable.png create mode 100644 public/img/pwa/icon-512x512.png create mode 100644 public/img/pwa/screenshot-dashboard.png create mode 100644 public/img/pwa/screenshot-invoice.png create mode 100644 public/img/pwa/splash-1242x2208.png create mode 100644 public/img/pwa/splash-1242x2688.png create mode 100644 public/img/pwa/splash-1536x2048.png create mode 100644 public/img/pwa/splash-1668x2224.png create mode 100644 public/img/pwa/splash-1668x2388.png create mode 100644 public/img/pwa/splash-2048x2732.png create mode 100644 public/img/pwa/splash-640x1136.png create mode 100644 public/img/pwa/splash-750x1334.png create mode 100644 public/img/pwa/splash-828x1792.png create mode 100644 public/img/tailwind_icons/not-equal.svg create mode 100644 public/img/transfer_templates/default.png create mode 100644 public/img/transfer_templates/second.png create mode 100644 public/img/transfer_templates/third.png create mode 100644 public/img/user.svg create mode 100644 public/img/wizard-modules.png create mode 100644 public/img/wizard-rocket.gif create mode 100644 public/index.php create mode 100644 public/money.json create mode 100644 public/shortcuts-config.json create mode 100644 resources/assets/css/creditcard/style.scss create mode 100644 resources/assets/js/Install.vue create mode 100644 resources/assets/js/Wizard.vue create mode 100644 resources/assets/js/bootstrap.js create mode 100644 resources/assets/js/components/AkauntingColor.vue create mode 100644 resources/assets/js/components/AkauntingCompanyEdit.vue create mode 100644 resources/assets/js/components/AkauntingConnectTransactions.vue create mode 100644 resources/assets/js/components/AkauntingContactCard.vue create mode 100644 resources/assets/js/components/AkauntingCountdown.vue create mode 100644 resources/assets/js/components/AkauntingCurrencyConversion.vue create mode 100644 resources/assets/js/components/AkauntingDate.vue create mode 100644 resources/assets/js/components/AkauntingDocumentButton.vue create mode 100644 resources/assets/js/components/AkauntingDropzoneFileUpload.vue create mode 100644 resources/assets/js/components/AkauntingEditItemColumns.vue create mode 100644 resources/assets/js/components/AkauntingHtmlEditor.vue create mode 100644 resources/assets/js/components/AkauntingImport.vue create mode 100644 resources/assets/js/components/AkauntingItemButton.vue create mode 100644 resources/assets/js/components/AkauntingModal.vue create mode 100644 resources/assets/js/components/AkauntingModalAddNew.vue create mode 100644 resources/assets/js/components/AkauntingMoney.vue create mode 100644 resources/assets/js/components/AkauntingRadioGroup.vue create mode 100644 resources/assets/js/components/AkauntingRecurring.vue create mode 100644 resources/assets/js/components/AkauntingSearch.vue create mode 100644 resources/assets/js/components/AkauntingSelect.vue create mode 100644 resources/assets/js/components/AkauntingSelectRemote.vue create mode 100644 resources/assets/js/components/AkauntingSlider.vue create mode 100644 resources/assets/js/components/AkauntingSwitch.vue create mode 100644 resources/assets/js/components/AkauntingWidget.vue create mode 100644 resources/assets/js/components/Badge.vue create mode 100644 resources/assets/js/components/BaseAlert.vue create mode 100644 resources/assets/js/components/BaseButton.vue create mode 100644 resources/assets/js/components/Cards/Card.vue create mode 100644 resources/assets/js/components/Cards/StatsCard.vue create mode 100644 resources/assets/js/components/CreditCard/Card.vue create mode 100644 resources/assets/js/components/CreditCard/CardForm.vue create mode 100644 resources/assets/js/components/Inputs/BaseInput.vue create mode 100644 resources/assets/js/components/Modal.vue create mode 100644 resources/assets/js/components/NotificationPlugin/Notification.vue create mode 100644 resources/assets/js/components/NotificationPlugin/Notifications.vue create mode 100644 resources/assets/js/components/NotificationPlugin/index.js create mode 100644 resources/assets/js/components/SearchString/FilterMain.vue create mode 100644 resources/assets/js/components/SearchString/FilteredList.vue create mode 100644 resources/assets/js/components/SearchString/Filters.vue create mode 100644 resources/assets/js/components/SearchString/SearchString.vue create mode 100644 resources/assets/js/directives/click-ouside.js create mode 100644 resources/assets/js/exceptions/trackers/bugsnag.js create mode 100644 resources/assets/js/exceptions/trackers/sentry.js create mode 100644 resources/assets/js/install.js create mode 100644 resources/assets/js/mixins/global.js create mode 100644 resources/assets/js/mixins/wizardAction.js create mode 100644 resources/assets/js/plugins/bulk-action.js create mode 100644 resources/assets/js/plugins/currency.js create mode 100644 resources/assets/js/plugins/dashboard-plugin.js create mode 100644 resources/assets/js/plugins/error.js create mode 100644 resources/assets/js/plugins/form.js create mode 100644 resources/assets/js/plugins/functions.js create mode 100644 resources/assets/js/plugins/globalComponents.js create mode 100644 resources/assets/js/plugins/globalDirectives.js create mode 100644 resources/assets/js/plugins/money.js create mode 100644 resources/assets/js/plugins/nprogress-axios.js create mode 100644 resources/assets/js/polyfills.js create mode 100644 resources/assets/js/views/auth/common.js create mode 100644 resources/assets/js/views/auth/users.js create mode 100644 resources/assets/js/views/banking/accounts.js create mode 100644 resources/assets/js/views/banking/reconciliations.js create mode 100644 resources/assets/js/views/banking/transactions.js create mode 100644 resources/assets/js/views/banking/transfers.js create mode 100644 resources/assets/js/views/common/companies.js create mode 100644 resources/assets/js/views/common/contacts.js create mode 100644 resources/assets/js/views/common/dashboards.js create mode 100644 resources/assets/js/views/common/documents.js create mode 100644 resources/assets/js/views/common/imports.js create mode 100644 resources/assets/js/views/common/items.js create mode 100644 resources/assets/js/views/common/reports.js create mode 100644 resources/assets/js/views/install/Database.vue create mode 100644 resources/assets/js/views/install/Language.vue create mode 100644 resources/assets/js/views/install/Requirements.vue create mode 100644 resources/assets/js/views/install/Settings.vue create mode 100644 resources/assets/js/views/install/Steps.vue create mode 100644 resources/assets/js/views/install/update.js create mode 100644 resources/assets/js/views/modules/apps.js create mode 100644 resources/assets/js/views/portal/apps.js create mode 100644 resources/assets/js/views/settings/categories.js create mode 100644 resources/assets/js/views/settings/currencies.js create mode 100644 resources/assets/js/views/settings/settings.js create mode 100644 resources/assets/js/views/settings/taxes.js create mode 100644 resources/assets/js/views/wizard/Company.vue create mode 100644 resources/assets/js/views/wizard/Currencies.vue create mode 100644 resources/assets/js/views/wizard/Finish.vue create mode 100644 resources/assets/js/views/wizard/Steps.vue create mode 100644 resources/assets/js/wizard.js create mode 100644 resources/assets/sass/app.css create mode 100644 resources/lang/ar-SA/accounts.php create mode 100644 resources/lang/ar-SA/auth.php create mode 100644 resources/lang/ar-SA/bills.php create mode 100644 resources/lang/ar-SA/bulk_actions.php create mode 100644 resources/lang/ar-SA/categories.php create mode 100644 resources/lang/ar-SA/companies.php create mode 100644 resources/lang/ar-SA/countries.php create mode 100644 resources/lang/ar-SA/currencies.php create mode 100644 resources/lang/ar-SA/customers.php create mode 100644 resources/lang/ar-SA/dashboards.php create mode 100644 resources/lang/ar-SA/demo.php create mode 100644 resources/lang/ar-SA/documents.php create mode 100644 resources/lang/ar-SA/email_templates.php create mode 100644 resources/lang/ar-SA/errors.php create mode 100644 resources/lang/ar-SA/footer.php create mode 100644 resources/lang/ar-SA/general.php create mode 100644 resources/lang/ar-SA/header.php create mode 100644 resources/lang/ar-SA/import.php create mode 100644 resources/lang/ar-SA/install.php create mode 100644 resources/lang/ar-SA/invoices.php create mode 100644 resources/lang/ar-SA/items.php create mode 100644 resources/lang/ar-SA/maintenance.php create mode 100644 resources/lang/ar-SA/messages.php create mode 100644 resources/lang/ar-SA/modules.php create mode 100644 resources/lang/ar-SA/notifications.php create mode 100644 resources/lang/ar-SA/pagination.php create mode 100644 resources/lang/ar-SA/passwords.php create mode 100644 resources/lang/ar-SA/portal.php create mode 100644 resources/lang/ar-SA/reconciliations.php create mode 100644 resources/lang/ar-SA/recurring.php create mode 100644 resources/lang/ar-SA/reports.php create mode 100644 resources/lang/ar-SA/search_string.php create mode 100644 resources/lang/ar-SA/settings.php create mode 100644 resources/lang/ar-SA/taxes.php create mode 100644 resources/lang/ar-SA/transactions.php create mode 100644 resources/lang/ar-SA/transfers.php create mode 100644 resources/lang/ar-SA/updates.php create mode 100644 resources/lang/ar-SA/validation.php create mode 100644 resources/lang/ar-SA/vendors.php create mode 100644 resources/lang/ar-SA/widgets.php create mode 100644 resources/lang/az-AZ/accounts.php create mode 100644 resources/lang/az-AZ/auth.php create mode 100644 resources/lang/az-AZ/bills.php create mode 100644 resources/lang/az-AZ/bulk_actions.php create mode 100644 resources/lang/az-AZ/companies.php create mode 100644 resources/lang/az-AZ/countries.php create mode 100644 resources/lang/az-AZ/currencies.php create mode 100644 resources/lang/az-AZ/customers.php create mode 100644 resources/lang/az-AZ/dashboards.php create mode 100644 resources/lang/az-AZ/demo.php create mode 100644 resources/lang/az-AZ/documents.php create mode 100644 resources/lang/az-AZ/email_templates.php create mode 100644 resources/lang/az-AZ/errors.php create mode 100644 resources/lang/az-AZ/footer.php create mode 100644 resources/lang/az-AZ/general.php create mode 100644 resources/lang/az-AZ/header.php create mode 100644 resources/lang/az-AZ/import.php create mode 100644 resources/lang/az-AZ/install.php create mode 100644 resources/lang/az-AZ/invoices.php create mode 100644 resources/lang/az-AZ/items.php create mode 100644 resources/lang/az-AZ/maintenance.php create mode 100644 resources/lang/az-AZ/messages.php create mode 100644 resources/lang/az-AZ/modules.php create mode 100644 resources/lang/az-AZ/notifications.php create mode 100644 resources/lang/az-AZ/pagination.php create mode 100644 resources/lang/az-AZ/passwords.php create mode 100644 resources/lang/az-AZ/payments.php create mode 100644 resources/lang/az-AZ/reconciliations.php create mode 100644 resources/lang/az-AZ/recurring.php create mode 100644 resources/lang/az-AZ/reports.php create mode 100644 resources/lang/az-AZ/search_string.php create mode 100644 resources/lang/az-AZ/settings.php create mode 100644 resources/lang/az-AZ/taxes.php create mode 100644 resources/lang/az-AZ/transfers.php create mode 100644 resources/lang/az-AZ/updates.php create mode 100644 resources/lang/az-AZ/validation.php create mode 100644 resources/lang/az-AZ/widgets.php create mode 100644 resources/lang/bg-BG/accounts.php create mode 100644 resources/lang/bg-BG/auth.php create mode 100644 resources/lang/bg-BG/bills.php create mode 100644 resources/lang/bg-BG/bulk_actions.php create mode 100644 resources/lang/bg-BG/categories.php create mode 100644 resources/lang/bg-BG/companies.php create mode 100644 resources/lang/bg-BG/countries.php create mode 100644 resources/lang/bg-BG/currencies.php create mode 100644 resources/lang/bg-BG/customers.php create mode 100644 resources/lang/bg-BG/dashboards.php create mode 100644 resources/lang/bg-BG/demo.php create mode 100644 resources/lang/bg-BG/documents.php create mode 100644 resources/lang/bg-BG/email_templates.php create mode 100644 resources/lang/bg-BG/errors.php create mode 100644 resources/lang/bg-BG/footer.php create mode 100644 resources/lang/bg-BG/general.php create mode 100644 resources/lang/bg-BG/header.php create mode 100644 resources/lang/bg-BG/import.php create mode 100644 resources/lang/bg-BG/install.php create mode 100644 resources/lang/bg-BG/invoices.php create mode 100644 resources/lang/bg-BG/items.php create mode 100644 resources/lang/bg-BG/maintenance.php create mode 100644 resources/lang/bg-BG/messages.php create mode 100644 resources/lang/bg-BG/modules.php create mode 100644 resources/lang/bg-BG/notifications.php create mode 100644 resources/lang/bg-BG/pagination.php create mode 100644 resources/lang/bg-BG/passwords.php create mode 100644 resources/lang/bg-BG/portal.php create mode 100644 resources/lang/bg-BG/reconciliations.php create mode 100644 resources/lang/bg-BG/recurring.php create mode 100644 resources/lang/bg-BG/reports.php create mode 100644 resources/lang/bg-BG/search_string.php create mode 100644 resources/lang/bg-BG/settings.php create mode 100644 resources/lang/bg-BG/taxes.php create mode 100644 resources/lang/bg-BG/transactions.php create mode 100644 resources/lang/bg-BG/transfers.php create mode 100644 resources/lang/bg-BG/updates.php create mode 100644 resources/lang/bg-BG/validation.php create mode 100644 resources/lang/bg-BG/vendors.php create mode 100644 resources/lang/bg-BG/widgets.php create mode 100644 resources/lang/bn-BD/accounts.php create mode 100644 resources/lang/bn-BD/auth.php create mode 100644 resources/lang/bn-BD/bills.php create mode 100644 resources/lang/bn-BD/bulk_actions.php create mode 100644 resources/lang/bn-BD/companies.php create mode 100644 resources/lang/bn-BD/countries.php create mode 100644 resources/lang/bn-BD/currencies.php create mode 100644 resources/lang/bn-BD/customers.php create mode 100644 resources/lang/bn-BD/dashboards.php create mode 100644 resources/lang/bn-BD/demo.php create mode 100644 resources/lang/bn-BD/email_templates.php create mode 100644 resources/lang/bn-BD/errors.php create mode 100644 resources/lang/bn-BD/footer.php create mode 100644 resources/lang/bn-BD/general.php create mode 100644 resources/lang/bn-BD/header.php create mode 100644 resources/lang/bn-BD/import.php create mode 100644 resources/lang/bn-BD/install.php create mode 100644 resources/lang/bn-BD/invoices.php create mode 100644 resources/lang/bn-BD/items.php create mode 100644 resources/lang/bn-BD/maintenance.php create mode 100644 resources/lang/bn-BD/messages.php create mode 100644 resources/lang/bn-BD/modules.php create mode 100644 resources/lang/bn-BD/notifications.php create mode 100644 resources/lang/bn-BD/pagination.php create mode 100644 resources/lang/bn-BD/passwords.php create mode 100644 resources/lang/bn-BD/reconciliations.php create mode 100644 resources/lang/bn-BD/recurring.php create mode 100644 resources/lang/bn-BD/reports.php create mode 100644 resources/lang/bn-BD/settings.php create mode 100644 resources/lang/bn-BD/taxes.php create mode 100644 resources/lang/bn-BD/transfers.php create mode 100644 resources/lang/bn-BD/updates.php create mode 100644 resources/lang/bn-BD/validation.php create mode 100644 resources/lang/bn-BD/widgets.php create mode 100644 resources/lang/bs-BA/accounts.php create mode 100644 resources/lang/bs-BA/auth.php create mode 100644 resources/lang/bs-BA/bills.php create mode 100644 resources/lang/bs-BA/bulk_actions.php create mode 100644 resources/lang/bs-BA/categories.php create mode 100644 resources/lang/bs-BA/companies.php create mode 100644 resources/lang/bs-BA/countries.php create mode 100644 resources/lang/bs-BA/currencies.php create mode 100644 resources/lang/bs-BA/customers.php create mode 100644 resources/lang/bs-BA/dashboards.php create mode 100644 resources/lang/bs-BA/demo.php create mode 100644 resources/lang/bs-BA/documents.php create mode 100644 resources/lang/bs-BA/email_templates.php create mode 100644 resources/lang/bs-BA/errors.php create mode 100644 resources/lang/bs-BA/footer.php create mode 100644 resources/lang/bs-BA/general.php create mode 100644 resources/lang/bs-BA/header.php create mode 100644 resources/lang/bs-BA/import.php create mode 100644 resources/lang/bs-BA/install.php create mode 100644 resources/lang/bs-BA/invoices.php create mode 100644 resources/lang/bs-BA/items.php create mode 100644 resources/lang/bs-BA/maintenance.php create mode 100644 resources/lang/bs-BA/messages.php create mode 100644 resources/lang/bs-BA/modules.php create mode 100644 resources/lang/bs-BA/notifications.php create mode 100644 resources/lang/bs-BA/pagination.php create mode 100644 resources/lang/bs-BA/passwords.php create mode 100644 resources/lang/bs-BA/payments.php create mode 100644 resources/lang/bs-BA/portal.php create mode 100644 resources/lang/bs-BA/reconciliations.php create mode 100644 resources/lang/bs-BA/recurring.php create mode 100644 resources/lang/bs-BA/reports.php create mode 100644 resources/lang/bs-BA/revenues.php create mode 100644 resources/lang/bs-BA/search_string.php create mode 100644 resources/lang/bs-BA/settings.php create mode 100644 resources/lang/bs-BA/taxes.php create mode 100644 resources/lang/bs-BA/transactions.php create mode 100644 resources/lang/bs-BA/transfers.php create mode 100644 resources/lang/bs-BA/updates.php create mode 100644 resources/lang/bs-BA/validation.php create mode 100644 resources/lang/bs-BA/vendors.php create mode 100644 resources/lang/bs-BA/widgets.php create mode 100644 resources/lang/ca-ES/accounts.php create mode 100644 resources/lang/ca-ES/auth.php create mode 100644 resources/lang/ca-ES/bills.php create mode 100644 resources/lang/ca-ES/bulk_actions.php create mode 100644 resources/lang/ca-ES/categories.php create mode 100644 resources/lang/ca-ES/companies.php create mode 100644 resources/lang/ca-ES/countries.php create mode 100644 resources/lang/ca-ES/currencies.php create mode 100644 resources/lang/ca-ES/customers.php create mode 100644 resources/lang/ca-ES/dashboards.php create mode 100644 resources/lang/ca-ES/demo.php create mode 100644 resources/lang/ca-ES/documents.php create mode 100644 resources/lang/ca-ES/email_templates.php create mode 100644 resources/lang/ca-ES/errors.php create mode 100644 resources/lang/ca-ES/footer.php create mode 100644 resources/lang/ca-ES/general.php create mode 100644 resources/lang/ca-ES/header.php create mode 100644 resources/lang/ca-ES/import.php create mode 100644 resources/lang/ca-ES/install.php create mode 100644 resources/lang/ca-ES/invoices.php create mode 100644 resources/lang/ca-ES/items.php create mode 100644 resources/lang/ca-ES/maintenance.php create mode 100644 resources/lang/ca-ES/messages.php create mode 100644 resources/lang/ca-ES/modules.php create mode 100644 resources/lang/ca-ES/notifications.php create mode 100644 resources/lang/ca-ES/pagination.php create mode 100644 resources/lang/ca-ES/passwords.php create mode 100644 resources/lang/ca-ES/payments.php create mode 100644 resources/lang/ca-ES/portal.php create mode 100644 resources/lang/ca-ES/reconciliations.php create mode 100644 resources/lang/ca-ES/recurring.php create mode 100644 resources/lang/ca-ES/reports.php create mode 100644 resources/lang/ca-ES/revenues.php create mode 100644 resources/lang/ca-ES/search_string.php create mode 100644 resources/lang/ca-ES/settings.php create mode 100644 resources/lang/ca-ES/taxes.php create mode 100644 resources/lang/ca-ES/transactions.php create mode 100644 resources/lang/ca-ES/transfers.php create mode 100644 resources/lang/ca-ES/updates.php create mode 100644 resources/lang/ca-ES/validation.php create mode 100644 resources/lang/ca-ES/vendors.php create mode 100644 resources/lang/ca-ES/widgets.php create mode 100644 resources/lang/cs-CZ/accounts.php create mode 100644 resources/lang/cs-CZ/auth.php create mode 100644 resources/lang/cs-CZ/bills.php create mode 100644 resources/lang/cs-CZ/bulk_actions.php create mode 100644 resources/lang/cs-CZ/categories.php create mode 100644 resources/lang/cs-CZ/companies.php create mode 100644 resources/lang/cs-CZ/countries.php create mode 100644 resources/lang/cs-CZ/currencies.php create mode 100644 resources/lang/cs-CZ/customers.php create mode 100644 resources/lang/cs-CZ/dashboard.php create mode 100644 resources/lang/cs-CZ/dashboards.php create mode 100644 resources/lang/cs-CZ/demo.php create mode 100644 resources/lang/cs-CZ/documents.php create mode 100644 resources/lang/cs-CZ/email_templates.php create mode 100644 resources/lang/cs-CZ/errors.php create mode 100644 resources/lang/cs-CZ/footer.php create mode 100644 resources/lang/cs-CZ/general.php create mode 100644 resources/lang/cs-CZ/header.php create mode 100644 resources/lang/cs-CZ/import.php create mode 100644 resources/lang/cs-CZ/install.php create mode 100644 resources/lang/cs-CZ/invoices.php create mode 100644 resources/lang/cs-CZ/items.php create mode 100644 resources/lang/cs-CZ/maintenance.php create mode 100644 resources/lang/cs-CZ/messages.php create mode 100644 resources/lang/cs-CZ/modules.php create mode 100644 resources/lang/cs-CZ/notifications.php create mode 100644 resources/lang/cs-CZ/pagination.php create mode 100644 resources/lang/cs-CZ/passwords.php create mode 100644 resources/lang/cs-CZ/portal.php create mode 100644 resources/lang/cs-CZ/reconciliations.php create mode 100644 resources/lang/cs-CZ/recurring.php create mode 100644 resources/lang/cs-CZ/reports.php create mode 100644 resources/lang/cs-CZ/search_string.php create mode 100644 resources/lang/cs-CZ/settings.php create mode 100644 resources/lang/cs-CZ/taxes.php create mode 100644 resources/lang/cs-CZ/transactions.php create mode 100644 resources/lang/cs-CZ/transfers.php create mode 100644 resources/lang/cs-CZ/updates.php create mode 100644 resources/lang/cs-CZ/validation.php create mode 100644 resources/lang/cs-CZ/vendors.php create mode 100644 resources/lang/cs-CZ/widgets.php create mode 100644 resources/lang/da-DK/accounts.php create mode 100644 resources/lang/da-DK/auth.php create mode 100644 resources/lang/da-DK/bills.php create mode 100644 resources/lang/da-DK/bulk_actions.php create mode 100644 resources/lang/da-DK/categories.php create mode 100644 resources/lang/da-DK/companies.php create mode 100644 resources/lang/da-DK/countries.php create mode 100644 resources/lang/da-DK/currencies.php create mode 100644 resources/lang/da-DK/customers.php create mode 100644 resources/lang/da-DK/dashboards.php create mode 100644 resources/lang/da-DK/demo.php create mode 100644 resources/lang/da-DK/documents.php create mode 100644 resources/lang/da-DK/email_templates.php create mode 100644 resources/lang/da-DK/errors.php create mode 100644 resources/lang/da-DK/footer.php create mode 100644 resources/lang/da-DK/general.php create mode 100644 resources/lang/da-DK/header.php create mode 100644 resources/lang/da-DK/import.php create mode 100644 resources/lang/da-DK/install.php create mode 100644 resources/lang/da-DK/invoices.php create mode 100644 resources/lang/da-DK/items.php create mode 100644 resources/lang/da-DK/maintenance.php create mode 100644 resources/lang/da-DK/messages.php create mode 100644 resources/lang/da-DK/modules.php create mode 100644 resources/lang/da-DK/notifications.php create mode 100644 resources/lang/da-DK/pagination.php create mode 100644 resources/lang/da-DK/passwords.php create mode 100644 resources/lang/da-DK/payments.php create mode 100644 resources/lang/da-DK/portal.php create mode 100644 resources/lang/da-DK/reconciliations.php create mode 100644 resources/lang/da-DK/recurring.php create mode 100644 resources/lang/da-DK/reports.php create mode 100644 resources/lang/da-DK/revenues.php create mode 100644 resources/lang/da-DK/search_string.php create mode 100644 resources/lang/da-DK/settings.php create mode 100644 resources/lang/da-DK/taxes.php create mode 100644 resources/lang/da-DK/transfers.php create mode 100644 resources/lang/da-DK/updates.php create mode 100644 resources/lang/da-DK/validation.php create mode 100644 resources/lang/da-DK/widgets.php create mode 100644 resources/lang/de-DE/accounts.php create mode 100644 resources/lang/de-DE/auth.php create mode 100644 resources/lang/de-DE/bills.php create mode 100644 resources/lang/de-DE/bulk_actions.php create mode 100644 resources/lang/de-DE/categories.php create mode 100644 resources/lang/de-DE/companies.php create mode 100644 resources/lang/de-DE/countries.php create mode 100644 resources/lang/de-DE/currencies.php create mode 100644 resources/lang/de-DE/customers.php create mode 100644 resources/lang/de-DE/dashboards.php create mode 100644 resources/lang/de-DE/demo.php create mode 100644 resources/lang/de-DE/documents.php create mode 100644 resources/lang/de-DE/email_templates.php create mode 100644 resources/lang/de-DE/errors.php create mode 100644 resources/lang/de-DE/footer.php create mode 100644 resources/lang/de-DE/general.php create mode 100644 resources/lang/de-DE/header.php create mode 100644 resources/lang/de-DE/import.php create mode 100644 resources/lang/de-DE/install.php create mode 100644 resources/lang/de-DE/invoices.php create mode 100644 resources/lang/de-DE/items.php create mode 100644 resources/lang/de-DE/maintenance.php create mode 100644 resources/lang/de-DE/messages.php create mode 100644 resources/lang/de-DE/modules.php create mode 100644 resources/lang/de-DE/notifications.php create mode 100644 resources/lang/de-DE/pagination.php create mode 100644 resources/lang/de-DE/passwords.php create mode 100644 resources/lang/de-DE/portal.php create mode 100644 resources/lang/de-DE/reconciliations.php create mode 100644 resources/lang/de-DE/recurring.php create mode 100644 resources/lang/de-DE/reports.php create mode 100644 resources/lang/de-DE/search_string.php create mode 100644 resources/lang/de-DE/settings.php create mode 100644 resources/lang/de-DE/taxes.php create mode 100644 resources/lang/de-DE/transactions.php create mode 100644 resources/lang/de-DE/transfers.php create mode 100644 resources/lang/de-DE/updates.php create mode 100644 resources/lang/de-DE/validation.php create mode 100644 resources/lang/de-DE/vendors.php create mode 100644 resources/lang/de-DE/widgets.php create mode 100644 resources/lang/el-GR/accounts.php create mode 100644 resources/lang/el-GR/auth.php create mode 100644 resources/lang/el-GR/bills.php create mode 100644 resources/lang/el-GR/bulk_actions.php create mode 100644 resources/lang/el-GR/categories.php create mode 100644 resources/lang/el-GR/companies.php create mode 100644 resources/lang/el-GR/countries.php create mode 100644 resources/lang/el-GR/currencies.php create mode 100644 resources/lang/el-GR/customers.php create mode 100644 resources/lang/el-GR/dashboards.php create mode 100644 resources/lang/el-GR/demo.php create mode 100644 resources/lang/el-GR/documents.php create mode 100644 resources/lang/el-GR/email_templates.php create mode 100644 resources/lang/el-GR/errors.php create mode 100644 resources/lang/el-GR/footer.php create mode 100644 resources/lang/el-GR/general.php create mode 100644 resources/lang/el-GR/header.php create mode 100644 resources/lang/el-GR/import.php create mode 100644 resources/lang/el-GR/install.php create mode 100644 resources/lang/el-GR/invoices.php create mode 100644 resources/lang/el-GR/items.php create mode 100644 resources/lang/el-GR/maintenance.php create mode 100644 resources/lang/el-GR/messages.php create mode 100644 resources/lang/el-GR/modules.php create mode 100644 resources/lang/el-GR/notifications.php create mode 100644 resources/lang/el-GR/pagination.php create mode 100644 resources/lang/el-GR/passwords.php create mode 100644 resources/lang/el-GR/portal.php create mode 100644 resources/lang/el-GR/reconciliations.php create mode 100644 resources/lang/el-GR/recurring.php create mode 100644 resources/lang/el-GR/reports.php create mode 100644 resources/lang/el-GR/search_string.php create mode 100644 resources/lang/el-GR/settings.php create mode 100644 resources/lang/el-GR/taxes.php create mode 100644 resources/lang/el-GR/transactions.php create mode 100644 resources/lang/el-GR/transfers.php create mode 100644 resources/lang/el-GR/updates.php create mode 100644 resources/lang/el-GR/validation.php create mode 100644 resources/lang/el-GR/vendors.php create mode 100644 resources/lang/el-GR/widgets.php create mode 100644 resources/lang/en-AU/accounts.php create mode 100644 resources/lang/en-AU/auth.php create mode 100644 resources/lang/en-AU/bills.php create mode 100644 resources/lang/en-AU/bulk_actions.php create mode 100644 resources/lang/en-AU/categories.php create mode 100644 resources/lang/en-AU/companies.php create mode 100644 resources/lang/en-AU/countries.php create mode 100644 resources/lang/en-AU/currencies.php create mode 100644 resources/lang/en-AU/customers.php create mode 100644 resources/lang/en-AU/dashboards.php create mode 100644 resources/lang/en-AU/demo.php create mode 100644 resources/lang/en-AU/documents.php create mode 100644 resources/lang/en-AU/email_templates.php create mode 100644 resources/lang/en-AU/errors.php create mode 100644 resources/lang/en-AU/footer.php create mode 100644 resources/lang/en-AU/general.php create mode 100644 resources/lang/en-AU/header.php create mode 100644 resources/lang/en-AU/import.php create mode 100644 resources/lang/en-AU/install.php create mode 100644 resources/lang/en-AU/invoices.php create mode 100644 resources/lang/en-AU/items.php create mode 100644 resources/lang/en-AU/maintenance.php create mode 100644 resources/lang/en-AU/messages.php create mode 100644 resources/lang/en-AU/modules.php create mode 100644 resources/lang/en-AU/notifications.php create mode 100644 resources/lang/en-AU/pagination.php create mode 100644 resources/lang/en-AU/passwords.php create mode 100644 resources/lang/en-AU/portal.php create mode 100644 resources/lang/en-AU/reconciliations.php create mode 100644 resources/lang/en-AU/recurring.php create mode 100644 resources/lang/en-AU/reports.php create mode 100644 resources/lang/en-AU/search_string.php create mode 100644 resources/lang/en-AU/settings.php create mode 100644 resources/lang/en-AU/taxes.php create mode 100644 resources/lang/en-AU/transactions.php create mode 100644 resources/lang/en-AU/transfers.php create mode 100644 resources/lang/en-AU/updates.php create mode 100644 resources/lang/en-AU/validation.php create mode 100644 resources/lang/en-AU/vendors.php create mode 100644 resources/lang/en-AU/widgets.php create mode 100644 resources/lang/en-GB/accounts.php create mode 100644 resources/lang/en-GB/auth.php create mode 100644 resources/lang/en-GB/bills.php create mode 100644 resources/lang/en-GB/bulk_actions.php create mode 100644 resources/lang/en-GB/categories.php create mode 100644 resources/lang/en-GB/companies.php create mode 100644 resources/lang/en-GB/countries.php create mode 100644 resources/lang/en-GB/currencies.php create mode 100644 resources/lang/en-GB/customers.php create mode 100644 resources/lang/en-GB/dashboards.php create mode 100644 resources/lang/en-GB/demo.php create mode 100644 resources/lang/en-GB/documents.php create mode 100644 resources/lang/en-GB/email_templates.php create mode 100644 resources/lang/en-GB/errors.php create mode 100644 resources/lang/en-GB/footer.php create mode 100644 resources/lang/en-GB/general.php create mode 100644 resources/lang/en-GB/header.php create mode 100644 resources/lang/en-GB/import.php create mode 100644 resources/lang/en-GB/install.php create mode 100644 resources/lang/en-GB/invoices.php create mode 100644 resources/lang/en-GB/items.php create mode 100644 resources/lang/en-GB/maintenance.php create mode 100644 resources/lang/en-GB/messages.php create mode 100644 resources/lang/en-GB/modules.php create mode 100644 resources/lang/en-GB/notifications.php create mode 100644 resources/lang/en-GB/pagination.php create mode 100644 resources/lang/en-GB/passwords.php create mode 100644 resources/lang/en-GB/portal.php create mode 100644 resources/lang/en-GB/reconciliations.php create mode 100644 resources/lang/en-GB/recurring.php create mode 100644 resources/lang/en-GB/reports.php create mode 100644 resources/lang/en-GB/search_string.php create mode 100644 resources/lang/en-GB/settings.php create mode 100644 resources/lang/en-GB/taxes.php create mode 100644 resources/lang/en-GB/transactions.php create mode 100644 resources/lang/en-GB/transfers.php create mode 100644 resources/lang/en-GB/updates.php create mode 100644 resources/lang/en-GB/validation.php create mode 100644 resources/lang/en-GB/vendors.php create mode 100644 resources/lang/en-GB/widgets.php create mode 100644 resources/lang/en-US/accounts.php create mode 100644 resources/lang/en-US/auth.php create mode 100644 resources/lang/en-US/bills.php create mode 100644 resources/lang/en-US/bulk_actions.php create mode 100644 resources/lang/en-US/categories.php create mode 100644 resources/lang/en-US/companies.php create mode 100644 resources/lang/en-US/countries.php create mode 100644 resources/lang/en-US/currencies.php create mode 100644 resources/lang/en-US/customers.php create mode 100644 resources/lang/en-US/dashboards.php create mode 100644 resources/lang/en-US/demo.php create mode 100644 resources/lang/en-US/documents.php create mode 100644 resources/lang/en-US/email_templates.php create mode 100644 resources/lang/en-US/errors.php create mode 100644 resources/lang/en-US/footer.php create mode 100644 resources/lang/en-US/general.php create mode 100644 resources/lang/en-US/header.php create mode 100644 resources/lang/en-US/import.php create mode 100644 resources/lang/en-US/install.php create mode 100644 resources/lang/en-US/invoices.php create mode 100644 resources/lang/en-US/items.php create mode 100644 resources/lang/en-US/maintenance.php create mode 100644 resources/lang/en-US/messages.php create mode 100644 resources/lang/en-US/modules.php create mode 100644 resources/lang/en-US/notifications.php create mode 100644 resources/lang/en-US/pagination.php create mode 100644 resources/lang/en-US/passwords.php create mode 100644 resources/lang/en-US/portal.php create mode 100644 resources/lang/en-US/reconciliations.php create mode 100644 resources/lang/en-US/recurring.php create mode 100644 resources/lang/en-US/reports.php create mode 100644 resources/lang/en-US/search_string.php create mode 100644 resources/lang/en-US/settings.php create mode 100644 resources/lang/en-US/taxes.php create mode 100644 resources/lang/en-US/transactions.php create mode 100644 resources/lang/en-US/transfers.php create mode 100644 resources/lang/en-US/updates.php create mode 100644 resources/lang/en-US/validation.php create mode 100644 resources/lang/en-US/vendors.php create mode 100644 resources/lang/en-US/widgets.php create mode 100644 resources/lang/es-AR/accounts.php create mode 100644 resources/lang/es-AR/auth.php create mode 100644 resources/lang/es-AR/bills.php create mode 100644 resources/lang/es-AR/bulk_actions.php create mode 100644 resources/lang/es-AR/categories.php create mode 100644 resources/lang/es-AR/companies.php create mode 100644 resources/lang/es-AR/countries.php create mode 100644 resources/lang/es-AR/currencies.php create mode 100644 resources/lang/es-AR/customers.php create mode 100644 resources/lang/es-AR/dashboards.php create mode 100644 resources/lang/es-AR/demo.php create mode 100644 resources/lang/es-AR/documents.php create mode 100644 resources/lang/es-AR/email_templates.php create mode 100644 resources/lang/es-AR/errors.php create mode 100644 resources/lang/es-AR/footer.php create mode 100644 resources/lang/es-AR/general.php create mode 100644 resources/lang/es-AR/header.php create mode 100644 resources/lang/es-AR/import.php create mode 100644 resources/lang/es-AR/install.php create mode 100644 resources/lang/es-AR/invoices.php create mode 100644 resources/lang/es-AR/items.php create mode 100644 resources/lang/es-AR/maintenance.php create mode 100644 resources/lang/es-AR/messages.php create mode 100644 resources/lang/es-AR/modules.php create mode 100644 resources/lang/es-AR/notifications.php create mode 100644 resources/lang/es-AR/pagination.php create mode 100644 resources/lang/es-AR/passwords.php create mode 100644 resources/lang/es-AR/reconciliations.php create mode 100644 resources/lang/es-AR/recurring.php create mode 100644 resources/lang/es-AR/reports.php create mode 100644 resources/lang/es-AR/search_string.php create mode 100644 resources/lang/es-AR/settings.php create mode 100644 resources/lang/es-AR/taxes.php create mode 100644 resources/lang/es-AR/transfers.php create mode 100644 resources/lang/es-AR/updates.php create mode 100644 resources/lang/es-AR/validation.php create mode 100644 resources/lang/es-AR/widgets.php create mode 100644 resources/lang/es-CO/accounts.php create mode 100644 resources/lang/es-CO/auth.php create mode 100644 resources/lang/es-CO/bills.php create mode 100644 resources/lang/es-CO/bulk_actions.php create mode 100644 resources/lang/es-CO/categories.php create mode 100644 resources/lang/es-CO/companies.php create mode 100644 resources/lang/es-CO/countries.php create mode 100644 resources/lang/es-CO/currencies.php create mode 100644 resources/lang/es-CO/customers.php create mode 100644 resources/lang/es-CO/dashboards.php create mode 100644 resources/lang/es-CO/demo.php create mode 100644 resources/lang/es-CO/documents.php create mode 100644 resources/lang/es-CO/errors.php create mode 100644 resources/lang/es-CO/footer.php create mode 100644 resources/lang/es-CO/pagination.php create mode 100644 resources/lang/es-CO/passwords.php create mode 100644 resources/lang/es-CO/search_string.php create mode 100644 resources/lang/es-CO/taxes.php create mode 100644 resources/lang/es-CO/vendors.php create mode 100644 resources/lang/es-CO/widgets.php create mode 100644 resources/lang/es-ES/accounts.php create mode 100644 resources/lang/es-ES/auth.php create mode 100644 resources/lang/es-ES/bills.php create mode 100644 resources/lang/es-ES/bulk_actions.php create mode 100644 resources/lang/es-ES/categories.php create mode 100644 resources/lang/es-ES/companies.php create mode 100644 resources/lang/es-ES/countries.php create mode 100644 resources/lang/es-ES/currencies.php create mode 100644 resources/lang/es-ES/customers.php create mode 100644 resources/lang/es-ES/dashboards.php create mode 100644 resources/lang/es-ES/demo.php create mode 100644 resources/lang/es-ES/documents.php create mode 100644 resources/lang/es-ES/email_templates.php create mode 100644 resources/lang/es-ES/errors.php create mode 100644 resources/lang/es-ES/footer.php create mode 100644 resources/lang/es-ES/general.php create mode 100644 resources/lang/es-ES/header.php create mode 100644 resources/lang/es-ES/import.php create mode 100644 resources/lang/es-ES/install.php create mode 100644 resources/lang/es-ES/invoices.php create mode 100644 resources/lang/es-ES/items.php create mode 100644 resources/lang/es-ES/maintenance.php create mode 100644 resources/lang/es-ES/messages.php create mode 100644 resources/lang/es-ES/modules.php create mode 100644 resources/lang/es-ES/notifications.php create mode 100644 resources/lang/es-ES/pagination.php create mode 100644 resources/lang/es-ES/passwords.php create mode 100644 resources/lang/es-ES/portal.php create mode 100644 resources/lang/es-ES/reconciliations.php create mode 100644 resources/lang/es-ES/recurring.php create mode 100644 resources/lang/es-ES/reports.php create mode 100644 resources/lang/es-ES/search_string.php create mode 100644 resources/lang/es-ES/settings.php create mode 100644 resources/lang/es-ES/taxes.php create mode 100644 resources/lang/es-ES/transactions.php create mode 100644 resources/lang/es-ES/transfers.php create mode 100644 resources/lang/es-ES/updates.php create mode 100644 resources/lang/es-ES/validation.php create mode 100644 resources/lang/es-ES/vendors.php create mode 100644 resources/lang/es-ES/widgets.php create mode 100644 resources/lang/es-MX/accounts.php create mode 100644 resources/lang/es-MX/auth.php create mode 100644 resources/lang/es-MX/bills.php create mode 100644 resources/lang/es-MX/bulk_actions.php create mode 100644 resources/lang/es-MX/categories.php create mode 100644 resources/lang/es-MX/companies.php create mode 100644 resources/lang/es-MX/countries.php create mode 100644 resources/lang/es-MX/currencies.php create mode 100644 resources/lang/es-MX/customers.php create mode 100644 resources/lang/es-MX/dashboards.php create mode 100644 resources/lang/es-MX/demo.php create mode 100644 resources/lang/es-MX/documents.php create mode 100644 resources/lang/es-MX/email_templates.php create mode 100644 resources/lang/es-MX/errors.php create mode 100644 resources/lang/es-MX/footer.php create mode 100644 resources/lang/es-MX/general.php create mode 100644 resources/lang/es-MX/header.php create mode 100644 resources/lang/es-MX/import.php create mode 100644 resources/lang/es-MX/install.php create mode 100644 resources/lang/es-MX/invoices.php create mode 100644 resources/lang/es-MX/items.php create mode 100644 resources/lang/es-MX/maintenance.php create mode 100644 resources/lang/es-MX/messages.php create mode 100644 resources/lang/es-MX/modules.php create mode 100644 resources/lang/es-MX/notifications.php create mode 100644 resources/lang/es-MX/pagination.php create mode 100644 resources/lang/es-MX/passwords.php create mode 100644 resources/lang/es-MX/portal.php create mode 100644 resources/lang/es-MX/reconciliations.php create mode 100644 resources/lang/es-MX/recurring.php create mode 100644 resources/lang/es-MX/reports.php create mode 100644 resources/lang/es-MX/search_string.php create mode 100644 resources/lang/es-MX/settings.php create mode 100644 resources/lang/es-MX/taxes.php create mode 100644 resources/lang/es-MX/transactions.php create mode 100644 resources/lang/es-MX/transfers.php create mode 100644 resources/lang/es-MX/updates.php create mode 100644 resources/lang/es-MX/validation.php create mode 100644 resources/lang/es-MX/vendors.php create mode 100644 resources/lang/es-MX/widgets.php create mode 100644 resources/lang/et-EE/accounts.php create mode 100644 resources/lang/et-EE/auth.php create mode 100644 resources/lang/et-EE/bills.php create mode 100644 resources/lang/et-EE/bulk_actions.php create mode 100644 resources/lang/et-EE/companies.php create mode 100644 resources/lang/et-EE/countries.php create mode 100644 resources/lang/et-EE/currencies.php create mode 100644 resources/lang/et-EE/customers.php create mode 100644 resources/lang/et-EE/dashboards.php create mode 100644 resources/lang/et-EE/demo.php create mode 100644 resources/lang/et-EE/documents.php create mode 100644 resources/lang/et-EE/email_templates.php create mode 100644 resources/lang/et-EE/errors.php create mode 100644 resources/lang/et-EE/footer.php create mode 100644 resources/lang/et-EE/general.php create mode 100644 resources/lang/et-EE/header.php create mode 100644 resources/lang/et-EE/import.php create mode 100644 resources/lang/et-EE/install.php create mode 100644 resources/lang/et-EE/invoices.php create mode 100644 resources/lang/et-EE/items.php create mode 100644 resources/lang/et-EE/maintenance.php create mode 100644 resources/lang/et-EE/messages.php create mode 100644 resources/lang/et-EE/modules.php create mode 100644 resources/lang/et-EE/notifications.php create mode 100644 resources/lang/et-EE/pagination.php create mode 100644 resources/lang/et-EE/passwords.php create mode 100644 resources/lang/et-EE/reconciliations.php create mode 100644 resources/lang/et-EE/recurring.php create mode 100644 resources/lang/et-EE/reports.php create mode 100644 resources/lang/et-EE/search_string.php create mode 100644 resources/lang/et-EE/settings.php create mode 100644 resources/lang/et-EE/taxes.php create mode 100644 resources/lang/et-EE/transfers.php create mode 100644 resources/lang/et-EE/updates.php create mode 100644 resources/lang/et-EE/validation.php create mode 100644 resources/lang/et-EE/widgets.php create mode 100644 resources/lang/fa-IR/accounts.php create mode 100644 resources/lang/fa-IR/auth.php create mode 100644 resources/lang/fa-IR/bills.php create mode 100644 resources/lang/fa-IR/bulk_actions.php create mode 100644 resources/lang/fa-IR/categories.php create mode 100644 resources/lang/fa-IR/companies.php create mode 100644 resources/lang/fa-IR/countries.php create mode 100644 resources/lang/fa-IR/currencies.php create mode 100644 resources/lang/fa-IR/customers.php create mode 100644 resources/lang/fa-IR/dashboards.php create mode 100644 resources/lang/fa-IR/demo.php create mode 100644 resources/lang/fa-IR/email_templates.php create mode 100644 resources/lang/fa-IR/errors.php create mode 100644 resources/lang/fa-IR/footer.php create mode 100644 resources/lang/fa-IR/general.php create mode 100644 resources/lang/fa-IR/header.php create mode 100644 resources/lang/fa-IR/import.php create mode 100644 resources/lang/fa-IR/install.php create mode 100644 resources/lang/fa-IR/invoices.php create mode 100644 resources/lang/fa-IR/items.php create mode 100644 resources/lang/fa-IR/maintenance.php create mode 100644 resources/lang/fa-IR/messages.php create mode 100644 resources/lang/fa-IR/modules.php create mode 100644 resources/lang/fa-IR/notifications.php create mode 100644 resources/lang/fa-IR/pagination.php create mode 100644 resources/lang/fa-IR/passwords.php create mode 100644 resources/lang/fa-IR/payments.php create mode 100644 resources/lang/fa-IR/reconciliations.php create mode 100644 resources/lang/fa-IR/recurring.php create mode 100644 resources/lang/fa-IR/reports.php create mode 100644 resources/lang/fa-IR/revenues.php create mode 100644 resources/lang/fa-IR/settings.php create mode 100644 resources/lang/fa-IR/taxes.php create mode 100644 resources/lang/fa-IR/transfers.php create mode 100644 resources/lang/fa-IR/updates.php create mode 100644 resources/lang/fa-IR/validation.php create mode 100644 resources/lang/fa-IR/widgets.php create mode 100644 resources/lang/fi-FI/accounts.php create mode 100644 resources/lang/fi-FI/auth.php create mode 100644 resources/lang/fi-FI/bills.php create mode 100644 resources/lang/fi-FI/bulk_actions.php create mode 100644 resources/lang/fi-FI/categories.php create mode 100644 resources/lang/fi-FI/companies.php create mode 100644 resources/lang/fi-FI/countries.php create mode 100644 resources/lang/fi-FI/currencies.php create mode 100644 resources/lang/fi-FI/customers.php create mode 100644 resources/lang/fi-FI/dashboards.php create mode 100644 resources/lang/fi-FI/demo.php create mode 100644 resources/lang/fi-FI/documents.php create mode 100644 resources/lang/fi-FI/email_templates.php create mode 100644 resources/lang/fi-FI/errors.php create mode 100644 resources/lang/fi-FI/footer.php create mode 100644 resources/lang/fi-FI/general.php create mode 100644 resources/lang/fi-FI/header.php create mode 100644 resources/lang/fi-FI/import.php create mode 100644 resources/lang/fi-FI/install.php create mode 100644 resources/lang/fi-FI/invoices.php create mode 100644 resources/lang/fi-FI/items.php create mode 100644 resources/lang/fi-FI/maintenance.php create mode 100644 resources/lang/fi-FI/messages.php create mode 100644 resources/lang/fi-FI/modules.php create mode 100644 resources/lang/fi-FI/notifications.php create mode 100644 resources/lang/fi-FI/pagination.php create mode 100644 resources/lang/fi-FI/passwords.php create mode 100644 resources/lang/fi-FI/reconciliations.php create mode 100644 resources/lang/fi-FI/recurring.php create mode 100644 resources/lang/fi-FI/reports.php create mode 100644 resources/lang/fi-FI/search_string.php create mode 100644 resources/lang/fi-FI/settings.php create mode 100644 resources/lang/fi-FI/taxes.php create mode 100644 resources/lang/fi-FI/transfers.php create mode 100644 resources/lang/fi-FI/updates.php create mode 100644 resources/lang/fi-FI/validation.php create mode 100644 resources/lang/fi-FI/widgets.php create mode 100644 resources/lang/fr-FR/accounts.php create mode 100644 resources/lang/fr-FR/auth.php create mode 100644 resources/lang/fr-FR/bills.php create mode 100644 resources/lang/fr-FR/bulk_actions.php create mode 100644 resources/lang/fr-FR/categories.php create mode 100644 resources/lang/fr-FR/companies.php create mode 100644 resources/lang/fr-FR/countries.php create mode 100644 resources/lang/fr-FR/currencies.php create mode 100644 resources/lang/fr-FR/customers.php create mode 100644 resources/lang/fr-FR/dashboards.php create mode 100644 resources/lang/fr-FR/demo.php create mode 100644 resources/lang/fr-FR/documents.php create mode 100644 resources/lang/fr-FR/email_templates.php create mode 100644 resources/lang/fr-FR/errors.php create mode 100644 resources/lang/fr-FR/footer.php create mode 100644 resources/lang/fr-FR/general.php create mode 100644 resources/lang/fr-FR/header.php create mode 100644 resources/lang/fr-FR/import.php create mode 100644 resources/lang/fr-FR/install.php create mode 100644 resources/lang/fr-FR/invoices.php create mode 100644 resources/lang/fr-FR/items.php create mode 100644 resources/lang/fr-FR/maintenance.php create mode 100644 resources/lang/fr-FR/messages.php create mode 100644 resources/lang/fr-FR/modules.php create mode 100644 resources/lang/fr-FR/notifications.php create mode 100644 resources/lang/fr-FR/pagination.php create mode 100644 resources/lang/fr-FR/passwords.php create mode 100644 resources/lang/fr-FR/portal.php create mode 100644 resources/lang/fr-FR/reconciliations.php create mode 100644 resources/lang/fr-FR/recurring.php create mode 100644 resources/lang/fr-FR/reports.php create mode 100644 resources/lang/fr-FR/search_string.php create mode 100644 resources/lang/fr-FR/settings.php create mode 100644 resources/lang/fr-FR/taxes.php create mode 100644 resources/lang/fr-FR/transactions.php create mode 100644 resources/lang/fr-FR/transfers.php create mode 100644 resources/lang/fr-FR/updates.php create mode 100644 resources/lang/fr-FR/validation.php create mode 100644 resources/lang/fr-FR/vendors.php create mode 100644 resources/lang/fr-FR/widgets.php create mode 100644 resources/lang/he-IL/accounts.php create mode 100644 resources/lang/he-IL/auth.php create mode 100644 resources/lang/he-IL/bills.php create mode 100644 resources/lang/he-IL/categories.php create mode 100644 resources/lang/he-IL/companies.php create mode 100644 resources/lang/he-IL/countries.php create mode 100644 resources/lang/he-IL/currencies.php create mode 100644 resources/lang/he-IL/customers.php create mode 100644 resources/lang/he-IL/dashboard.php create mode 100644 resources/lang/he-IL/dashboards.php create mode 100644 resources/lang/he-IL/demo.php create mode 100644 resources/lang/he-IL/errors.php create mode 100644 resources/lang/he-IL/footer.php create mode 100644 resources/lang/he-IL/general.php create mode 100644 resources/lang/he-IL/header.php create mode 100644 resources/lang/he-IL/import.php create mode 100644 resources/lang/he-IL/install.php create mode 100644 resources/lang/he-IL/invoices.php create mode 100644 resources/lang/he-IL/items.php create mode 100644 resources/lang/he-IL/messages.php create mode 100644 resources/lang/he-IL/modules.php create mode 100644 resources/lang/he-IL/notifications.php create mode 100644 resources/lang/he-IL/pagination.php create mode 100644 resources/lang/he-IL/passwords.php create mode 100644 resources/lang/he-IL/reconciliations.php create mode 100644 resources/lang/he-IL/recurring.php create mode 100644 resources/lang/he-IL/reports.php create mode 100644 resources/lang/he-IL/settings.php create mode 100644 resources/lang/he-IL/taxes.php create mode 100644 resources/lang/he-IL/transfers.php create mode 100644 resources/lang/he-IL/updates.php create mode 100644 resources/lang/he-IL/validation.php create mode 100644 resources/lang/hi-IN/accounts.php create mode 100644 resources/lang/hi-IN/auth.php create mode 100644 resources/lang/hi-IN/bills.php create mode 100644 resources/lang/hi-IN/bulk_actions.php create mode 100644 resources/lang/hi-IN/categories.php create mode 100644 resources/lang/hi-IN/companies.php create mode 100644 resources/lang/hi-IN/countries.php create mode 100644 resources/lang/hi-IN/currencies.php create mode 100644 resources/lang/hi-IN/customers.php create mode 100644 resources/lang/hi-IN/dashboards.php create mode 100644 resources/lang/hi-IN/demo.php create mode 100644 resources/lang/hi-IN/documents.php create mode 100644 resources/lang/hi-IN/email_templates.php create mode 100644 resources/lang/hi-IN/errors.php create mode 100644 resources/lang/hi-IN/footer.php create mode 100644 resources/lang/hi-IN/general.php create mode 100644 resources/lang/hi-IN/header.php create mode 100644 resources/lang/hi-IN/import.php create mode 100644 resources/lang/hi-IN/install.php create mode 100644 resources/lang/hi-IN/invoices.php create mode 100644 resources/lang/hi-IN/items.php create mode 100644 resources/lang/hi-IN/maintenance.php create mode 100644 resources/lang/hi-IN/messages.php create mode 100644 resources/lang/hi-IN/modules.php create mode 100644 resources/lang/hi-IN/notifications.php create mode 100644 resources/lang/hi-IN/pagination.php create mode 100644 resources/lang/hi-IN/passwords.php create mode 100644 resources/lang/hi-IN/portal.php create mode 100644 resources/lang/hi-IN/reconciliations.php create mode 100644 resources/lang/hi-IN/recurring.php create mode 100644 resources/lang/hi-IN/reports.php create mode 100644 resources/lang/hi-IN/search_string.php create mode 100644 resources/lang/hi-IN/settings.php create mode 100644 resources/lang/hi-IN/taxes.php create mode 100644 resources/lang/hi-IN/transactions.php create mode 100644 resources/lang/hi-IN/transfers.php create mode 100644 resources/lang/hi-IN/updates.php create mode 100644 resources/lang/hi-IN/validation.php create mode 100644 resources/lang/hi-IN/vendors.php create mode 100644 resources/lang/hi-IN/widgets.php create mode 100644 resources/lang/hr-HR/accounts.php create mode 100644 resources/lang/hr-HR/auth.php create mode 100644 resources/lang/hr-HR/bills.php create mode 100644 resources/lang/hr-HR/bulk_actions.php create mode 100644 resources/lang/hr-HR/companies.php create mode 100644 resources/lang/hr-HR/countries.php create mode 100644 resources/lang/hr-HR/currencies.php create mode 100644 resources/lang/hr-HR/customers.php create mode 100644 resources/lang/hr-HR/dashboards.php create mode 100644 resources/lang/hr-HR/demo.php create mode 100644 resources/lang/hr-HR/email_templates.php create mode 100644 resources/lang/hr-HR/errors.php create mode 100644 resources/lang/hr-HR/footer.php create mode 100644 resources/lang/hr-HR/general.php create mode 100644 resources/lang/hr-HR/header.php create mode 100644 resources/lang/hr-HR/import.php create mode 100644 resources/lang/hr-HR/install.php create mode 100644 resources/lang/hr-HR/invoices.php create mode 100644 resources/lang/hr-HR/items.php create mode 100644 resources/lang/hr-HR/maintenance.php create mode 100644 resources/lang/hr-HR/messages.php create mode 100644 resources/lang/hr-HR/modules.php create mode 100644 resources/lang/hr-HR/notifications.php create mode 100644 resources/lang/hr-HR/pagination.php create mode 100644 resources/lang/hr-HR/passwords.php create mode 100644 resources/lang/hr-HR/reconciliations.php create mode 100644 resources/lang/hr-HR/recurring.php create mode 100644 resources/lang/hr-HR/reports.php create mode 100644 resources/lang/hr-HR/settings.php create mode 100644 resources/lang/hr-HR/taxes.php create mode 100644 resources/lang/hr-HR/transfers.php create mode 100644 resources/lang/hr-HR/updates.php create mode 100644 resources/lang/hr-HR/validation.php create mode 100644 resources/lang/hr-HR/widgets.php create mode 100644 resources/lang/hu-HU/accounts.php create mode 100644 resources/lang/hu-HU/auth.php create mode 100644 resources/lang/hu-HU/bills.php create mode 100644 resources/lang/hu-HU/bulk_actions.php create mode 100644 resources/lang/hu-HU/companies.php create mode 100644 resources/lang/hu-HU/countries.php create mode 100644 resources/lang/hu-HU/currencies.php create mode 100644 resources/lang/hu-HU/customers.php create mode 100644 resources/lang/hu-HU/dashboards.php create mode 100644 resources/lang/hu-HU/demo.php create mode 100644 resources/lang/hu-HU/email_templates.php create mode 100644 resources/lang/hu-HU/errors.php create mode 100644 resources/lang/hu-HU/footer.php create mode 100644 resources/lang/hu-HU/header.php create mode 100644 resources/lang/hu-HU/import.php create mode 100644 resources/lang/hu-HU/install.php create mode 100644 resources/lang/hu-HU/invoices.php create mode 100644 resources/lang/hu-HU/items.php create mode 100644 resources/lang/hu-HU/maintenance.php create mode 100644 resources/lang/hu-HU/messages.php create mode 100644 resources/lang/hu-HU/modules.php create mode 100644 resources/lang/hu-HU/notifications.php create mode 100644 resources/lang/hu-HU/pagination.php create mode 100644 resources/lang/hu-HU/passwords.php create mode 100644 resources/lang/hu-HU/reconciliations.php create mode 100644 resources/lang/hu-HU/recurring.php create mode 100644 resources/lang/hu-HU/reports.php create mode 100644 resources/lang/hu-HU/taxes.php create mode 100644 resources/lang/hu-HU/transfers.php create mode 100644 resources/lang/hu-HU/updates.php create mode 100644 resources/lang/hu-HU/validation.php create mode 100644 resources/lang/hu-HU/vendors.php create mode 100644 resources/lang/hu-HU/widgets.php create mode 100644 resources/lang/id-ID/accounts.php create mode 100644 resources/lang/id-ID/auth.php create mode 100644 resources/lang/id-ID/bills.php create mode 100644 resources/lang/id-ID/bulk_actions.php create mode 100644 resources/lang/id-ID/categories.php create mode 100644 resources/lang/id-ID/companies.php create mode 100644 resources/lang/id-ID/countries.php create mode 100644 resources/lang/id-ID/currencies.php create mode 100644 resources/lang/id-ID/customers.php create mode 100644 resources/lang/id-ID/dashboards.php create mode 100644 resources/lang/id-ID/demo.php create mode 100644 resources/lang/id-ID/documents.php create mode 100644 resources/lang/id-ID/email_templates.php create mode 100644 resources/lang/id-ID/errors.php create mode 100644 resources/lang/id-ID/footer.php create mode 100644 resources/lang/id-ID/general.php create mode 100644 resources/lang/id-ID/header.php create mode 100644 resources/lang/id-ID/import.php create mode 100644 resources/lang/id-ID/install.php create mode 100644 resources/lang/id-ID/invoices.php create mode 100644 resources/lang/id-ID/items.php create mode 100644 resources/lang/id-ID/maintenance.php create mode 100644 resources/lang/id-ID/messages.php create mode 100644 resources/lang/id-ID/modules.php create mode 100644 resources/lang/id-ID/notifications.php create mode 100644 resources/lang/id-ID/pagination.php create mode 100644 resources/lang/id-ID/passwords.php create mode 100644 resources/lang/id-ID/portal.php create mode 100644 resources/lang/id-ID/reconciliations.php create mode 100644 resources/lang/id-ID/recurring.php create mode 100644 resources/lang/id-ID/reports.php create mode 100644 resources/lang/id-ID/search_string.php create mode 100644 resources/lang/id-ID/settings.php create mode 100644 resources/lang/id-ID/taxes.php create mode 100644 resources/lang/id-ID/transactions.php create mode 100644 resources/lang/id-ID/transfers.php create mode 100644 resources/lang/id-ID/updates.php create mode 100644 resources/lang/id-ID/validation.php create mode 100644 resources/lang/id-ID/vendors.php create mode 100644 resources/lang/id-ID/widgets.php create mode 100644 resources/lang/is-IS/accounts.php create mode 100644 resources/lang/is-IS/auth.php create mode 100644 resources/lang/is-IS/bills.php create mode 100644 resources/lang/is-IS/bulk_actions.php create mode 100644 resources/lang/is-IS/companies.php create mode 100644 resources/lang/is-IS/countries.php create mode 100644 resources/lang/is-IS/currencies.php create mode 100644 resources/lang/is-IS/customers.php create mode 100644 resources/lang/is-IS/dashboards.php create mode 100644 resources/lang/is-IS/demo.php create mode 100644 resources/lang/is-IS/email_templates.php create mode 100644 resources/lang/is-IS/errors.php create mode 100644 resources/lang/is-IS/footer.php create mode 100644 resources/lang/is-IS/general.php create mode 100644 resources/lang/is-IS/header.php create mode 100644 resources/lang/is-IS/import.php create mode 100644 resources/lang/is-IS/install.php create mode 100644 resources/lang/is-IS/invoices.php create mode 100644 resources/lang/is-IS/items.php create mode 100644 resources/lang/is-IS/maintenance.php create mode 100644 resources/lang/is-IS/messages.php create mode 100644 resources/lang/is-IS/modules.php create mode 100644 resources/lang/is-IS/notifications.php create mode 100644 resources/lang/is-IS/pagination.php create mode 100644 resources/lang/is-IS/passwords.php create mode 100644 resources/lang/is-IS/reconciliations.php create mode 100644 resources/lang/is-IS/recurring.php create mode 100644 resources/lang/is-IS/reports.php create mode 100644 resources/lang/is-IS/settings.php create mode 100644 resources/lang/is-IS/taxes.php create mode 100644 resources/lang/is-IS/transfers.php create mode 100644 resources/lang/is-IS/updates.php create mode 100644 resources/lang/is-IS/validation.php create mode 100644 resources/lang/is-IS/widgets.php create mode 100644 resources/lang/it-IT/accounts.php create mode 100644 resources/lang/it-IT/auth.php create mode 100644 resources/lang/it-IT/bills.php create mode 100644 resources/lang/it-IT/bulk_actions.php create mode 100644 resources/lang/it-IT/categories.php create mode 100644 resources/lang/it-IT/companies.php create mode 100644 resources/lang/it-IT/countries.php create mode 100644 resources/lang/it-IT/currencies.php create mode 100644 resources/lang/it-IT/customers.php create mode 100644 resources/lang/it-IT/dashboards.php create mode 100644 resources/lang/it-IT/demo.php create mode 100644 resources/lang/it-IT/documents.php create mode 100644 resources/lang/it-IT/email_templates.php create mode 100644 resources/lang/it-IT/errors.php create mode 100644 resources/lang/it-IT/footer.php create mode 100644 resources/lang/it-IT/general.php create mode 100644 resources/lang/it-IT/header.php create mode 100644 resources/lang/it-IT/import.php create mode 100644 resources/lang/it-IT/install.php create mode 100644 resources/lang/it-IT/invoices.php create mode 100644 resources/lang/it-IT/items.php create mode 100644 resources/lang/it-IT/maintenance.php create mode 100644 resources/lang/it-IT/messages.php create mode 100644 resources/lang/it-IT/modules.php create mode 100644 resources/lang/it-IT/notifications.php create mode 100644 resources/lang/it-IT/pagination.php create mode 100644 resources/lang/it-IT/passwords.php create mode 100644 resources/lang/it-IT/portal.php create mode 100644 resources/lang/it-IT/reconciliations.php create mode 100644 resources/lang/it-IT/recurring.php create mode 100644 resources/lang/it-IT/reports.php create mode 100644 resources/lang/it-IT/search_string.php create mode 100644 resources/lang/it-IT/settings.php create mode 100644 resources/lang/it-IT/taxes.php create mode 100644 resources/lang/it-IT/transactions.php create mode 100644 resources/lang/it-IT/transfers.php create mode 100644 resources/lang/it-IT/updates.php create mode 100644 resources/lang/it-IT/validation.php create mode 100644 resources/lang/it-IT/vendors.php create mode 100644 resources/lang/it-IT/widgets.php create mode 100644 resources/lang/ja-JP/accounts.php create mode 100644 resources/lang/ja-JP/auth.php create mode 100644 resources/lang/ja-JP/bills.php create mode 100644 resources/lang/ja-JP/bulk_actions.php create mode 100644 resources/lang/ja-JP/companies.php create mode 100644 resources/lang/ja-JP/countries.php create mode 100644 resources/lang/ja-JP/currencies.php create mode 100644 resources/lang/ja-JP/customers.php create mode 100644 resources/lang/ja-JP/dashboards.php create mode 100644 resources/lang/ja-JP/demo.php create mode 100644 resources/lang/ja-JP/email_templates.php create mode 100644 resources/lang/ja-JP/errors.php create mode 100644 resources/lang/ja-JP/footer.php create mode 100644 resources/lang/ja-JP/general.php create mode 100644 resources/lang/ja-JP/header.php create mode 100644 resources/lang/ja-JP/import.php create mode 100644 resources/lang/ja-JP/install.php create mode 100644 resources/lang/ja-JP/invoices.php create mode 100644 resources/lang/ja-JP/items.php create mode 100644 resources/lang/ja-JP/maintenance.php create mode 100644 resources/lang/ja-JP/messages.php create mode 100644 resources/lang/ja-JP/modules.php create mode 100644 resources/lang/ja-JP/notifications.php create mode 100644 resources/lang/ja-JP/pagination.php create mode 100644 resources/lang/ja-JP/passwords.php create mode 100644 resources/lang/ja-JP/reconciliations.php create mode 100644 resources/lang/ja-JP/recurring.php create mode 100644 resources/lang/ja-JP/reports.php create mode 100644 resources/lang/ja-JP/settings.php create mode 100644 resources/lang/ja-JP/taxes.php create mode 100644 resources/lang/ja-JP/transfers.php create mode 100644 resources/lang/ja-JP/updates.php create mode 100644 resources/lang/ja-JP/validation.php create mode 100644 resources/lang/ja-JP/widgets.php create mode 100644 resources/lang/ka-GE/accounts.php create mode 100644 resources/lang/ka-GE/auth.php create mode 100644 resources/lang/ka-GE/bills.php create mode 100644 resources/lang/ka-GE/companies.php create mode 100644 resources/lang/ka-GE/countries.php create mode 100644 resources/lang/ka-GE/currencies.php create mode 100644 resources/lang/ka-GE/customers.php create mode 100644 resources/lang/ka-GE/dashboard.php create mode 100644 resources/lang/ka-GE/demo.php create mode 100644 resources/lang/ka-GE/email_templates.php create mode 100644 resources/lang/ka-GE/errors.php create mode 100644 resources/lang/ka-GE/footer.php create mode 100644 resources/lang/ka-GE/general.php create mode 100644 resources/lang/ka-GE/header.php create mode 100644 resources/lang/ka-GE/import.php create mode 100644 resources/lang/ka-GE/install.php create mode 100644 resources/lang/ka-GE/invoices.php create mode 100644 resources/lang/ka-GE/items.php create mode 100644 resources/lang/ka-GE/maintenance.php create mode 100644 resources/lang/ka-GE/messages.php create mode 100644 resources/lang/ka-GE/modules.php create mode 100644 resources/lang/ka-GE/notifications.php create mode 100644 resources/lang/ka-GE/pagination.php create mode 100644 resources/lang/ka-GE/passwords.php create mode 100644 resources/lang/ka-GE/reconciliations.php create mode 100644 resources/lang/ka-GE/recurring.php create mode 100644 resources/lang/ka-GE/reports.php create mode 100644 resources/lang/ka-GE/settings.php create mode 100644 resources/lang/ka-GE/taxes.php create mode 100644 resources/lang/ka-GE/transfers.php create mode 100644 resources/lang/ka-GE/updates.php create mode 100644 resources/lang/ka-GE/validation.php create mode 100644 resources/lang/ko-KR/accounts.php create mode 100644 resources/lang/ko-KR/auth.php create mode 100644 resources/lang/ko-KR/bills.php create mode 100644 resources/lang/ko-KR/companies.php create mode 100644 resources/lang/ko-KR/countries.php create mode 100644 resources/lang/ko-KR/currencies.php create mode 100644 resources/lang/ko-KR/customers.php create mode 100644 resources/lang/ko-KR/dashboard.php create mode 100644 resources/lang/ko-KR/demo.php create mode 100644 resources/lang/ko-KR/footer.php create mode 100644 resources/lang/ko-KR/general.php create mode 100644 resources/lang/ko-KR/header.php create mode 100644 resources/lang/ko-KR/import.php create mode 100644 resources/lang/ko-KR/install.php create mode 100644 resources/lang/ko-KR/invoices.php create mode 100644 resources/lang/ko-KR/items.php create mode 100644 resources/lang/ko-KR/messages.php create mode 100644 resources/lang/ko-KR/modules.php create mode 100644 resources/lang/ko-KR/notifications.php create mode 100644 resources/lang/ko-KR/pagination.php create mode 100644 resources/lang/ko-KR/passwords.php create mode 100644 resources/lang/ko-KR/reconciliations.php create mode 100644 resources/lang/ko-KR/recurring.php create mode 100644 resources/lang/ko-KR/reports.php create mode 100644 resources/lang/ko-KR/settings.php create mode 100644 resources/lang/ko-KR/taxes.php create mode 100644 resources/lang/ko-KR/transfers.php create mode 100644 resources/lang/ko-KR/updates.php create mode 100644 resources/lang/ko-KR/validation.php create mode 100644 resources/lang/lt-LT/accounts.php create mode 100644 resources/lang/lt-LT/auth.php create mode 100644 resources/lang/lt-LT/bills.php create mode 100644 resources/lang/lt-LT/bulk_actions.php create mode 100644 resources/lang/lt-LT/companies.php create mode 100644 resources/lang/lt-LT/countries.php create mode 100644 resources/lang/lt-LT/currencies.php create mode 100644 resources/lang/lt-LT/customers.php create mode 100644 resources/lang/lt-LT/dashboards.php create mode 100644 resources/lang/lt-LT/demo.php create mode 100644 resources/lang/lt-LT/email_templates.php create mode 100644 resources/lang/lt-LT/errors.php create mode 100644 resources/lang/lt-LT/footer.php create mode 100644 resources/lang/lt-LT/general.php create mode 100644 resources/lang/lt-LT/header.php create mode 100644 resources/lang/lt-LT/import.php create mode 100644 resources/lang/lt-LT/install.php create mode 100644 resources/lang/lt-LT/invoices.php create mode 100644 resources/lang/lt-LT/items.php create mode 100644 resources/lang/lt-LT/maintenance.php create mode 100644 resources/lang/lt-LT/messages.php create mode 100644 resources/lang/lt-LT/modules.php create mode 100644 resources/lang/lt-LT/notifications.php create mode 100644 resources/lang/lt-LT/pagination.php create mode 100644 resources/lang/lt-LT/passwords.php create mode 100644 resources/lang/lt-LT/reconciliations.php create mode 100644 resources/lang/lt-LT/recurring.php create mode 100644 resources/lang/lt-LT/reports.php create mode 100644 resources/lang/lt-LT/settings.php create mode 100644 resources/lang/lt-LT/taxes.php create mode 100644 resources/lang/lt-LT/transfers.php create mode 100644 resources/lang/lt-LT/updates.php create mode 100644 resources/lang/lt-LT/validation.php create mode 100644 resources/lang/lt-LT/widgets.php create mode 100644 resources/lang/lv-LV/accounts.php create mode 100644 resources/lang/lv-LV/auth.php create mode 100644 resources/lang/lv-LV/bills.php create mode 100644 resources/lang/lv-LV/bulk_actions.php create mode 100644 resources/lang/lv-LV/categories.php create mode 100644 resources/lang/lv-LV/companies.php create mode 100644 resources/lang/lv-LV/countries.php create mode 100644 resources/lang/lv-LV/currencies.php create mode 100644 resources/lang/lv-LV/customers.php create mode 100644 resources/lang/lv-LV/dashboards.php create mode 100644 resources/lang/lv-LV/demo.php create mode 100644 resources/lang/lv-LV/documents.php create mode 100644 resources/lang/lv-LV/email_templates.php create mode 100644 resources/lang/lv-LV/errors.php create mode 100644 resources/lang/lv-LV/footer.php create mode 100644 resources/lang/lv-LV/general.php create mode 100644 resources/lang/lv-LV/header.php create mode 100644 resources/lang/lv-LV/import.php create mode 100644 resources/lang/lv-LV/install.php create mode 100644 resources/lang/lv-LV/invoices.php create mode 100644 resources/lang/lv-LV/items.php create mode 100644 resources/lang/lv-LV/maintenance.php create mode 100644 resources/lang/lv-LV/messages.php create mode 100644 resources/lang/lv-LV/modules.php create mode 100644 resources/lang/lv-LV/notifications.php create mode 100644 resources/lang/lv-LV/pagination.php create mode 100644 resources/lang/lv-LV/passwords.php create mode 100644 resources/lang/lv-LV/portal.php create mode 100644 resources/lang/lv-LV/reconciliations.php create mode 100644 resources/lang/lv-LV/recurring.php create mode 100644 resources/lang/lv-LV/reports.php create mode 100644 resources/lang/lv-LV/search_string.php create mode 100644 resources/lang/lv-LV/settings.php create mode 100644 resources/lang/lv-LV/taxes.php create mode 100644 resources/lang/lv-LV/transactions.php create mode 100644 resources/lang/lv-LV/transfers.php create mode 100644 resources/lang/lv-LV/updates.php create mode 100644 resources/lang/lv-LV/validation.php create mode 100644 resources/lang/lv-LV/vendors.php create mode 100644 resources/lang/lv-LV/widgets.php create mode 100644 resources/lang/mk-MK/accounts.php create mode 100644 resources/lang/mk-MK/auth.php create mode 100644 resources/lang/mk-MK/bills.php create mode 100644 resources/lang/mk-MK/categories.php create mode 100644 resources/lang/mk-MK/companies.php create mode 100644 resources/lang/mk-MK/countries.php create mode 100644 resources/lang/mk-MK/currencies.php create mode 100644 resources/lang/mk-MK/customers.php create mode 100644 resources/lang/mk-MK/dashboard.php create mode 100644 resources/lang/mk-MK/dashboards.php create mode 100644 resources/lang/mk-MK/demo.php create mode 100644 resources/lang/mk-MK/documents.php create mode 100644 resources/lang/mk-MK/email_templates.php create mode 100644 resources/lang/mk-MK/errors.php create mode 100644 resources/lang/mk-MK/footer.php create mode 100644 resources/lang/mk-MK/general.php create mode 100644 resources/lang/mk-MK/header.php create mode 100644 resources/lang/mk-MK/import.php create mode 100644 resources/lang/mk-MK/install.php create mode 100644 resources/lang/mk-MK/invoices.php create mode 100644 resources/lang/mk-MK/items.php create mode 100644 resources/lang/mk-MK/maintenance.php create mode 100644 resources/lang/mk-MK/messages.php create mode 100644 resources/lang/mk-MK/modules.php create mode 100644 resources/lang/mk-MK/notifications.php create mode 100644 resources/lang/mk-MK/pagination.php create mode 100644 resources/lang/mk-MK/passwords.php create mode 100644 resources/lang/mk-MK/payments.php create mode 100644 resources/lang/mk-MK/portal.php create mode 100644 resources/lang/mk-MK/reconciliations.php create mode 100644 resources/lang/mk-MK/recurring.php create mode 100644 resources/lang/mk-MK/reports.php create mode 100644 resources/lang/mk-MK/revenues.php create mode 100644 resources/lang/mk-MK/search_string.php create mode 100644 resources/lang/mk-MK/settings.php create mode 100644 resources/lang/mk-MK/taxes.php create mode 100644 resources/lang/mk-MK/transactions.php create mode 100644 resources/lang/mk-MK/transfers.php create mode 100644 resources/lang/mk-MK/updates.php create mode 100644 resources/lang/mk-MK/validation.php create mode 100644 resources/lang/mk-MK/vendors.php create mode 100644 resources/lang/mk-MK/widgets.php create mode 100644 resources/lang/ms-MY/accounts.php create mode 100644 resources/lang/ms-MY/auth.php create mode 100644 resources/lang/ms-MY/bills.php create mode 100644 resources/lang/ms-MY/bulk_actions.php create mode 100644 resources/lang/ms-MY/categories.php create mode 100644 resources/lang/ms-MY/companies.php create mode 100644 resources/lang/ms-MY/countries.php create mode 100644 resources/lang/ms-MY/currencies.php create mode 100644 resources/lang/ms-MY/customers.php create mode 100644 resources/lang/ms-MY/dashboards.php create mode 100644 resources/lang/ms-MY/demo.php create mode 100644 resources/lang/ms-MY/documents.php create mode 100644 resources/lang/ms-MY/email_templates.php create mode 100644 resources/lang/ms-MY/errors.php create mode 100644 resources/lang/ms-MY/footer.php create mode 100644 resources/lang/ms-MY/general.php create mode 100644 resources/lang/ms-MY/header.php create mode 100644 resources/lang/ms-MY/import.php create mode 100644 resources/lang/ms-MY/install.php create mode 100644 resources/lang/ms-MY/invoices.php create mode 100644 resources/lang/ms-MY/items.php create mode 100644 resources/lang/ms-MY/maintenance.php create mode 100644 resources/lang/ms-MY/messages.php create mode 100644 resources/lang/ms-MY/modules.php create mode 100644 resources/lang/ms-MY/notifications.php create mode 100644 resources/lang/ms-MY/pagination.php create mode 100644 resources/lang/ms-MY/passwords.php create mode 100644 resources/lang/ms-MY/portal.php create mode 100644 resources/lang/ms-MY/reconciliations.php create mode 100644 resources/lang/ms-MY/recurring.php create mode 100644 resources/lang/ms-MY/reports.php create mode 100644 resources/lang/ms-MY/search_string.php create mode 100644 resources/lang/ms-MY/settings.php create mode 100644 resources/lang/ms-MY/taxes.php create mode 100644 resources/lang/ms-MY/transactions.php create mode 100644 resources/lang/ms-MY/transfers.php create mode 100644 resources/lang/ms-MY/updates.php create mode 100644 resources/lang/ms-MY/validation.php create mode 100644 resources/lang/ms-MY/vendors.php create mode 100644 resources/lang/ms-MY/widgets.php create mode 100644 resources/lang/nb-NO/accounts.php create mode 100644 resources/lang/nb-NO/auth.php create mode 100644 resources/lang/nb-NO/bills.php create mode 100644 resources/lang/nb-NO/bulk_actions.php create mode 100644 resources/lang/nb-NO/categories.php create mode 100644 resources/lang/nb-NO/companies.php create mode 100644 resources/lang/nb-NO/countries.php create mode 100644 resources/lang/nb-NO/currencies.php create mode 100644 resources/lang/nb-NO/customers.php create mode 100644 resources/lang/nb-NO/dashboards.php create mode 100644 resources/lang/nb-NO/demo.php create mode 100644 resources/lang/nb-NO/documents.php create mode 100644 resources/lang/nb-NO/email_templates.php create mode 100644 resources/lang/nb-NO/errors.php create mode 100644 resources/lang/nb-NO/footer.php create mode 100644 resources/lang/nb-NO/general.php create mode 100644 resources/lang/nb-NO/header.php create mode 100644 resources/lang/nb-NO/import.php create mode 100644 resources/lang/nb-NO/install.php create mode 100644 resources/lang/nb-NO/invoices.php create mode 100644 resources/lang/nb-NO/items.php create mode 100644 resources/lang/nb-NO/maintenance.php create mode 100644 resources/lang/nb-NO/messages.php create mode 100644 resources/lang/nb-NO/modules.php create mode 100644 resources/lang/nb-NO/notifications.php create mode 100644 resources/lang/nb-NO/pagination.php create mode 100644 resources/lang/nb-NO/passwords.php create mode 100644 resources/lang/nb-NO/payments.php create mode 100644 resources/lang/nb-NO/portal.php create mode 100644 resources/lang/nb-NO/reconciliations.php create mode 100644 resources/lang/nb-NO/recurring.php create mode 100644 resources/lang/nb-NO/reports.php create mode 100644 resources/lang/nb-NO/revenues.php create mode 100644 resources/lang/nb-NO/search_string.php create mode 100644 resources/lang/nb-NO/settings.php create mode 100644 resources/lang/nb-NO/taxes.php create mode 100644 resources/lang/nb-NO/transactions.php create mode 100644 resources/lang/nb-NO/transfers.php create mode 100644 resources/lang/nb-NO/updates.php create mode 100644 resources/lang/nb-NO/validation.php create mode 100644 resources/lang/nb-NO/vendors.php create mode 100644 resources/lang/nb-NO/widgets.php create mode 100644 resources/lang/ne-NP/accounts.php create mode 100644 resources/lang/ne-NP/auth.php create mode 100644 resources/lang/ne-NP/bills.php create mode 100644 resources/lang/ne-NP/bulk_actions.php create mode 100644 resources/lang/ne-NP/companies.php create mode 100644 resources/lang/ne-NP/countries.php create mode 100644 resources/lang/ne-NP/currencies.php create mode 100644 resources/lang/ne-NP/customers.php create mode 100644 resources/lang/ne-NP/dashboards.php create mode 100644 resources/lang/ne-NP/demo.php create mode 100644 resources/lang/ne-NP/documents.php create mode 100644 resources/lang/ne-NP/email_templates.php create mode 100644 resources/lang/ne-NP/errors.php create mode 100644 resources/lang/ne-NP/footer.php create mode 100644 resources/lang/ne-NP/general.php create mode 100644 resources/lang/ne-NP/header.php create mode 100644 resources/lang/ne-NP/import.php create mode 100644 resources/lang/ne-NP/install.php create mode 100644 resources/lang/ne-NP/invoices.php create mode 100644 resources/lang/ne-NP/items.php create mode 100644 resources/lang/ne-NP/maintenance.php create mode 100644 resources/lang/ne-NP/messages.php create mode 100644 resources/lang/ne-NP/modules.php create mode 100644 resources/lang/ne-NP/notifications.php create mode 100644 resources/lang/ne-NP/pagination.php create mode 100644 resources/lang/ne-NP/passwords.php create mode 100644 resources/lang/ne-NP/reconciliations.php create mode 100644 resources/lang/ne-NP/recurring.php create mode 100644 resources/lang/ne-NP/reports.php create mode 100644 resources/lang/ne-NP/search_string.php create mode 100644 resources/lang/ne-NP/settings.php create mode 100644 resources/lang/ne-NP/taxes.php create mode 100644 resources/lang/ne-NP/transfers.php create mode 100644 resources/lang/ne-NP/updates.php create mode 100644 resources/lang/ne-NP/validation.php create mode 100644 resources/lang/ne-NP/widgets.php create mode 100644 resources/lang/nl-NL/accounts.php create mode 100644 resources/lang/nl-NL/auth.php create mode 100644 resources/lang/nl-NL/bills.php create mode 100644 resources/lang/nl-NL/bulk_actions.php create mode 100644 resources/lang/nl-NL/categories.php create mode 100644 resources/lang/nl-NL/companies.php create mode 100644 resources/lang/nl-NL/countries.php create mode 100644 resources/lang/nl-NL/currencies.php create mode 100644 resources/lang/nl-NL/customers.php create mode 100644 resources/lang/nl-NL/dashboards.php create mode 100644 resources/lang/nl-NL/demo.php create mode 100644 resources/lang/nl-NL/documents.php create mode 100644 resources/lang/nl-NL/email_templates.php create mode 100644 resources/lang/nl-NL/errors.php create mode 100644 resources/lang/nl-NL/footer.php create mode 100644 resources/lang/nl-NL/general.php create mode 100644 resources/lang/nl-NL/header.php create mode 100644 resources/lang/nl-NL/import.php create mode 100644 resources/lang/nl-NL/install.php create mode 100644 resources/lang/nl-NL/invoices.php create mode 100644 resources/lang/nl-NL/items.php create mode 100644 resources/lang/nl-NL/maintenance.php create mode 100644 resources/lang/nl-NL/messages.php create mode 100644 resources/lang/nl-NL/modules.php create mode 100644 resources/lang/nl-NL/notifications.php create mode 100644 resources/lang/nl-NL/pagination.php create mode 100644 resources/lang/nl-NL/passwords.php create mode 100644 resources/lang/nl-NL/portal.php create mode 100644 resources/lang/nl-NL/reconciliations.php create mode 100644 resources/lang/nl-NL/recurring.php create mode 100644 resources/lang/nl-NL/reports.php create mode 100644 resources/lang/nl-NL/search_string.php create mode 100644 resources/lang/nl-NL/settings.php create mode 100644 resources/lang/nl-NL/taxes.php create mode 100644 resources/lang/nl-NL/transactions.php create mode 100644 resources/lang/nl-NL/transfers.php create mode 100644 resources/lang/nl-NL/updates.php create mode 100644 resources/lang/nl-NL/validation.php create mode 100644 resources/lang/nl-NL/vendors.php create mode 100644 resources/lang/nl-NL/widgets.php create mode 100644 resources/lang/pl-PL/accounts.php create mode 100644 resources/lang/pl-PL/auth.php create mode 100644 resources/lang/pl-PL/bills.php create mode 100644 resources/lang/pl-PL/bulk_actions.php create mode 100644 resources/lang/pl-PL/categories.php create mode 100644 resources/lang/pl-PL/companies.php create mode 100644 resources/lang/pl-PL/countries.php create mode 100644 resources/lang/pl-PL/currencies.php create mode 100644 resources/lang/pl-PL/customers.php create mode 100644 resources/lang/pl-PL/dashboards.php create mode 100644 resources/lang/pl-PL/demo.php create mode 100644 resources/lang/pl-PL/documents.php create mode 100644 resources/lang/pl-PL/email_templates.php create mode 100644 resources/lang/pl-PL/errors.php create mode 100644 resources/lang/pl-PL/footer.php create mode 100644 resources/lang/pl-PL/general.php create mode 100644 resources/lang/pl-PL/header.php create mode 100644 resources/lang/pl-PL/import.php create mode 100644 resources/lang/pl-PL/install.php create mode 100644 resources/lang/pl-PL/invoices.php create mode 100644 resources/lang/pl-PL/items.php create mode 100644 resources/lang/pl-PL/maintenance.php create mode 100644 resources/lang/pl-PL/messages.php create mode 100644 resources/lang/pl-PL/modules.php create mode 100644 resources/lang/pl-PL/notifications.php create mode 100644 resources/lang/pl-PL/pagination.php create mode 100644 resources/lang/pl-PL/passwords.php create mode 100644 resources/lang/pl-PL/portal.php create mode 100644 resources/lang/pl-PL/reconciliations.php create mode 100644 resources/lang/pl-PL/recurring.php create mode 100644 resources/lang/pl-PL/reports.php create mode 100644 resources/lang/pl-PL/search_string.php create mode 100644 resources/lang/pl-PL/settings.php create mode 100644 resources/lang/pl-PL/taxes.php create mode 100644 resources/lang/pl-PL/transactions.php create mode 100644 resources/lang/pl-PL/transfers.php create mode 100644 resources/lang/pl-PL/updates.php create mode 100644 resources/lang/pl-PL/validation.php create mode 100644 resources/lang/pl-PL/vendors.php create mode 100644 resources/lang/pl-PL/widgets.php create mode 100644 resources/lang/pt-BR/accounts.php create mode 100644 resources/lang/pt-BR/auth.php create mode 100644 resources/lang/pt-BR/bills.php create mode 100644 resources/lang/pt-BR/bulk_actions.php create mode 100644 resources/lang/pt-BR/categories.php create mode 100644 resources/lang/pt-BR/companies.php create mode 100644 resources/lang/pt-BR/countries.php create mode 100644 resources/lang/pt-BR/currencies.php create mode 100644 resources/lang/pt-BR/customers.php create mode 100644 resources/lang/pt-BR/dashboards.php create mode 100644 resources/lang/pt-BR/demo.php create mode 100644 resources/lang/pt-BR/documents.php create mode 100644 resources/lang/pt-BR/email_templates.php create mode 100644 resources/lang/pt-BR/errors.php create mode 100644 resources/lang/pt-BR/footer.php create mode 100644 resources/lang/pt-BR/general.php create mode 100644 resources/lang/pt-BR/header.php create mode 100644 resources/lang/pt-BR/import.php create mode 100644 resources/lang/pt-BR/install.php create mode 100644 resources/lang/pt-BR/invoices.php create mode 100644 resources/lang/pt-BR/items.php create mode 100644 resources/lang/pt-BR/maintenance.php create mode 100644 resources/lang/pt-BR/messages.php create mode 100644 resources/lang/pt-BR/modules.php create mode 100644 resources/lang/pt-BR/notifications.php create mode 100644 resources/lang/pt-BR/pagination.php create mode 100644 resources/lang/pt-BR/passwords.php create mode 100644 resources/lang/pt-BR/portal.php create mode 100644 resources/lang/pt-BR/reconciliations.php create mode 100644 resources/lang/pt-BR/recurring.php create mode 100644 resources/lang/pt-BR/reports.php create mode 100644 resources/lang/pt-BR/search_string.php create mode 100644 resources/lang/pt-BR/settings.php create mode 100644 resources/lang/pt-BR/taxes.php create mode 100644 resources/lang/pt-BR/transactions.php create mode 100644 resources/lang/pt-BR/transfers.php create mode 100644 resources/lang/pt-BR/updates.php create mode 100644 resources/lang/pt-BR/validation.php create mode 100644 resources/lang/pt-BR/vendors.php create mode 100644 resources/lang/pt-BR/widgets.php create mode 100644 resources/lang/pt-PT/accounts.php create mode 100644 resources/lang/pt-PT/auth.php create mode 100644 resources/lang/pt-PT/bills.php create mode 100644 resources/lang/pt-PT/bulk_actions.php create mode 100644 resources/lang/pt-PT/companies.php create mode 100644 resources/lang/pt-PT/countries.php create mode 100644 resources/lang/pt-PT/currencies.php create mode 100644 resources/lang/pt-PT/customers.php create mode 100644 resources/lang/pt-PT/dashboards.php create mode 100644 resources/lang/pt-PT/demo.php create mode 100644 resources/lang/pt-PT/email_templates.php create mode 100644 resources/lang/pt-PT/errors.php create mode 100644 resources/lang/pt-PT/footer.php create mode 100644 resources/lang/pt-PT/general.php create mode 100644 resources/lang/pt-PT/header.php create mode 100644 resources/lang/pt-PT/import.php create mode 100644 resources/lang/pt-PT/install.php create mode 100644 resources/lang/pt-PT/invoices.php create mode 100644 resources/lang/pt-PT/items.php create mode 100644 resources/lang/pt-PT/maintenance.php create mode 100644 resources/lang/pt-PT/messages.php create mode 100644 resources/lang/pt-PT/modules.php create mode 100644 resources/lang/pt-PT/notifications.php create mode 100644 resources/lang/pt-PT/pagination.php create mode 100644 resources/lang/pt-PT/passwords.php create mode 100644 resources/lang/pt-PT/reconciliations.php create mode 100644 resources/lang/pt-PT/recurring.php create mode 100644 resources/lang/pt-PT/reports.php create mode 100644 resources/lang/pt-PT/settings.php create mode 100644 resources/lang/pt-PT/taxes.php create mode 100644 resources/lang/pt-PT/transfers.php create mode 100644 resources/lang/pt-PT/updates.php create mode 100644 resources/lang/pt-PT/validation.php create mode 100644 resources/lang/pt-PT/widgets.php create mode 100644 resources/lang/ro-RO/accounts.php create mode 100644 resources/lang/ro-RO/auth.php create mode 100644 resources/lang/ro-RO/bills.php create mode 100644 resources/lang/ro-RO/bulk_actions.php create mode 100644 resources/lang/ro-RO/categories.php create mode 100644 resources/lang/ro-RO/companies.php create mode 100644 resources/lang/ro-RO/countries.php create mode 100644 resources/lang/ro-RO/currencies.php create mode 100644 resources/lang/ro-RO/customers.php create mode 100644 resources/lang/ro-RO/dashboard.php create mode 100644 resources/lang/ro-RO/dashboards.php create mode 100644 resources/lang/ro-RO/demo.php create mode 100644 resources/lang/ro-RO/documents.php create mode 100644 resources/lang/ro-RO/email_templates.php create mode 100644 resources/lang/ro-RO/errors.php create mode 100644 resources/lang/ro-RO/footer.php create mode 100644 resources/lang/ro-RO/general.php create mode 100644 resources/lang/ro-RO/header.php create mode 100644 resources/lang/ro-RO/import.php create mode 100644 resources/lang/ro-RO/install.php create mode 100644 resources/lang/ro-RO/invoices.php create mode 100644 resources/lang/ro-RO/items.php create mode 100644 resources/lang/ro-RO/maintenance.php create mode 100644 resources/lang/ro-RO/messages.php create mode 100644 resources/lang/ro-RO/modules.php create mode 100644 resources/lang/ro-RO/notifications.php create mode 100644 resources/lang/ro-RO/pagination.php create mode 100644 resources/lang/ro-RO/passwords.php create mode 100644 resources/lang/ro-RO/portal.php create mode 100644 resources/lang/ro-RO/reconciliations.php create mode 100644 resources/lang/ro-RO/recurring.php create mode 100644 resources/lang/ro-RO/reports.php create mode 100644 resources/lang/ro-RO/search_string.php create mode 100644 resources/lang/ro-RO/settings.php create mode 100644 resources/lang/ro-RO/taxes.php create mode 100644 resources/lang/ro-RO/transfers.php create mode 100644 resources/lang/ro-RO/updates.php create mode 100644 resources/lang/ro-RO/validation.php create mode 100644 resources/lang/ro-RO/vendors.php create mode 100644 resources/lang/ro-RO/widgets.php create mode 100644 resources/lang/ru-RU/accounts.php create mode 100644 resources/lang/ru-RU/auth.php create mode 100644 resources/lang/ru-RU/bills.php create mode 100644 resources/lang/ru-RU/bulk_actions.php create mode 100644 resources/lang/ru-RU/categories.php create mode 100644 resources/lang/ru-RU/companies.php create mode 100644 resources/lang/ru-RU/countries.php create mode 100644 resources/lang/ru-RU/currencies.php create mode 100644 resources/lang/ru-RU/customers.php create mode 100644 resources/lang/ru-RU/dashboards.php create mode 100644 resources/lang/ru-RU/demo.php create mode 100644 resources/lang/ru-RU/documents.php create mode 100644 resources/lang/ru-RU/email_templates.php create mode 100644 resources/lang/ru-RU/errors.php create mode 100644 resources/lang/ru-RU/footer.php create mode 100644 resources/lang/ru-RU/general.php create mode 100644 resources/lang/ru-RU/header.php create mode 100644 resources/lang/ru-RU/import.php create mode 100644 resources/lang/ru-RU/install.php create mode 100644 resources/lang/ru-RU/invoices.php create mode 100644 resources/lang/ru-RU/items.php create mode 100644 resources/lang/ru-RU/maintenance.php create mode 100644 resources/lang/ru-RU/messages.php create mode 100644 resources/lang/ru-RU/modules.php create mode 100644 resources/lang/ru-RU/notifications.php create mode 100644 resources/lang/ru-RU/pagination.php create mode 100644 resources/lang/ru-RU/passwords.php create mode 100644 resources/lang/ru-RU/portal.php create mode 100644 resources/lang/ru-RU/reconciliations.php create mode 100644 resources/lang/ru-RU/recurring.php create mode 100644 resources/lang/ru-RU/reports.php create mode 100644 resources/lang/ru-RU/search_string.php create mode 100644 resources/lang/ru-RU/settings.php create mode 100644 resources/lang/ru-RU/taxes.php create mode 100644 resources/lang/ru-RU/transactions.php create mode 100644 resources/lang/ru-RU/transfers.php create mode 100644 resources/lang/ru-RU/updates.php create mode 100644 resources/lang/ru-RU/validation.php create mode 100644 resources/lang/ru-RU/vendors.php create mode 100644 resources/lang/ru-RU/widgets.php create mode 100644 resources/lang/si-LK/passwords.php create mode 100644 resources/lang/sk-SK/accounts.php create mode 100644 resources/lang/sk-SK/auth.php create mode 100644 resources/lang/sk-SK/bills.php create mode 100644 resources/lang/sk-SK/bulk_actions.php create mode 100644 resources/lang/sk-SK/categories.php create mode 100644 resources/lang/sk-SK/companies.php create mode 100644 resources/lang/sk-SK/countries.php create mode 100644 resources/lang/sk-SK/currencies.php create mode 100644 resources/lang/sk-SK/customers.php create mode 100644 resources/lang/sk-SK/dashboard.php create mode 100644 resources/lang/sk-SK/dashboards.php create mode 100644 resources/lang/sk-SK/demo.php create mode 100644 resources/lang/sk-SK/errors.php create mode 100644 resources/lang/sk-SK/footer.php create mode 100644 resources/lang/sk-SK/general.php create mode 100644 resources/lang/sk-SK/header.php create mode 100644 resources/lang/sk-SK/import.php create mode 100644 resources/lang/sk-SK/install.php create mode 100644 resources/lang/sk-SK/invoices.php create mode 100644 resources/lang/sk-SK/items.php create mode 100644 resources/lang/sk-SK/messages.php create mode 100644 resources/lang/sk-SK/modules.php create mode 100644 resources/lang/sk-SK/notifications.php create mode 100644 resources/lang/sk-SK/pagination.php create mode 100644 resources/lang/sk-SK/passwords.php create mode 100644 resources/lang/sk-SK/reconciliations.php create mode 100644 resources/lang/sk-SK/recurring.php create mode 100644 resources/lang/sk-SK/reports.php create mode 100644 resources/lang/sk-SK/search_string.php create mode 100644 resources/lang/sk-SK/settings.php create mode 100644 resources/lang/sk-SK/taxes.php create mode 100644 resources/lang/sk-SK/transfers.php create mode 100644 resources/lang/sk-SK/updates.php create mode 100644 resources/lang/sk-SK/validation.php create mode 100644 resources/lang/sl-SI/accounts.php create mode 100644 resources/lang/sl-SI/auth.php create mode 100644 resources/lang/sl-SI/bills.php create mode 100644 resources/lang/sl-SI/bulk_actions.php create mode 100644 resources/lang/sl-SI/categories.php create mode 100644 resources/lang/sl-SI/companies.php create mode 100644 resources/lang/sl-SI/countries.php create mode 100644 resources/lang/sl-SI/currencies.php create mode 100644 resources/lang/sl-SI/customers.php create mode 100644 resources/lang/sl-SI/dashboards.php create mode 100644 resources/lang/sl-SI/demo.php create mode 100644 resources/lang/sl-SI/documents.php create mode 100644 resources/lang/sl-SI/email_templates.php create mode 100644 resources/lang/sl-SI/errors.php create mode 100644 resources/lang/sl-SI/footer.php create mode 100644 resources/lang/sl-SI/general.php create mode 100644 resources/lang/sl-SI/header.php create mode 100644 resources/lang/sl-SI/import.php create mode 100644 resources/lang/sl-SI/install.php create mode 100644 resources/lang/sl-SI/invoices.php create mode 100644 resources/lang/sl-SI/items.php create mode 100644 resources/lang/sl-SI/maintenance.php create mode 100644 resources/lang/sl-SI/messages.php create mode 100644 resources/lang/sl-SI/modules.php create mode 100644 resources/lang/sl-SI/notifications.php create mode 100644 resources/lang/sl-SI/pagination.php create mode 100644 resources/lang/sl-SI/passwords.php create mode 100644 resources/lang/sl-SI/payments.php create mode 100644 resources/lang/sl-SI/portal.php create mode 100644 resources/lang/sl-SI/reconciliations.php create mode 100644 resources/lang/sl-SI/recurring.php create mode 100644 resources/lang/sl-SI/reports.php create mode 100644 resources/lang/sl-SI/revenues.php create mode 100644 resources/lang/sl-SI/search_string.php create mode 100644 resources/lang/sl-SI/settings.php create mode 100644 resources/lang/sl-SI/taxes.php create mode 100644 resources/lang/sl-SI/transactions.php create mode 100644 resources/lang/sl-SI/transfers.php create mode 100644 resources/lang/sl-SI/updates.php create mode 100644 resources/lang/sl-SI/validation.php create mode 100644 resources/lang/sl-SI/vendors.php create mode 100644 resources/lang/sl-SI/widgets.php create mode 100644 resources/lang/sq-AL/accounts.php create mode 100644 resources/lang/sq-AL/auth.php create mode 100644 resources/lang/sq-AL/bills.php create mode 100644 resources/lang/sq-AL/bulk_actions.php create mode 100644 resources/lang/sq-AL/companies.php create mode 100644 resources/lang/sq-AL/countries.php create mode 100644 resources/lang/sq-AL/currencies.php create mode 100644 resources/lang/sq-AL/customers.php create mode 100644 resources/lang/sq-AL/dashboards.php create mode 100644 resources/lang/sq-AL/demo.php create mode 100644 resources/lang/sq-AL/documents.php create mode 100644 resources/lang/sq-AL/email_templates.php create mode 100644 resources/lang/sq-AL/errors.php create mode 100644 resources/lang/sq-AL/footer.php create mode 100644 resources/lang/sq-AL/general.php create mode 100644 resources/lang/sq-AL/header.php create mode 100644 resources/lang/sq-AL/import.php create mode 100644 resources/lang/sq-AL/install.php create mode 100644 resources/lang/sq-AL/invoices.php create mode 100644 resources/lang/sq-AL/items.php create mode 100644 resources/lang/sq-AL/maintenance.php create mode 100644 resources/lang/sq-AL/messages.php create mode 100644 resources/lang/sq-AL/modules.php create mode 100644 resources/lang/sq-AL/notifications.php create mode 100644 resources/lang/sq-AL/pagination.php create mode 100644 resources/lang/sq-AL/passwords.php create mode 100644 resources/lang/sq-AL/reconciliations.php create mode 100644 resources/lang/sq-AL/recurring.php create mode 100644 resources/lang/sq-AL/reports.php create mode 100644 resources/lang/sq-AL/search_string.php create mode 100644 resources/lang/sq-AL/settings.php create mode 100644 resources/lang/sq-AL/taxes.php create mode 100644 resources/lang/sq-AL/transfers.php create mode 100644 resources/lang/sq-AL/updates.php create mode 100644 resources/lang/sq-AL/validation.php create mode 100644 resources/lang/sq-AL/widgets.php create mode 100644 resources/lang/sr-CS/accounts.php create mode 100644 resources/lang/sr-CS/auth.php create mode 100644 resources/lang/sr-CS/bills.php create mode 100644 resources/lang/sr-CS/bulk_actions.php create mode 100644 resources/lang/sr-CS/companies.php create mode 100644 resources/lang/sr-CS/countries.php create mode 100644 resources/lang/sr-CS/currencies.php create mode 100644 resources/lang/sr-CS/customers.php create mode 100644 resources/lang/sr-CS/dashboards.php create mode 100644 resources/lang/sr-CS/demo.php create mode 100644 resources/lang/sr-CS/documents.php create mode 100644 resources/lang/sr-CS/errors.php create mode 100644 resources/lang/sr-CS/footer.php create mode 100644 resources/lang/sr-CS/import.php create mode 100644 resources/lang/sr-CS/items.php create mode 100644 resources/lang/sr-CS/pagination.php create mode 100644 resources/lang/sr-CS/passwords.php create mode 100644 resources/lang/sr-CS/search_string.php create mode 100644 resources/lang/sr-CS/taxes.php create mode 100644 resources/lang/sr-CS/transfers.php create mode 100644 resources/lang/sr-CS/updates.php create mode 100644 resources/lang/sr-CS/widgets.php create mode 100644 resources/lang/sr-RS/accounts.php create mode 100644 resources/lang/sr-RS/auth.php create mode 100644 resources/lang/sr-RS/bills.php create mode 100644 resources/lang/sr-RS/companies.php create mode 100644 resources/lang/sr-RS/countries.php create mode 100644 resources/lang/sr-RS/currencies.php create mode 100644 resources/lang/sr-RS/customers.php create mode 100644 resources/lang/sr-RS/dashboard.php create mode 100644 resources/lang/sr-RS/demo.php create mode 100644 resources/lang/sr-RS/footer.php create mode 100644 resources/lang/sr-RS/general.php create mode 100644 resources/lang/sr-RS/header.php create mode 100644 resources/lang/sr-RS/import.php create mode 100644 resources/lang/sr-RS/install.php create mode 100644 resources/lang/sr-RS/invoices.php create mode 100644 resources/lang/sr-RS/items.php create mode 100644 resources/lang/sr-RS/messages.php create mode 100644 resources/lang/sr-RS/modules.php create mode 100644 resources/lang/sr-RS/notifications.php create mode 100644 resources/lang/sr-RS/pagination.php create mode 100644 resources/lang/sr-RS/passwords.php create mode 100644 resources/lang/sr-RS/reconciliations.php create mode 100644 resources/lang/sr-RS/recurring.php create mode 100644 resources/lang/sr-RS/reports.php create mode 100644 resources/lang/sr-RS/settings.php create mode 100644 resources/lang/sr-RS/taxes.php create mode 100644 resources/lang/sr-RS/transfers.php create mode 100644 resources/lang/sr-RS/updates.php create mode 100644 resources/lang/sr-RS/validation.php create mode 100644 resources/lang/sr-SP/accounts.php create mode 100644 resources/lang/sr-SP/auth.php create mode 100644 resources/lang/sr-SP/bills.php create mode 100644 resources/lang/sr-SP/countries.php create mode 100644 resources/lang/sr-SP/currencies.php create mode 100644 resources/lang/sr-SP/footer.php create mode 100644 resources/lang/sr-SP/header.php create mode 100644 resources/lang/sr-SP/import.php create mode 100644 resources/lang/sr-SP/items.php create mode 100644 resources/lang/sr-SP/notifications.php create mode 100644 resources/lang/sr-SP/recurring.php create mode 100644 resources/lang/sr-SP/transfers.php create mode 100644 resources/lang/sr-SP/updates.php create mode 100644 resources/lang/sv-SE/accounts.php create mode 100644 resources/lang/sv-SE/auth.php create mode 100644 resources/lang/sv-SE/bills.php create mode 100644 resources/lang/sv-SE/bulk_actions.php create mode 100644 resources/lang/sv-SE/categories.php create mode 100644 resources/lang/sv-SE/companies.php create mode 100644 resources/lang/sv-SE/countries.php create mode 100644 resources/lang/sv-SE/currencies.php create mode 100644 resources/lang/sv-SE/customers.php create mode 100644 resources/lang/sv-SE/dashboards.php create mode 100644 resources/lang/sv-SE/demo.php create mode 100644 resources/lang/sv-SE/documents.php create mode 100644 resources/lang/sv-SE/email_templates.php create mode 100644 resources/lang/sv-SE/errors.php create mode 100644 resources/lang/sv-SE/footer.php create mode 100644 resources/lang/sv-SE/general.php create mode 100644 resources/lang/sv-SE/header.php create mode 100644 resources/lang/sv-SE/import.php create mode 100644 resources/lang/sv-SE/install.php create mode 100644 resources/lang/sv-SE/invoices.php create mode 100644 resources/lang/sv-SE/items.php create mode 100644 resources/lang/sv-SE/maintenance.php create mode 100644 resources/lang/sv-SE/messages.php create mode 100644 resources/lang/sv-SE/modules.php create mode 100644 resources/lang/sv-SE/notifications.php create mode 100644 resources/lang/sv-SE/pagination.php create mode 100644 resources/lang/sv-SE/passwords.php create mode 100644 resources/lang/sv-SE/payments.php create mode 100644 resources/lang/sv-SE/portal.php create mode 100644 resources/lang/sv-SE/reconciliations.php create mode 100644 resources/lang/sv-SE/recurring.php create mode 100644 resources/lang/sv-SE/reports.php create mode 100644 resources/lang/sv-SE/revenues.php create mode 100644 resources/lang/sv-SE/search_string.php create mode 100644 resources/lang/sv-SE/settings.php create mode 100644 resources/lang/sv-SE/taxes.php create mode 100644 resources/lang/sv-SE/transactions.php create mode 100644 resources/lang/sv-SE/transfers.php create mode 100644 resources/lang/sv-SE/updates.php create mode 100644 resources/lang/sv-SE/validation.php create mode 100644 resources/lang/sv-SE/vendors.php create mode 100644 resources/lang/sv-SE/widgets.php create mode 100644 resources/lang/sw-KE/passwords.php create mode 100644 resources/lang/ta-IN/accounts.php create mode 100644 resources/lang/ta-IN/auth.php create mode 100644 resources/lang/ta-IN/countries.php create mode 100644 resources/lang/th-TH/accounts.php create mode 100644 resources/lang/th-TH/auth.php create mode 100644 resources/lang/th-TH/bills.php create mode 100644 resources/lang/th-TH/companies.php create mode 100644 resources/lang/th-TH/countries.php create mode 100644 resources/lang/th-TH/currencies.php create mode 100644 resources/lang/th-TH/customers.php create mode 100644 resources/lang/th-TH/dashboard.php create mode 100644 resources/lang/th-TH/dashboards.php create mode 100644 resources/lang/th-TH/demo.php create mode 100644 resources/lang/th-TH/errors.php create mode 100644 resources/lang/th-TH/footer.php create mode 100644 resources/lang/th-TH/general.php create mode 100644 resources/lang/th-TH/header.php create mode 100644 resources/lang/th-TH/import.php create mode 100644 resources/lang/th-TH/install.php create mode 100644 resources/lang/th-TH/invoices.php create mode 100644 resources/lang/th-TH/items.php create mode 100644 resources/lang/th-TH/maintenance.php create mode 100644 resources/lang/th-TH/messages.php create mode 100644 resources/lang/th-TH/modules.php create mode 100644 resources/lang/th-TH/notifications.php create mode 100644 resources/lang/th-TH/pagination.php create mode 100644 resources/lang/th-TH/passwords.php create mode 100644 resources/lang/th-TH/reconciliations.php create mode 100644 resources/lang/th-TH/recurring.php create mode 100644 resources/lang/th-TH/reports.php create mode 100644 resources/lang/th-TH/settings.php create mode 100644 resources/lang/th-TH/taxes.php create mode 100644 resources/lang/th-TH/transfers.php create mode 100644 resources/lang/th-TH/updates.php create mode 100644 resources/lang/th-TH/validation.php create mode 100644 resources/lang/tr-TR/accounts.php create mode 100644 resources/lang/tr-TR/auth.php create mode 100644 resources/lang/tr-TR/bills.php create mode 100644 resources/lang/tr-TR/bulk_actions.php create mode 100644 resources/lang/tr-TR/categories.php create mode 100644 resources/lang/tr-TR/companies.php create mode 100644 resources/lang/tr-TR/countries.php create mode 100644 resources/lang/tr-TR/currencies.php create mode 100644 resources/lang/tr-TR/customers.php create mode 100644 resources/lang/tr-TR/dashboards.php create mode 100644 resources/lang/tr-TR/demo.php create mode 100644 resources/lang/tr-TR/documents.php create mode 100644 resources/lang/tr-TR/email_templates.php create mode 100644 resources/lang/tr-TR/errors.php create mode 100644 resources/lang/tr-TR/footer.php create mode 100644 resources/lang/tr-TR/general.php create mode 100644 resources/lang/tr-TR/header.php create mode 100644 resources/lang/tr-TR/import.php create mode 100644 resources/lang/tr-TR/install.php create mode 100644 resources/lang/tr-TR/invoices.php create mode 100644 resources/lang/tr-TR/items.php create mode 100644 resources/lang/tr-TR/maintenance.php create mode 100644 resources/lang/tr-TR/messages.php create mode 100644 resources/lang/tr-TR/modules.php create mode 100644 resources/lang/tr-TR/notifications.php create mode 100644 resources/lang/tr-TR/pagination.php create mode 100644 resources/lang/tr-TR/passwords.php create mode 100644 resources/lang/tr-TR/portal.php create mode 100644 resources/lang/tr-TR/reconciliations.php create mode 100644 resources/lang/tr-TR/recurring.php create mode 100644 resources/lang/tr-TR/reports.php create mode 100644 resources/lang/tr-TR/search_string.php create mode 100644 resources/lang/tr-TR/settings.php create mode 100644 resources/lang/tr-TR/taxes.php create mode 100644 resources/lang/tr-TR/transactions.php create mode 100644 resources/lang/tr-TR/transfers.php create mode 100644 resources/lang/tr-TR/updates.php create mode 100644 resources/lang/tr-TR/validation.php create mode 100644 resources/lang/tr-TR/vendors.php create mode 100644 resources/lang/tr-TR/widgets.php create mode 100644 resources/lang/uk-UA/accounts.php create mode 100644 resources/lang/uk-UA/auth.php create mode 100644 resources/lang/uk-UA/bills.php create mode 100644 resources/lang/uk-UA/bulk_actions.php create mode 100644 resources/lang/uk-UA/companies.php create mode 100644 resources/lang/uk-UA/countries.php create mode 100644 resources/lang/uk-UA/currencies.php create mode 100644 resources/lang/uk-UA/customers.php create mode 100644 resources/lang/uk-UA/dashboards.php create mode 100644 resources/lang/uk-UA/demo.php create mode 100644 resources/lang/uk-UA/email_templates.php create mode 100644 resources/lang/uk-UA/errors.php create mode 100644 resources/lang/uk-UA/footer.php create mode 100644 resources/lang/uk-UA/general.php create mode 100644 resources/lang/uk-UA/header.php create mode 100644 resources/lang/uk-UA/import.php create mode 100644 resources/lang/uk-UA/install.php create mode 100644 resources/lang/uk-UA/invoices.php create mode 100644 resources/lang/uk-UA/items.php create mode 100644 resources/lang/uk-UA/maintenance.php create mode 100644 resources/lang/uk-UA/messages.php create mode 100644 resources/lang/uk-UA/modules.php create mode 100644 resources/lang/uk-UA/notifications.php create mode 100644 resources/lang/uk-UA/pagination.php create mode 100644 resources/lang/uk-UA/passwords.php create mode 100644 resources/lang/uk-UA/reconciliations.php create mode 100644 resources/lang/uk-UA/recurring.php create mode 100644 resources/lang/uk-UA/reports.php create mode 100644 resources/lang/uk-UA/settings.php create mode 100644 resources/lang/uk-UA/taxes.php create mode 100644 resources/lang/uk-UA/transfers.php create mode 100644 resources/lang/uk-UA/updates.php create mode 100644 resources/lang/uk-UA/validation.php create mode 100644 resources/lang/uk-UA/widgets.php create mode 100644 resources/lang/ur-PK/accounts.php create mode 100644 resources/lang/ur-PK/auth.php create mode 100644 resources/lang/ur-PK/bills.php create mode 100644 resources/lang/ur-PK/companies.php create mode 100644 resources/lang/ur-PK/countries.php create mode 100644 resources/lang/ur-PK/currencies.php create mode 100644 resources/lang/ur-PK/customers.php create mode 100644 resources/lang/ur-PK/dashboard.php create mode 100644 resources/lang/ur-PK/demo.php create mode 100644 resources/lang/ur-PK/errors.php create mode 100644 resources/lang/ur-PK/footer.php create mode 100644 resources/lang/ur-PK/general.php create mode 100644 resources/lang/ur-PK/header.php create mode 100644 resources/lang/ur-PK/import.php create mode 100644 resources/lang/ur-PK/install.php create mode 100644 resources/lang/ur-PK/invoices.php create mode 100644 resources/lang/ur-PK/items.php create mode 100644 resources/lang/ur-PK/messages.php create mode 100644 resources/lang/ur-PK/modules.php create mode 100644 resources/lang/ur-PK/notifications.php create mode 100644 resources/lang/ur-PK/pagination.php create mode 100644 resources/lang/ur-PK/passwords.php create mode 100644 resources/lang/ur-PK/reconciliations.php create mode 100644 resources/lang/ur-PK/recurring.php create mode 100644 resources/lang/ur-PK/reports.php create mode 100644 resources/lang/ur-PK/settings.php create mode 100644 resources/lang/ur-PK/taxes.php create mode 100644 resources/lang/ur-PK/transfers.php create mode 100644 resources/lang/ur-PK/updates.php create mode 100644 resources/lang/ur-PK/validation.php create mode 100644 resources/lang/uz-UZ/accounts.php create mode 100644 resources/lang/uz-UZ/auth.php create mode 100644 resources/lang/uz-UZ/bills.php create mode 100644 resources/lang/uz-UZ/bulk_actions.php create mode 100644 resources/lang/uz-UZ/companies.php create mode 100644 resources/lang/uz-UZ/countries.php create mode 100644 resources/lang/uz-UZ/currencies.php create mode 100644 resources/lang/uz-UZ/customers.php create mode 100644 resources/lang/uz-UZ/dashboard.php create mode 100644 resources/lang/uz-UZ/dashboards.php create mode 100644 resources/lang/uz-UZ/demo.php create mode 100644 resources/lang/uz-UZ/email_templates.php create mode 100644 resources/lang/uz-UZ/errors.php create mode 100644 resources/lang/uz-UZ/footer.php create mode 100644 resources/lang/uz-UZ/general.php create mode 100644 resources/lang/uz-UZ/header.php create mode 100644 resources/lang/uz-UZ/import.php create mode 100644 resources/lang/uz-UZ/install.php create mode 100644 resources/lang/uz-UZ/invoices.php create mode 100644 resources/lang/uz-UZ/items.php create mode 100644 resources/lang/uz-UZ/maintenance.php create mode 100644 resources/lang/uz-UZ/messages.php create mode 100644 resources/lang/uz-UZ/modules.php create mode 100644 resources/lang/uz-UZ/notifications.php create mode 100644 resources/lang/uz-UZ/pagination.php create mode 100644 resources/lang/uz-UZ/passwords.php create mode 100644 resources/lang/uz-UZ/reconciliations.php create mode 100644 resources/lang/uz-UZ/recurring.php create mode 100644 resources/lang/uz-UZ/reports.php create mode 100644 resources/lang/uz-UZ/settings.php create mode 100644 resources/lang/uz-UZ/taxes.php create mode 100644 resources/lang/uz-UZ/transfers.php create mode 100644 resources/lang/uz-UZ/updates.php create mode 100644 resources/lang/uz-UZ/validation.php create mode 100644 resources/lang/uz-UZ/widgets.php create mode 100644 resources/lang/vi-VN/accounts.php create mode 100644 resources/lang/vi-VN/auth.php create mode 100644 resources/lang/vi-VN/bills.php create mode 100644 resources/lang/vi-VN/companies.php create mode 100644 resources/lang/vi-VN/countries.php create mode 100644 resources/lang/vi-VN/currencies.php create mode 100644 resources/lang/vi-VN/customers.php create mode 100644 resources/lang/vi-VN/dashboard.php create mode 100644 resources/lang/vi-VN/demo.php create mode 100644 resources/lang/vi-VN/footer.php create mode 100644 resources/lang/vi-VN/general.php create mode 100644 resources/lang/vi-VN/header.php create mode 100644 resources/lang/vi-VN/import.php create mode 100644 resources/lang/vi-VN/install.php create mode 100644 resources/lang/vi-VN/invoices.php create mode 100644 resources/lang/vi-VN/items.php create mode 100644 resources/lang/vi-VN/messages.php create mode 100644 resources/lang/vi-VN/modules.php create mode 100644 resources/lang/vi-VN/notifications.php create mode 100644 resources/lang/vi-VN/pagination.php create mode 100644 resources/lang/vi-VN/passwords.php create mode 100644 resources/lang/vi-VN/reconciliations.php create mode 100644 resources/lang/vi-VN/recurring.php create mode 100644 resources/lang/vi-VN/reports.php create mode 100644 resources/lang/vi-VN/settings.php create mode 100644 resources/lang/vi-VN/taxes.php create mode 100644 resources/lang/vi-VN/transfers.php create mode 100644 resources/lang/vi-VN/updates.php create mode 100644 resources/lang/vi-VN/validation.php create mode 100644 resources/lang/zh-CN/accounts.php create mode 100644 resources/lang/zh-CN/auth.php create mode 100644 resources/lang/zh-CN/bills.php create mode 100644 resources/lang/zh-CN/bulk_actions.php create mode 100644 resources/lang/zh-CN/categories.php create mode 100644 resources/lang/zh-CN/companies.php create mode 100644 resources/lang/zh-CN/countries.php create mode 100644 resources/lang/zh-CN/currencies.php create mode 100644 resources/lang/zh-CN/customers.php create mode 100644 resources/lang/zh-CN/dashboards.php create mode 100644 resources/lang/zh-CN/demo.php create mode 100644 resources/lang/zh-CN/documents.php create mode 100644 resources/lang/zh-CN/email_templates.php create mode 100644 resources/lang/zh-CN/errors.php create mode 100644 resources/lang/zh-CN/footer.php create mode 100644 resources/lang/zh-CN/fund_accounts.php create mode 100644 resources/lang/zh-CN/general.php create mode 100644 resources/lang/zh-CN/header.php create mode 100644 resources/lang/zh-CN/import.php create mode 100644 resources/lang/zh-CN/install.php create mode 100644 resources/lang/zh-CN/invoices.php create mode 100644 resources/lang/zh-CN/items.php create mode 100644 resources/lang/zh-CN/maintenance.php create mode 100644 resources/lang/zh-CN/messages.php create mode 100644 resources/lang/zh-CN/modules.php create mode 100644 resources/lang/zh-CN/notifications.php create mode 100644 resources/lang/zh-CN/pagination.php create mode 100644 resources/lang/zh-CN/passwords.php create mode 100644 resources/lang/zh-CN/portal.php create mode 100644 resources/lang/zh-CN/reconciliations.php create mode 100644 resources/lang/zh-CN/recurring.php create mode 100644 resources/lang/zh-CN/reports.php create mode 100644 resources/lang/zh-CN/search_string.php create mode 100644 resources/lang/zh-CN/settings.php create mode 100644 resources/lang/zh-CN/taxes.php create mode 100644 resources/lang/zh-CN/transactions.php create mode 100644 resources/lang/zh-CN/transfers.php create mode 100644 resources/lang/zh-CN/updates.php create mode 100644 resources/lang/zh-CN/validation.php create mode 100644 resources/lang/zh-CN/vendors.php create mode 100644 resources/lang/zh-CN/widgets.php create mode 100644 resources/lang/zh-TW/accounts.php create mode 100644 resources/lang/zh-TW/auth.php create mode 100644 resources/lang/zh-TW/bills.php create mode 100644 resources/lang/zh-TW/bulk_actions.php create mode 100644 resources/lang/zh-TW/companies.php create mode 100644 resources/lang/zh-TW/countries.php create mode 100644 resources/lang/zh-TW/currencies.php create mode 100644 resources/lang/zh-TW/customers.php create mode 100644 resources/lang/zh-TW/dashboards.php create mode 100644 resources/lang/zh-TW/demo.php create mode 100644 resources/lang/zh-TW/email_templates.php create mode 100644 resources/lang/zh-TW/errors.php create mode 100644 resources/lang/zh-TW/footer.php create mode 100644 resources/lang/zh-TW/general.php create mode 100644 resources/lang/zh-TW/header.php create mode 100644 resources/lang/zh-TW/import.php create mode 100644 resources/lang/zh-TW/install.php create mode 100644 resources/lang/zh-TW/invoices.php create mode 100644 resources/lang/zh-TW/items.php create mode 100644 resources/lang/zh-TW/maintenance.php create mode 100644 resources/lang/zh-TW/messages.php create mode 100644 resources/lang/zh-TW/modules.php create mode 100644 resources/lang/zh-TW/notifications.php create mode 100644 resources/lang/zh-TW/pagination.php create mode 100644 resources/lang/zh-TW/passwords.php create mode 100644 resources/lang/zh-TW/reconciliations.php create mode 100644 resources/lang/zh-TW/recurring.php create mode 100644 resources/lang/zh-TW/reports.php create mode 100644 resources/lang/zh-TW/settings.php create mode 100644 resources/lang/zh-TW/taxes.php create mode 100644 resources/lang/zh-TW/transfers.php create mode 100644 resources/lang/zh-TW/updates.php create mode 100644 resources/lang/zh-TW/validation.php create mode 100644 resources/lang/zh-TW/widgets.php create mode 100644 resources/views/auth/forgot/create.blade.php create mode 100644 resources/views/auth/login/create.blade.php create mode 100644 resources/views/auth/register/create.blade.php create mode 100644 resources/views/auth/reset/create.blade.php create mode 100644 resources/views/auth/users/create.blade.php create mode 100644 resources/views/auth/users/edit.blade.php create mode 100644 resources/views/auth/users/index.blade.php create mode 100644 resources/views/auth/users/show.blade.php create mode 100644 resources/views/banking/accounts/create.blade.php create mode 100644 resources/views/banking/accounts/edit.blade.php create mode 100644 resources/views/banking/accounts/index.blade.php create mode 100644 resources/views/banking/accounts/show.blade.php create mode 100644 resources/views/banking/fund_accounts/form.blade.php create mode 100644 resources/views/banking/fund_accounts/index.blade.php create mode 100644 resources/views/banking/fund_accounts/store-index.blade.php create mode 100644 resources/views/banking/reconciliations/create.blade.php create mode 100644 resources/views/banking/reconciliations/edit.blade.php create mode 100644 resources/views/banking/reconciliations/index.blade.php create mode 100644 resources/views/banking/recurring_transactions/create.blade.php create mode 100644 resources/views/banking/recurring_transactions/edit.blade.php create mode 100644 resources/views/banking/recurring_transactions/index.blade.php create mode 100644 resources/views/banking/recurring_transactions/show.blade.php create mode 100644 resources/views/banking/transactions/create.blade.php create mode 100644 resources/views/banking/transactions/edit.blade.php create mode 100644 resources/views/banking/transactions/import.blade.php create mode 100644 resources/views/banking/transactions/index.blade.php create mode 100644 resources/views/banking/transactions/print_default.blade.php create mode 100644 resources/views/banking/transactions/show.blade.php create mode 100644 resources/views/banking/transfers/create.blade.php create mode 100644 resources/views/banking/transfers/edit.blade.php create mode 100644 resources/views/banking/transfers/index.blade.php create mode 100644 resources/views/banking/transfers/print_default.blade.php create mode 100644 resources/views/banking/transfers/print_second.blade.php create mode 100644 resources/views/banking/transfers/print_third.blade.php create mode 100644 resources/views/banking/transfers/show.blade.php create mode 100644 resources/views/bulk-actions/auth/users/edit.blade.php create mode 100644 resources/views/bulk-actions/banking/accounts/edit.blade.php create mode 100644 resources/views/bulk-actions/banking/transactions/edit.blade.php create mode 100644 resources/views/bulk-actions/common/companies/edit.blade.php create mode 100644 resources/views/bulk-actions/common/items/edit.blade.php create mode 100644 resources/views/bulk-actions/purchases/bills/edit.blade.php create mode 100644 resources/views/bulk-actions/purchases/vendors/edit.blade.php create mode 100644 resources/views/bulk-actions/sales/customers/edit.blade.php create mode 100644 resources/views/bulk-actions/sales/invoices/edit.blade.php create mode 100644 resources/views/bulk-actions/settings/categories/edit.blade.php create mode 100644 resources/views/bulk-actions/settings/currencies/edit.blade.php create mode 100644 resources/views/bulk-actions/settings/taxes/edit.blade.php create mode 100644 resources/views/business/pages/show.blade.php create mode 100644 resources/views/common/companies/create.blade.php create mode 100644 resources/views/common/companies/edit.blade.php create mode 100644 resources/views/common/companies/index.blade.php create mode 100644 resources/views/common/dashboards/create.blade.php create mode 100644 resources/views/common/dashboards/edit.blade.php create mode 100644 resources/views/common/dashboards/index.blade.php create mode 100644 resources/views/common/dashboards/show.blade.php create mode 100644 resources/views/common/import/create.blade.php create mode 100644 resources/views/common/items/create.blade.php create mode 100644 resources/views/common/items/edit.blade.php create mode 100644 resources/views/common/items/index.blade.php create mode 100644 resources/views/common/reports/create.blade.php create mode 100644 resources/views/common/reports/edit.blade.php create mode 100644 resources/views/common/reports/index.blade.php create mode 100644 resources/views/components/alert/border.blade.php create mode 100644 resources/views/components/alert/call.blade.php create mode 100644 resources/views/components/alert/danger.blade.php create mode 100644 resources/views/components/alert/index.blade.php create mode 100644 resources/views/components/alert/info.blade.php create mode 100644 resources/views/components/alert/success.blade.php create mode 100644 resources/views/components/alert/warning.blade.php create mode 100644 resources/views/components/button/hover.blade.php create mode 100644 resources/views/components/button/index.blade.php create mode 100644 resources/views/components/button/loading.blade.php create mode 100644 resources/views/components/contacts/form/address.blade.php create mode 100644 resources/views/components/contacts/form/billing.blade.php create mode 100644 resources/views/components/contacts/form/buttons.blade.php create mode 100644 resources/views/components/contacts/form/content.blade.php create mode 100644 resources/views/components/contacts/form/general.blade.php create mode 100644 resources/views/components/contacts/form/persons.blade.php create mode 100644 resources/views/components/contacts/index/buttons.blade.php create mode 100644 resources/views/components/contacts/index/content.blade.php create mode 100644 resources/views/components/contacts/index/more-buttons.blade.php create mode 100644 resources/views/components/contacts/script.blade.php create mode 100644 resources/views/components/contacts/show/buttons.blade.php create mode 100644 resources/views/components/contacts/show/content.blade.php create mode 100644 resources/views/components/contacts/show/more-buttons.blade.php create mode 100644 resources/views/components/date.blade.php create mode 100644 resources/views/components/delete-button.blade.php create mode 100644 resources/views/components/delete-link.blade.php create mode 100644 resources/views/components/documents/form/advanced.blade.php create mode 100644 resources/views/components/documents/form/buttons.blade.php create mode 100644 resources/views/components/documents/form/company.blade.php create mode 100644 resources/views/components/documents/form/contact.blade.php create mode 100644 resources/views/components/documents/form/content.blade.php create mode 100644 resources/views/components/documents/form/item-button.blade.php create mode 100644 resources/views/components/documents/form/item-columns.blade.php create mode 100644 resources/views/components/documents/form/items.blade.php create mode 100644 resources/views/components/documents/form/line-item.blade.php create mode 100644 resources/views/components/documents/form/main.blade.php create mode 100644 resources/views/components/documents/form/metadata.blade.php create mode 100644 resources/views/components/documents/form/note.blade.php create mode 100644 resources/views/components/documents/form/recurring.blade.php create mode 100644 resources/views/components/documents/form/recurring_metadata.blade.php create mode 100644 resources/views/components/documents/form/totals.blade.php create mode 100644 resources/views/components/documents/index/bulk-action.blade.php create mode 100644 resources/views/components/documents/index/buttons.blade.php create mode 100644 resources/views/components/documents/index/content.blade.php create mode 100644 resources/views/components/documents/index/document.blade.php create mode 100644 resources/views/components/documents/index/information.blade.php create mode 100644 resources/views/components/documents/index/more-buttons.blade.php create mode 100644 resources/views/components/documents/index/recurring_templates.blade.php create mode 100644 resources/views/components/documents/script.blade.php create mode 100644 resources/views/components/documents/show/attachment.blade.php create mode 100644 resources/views/components/documents/show/buttons.blade.php create mode 100644 resources/views/components/documents/show/children.blade.php create mode 100644 resources/views/components/documents/show/content.blade.php create mode 100644 resources/views/components/documents/show/create.blade.php create mode 100644 resources/views/components/documents/show/get-paid.blade.php create mode 100644 resources/views/components/documents/show/make-payment.blade.php create mode 100644 resources/views/components/documents/show/message.blade.php create mode 100644 resources/views/components/documents/show/more-buttons.blade.php create mode 100644 resources/views/components/documents/show/receive.blade.php create mode 100644 resources/views/components/documents/show/restore.blade.php create mode 100644 resources/views/components/documents/show/schedule.blade.php create mode 100644 resources/views/components/documents/show/send.blade.php create mode 100644 resources/views/components/documents/show/template.blade.php create mode 100644 resources/views/components/documents/template/classic.blade.php create mode 100644 resources/views/components/documents/template/default.blade.php create mode 100644 resources/views/components/documents/template/line-item.blade.php create mode 100644 resources/views/components/documents/template/modern.blade.php create mode 100644 resources/views/components/dropdown/button.blade.php create mode 100644 resources/views/components/dropdown/divider.blade.php create mode 100644 resources/views/components/dropdown/index.blade.php create mode 100644 resources/views/components/dropdown/link.blade.php create mode 100644 resources/views/components/email/body.blade.php create mode 100644 resources/views/components/email/footer.blade.php create mode 100644 resources/views/components/empty-data.blade.php create mode 100644 resources/views/components/empty-page.blade.php create mode 100644 resources/views/components/form/accordion/head.blade.php create mode 100644 resources/views/components/form/accordion/index.blade.php create mode 100644 resources/views/components/form/buttons.blade.php create mode 100644 resources/views/components/form/container.blade.php create mode 100644 resources/views/components/form/error.blade.php create mode 100644 resources/views/components/form/group/account.blade.php create mode 100644 resources/views/components/form/group/attachment.blade.php create mode 100644 resources/views/components/form/group/category.blade.php create mode 100644 resources/views/components/form/group/checkbox.blade.php create mode 100644 resources/views/components/form/group/color.blade.php create mode 100644 resources/views/components/form/group/contact.blade.php create mode 100644 resources/views/components/form/group/country.blade.php create mode 100644 resources/views/components/form/group/currency.blade.php create mode 100644 resources/views/components/form/group/customer.blade.php create mode 100644 resources/views/components/form/group/date.blade.php create mode 100644 resources/views/components/form/group/date_time.blade.php create mode 100644 resources/views/components/form/group/editor.blade.php create mode 100644 resources/views/components/form/group/email.blade.php create mode 100644 resources/views/components/form/group/file.blade.php create mode 100644 resources/views/components/form/group/import.blade.php create mode 100644 resources/views/components/form/group/invoice_text.blade.php create mode 100644 resources/views/components/form/group/locale.blade.php create mode 100644 resources/views/components/form/group/money.blade.php create mode 100644 resources/views/components/form/group/number.blade.php create mode 100644 resources/views/components/form/group/number_digit.blade.php create mode 100644 resources/views/components/form/group/password.blade.php create mode 100644 resources/views/components/form/group/payment_method.blade.php create mode 100644 resources/views/components/form/group/payment_term.blade.php create mode 100644 resources/views/components/form/group/radio.blade.php create mode 100644 resources/views/components/form/group/recurring.blade.php create mode 100644 resources/views/components/form/group/select.blade.php create mode 100644 resources/views/components/form/group/switch.blade.php create mode 100644 resources/views/components/form/group/tax.blade.php create mode 100644 resources/views/components/form/group/text.blade.php create mode 100644 resources/views/components/form/group/textarea.blade.php create mode 100644 resources/views/components/form/group/time.blade.php create mode 100644 resources/views/components/form/group/toggle.blade.php create mode 100644 resources/views/components/form/group/vendor.blade.php create mode 100644 resources/views/components/form/icon.blade.php create mode 100644 resources/views/components/form/index.blade.php create mode 100644 resources/views/components/form/input/checkbox.blade.php create mode 100644 resources/views/components/form/input/color.blade.php create mode 100644 resources/views/components/form/input/editor.blade.php create mode 100644 resources/views/components/form/input/email.blade.php create mode 100644 resources/views/components/form/input/file.blade.php create mode 100644 resources/views/components/form/input/hidden.blade.php create mode 100644 resources/views/components/form/input/import.blade.php create mode 100644 resources/views/components/form/input/input.blade.php create mode 100644 resources/views/components/form/input/money.blade.php create mode 100644 resources/views/components/form/input/number.blade.php create mode 100644 resources/views/components/form/input/password.blade.php create mode 100644 resources/views/components/form/input/radio.blade.php create mode 100644 resources/views/components/form/input/select.blade.php create mode 100644 resources/views/components/form/input/text.blade.php create mode 100644 resources/views/components/form/input/textarea.blade.php create mode 100644 resources/views/components/form/label.blade.php create mode 100644 resources/views/components/form/section/head.blade.php create mode 100644 resources/views/components/form/section/index.blade.php create mode 100644 resources/views/components/icon.blade.php create mode 100644 resources/views/components/index/balance.blade.php create mode 100644 resources/views/components/index/bulkaction/all.blade.php create mode 100644 resources/views/components/index/bulkaction/index.blade.php create mode 100644 resources/views/components/index/bulkaction/modal.blade.php create mode 100644 resources/views/components/index/bulkaction/single.blade.php create mode 100644 resources/views/components/index/category.blade.php create mode 100644 resources/views/components/index/container.blade.php create mode 100644 resources/views/components/index/country.blade.php create mode 100644 resources/views/components/index/currency.blade.php create mode 100644 resources/views/components/index/default.blade.php create mode 100644 resources/views/components/index/disable.blade.php create mode 100644 resources/views/components/index/search.blade.php create mode 100644 resources/views/components/index/status.blade.php create mode 100644 resources/views/components/index/summary.blade.php create mode 100644 resources/views/components/layouts/admin.blade.php create mode 100644 resources/views/components/layouts/admin/content.blade.php create mode 100644 resources/views/components/layouts/admin/footer.blade.php create mode 100644 resources/views/components/layouts/admin/head.blade.php create mode 100644 resources/views/components/layouts/admin/header.blade.php create mode 100644 resources/views/components/layouts/admin/menu.blade.php create mode 100644 resources/views/components/layouts/admin/notifications.blade.php create mode 100644 resources/views/components/layouts/admin/scripts.blade.php create mode 100644 resources/views/components/layouts/auth.blade.php create mode 100644 resources/views/components/layouts/auth/content.blade.php create mode 100644 resources/views/components/layouts/auth/footer.blade.php create mode 100644 resources/views/components/layouts/auth/head.blade.php create mode 100644 resources/views/components/layouts/auth/scripts.blade.php create mode 100644 resources/views/components/layouts/auth/slider.blade.php create mode 100644 resources/views/components/layouts/error.blade.php create mode 100644 resources/views/components/layouts/error/content.blade.php create mode 100644 resources/views/components/layouts/error/head.blade.php create mode 100644 resources/views/components/layouts/error/scripts.blade.php create mode 100644 resources/views/components/layouts/install.blade.php create mode 100644 resources/views/components/layouts/install/content.blade.php create mode 100644 resources/views/components/layouts/install/head.blade.php create mode 100644 resources/views/components/layouts/install/scripts.blade.php create mode 100644 resources/views/components/layouts/maintenance.blade.php create mode 100644 resources/views/components/layouts/maintenance/body.blade.php create mode 100644 resources/views/components/layouts/maintenance/head.blade.php create mode 100644 resources/views/components/layouts/modules.blade.php create mode 100644 resources/views/components/layouts/modules/bar.blade.php create mode 100644 resources/views/components/layouts/modules/head.blade.php create mode 100644 resources/views/components/layouts/modules/item.blade.php create mode 100644 resources/views/components/layouts/modules/my_apps_item.blade.php create mode 100644 resources/views/components/layouts/modules/releases.blade.php create mode 100644 resources/views/components/layouts/modules/reviews.blade.php create mode 100644 resources/views/components/layouts/modules/show/buttons.blade.php create mode 100644 resources/views/components/layouts/modules/show/changelog.blade.php create mode 100644 resources/views/components/layouts/modules/show/description.blade.php create mode 100644 resources/views/components/layouts/modules/show/documentation.blade.php create mode 100644 resources/views/components/layouts/modules/show/features.blade.php create mode 100644 resources/views/components/layouts/modules/show/information.blade.php create mode 100644 resources/views/components/layouts/modules/show/installation.blade.php create mode 100644 resources/views/components/layouts/modules/show/price.blade.php create mode 100644 resources/views/components/layouts/modules/show/releases.blade.php create mode 100644 resources/views/components/layouts/modules/show/reviews.blade.php create mode 100644 resources/views/components/layouts/modules/show/screenshots.blade.php create mode 100644 resources/views/components/layouts/modules/show/toggle.blade.php create mode 100644 resources/views/components/layouts/portal.blade.php create mode 100644 resources/views/components/layouts/portal/content.blade.php create mode 100644 resources/views/components/layouts/portal/finish/buttons.blade.php create mode 100644 resources/views/components/layouts/portal/finish/content.blade.php create mode 100644 resources/views/components/layouts/portal/footer.blade.php create mode 100644 resources/views/components/layouts/portal/head.blade.php create mode 100644 resources/views/components/layouts/portal/header.blade.php create mode 100644 resources/views/components/layouts/portal/menu.blade.php create mode 100644 resources/views/components/layouts/portal/scripts.blade.php create mode 100644 resources/views/components/layouts/preview.blade.php create mode 100644 resources/views/components/layouts/preview/content.blade.php create mode 100644 resources/views/components/layouts/preview/footer.blade.php create mode 100644 resources/views/components/layouts/preview/head.blade.php create mode 100644 resources/views/components/layouts/preview/header.blade.php create mode 100644 resources/views/components/layouts/preview/scripts.blade.php create mode 100644 resources/views/components/layouts/print.blade.php create mode 100644 resources/views/components/layouts/print/content.blade.php create mode 100644 resources/views/components/layouts/print/head.blade.php create mode 100644 resources/views/components/layouts/print/scripts.blade.php create mode 100644 resources/views/components/layouts/pwa/head.blade.php create mode 100644 resources/views/components/layouts/signed.blade.php create mode 100644 resources/views/components/layouts/signed/content.blade.php create mode 100644 resources/views/components/layouts/signed/footer.blade.php create mode 100644 resources/views/components/layouts/signed/head.blade.php create mode 100644 resources/views/components/layouts/signed/header.blade.php create mode 100644 resources/views/components/layouts/signed/scripts.blade.php create mode 100644 resources/views/components/layouts/wizard.blade.php create mode 100644 resources/views/components/layouts/wizard/background.blade.php create mode 100644 resources/views/components/layouts/wizard/content.blade.php create mode 100644 resources/views/components/layouts/wizard/head.blade.php create mode 100644 resources/views/components/layouts/wizard/scripts.blade.php create mode 100644 resources/views/components/link/hover.blade.php create mode 100644 resources/views/components/link/index.blade.php create mode 100644 resources/views/components/link/loading.blade.php create mode 100644 resources/views/components/loading/absolute.blade.php create mode 100644 resources/views/components/loading/content.blade.php create mode 100644 resources/views/components/loading/menu.blade.php create mode 100644 resources/views/components/media/file.blade.php create mode 100644 resources/views/components/menu/favorite.blade.php create mode 100644 resources/views/components/modules/banners.blade.php create mode 100644 resources/views/components/modules/free.blade.php create mode 100644 resources/views/components/modules/installed.blade.php create mode 100644 resources/views/components/modules/item.blade.php create mode 100644 resources/views/components/modules/items.blade.php create mode 100644 resources/views/components/modules/nnew.blade.php create mode 100644 resources/views/components/modules/no_apps.blade.php create mode 100644 resources/views/components/modules/paid.blade.php create mode 100644 resources/views/components/modules/pre_sale.blade.php create mode 100644 resources/views/components/modules/purchased.blade.php create mode 100644 resources/views/components/modules/raw_items.blade.php create mode 100644 resources/views/components/nl2br.blade.php create mode 100644 resources/views/components/pagination.blade.php create mode 100644 resources/views/components/payment_method.blade.php create mode 100644 resources/views/components/payment_method/hosted.blade.php create mode 100644 resources/views/components/payment_method/redirect.blade.php create mode 100644 resources/views/components/read-only.blade.php create mode 100644 resources/views/components/reports/detail.blade.php create mode 100644 resources/views/components/reports/detail/content/footer.blade.php create mode 100644 resources/views/components/reports/detail/content/header.blade.php create mode 100644 resources/views/components/reports/detail/table.blade.php create mode 100644 resources/views/components/reports/detail/table/body.blade.php create mode 100644 resources/views/components/reports/detail/table/footer.blade.php create mode 100644 resources/views/components/reports/detail/table/header.blade.php create mode 100644 resources/views/components/reports/detail/table/row.blade.php create mode 100644 resources/views/components/reports/fields.blade.php create mode 100644 resources/views/components/reports/filter.blade.php create mode 100644 resources/views/components/reports/print.blade.php create mode 100644 resources/views/components/reports/show.blade.php create mode 100644 resources/views/components/reports/summary.blade.php create mode 100644 resources/views/components/reports/summary/chart.blade.php create mode 100644 resources/views/components/reports/summary/content/footer.blade.php create mode 100644 resources/views/components/reports/summary/content/header.blade.php create mode 100644 resources/views/components/reports/summary/table.blade.php create mode 100644 resources/views/components/reports/summary/table/body.blade.php create mode 100644 resources/views/components/reports/summary/table/footer.blade.php create mode 100644 resources/views/components/reports/summary/table/header.blade.php create mode 100644 resources/views/components/reports/summary/table/row.blade.php create mode 100644 resources/views/components/script.blade.php create mode 100644 resources/views/components/script/declaration.blade.php create mode 100644 resources/views/components/script/exceptions/trackers.blade.php create mode 100644 resources/views/components/search-string.blade.php create mode 100644 resources/views/components/show/accordion/head.blade.php create mode 100644 resources/views/components/show/accordion/index.blade.php create mode 100644 resources/views/components/show/container.blade.php create mode 100644 resources/views/components/show/content/index.blade.php create mode 100644 resources/views/components/show/content/left.blade.php create mode 100644 resources/views/components/show/content/right.blade.php create mode 100644 resources/views/components/show/no-records.blade.php create mode 100644 resources/views/components/show/status.blade.php create mode 100644 resources/views/components/show/summary/index.blade.php create mode 100644 resources/views/components/show/summary/left.blade.php create mode 100644 resources/views/components/show/summary/right.blade.php create mode 100644 resources/views/components/suggestions.blade.php create mode 100644 resources/views/components/table/actions-mobile.blade.php create mode 100644 resources/views/components/table/actions.blade.php create mode 100644 resources/views/components/table/index.blade.php create mode 100644 resources/views/components/table/tbody.blade.php create mode 100644 resources/views/components/table/td.blade.php create mode 100644 resources/views/components/table/th.blade.php create mode 100644 resources/views/components/table/thead.blade.php create mode 100644 resources/views/components/table/tr.blade.php create mode 100644 resources/views/components/tabs/index.blade.php create mode 100644 resources/views/components/tabs/nav-link.blade.php create mode 100644 resources/views/components/tabs/nav-pin.blade.php create mode 100644 resources/views/components/tabs/nav.blade.php create mode 100644 resources/views/components/tabs/tab.blade.php create mode 100644 resources/views/components/tips/fixed.blade.php create mode 100644 resources/views/components/tips/relative.blade.php create mode 100644 resources/views/components/title.blade.php create mode 100644 resources/views/components/tooltip.blade.php create mode 100644 resources/views/components/transactions/index/bulk-action.blade.php create mode 100644 resources/views/components/transactions/index/buttons.blade.php create mode 100644 resources/views/components/transactions/index/content.blade.php create mode 100644 resources/views/components/transactions/index/more-buttons.blade.php create mode 100644 resources/views/components/transactions/index/recurring_templates.blade.php create mode 100644 resources/views/components/transactions/index/transaction.blade.php create mode 100644 resources/views/components/transactions/script.blade.php create mode 100644 resources/views/components/transactions/show/attachment.blade.php create mode 100644 resources/views/components/transactions/show/buttons.blade.php create mode 100644 resources/views/components/transactions/show/children.blade.php create mode 100644 resources/views/components/transactions/show/connect.blade.php create mode 100644 resources/views/components/transactions/show/content.blade.php create mode 100644 resources/views/components/transactions/show/create.blade.php create mode 100644 resources/views/components/transactions/show/more-buttons.blade.php create mode 100644 resources/views/components/transactions/show/schedule.blade.php create mode 100644 resources/views/components/transactions/show/template.blade.php create mode 100644 resources/views/components/transactions/show/transfer.blade.php create mode 100644 resources/views/components/transactions/template/default.blade.php create mode 100644 resources/views/components/transfers/script.blade.php create mode 100644 resources/views/components/transfers/show/attachment.blade.php create mode 100644 resources/views/components/transfers/show/buttons.blade.php create mode 100644 resources/views/components/transfers/show/content.blade.php create mode 100644 resources/views/components/transfers/show/create.blade.php create mode 100644 resources/views/components/transfers/show/more-buttons.blade.php create mode 100644 resources/views/components/transfers/show/template.blade.php create mode 100644 resources/views/components/transfers/show/transactions.blade.php create mode 100644 resources/views/components/transfers/template/default.blade.php create mode 100644 resources/views/components/transfers/template/second.blade.php create mode 100644 resources/views/components/transfers/template/third.blade.php create mode 100644 resources/views/components/update-alert.blade.php create mode 100644 resources/views/components/widgets/contact.blade.php create mode 100644 resources/views/components/widgets/header.blade.php create mode 100644 resources/views/components/widgets/invoice_history.blade.php create mode 100644 resources/views/components/widgets/last_payment.blade.php create mode 100644 resources/views/components/widgets/latest_invoices.blade.php create mode 100644 resources/views/components/widgets/outstanding_balance.blade.php create mode 100644 resources/views/components/widgets/payment_history.blade.php create mode 100644 resources/views/errors/403.blade.php create mode 100644 resources/views/errors/404.blade.php create mode 100644 resources/views/errors/500.blade.php create mode 100644 resources/views/install/database/create.blade.php create mode 100644 resources/views/install/language/create.blade.php create mode 100644 resources/views/install/requirements/show.blade.php create mode 100644 resources/views/install/settings/create.blade.php create mode 100644 resources/views/install/updates/edit.blade.php create mode 100644 resources/views/install/updates/index.blade.php create mode 100644 resources/views/livewire/common/search.blade.php create mode 100644 resources/views/livewire/menu/favorite.blade.php create mode 100644 resources/views/livewire/menu/favorites.blade.php create mode 100644 resources/views/livewire/menu/neww.blade.php create mode 100644 resources/views/livewire/menu/notifications.blade.php create mode 100644 resources/views/livewire/menu/profile.blade.php create mode 100644 resources/views/livewire/menu/settings.blade.php create mode 100644 resources/views/livewire/notification/browser/firefox.blade.php create mode 100644 resources/views/livewire/notification/browser/index.blade.php create mode 100644 resources/views/livewire/report/pin.blade.php create mode 100644 resources/views/livewire/report/pins.blade.php create mode 100644 resources/views/livewire/tab/pin.blade.php create mode 100644 resources/views/modals/accounts/create.blade.php create mode 100644 resources/views/modals/categories/create.blade.php create mode 100644 resources/views/modals/companies/edit.blade.php create mode 100644 resources/views/modals/currencies/create.blade.php create mode 100644 resources/views/modals/customers/create.blade.php create mode 100644 resources/views/modals/customers/edit.blade.php create mode 100644 resources/views/modals/documents/item_columns.blade.php create mode 100644 resources/views/modals/documents/payment.blade.php create mode 100644 resources/views/modals/invoices/email.blade.php create mode 100644 resources/views/modals/invoices/share.blade.php create mode 100644 resources/views/modals/items/create.blade.php create mode 100644 resources/views/modals/settings/transfer_template.blade.php create mode 100644 resources/views/modals/taxes/create.blade.php create mode 100644 resources/views/modals/transactions/connect.blade.php create mode 100644 resources/views/modals/transactions/email.blade.php create mode 100644 resources/views/modals/transactions/share.blade.php create mode 100644 resources/views/modals/vendors/create.blade.php create mode 100644 resources/views/modals/vendors/edit.blade.php create mode 100644 resources/views/modules/api_key/create.blade.php create mode 100644 resources/views/modules/home/index.blade.php create mode 100644 resources/views/modules/item/documentation.blade.php create mode 100644 resources/views/modules/item/pre_sale.blade.php create mode 100644 resources/views/modules/item/show.blade.php create mode 100644 resources/views/modules/my/index.blade.php create mode 100644 resources/views/modules/tiles/index.blade.php create mode 100644 resources/views/portal/dashboard/index.blade.php create mode 100644 resources/views/portal/invoices/finish.blade.php create mode 100644 resources/views/portal/invoices/index.blade.php create mode 100644 resources/views/portal/invoices/preview.blade.php create mode 100644 resources/views/portal/invoices/show.blade.php create mode 100644 resources/views/portal/invoices/signed.blade.php create mode 100644 resources/views/portal/payments/index.blade.php create mode 100644 resources/views/portal/payments/preview.blade.php create mode 100644 resources/views/portal/payments/show.blade.php create mode 100644 resources/views/portal/payments/signed.blade.php create mode 100644 resources/views/portal/profile/edit.blade.php create mode 100644 resources/views/purchases/bills/create.blade.php create mode 100644 resources/views/purchases/bills/edit.blade.php create mode 100644 resources/views/purchases/bills/index.blade.php create mode 100644 resources/views/purchases/bills/print.blade.php create mode 100644 resources/views/purchases/bills/show.blade.php create mode 100644 resources/views/purchases/recurring_bills/create.blade.php create mode 100644 resources/views/purchases/recurring_bills/edit.blade.php create mode 100644 resources/views/purchases/recurring_bills/index.blade.php create mode 100644 resources/views/purchases/recurring_bills/show.blade.php create mode 100644 resources/views/purchases/vendors/create.blade.php create mode 100644 resources/views/purchases/vendors/edit.blade.php create mode 100644 resources/views/purchases/vendors/index.blade.php create mode 100644 resources/views/purchases/vendors/show.blade.php create mode 100644 resources/views/reports/profit_loss/content/footer.blade.php create mode 100644 resources/views/reports/profit_loss/content/header.blade.php create mode 100644 resources/views/reports/profit_loss/detail.blade.php create mode 100644 resources/views/reports/profit_loss/table/footer.blade.php create mode 100644 resources/views/reports/profit_loss/table/header.blade.php create mode 100644 resources/views/reports/profit_loss/table/row.blade.php create mode 100644 resources/views/reports/tax_summary/content/footer.blade.php create mode 100644 resources/views/reports/tax_summary/content/header.blade.php create mode 100644 resources/views/reports/tax_summary/table/footer.blade.php create mode 100644 resources/views/reports/tax_summary/table/header.blade.php create mode 100644 resources/views/sales/customers/create.blade.php create mode 100644 resources/views/sales/customers/edit.blade.php create mode 100644 resources/views/sales/customers/index.blade.php create mode 100644 resources/views/sales/customers/show.blade.php create mode 100644 resources/views/sales/invoices/create.blade.php create mode 100644 resources/views/sales/invoices/edit.blade.php create mode 100644 resources/views/sales/invoices/index.blade.php create mode 100644 resources/views/sales/invoices/print_classic.blade.php create mode 100644 resources/views/sales/invoices/print_default.blade.php create mode 100644 resources/views/sales/invoices/print_modern.blade.php create mode 100644 resources/views/sales/invoices/show.blade.php create mode 100644 resources/views/sales/recurring_invoices/create.blade.php create mode 100644 resources/views/sales/recurring_invoices/edit.blade.php create mode 100644 resources/views/sales/recurring_invoices/index.blade.php create mode 100644 resources/views/sales/recurring_invoices/show.blade.php create mode 100644 resources/views/settings/categories/create.blade.php create mode 100644 resources/views/settings/categories/edit.blade.php create mode 100644 resources/views/settings/categories/index.blade.php create mode 100644 resources/views/settings/categories/sub_category.blade.php create mode 100644 resources/views/settings/company/edit.blade.php create mode 100644 resources/views/settings/currencies/create.blade.php create mode 100644 resources/views/settings/currencies/edit.blade.php create mode 100644 resources/views/settings/currencies/index.blade.php create mode 100644 resources/views/settings/default/edit.blade.php create mode 100644 resources/views/settings/email-templates/edit.blade.php create mode 100644 resources/views/settings/email/edit.blade.php create mode 100644 resources/views/settings/invoice/edit.blade.php create mode 100644 resources/views/settings/localisation/edit.blade.php create mode 100644 resources/views/settings/modules/edit.blade.php create mode 100644 resources/views/settings/navigation/edit.blade.php create mode 100644 resources/views/settings/schedule/edit.blade.php create mode 100644 resources/views/settings/taxes/create.blade.php create mode 100644 resources/views/settings/taxes/edit.blade.php create mode 100644 resources/views/settings/taxes/index.blade.php create mode 100644 resources/views/system_settings/databases/form.blade.php create mode 100644 resources/views/system_settings/databases/index.blade.php create mode 100644 resources/views/system_settings/partials/navigation.blade.php create mode 100644 resources/views/system_settings/sidebar/edit.blade.php create mode 100644 resources/views/system_settings/users/form.blade.php create mode 100644 resources/views/system_settings/users/index.blade.php create mode 100644 resources/views/widgets/account_balance.blade.php create mode 100644 resources/views/widgets/bank_feeds.blade.php create mode 100644 resources/views/widgets/bar_chart.blade.php create mode 100644 resources/views/widgets/cash_flow.blade.php create mode 100644 resources/views/widgets/currencies.blade.php create mode 100644 resources/views/widgets/donut_chart.blade.php create mode 100644 resources/views/widgets/line_chart.blade.php create mode 100644 resources/views/widgets/receivables_payables.blade.php create mode 100644 resources/views/wizard/companies/edit.blade.php create mode 100644 resources/views/wizard/currencies/index.blade.php create mode 100644 resources/views/wizard/finish/index.blade.php create mode 100644 routes/admin.php create mode 100644 routes/api.php create mode 100644 routes/channels.php create mode 100644 routes/common.php create mode 100644 routes/console.php create mode 100644 routes/guest.php create mode 100644 routes/install.php create mode 100644 routes/portal.php create mode 100644 routes/preview.php create mode 100644 routes/signed.php create mode 100644 routes/web.php create mode 100644 routes/wizard.php create mode 100644 safelist.txt create mode 100644 serviceworker.js create mode 100644 storage/app/.gitignore create mode 100644 storage/app/public/.gitignore create mode 100644 storage/app/temp/.gitignore create mode 100644 storage/app/uploads/.gitignore create mode 100644 storage/debugbar/.gitignore create mode 100644 storage/fonts/.gitignore create mode 100644 storage/framework/.gitignore create mode 100644 storage/framework/testing/.gitignore create mode 100644 tailwind.config.js create mode 100644 tests/CreatesApplication.php create mode 100644 tests/Feature/Auth/UsersTest.php create mode 100644 tests/Feature/Banking/AccountsTest.php create mode 100644 tests/Feature/Banking/DocumentTransactionsTest.php create mode 100644 tests/Feature/Banking/ReconciliationsTest.php create mode 100644 tests/Feature/Banking/SplitTransactionTest.php create mode 100644 tests/Feature/Banking/TransactionTaxesTest.php create mode 100644 tests/Feature/Banking/TransactionsTest.php create mode 100644 tests/Feature/Banking/TransfersTest.php create mode 100644 tests/Feature/Commands/BillReminderTest.php create mode 100644 tests/Feature/Commands/InvoiceReminderTest.php create mode 100644 tests/Feature/Commands/RecurringCheckTest.php create mode 100644 tests/Feature/Common/CompaniesTest.php create mode 100644 tests/Feature/Common/DashboardsTest.php create mode 100644 tests/Feature/Common/ItemsTest.php create mode 100644 tests/Feature/Common/ReportsTest.php create mode 100644 tests/Feature/Common/SourcesTest.php create mode 100644 tests/Feature/Document/CancelDocumentTest.php create mode 100644 tests/Feature/Email/TooManyEmailsSentTest.php create mode 100644 tests/Feature/FeatureTestCase.php create mode 100644 tests/Feature/PaymentTestCase.php create mode 100644 tests/Feature/Performance/N1QueryOptimizationTest.php create mode 100644 tests/Feature/Performance/VendorN1QueryTest.php create mode 100644 tests/Feature/Purchases/BillsTest.php create mode 100644 tests/Feature/Purchases/VendorsTest.php create mode 100644 tests/Feature/Sales/CustomersTest.php create mode 100644 tests/Feature/Sales/InvoicesTest.php create mode 100644 tests/Feature/Settings/CategoriesTest.php create mode 100644 tests/Feature/Settings/CurrenciesTest.php create mode 100644 tests/Feature/Settings/NavigationTest.php create mode 100644 tests/Feature/Settings/TaxesTest.php create mode 100644 tests/Feature/Wizard/CompaniesTest.php create mode 100644 tests/Feature/Wizard/CurrenciesTest.php create mode 100644 tests/TestCase.php create mode 100644 tests/Unit/ExampleTest.php create mode 100644 tests/Unit/Imports/Banking/TransfersImportTest.php create mode 100644 tests/Unit/UpdatesTest.php create mode 100644 tests/Unit/Utilities/CalculationToQuantityTest.php create mode 100644 tests/Unit/Utilities/DateImportParsingTest.php create mode 100644 web.config create mode 100644 webpack.mix.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..db545f77 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +.env +.git +.gitignore +.idea +.vscode +node_modules +database/database.sqlite +storage/debugbar +storage/framework/cache/data/* +storage/framework/sessions/* +storage/framework/views/* +storage/logs/* +tests +npm-debug.log +yarn-error.log + diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..4025c3ea --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at https://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.yml] +indent_size = 2 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..f7b24102 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,26 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text eol=lf + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.c text +*.h text + +# Declare files that will always have CRLF line endings on checkout. +*.sln text eol=crlf + +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary +*.gif binary +*.otf binary +*.eot binary +*.svg binary +*.ttf binary +*.woff binary +*.woff2 binary + +*.css linguist-vendored +*.scss linguist-vendored +*.js linguist-vendored +CHANGELOG.md export-ignore diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..6914934e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +patreon: akaunting \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..fd326bf6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,21 @@ +Please, submit only real issues. Use the forum for support, feature requests, proposals, new versions, help etc. https://akaunting.com/forum + +### Steps to reproduce the issue + + + +### Expected result + + + +### Actual result + + + +### System information (Akaunting, PHP versions) + + + +### Additional comments + + diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 00000000..c58a9459 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,63 @@ +name: Bug report +description: File a bug report related to Akaunting +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: input + id: akaunting + attributes: + label: Akaunting version + description: "Please provide the full Akaunting version of your installation." + placeholder: "2.1.20" + validations: + required: true + - type: input + id: php + attributes: + label: PHP version + description: "Please provide the full PHP version that is powering Akaunting." + placeholder: "7.4.10" + validations: + required: true + - type: input + id: os + attributes: + label: Operating system + description: "Which operating system do you use? Please provide the version as well." + placeholder: "Ubuntu 20.04" + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: Which steps do we need to take to reproduce this error? + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected result + description: What is the expected result? + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual result + description: What is the actual result? + validations: + required: true + - type: textarea + id: comments + attributes: + label: Additional comments + description: Anything else we should know about? + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Copy and paste any relevant log output. No need for backticks. + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..3cb53d44 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Feature request + url: https://akaunting.com/forum + about: 'For ideas or feature requests, start a new discussion' + - name: Support, help, and other + url: https://akaunting.com/support + about: 'This repository is only for reporting bugs' + - name: Documentation + url: https://github.com/akaunting/docs + about: For documentation improvements, open a pull request at the akaunting/docs repository diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..842a1409 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,51 @@ +name: Tests + +on: + push: + pull_request: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +jobs: + tests: + + name: PHP ${{ matrix.php }} + + runs-on: ubuntu-latest + + strategy: + matrix: + php: ['8.1', '8.2', '8.3'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache Composer + uses: actions/cache@v4 + with: + path: ~/.composer/cache/files + key: php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: bcmath, ctype, dom, fileinfo, intl, gd, json, mbstring, pdo, pdo_sqlite, openssl, sqlite, xml, zip + coverage: none + + - name: Copy .env + run: cp .env.testing .env + + - name: Install NPM + run: npm install + + - name: Compile assets + run: npm run production + + - name: Install Composer + run: composer test + + - name: Execute tests + run: php artisan test --parallel diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1c68dc9d --- /dev/null +++ b/.gitignore @@ -0,0 +1,98 @@ +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +/node_modules +/public/hot +/public/storage +/public/js +/public/mix-manifest.json +/storage/*.key +/vendor +/.idea +/.history +/.vscode +/.claude +/.vagrant +Homestead.json +Homestead.yaml +npm-debug.log +.env +robots.txt +_ide_helper.php +_ide_helper_models.php +.phpstorm.meta.php +/storage/debugbar/* +.phpunit.result.cache +modules/* +!modules/.gitkeep +.laravelstatsrc +.DS_Store diff --git a/.htaccess b/.htaccess new file mode 100644 index 00000000..cd495c3e --- /dev/null +++ b/.htaccess @@ -0,0 +1,49 @@ +# Prevent Directory Listing + + IndexIgnore * + + + + # Prevent Directory Listing + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Prevent Direct Access to Protected Files + + # Apache 2.2 syntax + + Order deny,allow + Deny from all + + # Apache 2.4 syntax + + Require all denied + + + + # Prevent Direct Access to Protected Files (OpenLiteSpeed syntax) + RewriteCond %{REQUEST_URI} (^|/)(\.env|\.log|artisan)$ [NC] + RewriteRule .* - [F,L] + + # Prevent Direct Access To Protected Folders + RewriteRule ^(app|bootstrap|config|database|overrides|resources|routes|storage|tests)/(.*) / [L,R=301] + + # Prevent Direct Access To modules/vendor Folders Except Assets + RewriteRule ^(modules|vendor)/(.*)\.((?!ico|gif|jpg|jpeg|png|js\b|css|less|sass|font|woff|woff2|eot|ttf|svg|xls|xlsx).)*$ / [L,R=301] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^(.*)/$ /$1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..0fbc1f0c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,72 @@ +# CaiWuXiTong 团队通用规则 + +适用范围:本仓库所有成员、所有分支,以及使用 Codex 等 AI 编程工具完成的工作。 + +## 1. 核心原则 + +- 本项目是财务系统。金额、权限、审批、数据导入、数据库迁移和生产配置均属于高风险内容。 +- AI 是协作工具,不是审批人。AI 生成的代码、命令和结论必须由提交人检查并完成实际验证。 +- 不确定当前环境、分支、命令作用或数据范围时,停止操作并向负责人或 Codex 说明现场,不要猜。 +- 一次只完成一个明确任务;不顺手重构、升级依赖或修改无关模块。 + +## 2. 分支与提交 + +- `main` 是稳定主线,禁止直接推送、禁止强推、禁止直接修改。 +- 每项任务都必须从最新 `main` 新建分支;不得长期共用或复用旧任务分支。 +- 分支命名:新功能用 `feature/用户名-任务名`,修复用 `fix/用户名-问题名`,紧急修复用 `hotfix/用户名-问题名`。 +- 示例:`feature/dodo-login`、`fix/wangshu-amount-rounding`。 +- 一个分支只做一个任务。任务完成并合并后,删除该分支;下一项任务重新从 `main` 新建。 +- 提交前必须查看 `git status` 和 `git diff`,确认没有把别人的改动、日志、构建产物、`.env` 或敏感文件带进去。 +- 禁止使用 `git push --force`、`git reset --hard`、`git clean -fd`,除非负责人明确给出目标和操作步骤。 + +## 3. 新人使用 AI 的固定流程 + +1. 先确认任务目标、验收方式、影响范围和不能触碰的模块。 +2. 进入正确项目和分支,先执行 `git status`;看到陌生改动,先停止并说明情况。 +3. 向 AI 提供:任务目标、当前分支、报错或现象、相关文件、不可修改的范围。 +4. 要求 AI 先说明改动计划、涉及文件、风险和验证步骤;确认后再修改。 +5. 修改后查看 diff,并要求 AI 用普通语言解释“改了什么、为什么、怎样测试”。 +6. 在本地或测试环境完成最小有效验证,记录实际结果。 +7. 推送任务分支并发起 PR;不得自行绕过 PR 合并到 `main`。 + +推荐向 AI 提问的模板: + +```text +项目:CaiWuXiTong(Laravel/PHP 财务系统)。 +任务:<要实现的功能或要修复的问题>。 +当前分支:<分支名>。 +现场:<报错、复现步骤、相关文件>。 +边界:不要修改 <模块/表/配置>;不要执行删除、部署、生产数据库操作。 +要求:先列出改动计划、风险和验证步骤,等我确认后再改。 +``` + +## 4. PR 与合并 + +- 所有任务分支必须通过 PR 合并到 `main`。 +- PR 作者必须填写:做了什么、Codex 改了哪些文件、怎样测试、是否涉及金额/权限/数据库/配置。 +- PR 至少由另一位成员检查后才能批准;作者不得把“AI 已完成”当作测试结果。 +- 涉及金额计算、权限、导入、数据库迁移、账户或系统配置时,必须在 PR 中明确标注,并由负责人最终确认。 +- 使用仓库默认的 Squash merge(压缩合并)。合并由负责人或指定合并人执行。 +- PR 合并后分支自动删除;不要把已合并分支继续用于新任务。 + +## 5. 测试与问题处理 + +- 修改完成不等于可用。至少验证正常流程、错误输入/无权限场景和本次改动影响的旧功能。 +- 出现冲突、测试失败或 PR 显示落后 `main` 时,不要自行猜命令。先让 AI 只分析同步方案,禁止强推和 rebase;必要时找负责人处理。 +- 提交问题时必须写清:环境、账号角色、复现步骤、实际结果、期望结果和报错截图/日志(脱敏后)。 + +## 6. 数据与安全红线 + +- 不得提交 `.env`、密码、Token、SSH 私钥、数据库连接串、真实财务数据、客户数据或生产日志。 +- 不得把上述内容粘贴到 AI 对话、工单、PR、聊天群或截图中。 +- 不得在不确认环境和影响范围时执行数据库写入、迁移、删除、导入、部署或服务器命令。 +- 任何包含金额、账户、登录凭据、权限或数据库连接的信息都按敏感数据处理。 +- 发现误提交、泄露或错误操作,立即告知负责人;不要自行删除记录掩盖问题。 + +## 7. 本仓库的最小验证要求 + +- 前端改动:至少检查目标页面、正常交互和错误提示。 +- 后端改动:至少执行相关测试或启动后走一次目标接口/页面。 +- 数据库迁移:新人不得自行在共享环境执行;先在 PR 说明影响、验证方式和回滚思路。 +- 未完成验证时,PR 必须明确写“未验证”及原因,不能写“应该没问题”。 + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..5ae8f60a --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,78 @@ +License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved. +“Business Source License” is a trademark of MariaDB Corporation Ab. + +Parameters + +Licensor: Akaunting, Inc. + +Licensed Work: Akaunting + +Additional Use Grant: You may make production use of the Licensed Work, provided + you do not use it in any of the following ways: + + (1) Use the Licensed Work with a total of more than two users or + one company or one thousand invoices. + + (2) Remove the name, logo, copyright, or other branding material + from the Licensed Work to create a "rebranded" or "white labeled" + version to distribute as part of any commercial or non-profit + product or service. + + (3) Use the Licensed Work for an Accounting Service. An + "Accounting Service" is a commercial offering that allows third + parties (other than your employees and contractors) to access + the functionality of the Licensed Work so that such third + parties directly benefit from the accounting or invoicing + features of the Licensed Work. + +Change Date: Four years from the date the Licensed Work is published. + +Change License: GPLv3 + +For information about alternative licensing arrangements for the Licensed Work, +please visit: https://akaunting.com/plans-on-premise + +Notice + +Business Source License 1.1 + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. diff --git a/LocalValetDriver.php b/LocalValetDriver.php new file mode 100644 index 00000000..98309a61 --- /dev/null +++ b/LocalValetDriver.php @@ -0,0 +1,23 @@ + [ + 'name' => 'general.enable', + 'message' => 'bulk_actions.message.enable', + 'permission' => 'update-common-items', + ], + 'disable' => [ + 'name' => 'general.disable', + 'message' => 'bulk_actions.message.disable', + 'permission' => 'update-common-items', + ], + 'delete' => [ + 'name' => 'general.delete', + 'message' => 'bulk_actions.message.delete', + 'permission' => 'delete-common-items', + ], + 'export' => [ + 'name' => 'general.export', + 'message' => 'bulk_actions.message.export', + 'type' => 'download' + ], + 'download' => [ + 'name' => 'general.download', + 'message' => 'bulk_actions.message.download', + 'type' => 'download', + ], + ]; + + public $icons = [ + 'enable' => 'check_circle', + 'disable' => 'hide_source', + 'delete' => 'delete', + 'duplicate' => 'file_copy', + 'export' => 'file_download', + 'download' => 'download', + 'reconcile' => 'published_with_changes', + 'unreconcile' => 'layers_clear', + 'received' => 'call_received', + 'cancelled' => 'cancel', + 'sent' => 'send', + 'approved' => 'approval', + 'refused' => 'do_not_disturb_on', + 'issued' => 'mark_email_read', + 'confirmed' => 'thumb_up_alt', + ]; + + public $messages = [ + 'general' => 'bulk_actions.success.general', + 'enable' => 'messages.success.enabled', + 'disable' => 'messages.success.disabled', + 'delete' => 'messages.success.deleted', + 'duplicate' => 'messages.success.duplicated', + 'invite' => 'messages.success.invited', + 'end' => 'messages.success.ended', + ]; + + public function getSelectedRecords($request, $relationships = null) + { + if (empty($relationships)) { + $model = $this->model::query(); + } else { + $relationships = Arr::wrap($relationships); + + $model = $this->model::with($relationships); + } + + return $model->find($this->getSelectedInput($request)); + } + + public function getSelectedInput($request) + { + return $request->get('selected', []); + } + + public function getUpdateRequest($request) + { + foreach ($request->all() as $key => $value) { + if (empty($value)) { + unset($request[$key]); + } + } + + return $request; + } + + /** + * Generate a response based on request type like HTML, JSON, or anything else. + * + * @param string $view + * @param array $data + * + * @return \Illuminate\Http\Response + */ + public function response($view, $data = []) + { + $method = request()->get('handle', 'edit'); + + $handle = $this->actions[$method]['handle'] ?? 'update'; + $url = route('bulk-actions.action', $this->path); + + $html = view('components.index.bulkaction.modal', [ + 'url' => $url, + 'handle' => $handle, + 'selected' => $data['selected'] ?? $this->getSelectedInput(request()), + 'html' => view($view, $data)->render(), + ])->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => '', + 'data' => [ + 'title' => $this->findTranslation($this->text), + 'path' => $url, + 'handle' => $handle, + ], + 'html' => $html, + ]); + } + + /** + * Duplicate the specified resource. + * + * @param $request + * + * @return Response + */ + public function duplicate($request) + { + $items = $this->getSelectedRecords($request); + + foreach ($items as $item) { + $item->duplicate(); + } + } + + /** + * Enable the specified resource. + * + * @param $request + * + * @return Response + */ + public function enable($request) + { + $items = $this->getSelectedRecords($request); + + foreach ($items as $item) { + $item->enabled = true; + $item->save(); + } + } + + /** + * Disable the specified resource. + * + * @param $request + * + * @return Response + */ + public function disable($request) + { + $items = $this->getSelectedRecords($request); + + foreach ($items as $item) { + $item->enabled = false; + $item->save(); + } + } + + /** + * Remove the specified resource from storage. + * + * @param $request + * + * @return Response + */ + public function delete($request) + { + $this->destroy($request); + } + + /** + * Remove the specified resource from storage. + * + * @param $request + * + * @return Response + */ + public function destroy($request) + { + $items = $this->getSelectedRecords($request); + + foreach ($items as $item) { + $item->delete(); + } + } + + public function disableContacts($request) + { + $contacts = $this->getSelectedRecords($request, 'user'); + + foreach ($contacts as $contact) { + try { + $this->dispatch(new UpdateContact($contact, request()->merge(['enabled' => 0]))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function deleteContacts($request) + { + $contacts = $this->getSelectedRecords($request, 'user'); + + foreach ($contacts as $contact) { + try { + $this->dispatch(new DeleteContact($contact)); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function deleteTransactions($request) + { + $transactions = $this->getSelectedRecords($request, 'category'); + + foreach ($transactions as $transaction) { + try { + $this->dispatch(new DeleteTransaction($transaction)); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + /** + * Import the excel file or catch errors + * + * @param $class + * @param $request + * @param $translation + * + * @return array + */ + public function importExcel($class, $request, $translation) + { + return Import::fromExcel($class, $request, $translation); + } + + /** + * Export the excel file or catch errors + * + * @param $class + * @param $translation + * @param $extension + * + * @return mixed + */ + public function exportExcel($class, $translation, $extension = 'xlsx') + { + return Export::toExcel($class, $translation, $extension); + } + + /** + * Download the pdf file or catch errors + * + * @param $class + * @param $file_name + * @param $translation + * + * @return mixed + */ + public function downloadPdf($selected, $class, $file_name, $translation) + { + try { + if (should_queue()) { + $batch[] = new CreateZipForDownload($selected, $class, $file_name); + + $batch[] = new CreateMediableForDownload(user(), $file_name, $translation); + + Bus::chain($batch)->onQueue('jobs')->dispatch(); + + $message = trans('messages.success.download_queued', ['type' => $translation]); + + flash($message)->success(); + + return back(); + } else { + $this->dispatch(new CreateZipForDownload($selected, $class, $file_name)); + + $folder_path = 'app' . DIRECTORY_SEPARATOR . 'temp' . DIRECTORY_SEPARATOR . company_id() . DIRECTORY_SEPARATOR . 'bulk_actions' . DIRECTORY_SEPARATOR; + + return response()->download(get_storage_path($folder_path . $file_name . '.zip'))->deleteFileAfterSend(true); + } + } catch (Throwable $e) { + report($e); + flash($e->getMessage())->error()->important(); + + return back(); + } + } +} diff --git a/app/Abstracts/Commands/Module.php b/app/Abstracts/Commands/Module.php new file mode 100644 index 00000000..910312dc --- /dev/null +++ b/app/Abstracts/Commands/Module.php @@ -0,0 +1,86 @@ +alias = Str::kebab($this->argument('alias')); + $this->company_id = (int) $this->argument('company'); + $this->locale = $this->argument('locale'); + } + + protected function changeRuntime() + { + $this->old_company_id = company_id(); + + company($this->company_id)->makeCurrent(); + + app()->setLocale($this->locale); + + // Disable model cache + config(['laravel-model-caching.enabled' => false]); + } + + protected function revertRuntime() + { + if (empty($this->old_company_id)) { + return; + } + + company($this->old_company_id)->makeCurrent(); + } + + protected function getModel() + { + $this->model = Model::companyId($this->company_id)->alias($this->alias)->first(); + + return $this->model; + } + + protected function createHistory($action) + { + if (empty($this->model)) { + return; + } + + ModelHistory::create([ + 'company_id' => $this->company_id, + 'module_id' => $this->model->id, + 'version' => module($this->alias)->get('version'), + 'description' => trans('modules.' . $action, ['module' => $this->alias]), + 'created_from' => source_name(), + 'created_by' => user_id(), + ]); + } + + /** + * Get the console command arguments. + * + * @return array + */ + protected function getArguments() + { + return [ + ['alias', InputArgument::REQUIRED, 'Module alias.'], + ['company', InputArgument::REQUIRED, 'Company ID.'], + ]; + } +} diff --git a/app/Abstracts/Event.php b/app/Abstracts/Event.php new file mode 100644 index 00000000..8849b3d7 --- /dev/null +++ b/app/Abstracts/Event.php @@ -0,0 +1,12 @@ +ids = $ids; + $this->fields = $this->fields(); + $this->column_validations = $this->columnValidations(); + $this->column_count = config('excel.exports.column_count'); + $this->row_count = config('excel.exports.row_count'); + $this->user = user(); + } + + public function title(): string + { + return Str::snake((new \ReflectionClass($this))->getShortName()); + } + + public function fields(): array + { + return []; + } + + public function map($model): array + { + $map = []; + + $date_fields = ['paid_at', 'invoiced_at', 'billed_at', 'due_at', 'issued_at', 'transferred_at']; + + $evil_chars = ['=', '+', '-', '@']; + + foreach ($this->fields as $field) { + $value = $model->$field; + + // created_by is equal to the owner id. Therefore, the value in export is owner email. + if ($field == 'created_by') { + $value = $model->owner->email ?? null; + } + + if (in_array($field, $date_fields)) { + $value = ExcelDate::PHPToExcel(Date::parse($value)->format('Y-m-d')); + } + + // Prevent CSV injection (OWASP CSV Injection) + if (is_string($value)) { + // Neutralize embedded newlines and tabs that could inject additional rows + $value = str_replace(["\t", "\r", "\n"], ' ', $value); + + // Prefix with apostrophe to neutralize formula characters, + // including whitespace-prefix bypass (e.g., " =SUM(...)") + if (Str::startsWith($value, $evil_chars) || Str::startsWith(ltrim($value), $evil_chars)) { + $value = "'" . $value; + } + } + + $map[] = $value; + } + + return $map; + } + + public function headings(): array + { + event(new HeadingsPreparing($this)); + + return $this->fields; + } + + public function prepareRows($rows) + { + event(new RowsPreparing($this, $rows)); + + return $rows; + } + + public function preferredLocale() + { + if (! $this->user) { + return setting('default.locale'); + } + + return $this->user->locale; + } + + public function failed(\Throwable $exception): void + { + if (! $this->user) { + return; + } + + $this->user->notify(new ExportFailed($exception->getMessage())); + } + + public function columnValidations(): array + { + return []; + } + + public function afterSheet($event) + { + $condition = class_exists($this->request_class) + ? ! ($request = new $this->request_class) instanceof FormRequest + : true; + + + if (empty($this->column_validations) && $condition) { + return []; + } + + $alphas = range('A', 'Z'); + + foreach ($this->fields as $key => $value) { + $drop_column = $alphas[$key]; + + if ($this->setColumnValidations($drop_column, $event, $value)) { + continue; + }; + + $this->validationWarning($drop_column, $event, $value, $request); + } + } + + public function setColumnValidations($drop_column, $event, $value) + { + if (! isset($this->column_validations[$value])) { + return false; + } + + $column_validation = $this->column_validations[$value]; + + // set dropdown list for first data row + $validation = $event->sheet->getCell("{$drop_column}2")->getDataValidation(); + + $validation->setType($column_validation['type'] ?? DataValidation::TYPE_LIST); + + if (empty($column_validation['hide_prompt'])) { + $validation->setAllowBlank($column_validation['allow_blank'] ?? false); + $validation->setShowInputMessage($column_validation['show_input_message'] ?? true); + $validation->setPromptTitle($column_validation['prompt_title'] ?? null); + $validation->setPrompt($column_validation['prompt'] ?? null); + } + + if (empty($column_validation['hide_error'])) { + $validation->setErrorStyle($column_validation['error_style'] ?? DataValidation::STYLE_INFORMATION); + $validation->setShowErrorMessage($column_validation['show_error_message'] ?? true); + $validation->setErrorTitle($column_validation['error_title'] ?? null); + $validation->setError($column_validation['error'] ?? null); + } + + if (! empty($column_validation['options'])) { + $validation->setFormula1(sprintf('"%s"', implode(',', $column_validation['options']))); + $validation->setShowDropDown($column_validation['show_dropdown'] ?? true); + } + + // clone validation to remaining rows + for ($i = 3; $i <= $this->row_count; $i++) { + $event->sheet->getCell("{$drop_column}{$i}")->setDataValidation(clone $validation); + } + + // set columns to autosize + for ($i = 1; $i <= $this->column_count; $i++) { + $column = Coordinate::stringFromColumnIndex($i); + $event->sheet->getColumnDimension($column)->setAutoSize(true); + } + + return true; + } + + public function validationWarning($drop_column, $event, $value, $request) + { + $rules = $this->prepareRules($request->rules()); + + if (! isset($rules[$value])) { + return false; + } + + $rule = explode('|', $rules[$value]); + + $prompt = ''; + + foreach ($rule as $r) { + if (strpos($r, 'unique') !== false) { + $r = 'unique'; + } + + if (strpos($r, 'amount') !== false) { + $r = 'double'; + } + + if (strpos($r, 'date_format') !== false) { + $prompt = $prompt . trans('validation.date_format', [ + 'attribute' => $value, + 'format' => str_replace('date_format:', '', $r) + ]) . ' '; + } + + if (strpos($r, 'required_without') !== false) { + $prompt = $prompt . trans('validation.required_without', [ + 'attribute' => $value, + 'values' => str_replace('required_without:', '', $r) + ]) . ' '; + } + + if (in_array($r, ['required', 'email', 'integer', 'unique', 'date_format', 'double'])) { + $prompt = $prompt . trans('validation.' . $r, ['attribute' => $value]) . ' '; + } + } + + $validation = $event->sheet->getCell("{$drop_column}2")->getDataValidation(); + + $validation->setAllowBlank(false); + $validation->setShowInputMessage(true); + $validation->setPromptTitle(trans('general.validation_warning')); + $validation->setPrompt($prompt ?? null); + + for ($i = 3; $i <= $this->row_count; $i++) { + $event->sheet->getCell("{$drop_column}{$i}")->setDataValidation(clone $validation); + } + + for ($i = 1; $i <= $this->column_count; $i++) { + $column = Coordinate::stringFromColumnIndex($i); + $event->sheet->getColumnDimension($column)->setAutoSize(true); + } + } + + public function getDropdownOptions($model, $select): array + { + $limit = 253; + $selects = []; + $totalLength = 0; + + $model::select($select)->each(function ($row) use (&$selects, &$totalLength, $limit, $select) { + $nameLength = mb_strlen($row->$select); + + if ($totalLength + $nameLength <= $limit && $nameLength !== 0) { + $selects[] = $row->$select; + $totalLength += $nameLength; + } + }); + + return $selects; + } + + /** + * You can override this method to add custom rules for each row. + */ + public function prepareRules(array $rules): array + { + return $rules; + } + + public function registerEvents(): array + { + return [ + AfterSheet::class => function(AfterSheet $event) { + $this->afterSheet($event); + }, + ]; + } +} diff --git a/app/Abstracts/Factory.php b/app/Abstracts/Factory.php new file mode 100644 index 00000000..668e3681 --- /dev/null +++ b/app/Abstracts/Factory.php @@ -0,0 +1,76 @@ + 'array']); + + // TODO: this location was put to make US | for "gmail.co.uk" issue + $this->faker = \Faker\Factory::create(); + + $this->setUser(); + + $this->setCompany(); + } + + public function getCompanyUsers(): array + { + return $this->company->users()->enabled()->get()->pluck('id')->toArray(); + } + + public function company(int $id): static + { + Cache::put('state_company_id', $id, Date::now()->addHour(6)); + + return $this->state([ + 'company_id' => $id, + ]); + } + + public function setUser(): void + { + $this->user = user_model_class()::first(); + } + + public function setCompany(): void + { + $state_id = Cache::get('state_company_id'); + + $this->company = ! is_null($state_id) ? company($state_id) : $this->user->companies()->first(); + + $this->company->makeCurrent(); + + //app('url')->defaults(['company_id' => $this->company->id]); + } + + public function getRawAttribute($key) + { + $raw = $this->state([])->getExpandedAttributes(null); + + return $raw[$key] ?? null; + } +} diff --git a/app/Abstracts/Http/ApiController.php b/app/Abstracts/Http/ApiController.php new file mode 100644 index 00000000..075f2573 --- /dev/null +++ b/app/Abstracts/Http/ApiController.php @@ -0,0 +1,186 @@ +assignPermissionsToController(); + } + + /** + * Create the response for when a request fails validation. + * + * @param \Illuminate\Http\Request $request + * @param array $errors + * @return \Symfony\Component\HttpFoundation\Response + */ + protected function buildFailedValidationResponse(Request $request, array $errors) + { + if ($request->expectsJson()) { + throw new ResourceException('Validation Error', $errors); + } + + return redirect()->to($this->getRedirectUrl())->withInput($request->input())->withErrors($errors, $this->errorBag()); + } + + /** + * Respond with a location and a created resource. + * + * @param string $location + * @param object $resource + * + * @return \Illuminate\Http\JsonResponse + */ + public function created($location, $resource): JsonResponse + { + return $resource + ->response() + ->setStatusCode(201) + ->header('Location', $location); + } + + /** + * Respond with a location and an accepted resource. + * + * @param string $location + * @param object $resource + * + * @return \Illuminate\Http\JsonResponse + */ + public function accepted($location, $resource): JsonResponse + { + return $resource + ->response() + ->setStatusCode(202) + ->header('Location', $location); + } + + /** + * Respond with empty content. + * + * @return \Illuminate\Http\Response + */ + public function noContent(): Response + { + return (new Response) + ->setStatusCode(204); + } + + /** + * Return an error response. + * + * @param string $message + * @param int $statusCode + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + * + * @return void + */ + public function error($message, $statusCode) + { + throw new HttpException($statusCode, $message); + } + + /** + * Return a 404 not found error. + * + * @param string $message + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + * + * @return void + */ + public function errorNotFound($message = 'Not Found') + { + $this->error($message, 404); + } + + /** + * Return a 400 bad request error. + * + * @param string $message + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + * + * @return void + */ + public function errorBadRequest($message = 'Bad Request') + { + $this->error($message, 400); + } + + /** + * Return a 403 forbidden error. + * + * @param string $message + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + * + * @return void + */ + public function errorForbidden($message = 'Forbidden') + { + $this->error($message, 403); + } + + /** + * Return a 500 internal server error. + * + * @param string $message + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + * + * @return void + */ + public function errorInternal($message = 'Internal Error') + { + $this->error($message, 500); + } + + /** + * Return a 401 unauthorized error. + * + * @param string $message + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + * + * @return void + */ + public function errorUnauthorized($message = 'Unauthorized') + { + $this->error($message, 401); + } + + /** + * Return a 405 method not allowed error. + * + * @param string $message + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + * + * @return void + */ + public function errorMethodNotAllowed($message = 'Method Not Allowed') + { + $this->error($message, 405); + } +} diff --git a/app/Abstracts/Http/Controller.php b/app/Abstracts/Http/Controller.php new file mode 100644 index 00000000..f4ec9e30 --- /dev/null +++ b/app/Abstracts/Http/Controller.php @@ -0,0 +1,272 @@ +assignPermissionsToController(); + } + + /** + * Generate a pagination collection. + * + * @param array|Collection $items + * @param int $perPage + * @param int $page + * @param array $options + * + * @return LengthAwarePaginator + */ + public function paginate($items, $perPage = null, $page = null, $options = []) + { + $perPage = $perPage ?: (int) request('limit', setting('default.list_limit', '25')); + + $page = $page ?: (Paginator::resolveCurrentPage() ?: 1); + + $items = $items instanceof Collection ? $items : Collection::make($items); + + return new LengthAwarePaginator($items->forPage($page, $perPage), $items->count(), $perPage, $page, $options); + } + + /** + * Generate a response based on request type like HTML, JSON, or anything else. + * + * @param string $view + * @param array $data + * + * @return \Illuminate\Http\Response + */ + public function response($view, $data = []) + { + $class_name = str_replace('Controllers', 'Responses', get_class($this)); + + if (class_exists($class_name)) { + $response = new $class_name($view, $data); + } else { + $response = new class($view, $data) extends Response {}; + } + + return $response; + } + + /** + * Import the excel file or catch errors + * + * @param $class + * @param $request + * @param $translation + * + * @return array + */ + public function importExcel($class, $request, $translation) + { + return Import::fromExcel($class, $request, $translation); + } + + /** + * Export the excel file or catch errors + * + * @param $class + * @param $translation + * @param $extension + * + * @return mixed + */ + public function exportExcel($class, $translation, $extension = 'xlsx') + { + return Export::toExcel($class, $translation, $extension); + } + + public function setActiveTabForDocuments(): void + { + // Added this method to set the active tab for documents + if (! request()->has('list_records') && ! request()->has('search')) { + $tab_pins = setting('favorites.tab.' . user()->id, []); + $tab_pins = ! empty($tab_pins) ? json_decode($tab_pins, true) : []; + + if (! empty($tab_pins) && ! empty($tab_pins[$this->type])) { + $data = config('type.document.' . $this->type . '.route.params.' . $tab_pins[$this->type]); + + if (! empty($data)) { + request()->merge($data); + } + } + } + + if (! request()->has('list_records') && request()->has('search')) { + $status = $this->getSearchStringValue('status'); + + if (empty($status)) { + $tab_pins = setting('favorites.tab.' . user()->id, []); + $tab_pins = ! empty($tab_pins) ? json_decode($tab_pins, true) : []; + + if (! empty($tab_pins) && (($tab_pins[$this->type] ?? null) === 'all')) { + request()->offsetSet('list_records', 'all'); + } + } + } + + if (request()->get('list_records') == 'all') { + return; + } + + $status = $this->getSearchStringValue('status'); + + if (empty($status)) { + $search = config('type.document.' . $this->type . '.route.params.unpaid.search'); + + request()->offsetSet('search', $search); + request()->offsetSet('programmatic', '1'); + } else { + $unpaid = str_replace('status:', '', config('type.document.' . $this->type . '.route.params.unpaid.search')); + $draft = str_replace('status:', '', config('type.document.' . $this->type . '.route.params.draft.search')); + + if (($status == $unpaid) || ($status == $draft)) { + return; + } + + request()->offsetSet('list_records', 'all'); + } + } + + public function setActiveTabForTransactions(): void + { + // Added this method to set the active tab for transactions + if (! request()->has('list_records') && ! request()->has('search')) { + $tab_pins = setting('favorites.tab.' . user()->id, []); + $tab_pins = ! empty($tab_pins) ? json_decode($tab_pins, true) : []; + + if (! empty($tab_pins) && ! empty($tab_pins['transactions'])) { + $data = config('type.transaction.transactions.route.params.' . $tab_pins['transactions']); + + if (! empty($data)) { + request()->merge($data); + } + } + } + + if (! request()->has('list_records') && request()->has('search')) { + $type = $this->getSearchStringValue('type'); + + if (empty($type)) { + $tab_pins = setting('favorites.tab.' . user()->id, []); + $tab_pins = ! empty($tab_pins) ? json_decode($tab_pins, true) : []; + + if (! empty($tab_pins) && (($tab_pins['transactions'] ?? null) === 'all')) { + request()->offsetSet('list_records', 'all'); + } + } + } + + if (request()->get('list_records') == 'all') { + return; + } + + $type = $this->getSearchStringValue('type'); + + if (empty($type)) { + $search = config('type.transaction.transactions.route.params.income.search'); + + request()->offsetSet('search', $search); + request()->offsetSet('programmatic', '1'); + } else { + $income = str_replace('type:', 'income', config('type.transaction.transactions.route.params.income.search')); + $expense = str_replace('type:', 'expense', config('type.transaction.transactions.route.params.expense.search')); + + if (($type == $income) || ($type == $expense)) { + return; + } + + request()->offsetSet('list_records', 'all'); + } + } + + public function setActiveTabForCategories(): void + { + if (! request()->has('list_records') && ! request()->has('search')) { + $tab_pins = setting('favorites.tab.' . user()->id, []); + $tab_pins = ! empty($tab_pins) ? json_decode($tab_pins, true) : []; + + if (! empty($tab_pins) && ! empty($tab_pins['categories'])) { + $tab = $tab_pins['categories']; + + if (! empty($tab)) { + + request()->offsetSet('list_records', $tab); + request()->offsetSet('programmatic', '1'); + } + } + } + + if (! request()->has('list_records') && request()->has('search')) { + $type = $this->getSearchStringValue('type'); + + if (empty($type)) { + $tab_pins = setting('favorites.tab.' . user()->id, []); + $tab_pins = ! empty($tab_pins) ? json_decode($tab_pins, true) : []; + + if (! empty($tab_pins) && (($tab_pins['categories'] ?? null) === 'all')) { + request()->offsetSet('list_records', 'all'); + } + } + } + + if (request()->get('list_records') == 'all') { + return; + } + + $types = $this->getSearchStringValue('type'); + + if (!empty($types)) { + $types = is_string($types) ? explode(',', $types) : $types; + + $tab = config('type.category.' . $types[0] . '.group') ? config('type.category.' . $types[0] . '.group') : 'all'; + + if (!empty($types) && count($types) > 0) { + request()->offsetSet('list_records', $tab); + + $currentSearch = request('search', ''); + + $searchParts = array_filter(explode(' ', $currentSearch), function($part) { + return !empty(trim($part)) && !str_starts_with(trim($part), 'type:'); + }); + + $searchParts[] = 'type:' . implode(',', $types); + + request()->offsetSet('search', implode(' ', $searchParts)); + request()->offsetSet('programmatic', '1'); + + return; + } + } + + if (empty($tab)) { + request()->offsetSet('list_records', 'all'); + request()->offsetSet('programmatic', '1'); + } + } +} diff --git a/app/Abstracts/Http/FormRequest.php b/app/Abstracts/Http/FormRequest.php new file mode 100644 index 00000000..ce3d1101 --- /dev/null +++ b/app/Abstracts/Http/FormRequest.php @@ -0,0 +1,20 @@ +merge([ + 'company_id' => company_id(), + ]); + } +} diff --git a/app/Abstracts/Http/PaymentController.php b/app/Abstracts/Http/PaymentController.php new file mode 100644 index 00000000..dba98547 --- /dev/null +++ b/app/Abstracts/Http/PaymentController.php @@ -0,0 +1,209 @@ +middleware(function ($request, $next) { + $this->setting = setting($this->alias); + $this->setting['code'] = $this->alias; + $this->setting['language'] = app()->getLocale(); + + $this->logger = $this->getLogger(); + + $this->user = user(); + + $this->module = module($this->alias); + + return $next($request); + }); + } + + public function show(Document $invoice, PaymentRequest $request, $cards = []) + { + $this->setContactFirstLastName($invoice); + + $confirm_url = $this->getConfirmUrl($invoice); + + $html = view('components.payment_method.' . $this->type, [ + 'setting' => $this->setting, + 'invoice' => $invoice, + 'confirm_url' => $confirm_url, + 'store_card' => !empty($this->setting['store_card']) ? true : false, + 'cards' => $cards, + ])->render(); + + return response()->json([ + 'code' => $this->setting['code'], + 'name' => $this->setting['name'], + 'description' => trans($this->alias . '::general.description'), + 'redirect' => false, + 'html' => $html, + ]); + } + + public function signed(Document $invoice, PaymentRequest $request) + { + return $this->show($invoice, $request); + } + + public function cancel(Document $invoice, $force_redirect = false) + { + $message = trans('messages.warning.payment_cancel', ['method' => setting($this->alias . '.name')]); + + $this->logger->info($this->module->getName() . ':: Invoice: ' . $invoice->id . ' - Cancel Message: ' . $message); + + flash($message)->warning()->important(); + + $invoice_url = $this->getInvoiceUrl($invoice); + + if ($force_redirect || ($this->type == 'redirect')) { + return redirect($invoice_url); + } + + return response()->json([ + 'error' => $message, + 'redirect' => $invoice_url, + 'success' => false, + 'data' => false, + ]); + } + + public function finish($invoice, $request, $force_redirect = false) + { + $this->dispatchPaidEvent($invoice, $request); + + $this->forgetReference($invoice); + + $message = trans('messages.success.added', ['type' => trans_choice('general.payments', 1)]); + + $this->logger->info($this->module->getName() . ':: Invoice: ' . $invoice->id . ' - Success Message: ' . $message); + + flash($message)->success(); + + $finish_url = $this->getFinishUrl($invoice); + + if ($force_redirect || ($this->type == 'redirect')) { + return redirect($finish_url); + } + + return response()->json([ + 'error' => $message, + 'redirect' => $finish_url, + 'success' => true, + 'data' => false, + ]); + } + + public function getInvoiceUrl($invoice) + { + return request()->isPortal($invoice->company_id) + ? route('portal.invoices.show', $invoice->id) + : URL::signedRoute('signed.invoices.show', [$invoice->id]); + } + + public function getFinishUrl($invoice) + { + return request()->isPortal($invoice->company_id) + ? route('portal.invoices.finish', $invoice->id) + : URL::signedRoute('signed.invoices.finish', [$invoice->id]); + } + + public function getConfirmUrl($invoice) + { + return $this->getModuleUrl($invoice, 'confirm'); + } + + public function getReturnUrl($invoice) + { + return $this->getModuleUrl($invoice, 'return'); + } + + public function getCancelUrl($invoice) + { + return $this->getModuleUrl($invoice, 'cancel'); + } + + public function getNotifyUrl($invoice) + { + return route('portal.' . $this->alias . '.invoices.notify', $invoice->id); + } + + public function getModuleUrl($invoice, $suffix) + { + return request()->isPortal($invoice->company_id) + ? route('portal.' . $this->alias . '.invoices.' . $suffix, $invoice->id) + : URL::signedRoute('signed.' . $this->alias . '.invoices.' . $suffix, [$invoice->id]); + } + + public function getLogger() + { + $log = new Logger($this->alias); + $log->pushHandler(new StreamHandler(Storage::path('logs/' . $this->alias . '.log')), Logger::INFO); + + return $log; + } + + public function dispatchPaidEvent($invoice, $request) + { + $request['company_id'] = $invoice->company_id; + $request['account_id'] = setting($this->alias . '.account_id', setting('default.account')); + $request['amount'] = $invoice->amount; + $request['payment_method'] = $this->alias; + $request['reference'] = $this->getReference($invoice); + $request['type'] = 'income'; + + event(new PaymentReceived($invoice, $request)); + } + + public function setReference($invoice, $reference) + { + session([ + $this->alias . '_' . $invoice->id . '_reference' => $reference + ]); + } + + public function getReference($invoice) + { + return session($this->alias . '_' . $invoice->id . '_reference'); + } + + public function forgetReference($invoice) + { + session()->forget($this->alias . '_' . $invoice->id . '_reference'); + } + + public function setContactFirstLastName(&$invoice) + { + $contact = explode(" ", $invoice->contact_name); + + $last_name = array_pop($contact); + $first_name = implode(" ", $contact); + + $invoice->first_name = $first_name; + $invoice->last_name = $last_name; + } +} diff --git a/app/Abstracts/Http/Response.php b/app/Abstracts/Http/Response.php new file mode 100644 index 00000000..75a3dac5 --- /dev/null +++ b/app/Abstracts/Http/Response.php @@ -0,0 +1,55 @@ +view = $view; + $this->data = $data; + } + + public function toJson() + { + return response()->json([ + 'success' => true, + 'error' => false, + 'data' => Arr::first($this->data), + 'message' => '', + ]); + } + + public function toHtml() + { + return view($this->view, $this->data); + } + + public function toResponse($request) + { + foreach ($this->accepts as $accept) { + $request_method = 'expects' . Str::studly($accept); + $response_method = 'to' . Str::studly($accept); + + if (!method_exists($request, $request_method) || !method_exists($this, $response_method)) { + continue; + } + + if ($request->{$request_method}()) { + return $this->{$response_method}(); + } + } + + return $this->toHtml(); + } +} diff --git a/app/Abstracts/Http/SettingController.php b/app/Abstracts/Http/SettingController.php new file mode 100644 index 00000000..1dca2bb9 --- /dev/null +++ b/app/Abstracts/Http/SettingController.php @@ -0,0 +1,174 @@ +all(); + $prefix = $request->get('_prefix', 'general'); + $company_id = $request->get('company_id'); + + event(new SettingUpdating($request)); + + if (empty($company_id)) { + $company_id = company_id(); + } + + $company = Company::find($company_id); + + $total_companies = Company::count(); + + // Clear setting media + foreach ($this->file_keys as $file_key) { + $keys = explode('.', $file_key); + + if ($prefix != $keys[0]) { + continue; + } + + if (! setting($file_key, false)) { + continue; + } + + $file_old_key = 'uploaded_' . $keys[1]; + if (array_key_exists($file_old_key, $fields)) { + continue; + } + + setting()->forget($file_key); + } + + foreach ($fields as $key => $value) { + $real_key = $prefix . '.' . $key; + + // Don't process unwanted keys + if (in_array($key, $this->skip_keys)) { + continue; + } + + // change dropzone middleware already uploaded file + if (in_array($real_key, $this->uploaded_file_keys)) { + continue; + } + + // Process file uploads + if (in_array($real_key, $this->file_keys)) { + // Upload attachment + if ($request->file($key)) { + $media = $this->getMedia($request->file($key), 'settings'); + + $company->attachMedia($media, Str::snake($real_key)); + + $value = $media->id; + } + + // Prevent reset + if (empty($value)) { + continue; + } + } + + if ($real_key == 'default.locale') { + if (! in_array($value, config('language.allowed'))) { + continue; + } + + user()->setAttribute('locale', $value)->save(); + } + + if ($real_key == 'default.currency') { + $currencies = Currency::enabled()->pluck('code')->toArray(); + + if (! in_array($value, $currencies)) { + continue; + } + + $currency = Currency::code($value)->first(); + $currency->rate = '1'; + $currency->save(); + } + + // If only 1 company + if ($total_companies == 1) { + $this->oneCompany($real_key, $value); + } + + setting()->set($real_key, $value); + } + + // Save all settings + setting()->save(); + + event(new SettingUpdated($request)); + + $message = trans('messages.success.updated', ['type' => trans_choice('general.settings', 2)]); + + $redirect_url = !empty($this->redirect_route) ? route($this->redirect_route) : url()->previous(); + + $response = [ + 'status' => null, + 'success' => true, + 'error' => false, + 'message' => $message, + 'data' => null, + 'redirect' => $redirect_url, + ]; + + flash($message)->success(); + + return response()->json($response); + } + + protected function oneCompany($real_key, $value): void + { + $key = match($real_key) { + 'default.locale' => 'APP_LOCALE', + 'schedule.time' => 'APP_SCHEDULE_TIME', + 'company.name' => 'MAIL_FROM_NAME', + 'company.email' => 'MAIL_FROM_ADDRESS', + 'email.protocol' => 'MAIL_MAILER', + 'email.smtp_host' => 'MAIL_HOST', + 'email.smtp_port' => 'MAIL_PORT', + 'email.smtp_username' => 'MAIL_USERNAME', + 'email.smtp_password' => 'MAIL_PASSWORD', + 'email.smtp_encryption' => 'MAIL_ENCRYPTION', + default => '', + }; + + if (empty($key)) { + return; + } + + Installer::updateEnv([$key => '"' . $value . '"']); + } +} diff --git a/app/Abstracts/Import.php b/app/Abstracts/Import.php new file mode 100644 index 00000000..4c33f92c --- /dev/null +++ b/app/Abstracts/Import.php @@ -0,0 +1,276 @@ +user = user(); + } + + public function map($row): array + { + $row['company_id'] = company_id(); + + // created_by is equal to the owner id. Therefore, the value in export is owner email. + $row['created_by'] = $this->getCreatedById($row); + + $row['created_from'] = $this->getSourcePrefix() . 'import'; + + // Make enabled field integer + if (isset($row['enabled'])) { + $row['enabled'] = (int) $row['enabled']; + } + + // Make reconciled field integer + if (isset($row['reconciled'])) { + $row['reconciled'] = (int) $row['reconciled']; + } + + $date_fields = ['paid_at', 'invoiced_at', 'billed_at', 'due_at', 'issued_at', 'transferred_at']; + foreach ($date_fields as $date_field) { + if (!isset($row[$date_field])) { + continue; + } + + try { + $row[$date_field] = is_numeric($row[$date_field]) + ? Date::parse(ExcelDate::excelToDateTimeObject($row[$date_field])) + ->format('Y-m-d H:i:s') + : Date::parseWithFallbackLocales($row[$date_field], null, [$this->preferredLocale()]) + ->format('Y-m-d H:i:s'); + } catch (InvalidFormatException | \Exception $e) { + Log::info($e->getMessage()); + } + } + + $event = new RowPreparing($this, $row); + + event($event); + + return $event->row; + } + + public function rules(): array + { + return []; + } + + /** + * You can override this method to add custom rules for each row. + */ + public function prepareRules(array $rules): array + { + return $rules; + } + + /** + * Validate each row data. + * + * @param \Illuminate\Validation\Validator $validator + * @throws ValidationException + */ + public function withValidator($validator) + { + $condition = class_exists($this->request_class) + ? ! ($request = new $this->request_class) instanceof FormRequest + : true; + + if ($condition) { + return; + } + + foreach ($validator->getData() as $row => $data) { + $request->initialize(request: $data); + + $rules = $this->prepareRules($request->rules()); + + try { + Validator::make($data, $rules)->validate(); + } catch (ValidationException $e) { + foreach ($e->validator->failed() as $attribute => $value) { + foreach ($value as $rule => $params) { + if ($rule === 'In' && !empty($params)) { + $actual_value = $data[$attribute] ?? 'null'; + $expected_values = implode(', ', $params); + $custom_message = trans('validation.in_detailed', [ + 'attribute' => $attribute, + 'value' => $actual_value, + 'values' => $expected_values + ]); + $validator->errors()->add($row . '.' . $attribute, $custom_message); + } else { + $validator->addFailure($row . '.' . $attribute, $rule, $params); + } + } + } + + throw new ValidationException($validator); + } + } + } + + public function chunkSize(): int + { + return config('excel.imports.chunk_size'); + } + + public function limit(): int + { + return config('excel.imports.row_limit'); + } + + public function isNotValid($row) + { + return Validator::make($row, $this->rules())->fails(); + } + + public function isEmpty($row, $fields) + { + $fields = Arr::wrap($fields); + + foreach ($fields as $field) { + if (!empty($row[$field])) { + continue; + } + + return true; + } + + return false; + } + + public function preferredLocale() + { + return $this->user->locale; + } + + protected function replaceForBatchRules(array $rules): array + { + $dependent_rules = [ + 'after:', + 'after_or_equal:', + 'before:', + 'before_or_equal:', + 'different:', + 'exclude_if:', + 'exclude_unless:', + 'gt:', + 'gte:', + 'in_array:', + 'lt:', + 'lte:', + 'prohibited_if:', + 'prohibited_unless:', + 'required_if:', + 'required_unless:', + 'required_with:', + 'required_with_all:', + 'required_without:', + 'required_without_all:', + 'same:', + ]; + + $batch_rules = [ + 'after:*.', + 'after_or_equal:*.', + 'before:*.', + 'before_or_equal:*.', + 'different:*.', + 'exclude_if:*.', + 'exclude_unless:*.', + 'gt:*.', + 'gte:*.', + 'in_array:*.', + 'lt:*.', + 'lte:*.', + 'prohibited_if:*.', + 'prohibited_unless:*.', + 'required_if:*.', + 'required_unless:*.', + 'required_with:*.', + 'required_with_all:*.', + 'required_without:*.', + 'required_without_all:*.', + 'same:*.', + ]; + + return str_replace($dependent_rules, $batch_rules, $rules); + } + + //This function is used in import classes. If the data in the row exists in the database, it is returned. + public function hasRow($row) + { + // must be models and columns + if (empty($this->model) || empty($this->columns)) { + return false; + } + + /* This function is called for each row. + This check is done in order not to query again for each row. + When the model to which the query is thrown changes, the new query should be discarded. + */ + if (! $this->has_row || ! $this->has_row instanceof $this->model) { + $this->has_row = $this->model::withoutEvents(function () { + if ($this->with_trashed) { + // This query should be used if there is no deleted_at field in the table or if the deleted data is to be retrieved. + return $this->model::withTrashed()->get($this->columns)->each(function ($data) { + $data->setAppends([]); + $data->unsetRelations(); + }); + } else { + return $this->model::get($this->columns)->each(function ($data) { + $data->setAppends([]); + $data->unsetRelations(); + }); + } + }); + } + + $search_value = []; + + //In the model, the fields to be searched for the row are determined. + foreach ($this->columns as $key) { + $search_value[$key] = isset($row[$key]) ? $row[$key] : null; + } + + return in_array($search_value, $this->has_row->toArray()); + } +} diff --git a/app/Abstracts/ImportMultipleSheets.php b/app/Abstracts/ImportMultipleSheets.php new file mode 100644 index 00000000..e8e5a163 --- /dev/null +++ b/app/Abstracts/ImportMultipleSheets.php @@ -0,0 +1,31 @@ +user = user(); + } + + public function chunkSize(): int + { + return config('excel.imports.chunk_size'); + } + + public function onUnknownSheet($sheetName) + { + // + } +} diff --git a/app/Abstracts/Job.php b/app/Abstracts/Job.php new file mode 100644 index 00000000..2a95d19e --- /dev/null +++ b/app/Abstracts/Job.php @@ -0,0 +1,136 @@ +booting(...$arguments); + $this->bootCreate(...$arguments); + $this->bootUpdate(...$arguments); + $this->bootDelete(...$arguments); + $this->booted(...$arguments); + } + + public function booting(...$arguments): void + { + // + } + + public function bootCreate(...$arguments): void + { + if (! $this instanceof ShouldCreate) { + return; + } + + if (empty($arguments[0])) { + $arguments[0] = []; + } + + $request = $this->getRequestInstance($arguments[0]); + if ($request instanceof Request) { + $this->request = $request; + } + + if ($this instanceof HasOwner) { + $this->setOwner(); + } + + if ($this instanceof HasSource) { + $this->setSource(); + } + } + + public function bootUpdate(...$arguments): void + { + if (! $this instanceof ShouldUpdate) { + return; + } + + if ($arguments[0] instanceof Model) { + $this->model = $arguments[0]; + } + + if (empty($arguments[1])) { + $arguments[1] = []; + } + + $request = $this->getRequestInstance($arguments[1]); + if ($request instanceof Request) { + $this->request = $request; + } + } + + public function bootDelete(...$arguments): void + { + if (! $this instanceof ShouldDelete) { + return; + } + + if ($arguments[0] instanceof Model) { + $this->model = $arguments[0]; + } + } + + public function booted(...$arguments): void + { + // + } + + public function getRequestInstance($request) + { + if (! is_array($request)) { + return $request; + } + + $class = new class() extends FormRequest {}; + + return $class->merge($request); + } + + public function setOwner(): void + { + if (! $this->request instanceof Request) { + return; + } + + if ($this->request->has('created_by')) { + return; + } + + $this->request->merge(['created_by' => user_id()]); + } + + public function setSource(): void + { + if (! $this->request instanceof Request) { + return; + } + + if ($this->request->has('created_from')) { + return; + } + + $this->request->merge(['created_from' => $this->getSourceName($this->request)]); + } +} diff --git a/app/Abstracts/JobShouldQueue.php b/app/Abstracts/JobShouldQueue.php new file mode 100644 index 00000000..5fcf20c5 --- /dev/null +++ b/app/Abstracts/JobShouldQueue.php @@ -0,0 +1,161 @@ +booting(...$arguments); + $this->bootCreate(...$arguments); + $this->bootUpdate(...$arguments); + $this->bootDelete(...$arguments); + $this->booted(...$arguments); + } + + public function booting(...$arguments): void + { + // + } + + public function bootCreate(...$arguments): void + { + if (! $this instanceof ShouldCreate) { + return; + } + + if (empty($arguments[0])) { + $arguments[0] = []; + } + + $request = $this->getRequestInstance($arguments[0]); + if ($request instanceof QueueCollection) { + $this->request = $request; + } + + if ($this instanceof HasOwner) { + $this->setOwner(); + } + + if ($this instanceof HasSource) { + $this->setSource(); + } + } + + public function bootUpdate(...$arguments): void + { + if (! $this instanceof ShouldUpdate) { + return; + } + + if ($arguments[0] instanceof Model) { + $this->model = $arguments[0]; + } + + if (empty($arguments[1])) { + $arguments[1] = []; + } + + $request = $this->getRequestInstance($arguments[1]); + if ($request instanceof QueueCollection) { + $this->request = $request; + } + } + + public function bootDelete(...$arguments): void + { + if (! $this instanceof ShouldDelete) { + return; + } + + if ($arguments[0] instanceof Model) { + $this->model = $arguments[0]; + } + } + + public function booted(...$arguments): void + { + // + } + + /** + * Check if request is array and if so, convert to a request class. + * + * @param mixed $request + * @return \Illuminate\Foundation\Http\FormRequest + * + * @deprecated Request is not serializable so can't use it with queues. + */ + public function getRequestInstance($request) + { + return $this->getRequestAsCollection($request); + } + + /** + * Covert the request to collection. + * + * @param mixed $request + * @return \App\Utilities\QueueCollection + */ + public function getRequestAsCollection($request) + { + if (is_array($request)) { + $data = $request; + + $request = new class() extends FormRequest {}; + + $request->merge($data); + } + + return new QueueCollection($request->all()); + } + + public function setOwner(): void + { + if (! $this->request instanceof QueueCollection) { + return; + } + + if ($this->request->has('created_by')) { + return; + } + + $this->request->merge(['created_by' => user_id()]); + } + + public function setSource(): void + { + if (! $this->request instanceof QueueCollection) { + return; + } + + if ($this->request->has('created_from')) { + return; + } + + $this->request->merge(['created_from' => $this->getSourceName($this->request)]); + } +} diff --git a/app/Abstracts/Listeners/Report.php b/app/Abstracts/Listeners/Report.php new file mode 100644 index 00000000..c321cc80 --- /dev/null +++ b/app/Abstracts/Listeners/Report.php @@ -0,0 +1,363 @@ +fireEvent(\App\Events\Report\SkipClass::class, $fire_event); + + return (empty($event->class) || !in_array(get_class($event->class), $this->classes)); + } + + public function skipRowsShowing($event, $group) + { + $fire_event = $event; + $fire_group = $group; + + $this->fireEvent(\App\Events\Report\SkipRowsShowing::class, $fire_event, $fire_group); + + return $this->skipThisClass($event) + || empty($event->class->getGroup()) + || ($event->class->getGroup() != $group); + } + + public function setDateFilter($event) + { + $financial_year = $this->getFinancialYear(); + + $start_date = request()->get('start_date', $financial_year->copy()->getStartDate()->toDateString()); + $end_date = request()->get('end_date', $financial_year->copy()->getEndDate()->toDateString()); + $default_value = $start_date . '-to-' . $end_date; + + $event->class->filters['date_range'] = $this->getDateRange(); + $event->class->filters['keys']['date_range'] = 'date_range'; + $event->class->filters['defaults']['date_range'] = $default_value; + $event->class->filters['operators']['date_range'] = [ + 'equal' => true, + 'not_equal' => false, + 'range' => false, + ]; + } + + public function getDateRange() + { + $date_range = []; + + $shortcuts = $this->getDatePickerShortcuts(); + + foreach ($shortcuts as $text => $shortcut) { + $date_range[$shortcut['start'] . '-to-' . $shortcut['end']] = $text; + } + + $date_range['custom'] = trans('general.date_range.custom'); + + return $date_range; + } + + public function getAccounts($limit = false) + { + $model = Account::enabled()->orderBy('name'); + + if ($limit !== false) { + $model->take(setting('default.select_limit')); + } + + return $model->pluck('name', 'id')->toArray(); + } + + public function getItemCategories($limit = false) + { + return $this->getCategories($this->getItemCategoryTypes(), $limit); + } + + public function getIncomeCategories($limit = false) + { + return $this->getCategories($this->getIncomeCategoryTypes(), $limit); + } + + public function getExpenseCategories($limit = false) + { + return $this->getCategories($this->getExpenseCategoryTypes(), $limit); + } + + public function getIncomeExpenseCategories($limit = false) + { + $types = array_merge($this->getIncomeCategoryTypes(), $this->getExpenseAndDirectCostCategoryTypes()); + + return $this->getCategories($types, $limit); + } + + public function getCategories($types, $limit = false) + { + $model = Category::withSubCategory()->type($types)->orderBy('name'); + + if ($limit !== false) { + $model->take(setting('default.select_limit')); + } + + return $model->pluck('name', 'id')->toArray(); + } + + public function getCustomers($limit = false) + { + return $this->getContacts($this->getCustomerTypes(), $limit); + } + + public function getVendors($limit = false) + { + return $this->getContacts($this->getVendorTypes(), $limit); + } + + public function getContacts($types = null, $limit = false) + { + if ($types) { + $model = Contact::type($types)->orderBy('name'); + } else { + $model = Contact::orderBy('name'); + } + + if ($limit !== false) { + $model->take(setting('default.select_limit')); + } + + return $model->pluck('name', 'id')->toArray(); + } + + public function getBasis() + { + return [ + 'cash' => trans('general.cash'), + 'accrual' => trans('general.accrual'), + ]; + } + + public function getPeriod() + { + return [ + 'weekly' => trans('general.weekly'), + 'monthly' => trans('general.monthly'), + 'quarterly' => trans('general.quarterly'), + 'yearly' => trans('general.yearly'), + ]; + } + + public function getDiscount() + { + return [ + 'item' => trans('settings.localisation.discount_location.item'), + 'total' => trans('settings.localisation.discount_location.total'), + 'both' => trans('settings.localisation.discount_location.both'), + ]; + } + + public function applyDateFilter($event) + { + $event->model->dateFilter($event->args['date_field']); + } + + public function applyDiscountFilter($event) + { + $input = request('search', ''); + $discount = $this->getSearchStringValue('discount', 'both', $input); + + switch ($discount) { + case 'item': + $discount_types = ['item_discount']; + break; + case 'total': + $discount_types = ['discount']; + break; + default: + $discount_types = ['item_discount', 'discount']; + break; + } + + $event->model->whereHas('totals', function ($query) use ($discount_types) { + $query->whereIn('code', $discount_types); + }); + } + + public function applySearchStringFilter($event) + { + $event->model->usingSearchString(); + } + + public function applyAccountGroup($event) + { + if ($event->model->getTable() != 'documents') { + return; + } + + $filter = explode(',', $this->getSearchStringValue('account_id')); + + $event->model->account_id = 0; + + foreach ($event->model->transactions as $transaction) { + if (!empty($filter) && !in_array($transaction->account_id, $filter)) { + continue; + } + + $event->model->account_id = $transaction->account_id; + + break; + } + } + + public function applyCustomerGroup($event) + { + foreach ($this->getCustomerTypes() as $type) { + $id_field = $type . '_id'; + + $event->model->$id_field = $event->model->contact_id; + } + } + + public function applyVendorGroup($event) + { + foreach ($this->getVendorTypes() as $type) { + $id_field = $type . '_id'; + + $event->model->$id_field = $event->model->contact_id; + } + } + + public function setRowNamesAndValues($event, $rows) + { + $nodes = []; + + foreach ($event->class->dates as $date) { + foreach ($event->class->tables as $table_key => $table_name) { + foreach ($rows as $id => $name) { + $event->class->row_names[$table_key][$id] = $name; + $event->class->row_values[$table_key][$id][$date] = 0; + + $nodes[$id] = null; + } + } + } + + $this->setTreeNodes($event, $nodes); + } + + public function setTreeNodes($event, $nodes) + { + foreach ($event->class->tables as $table_key => $table_name) { + foreach ($nodes as $id => $node) { + $event->class->row_tree_nodes[$table_key][$id] = $node; + } + } + } + + public function getCategoriesNodes($categories) + { + $nodes = []; + + foreach ($categories as $id => $name) { + $category = Category::withSubCategory()->find($id); + + if (!is_null($category->parent_id)) { + unset($categories[$id]); + + continue; + } + + $nodes[$id] = $this->getSubCategories($category); + } + + return $nodes; + } + + public function getSubCategories($category) + { + if ($category->sub_categories->count() == 0) { + return null; + } + + $sub_categories = []; + + foreach ($category->sub_categories as $sub_category) { + $sub_category->load('sub_categories'); + + $sub_categories[$sub_category->id] = $this->getSubCategories($sub_category); + } + + if (!empty($sub_categories)) { + $sub_categories[$category->id] = null; + } + + return $sub_categories; + } + + public function getFormattedDate($event, $date) + { + $period = $this->getSearchStringValue('period'); + + if (empty($period)) { + $period = $event->class->getPeriod(); + } + + return $this->getPeriodicDate($date, $period, $event->class->year); + } + + protected function fireEvent($event_class, $event, $group = null) + { + $this->class = $event->class; + + if ($group) { + $this->group = $group; + } + + event(new $event_class($this)); + } + + /** + * Register the listeners for the subscriber. + * + * @param \Illuminate\Events\Dispatcher $events + */ + public function subscribe($events) + { + $class = get_class($this); + + foreach ($this->events as $event) { + $method = 'handle' . (new \ReflectionClass($event))->getShortName(); + + if (!method_exists($class, $method)) { + continue; + } + + $events->listen( + $event, + $class . '@' . $method + ); + } + } +} diff --git a/app/Abstracts/Listeners/Update.php b/app/Abstracts/Listeners/Update.php new file mode 100644 index 00000000..b15c8363 --- /dev/null +++ b/app/Abstracts/Listeners/Update.php @@ -0,0 +1,36 @@ +alias != static::ALIAS) { + return true; + } + + return ! Versions::shouldUpdate(static::VERSION, $event->old, $event->new); + } + + /** + * @deprecated since 2.0 + */ + public function check($event) + { + return !$this->skipThisUpdate($event); + } +} diff --git a/app/Abstracts/Model.php b/app/Abstracts/Model.php new file mode 100644 index 00000000..bb289521 --- /dev/null +++ b/app/Abstracts/Model.php @@ -0,0 +1,269 @@ + 'double', + 'enabled' => 'boolean', + 'deleted_at' => 'datetime', + ]; + + public $allAttributes = []; + + /** + * Fill the model with an array of attributes. + * + * @param array $attributes + * @return $this + * + * @throws \Illuminate\Database\Eloquent\MassAssignmentException + */ + public function fill(array $attributes) + { + $this->allAttributes = $attributes; + + return parent::fill($attributes); + } + + /** + * Global company relation. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function company() + { + return $this->belongsTo('App\Models\Common\Company'); + } + + /** + * Owner relation. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function owner() + { + return $this->belongsTo(user_model_class(), 'created_by', 'id')->withDefault(['name' => trans('general.na')]); + } + + /** + * Scope to only include company data. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeAllCompanies($query) + { + return $query->withoutGlobalScope('App\Scopes\Company'); + } + + /** + * Scope to only include company data. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param $company_id + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeCompanyId($query, $company_id) + { + return $query->where($this->qualifyColumn('company_id'), '=', $company_id); + } + + /** + * Scope to get all rows filtered, sorted and paginated. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param $sort + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeCollect($query, $sort = 'name') + { + $request = request(); + + /** + * Modules that use the sort parameter in CRUD operations cause an error, + * so this sort parameter set back to old value after the query is executed. + * + * for Custom Fields module + */ + $request_sort = $request->get('sort'); + + $query->usingSearchString()->sortable($sort); + + if ($request->expectsJson() && $request->isNotApi()) { + return $query->get(); + } + + $request->merge(['sort' => $request_sort]); + // This line disabled because broken sortable issue. + //$request->offsetUnset('direction'); + $limit = (int) $request->get('limit', setting('default.list_limit', '25')); + + return $query->paginate($limit); + } + + public function scopeUsingSearchString(Builder $query, string|null $string = null) + { + event(new SearchStringApplying($query)); + + $string = $string ?: request('search', ''); + + $string = $this->stripUnknownSearchStringTokens($string, get_class($this)); + + $this->getSearchStringManager()->updateBuilder($query, $string); + + event(new SearchStringApplied($query)); + } + + /** + * Scope to export the rows of the current page filtered and sorted. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param $ids + * @param $sort + * @param $id_field + * + * @return \Illuminate\Support\LazyCollection + */ + public function scopeCollectForExport($query, $ids = [], $sort = 'name', $id_field = 'id') + { + $request = request(); + + if (!empty($ids)) { + $query->whereIn($id_field, (array) $ids); + } + + $search = $request->get('search'); + + $query->usingSearchString($search)->sortable($sort); + + $page = (int) $request->get('page'); + $limit = (int) $request->get('limit', setting('default.list_limit', '25')); + $offset = $page ? ($page - 1) * $limit : 0; + + if (! $this instanceof WithParentSheet && (empty($ids) || count((array) $ids) > $limit)) { + $query->offset($offset)->limit($limit); + } + + return $query->cursor(); + } + + /** + * Scope to only include active models. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeEnabled($query) + { + return $query->where($this->qualifyColumn('enabled'), 1); + } + + /** + * Scope to only include passive models. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeDisabled($query) + { + return $query->where($this->qualifyColumn('enabled'), 0); + } + + /** + * Scope to only include reconciled models. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param $value + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeReconciled($query, $value = 1) + { + return $query->where($this->qualifyColumn('reconciled'), $value); + } + + public function scopeAccount($query, $accounts) + { + if (empty($accounts)) { + return $query; + } + + return $query->whereIn($this->qualifyColumn('account_id'), (array) $accounts); + } + + public function scopeContact($query, $contacts) + { + if (empty($contacts)) { + return $query; + } + + return $query->whereIn($this->qualifyColumn('contact_id'), (array) $contacts); + } + + public function scopeSource($query, $source) + { + return $query->where($this->qualifyColumn('created_from'), $source); + } + + public function scopeIsOwner($query) + { + return $query->where($this->qualifyColumn('created_by'), user_id()); + } + + public function scopeIsNotOwner($query) + { + return $query->where($this->qualifyColumn('created_by'), '<>', user_id()); + } + + public function scopeIsRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), 'like', '%-recurring'); + } + + public function scopeIsNotRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), 'not like', '%-recurring'); + } + + public function scopeModuleEnabled(Builder $query, string $module): Builder + { + return $query->allCompanies()->whereHas('company', fn (Builder $q1) => + $q1->enabled()->whereHas('modules', fn (Builder $q2) => + $q2->allCompanies()->alias($module)->enabled(), + ) + ); + } + + public function ownerKey($owner) + { + if ($this->isNotOwnable()) { + return 0; + } + + return $this->created_by; + } +} diff --git a/app/Abstracts/Notification.php b/app/Abstracts/Notification.php new file mode 100644 index 00000000..0856f991 --- /dev/null +++ b/app/Abstracts/Notification.php @@ -0,0 +1,179 @@ +onQueue('notifications'); + } + + /** + * Get the notification's channels. + * + * @param mixed $notifiable + * @return array|string + */ + public function via($notifiable) + { + return ['mail', 'database']; + } + + /** + * Initialise the mail representation of the notification. + * + */ + public function initMailMessage(): MailMessage + { + app('url')->defaults(['company_id' => company_id()]); + + $message = (new MailMessage) + ->from(config('mail.from.address'), config('mail.from.name')) + ->subject($this->getSubject()) + ->view('components.email.body', ['body' => $this->getBody()]); + + if (!empty($this->custom_mail['cc'])) { + $message->cc($this->custom_mail['cc']); + } + + if (!empty($this->custom_mail['bcc'])) { + $message->bcc($this->custom_mail['bcc']); + } + + return $message; + } + + /** + * Initialise the array representation of the notification. + * + */ + public function initArrayMessage(): void + { + app('url')->defaults(['company_id' => company_id()]); + } + + public function getSubject(): string + { + return !empty($this->custom_mail['subject']) + ? $this->custom_mail['subject'] + : $this->replaceTags($this->template->subject); + } + + public function getBody() + { + $body = !empty($this->custom_mail['body']) + ? EmailTemplate::sanitizeBody($this->custom_mail['body']) + : $this->replaceTags($this->template->body); + + return $body . $this->getFooter(); + } + + public function replaceTags(string $content): string + { + $pattern = $this->getTagsPattern(); + $replacement = $this->applyQuote($this->getTagsReplacement()); + + return $this->revertQuote(preg_replace($pattern, $replacement, $content)); + } + + public function getFooter() + { + $url = null; + $get_started = '' . trans('footer.get_started') . ''; + + return view('components.email.footer', compact('url', 'get_started')); + } + + public function getTagsPattern(): array + { + $pattern = []; + + foreach($this->getTags() as $tag) { + $pattern[] = "/" . $tag . "/"; + } + + return $pattern; + } + + public function getTags(): array + { + return []; + } + + public function getTagsReplacement(): array + { + return []; + } + + public function getTagsBinding(): array + { + $bindings = []; + + $tags = $this->getTags(); + $replacements = $this->getTagsReplacement(); + + $wrappers = ['{', '}']; + + foreach ($tags as $index => $tag) { + $key = Str::replace($wrappers, '', $tag); + + $value = $replacements[$index] ?? ''; + + // HTML-encode string replacement values to prevent stored XSS (CWE-79). + // Notification description templates contain intentional HTML markup + // (, ) which must be preserved. This encodes only the + // user-supplied values (e.g. customer_name, vendor_name, email) + // while leaving the template's own markup untouched. Non-string + // values (Money, Carbon dates, numeric amounts) pass through + // unchanged since they do not carry attacker-controlled HTML. + $bindings[$key] = is_string($value) ? e($value) : $value; + } + + return $bindings; + } + + public function applyQuote(array $vars): array + { + $new_vars = []; + + foreach ($vars as $var) { + // Ensure $var is a string, default to an empty string if it is null + $new_vars[] = preg_quote($var ?? ''); + } + + return $new_vars; + } + + public function revertQuote(string $content): string + { + return str_replace('\\', '', $content); + } + + /** + * @deprecated 3.0 + */ + public function initMessage() + { + return $this->initMailMessage(); + } +} diff --git a/app/Abstracts/Observer.php b/app/Abstracts/Observer.php new file mode 100644 index 00000000..c10b0db3 --- /dev/null +++ b/app/Abstracts/Observer.php @@ -0,0 +1,10 @@ + [ + 'colors' => [ + '#6da252', + ], + + 'yaxis' => [ + 'labels' => [ + 'formatter' => '', + ], + ], + ], + 'donut' => [ + 'yaxis' => [ + 'labels' => [ + 'formatter' => '', + ], + ], + ], + ]; + + public $column_name_width = 'report-column-name'; + public $column_value_width = 'report-column-value'; + + public $row_tree_nodes = []; + + public function __construct(Model $model = null, $load_data = true) + { + $this->setGroups(); + + if (!$model) { + return; + } + + $this->model = $model; + + if (!$load_data) { + return; + } + + $this->load(); + } + + abstract public function setData(); + + public function load() + { + $this->setYear(); + $this->setViews(); + $this->setTables(); + $this->setDates(); + $this->setFilters(); + $this->setRows(); + $this->loadData(); + $this->setColumnWidth(); + $this->setChartLabelFormatter(); + + $this->loaded = true; + } + + public function loadData() + { + event(new DataLoading($this)); + + $this->setData(); + + event(new DataLoaded($this)); + } + + public function getDefaultName() + { + if (!empty($this->default_name)) { + return trans($this->default_name); + } + + return Str::title(str_replace('_', ' ', Str::snake((new \ReflectionClass($this))->getShortName()))); + } + + public function getCategory() + { + return trans($this->category); + } + + public function getCategoryDescription() + { + if (! empty($this->category_description)) { + return trans($this->category_description); + } + + return $this->findTranslation([ + $this->category . '_desc', + $this->category . '_description', + str_replace('general.', 'reports.', $this->category) . '_desc', + str_replace('general.', 'reports.', $this->category) . '_description', + ]); + } + + public function getIcon() + { + return $this->icon; + } + + public function getCharts($table_key) + { + return [ + 'bar' => $this->getBarChart($table_key), + 'donut' => $this->getDonutChart($table_key), + ]; + } + + public function getBarChart($table_key) + { + $chart = new Chart(); + + if (empty($this->chart)) { + return $chart; + } + + $options = !empty($this->chart[$table_key]) ? $this->chart[$table_key]['bar'] : $this->chart['bar']; + + $chart->setType('bar') + ->setOptions($options) + ->setDefaultLocale($this->getDefaultLocaleOfChart()) + ->setLocales($this->getLocaleTranslationOfChart()) + ->setLabels(array_values($this->dates)) + ->setDataset($this->tables[$table_key], 'column', array_values($this->footer_totals[$table_key])); + + return $chart; + } + + public function getDonutChart($table_key) + { + $chart = new Chart(); + + if (empty($this->chart)) { + return $chart; + } + + $tmp_values = []; + + if (! empty($this->row_values[$table_key])) { + foreach ($this->row_values[$table_key] as $id => $dates) { + $tmp_values[$id] = 0; + + foreach ($dates as $date) { + $tmp_values[$id] += $date; + } + } + } + + $tmp_values = collect($tmp_values)->sort()->reverse()->take(10)->all(); + + $total = array_sum($tmp_values); + $total = !empty($total) ? $total : 1; + + $group = $this->getGroup(); + + $labels = $colors = $values = []; + + foreach ($tmp_values as $id => $value) { + $labels[$id] = $this->row_names[$table_key][$id]; + + $colors[$id] = ($group == 'category') + ? Category::withSubCategory()->find($id)?->colorHexCode + : $this->randHexColor(); + + $values[$id] = round(($value * 100 / $total), 0); + } + + $options = !empty($this->chart[$table_key]) ? $this->chart[$table_key]['donut'] : $this->chart['donut']; + + $chart->setType('donut') + ->setOptions($options) + ->setDefaultLocale($this->getDefaultLocaleOfChart()) + ->setLocales($this->getLocaleTranslationOfChart()) + ->setLabels(array_values($labels)) + ->setColors(array_values($colors)) + ->setDataset($this->tables[$table_key], 'donut', array_values($values)); + + $chart->options['legend']['width'] = 105; + $chart->options['legend']['position'] = 'right'; + + return $chart; + } + + public function show() + { + return view($this->views['show'], ['print' => false])->with('class', $this); + } + + public function print() + { + return view($this->views['print'], ['print' => true])->with('class', $this); + } + + public function array(): array + { + $data = []; + + $group = Str::plural($this->group ?? $this->getGroup()); + + foreach ($this->tables as $table_key => $table_name) { + if (! isset($this->row_values[$table_key])) { + continue; + } + + foreach ($this->row_values[$table_key] as $key => $values) { + if (empty($this->row_names[$table_key][$key])) { + continue; + } + + if ($this->has_money) { + $values = array_map(fn($value) => money($value)->format(), $values); + } + + $data[$table_key][$group][$this->row_names[$table_key][$key]] = $values; + } + + $footer_totals = $this->footer_totals[$table_key]; + + if ($this->has_money) { + $footer_totals = array_map(fn($value) => money($value)->format(), $footer_totals); + } + + $data[$table_key]['totals'] = $footer_totals; + } + + return $data; + } + + public function pdf() + { + $view = view($this->views['print'], ['print' => true])->with('class', $this)->render(); + + $html = mb_convert_encoding($view, 'HTML-ENTITIES', 'UTF-8'); + + $pdf = app('dompdf.wrapper'); + $pdf->loadHTML($html); + + $pdf->setPaper('A4', 'landscape'); + + $file_name = $this->model->name . ' - ' . company()->name . '.pdf'; + + return $pdf->download($file_name); + } + + public function export() + { + return ExportHelper::toExcel(new Export($this->views[$this->type], $this), $this->model->name); + } + + public function setColumnWidth() + { + if (! $period = $this->getPeriod()) { + return; + } + + $width = ''; + + switch ($period) { + case 'quarterly': + $width = 'w-2/12 col-2'; + break; + case 'yearly': + $width = 'w-4/12 col-4'; + break; + case 'monthly': + $width = 'col-1 w-20'; + break; + case 'weekly': + $width = 'col-1 w-20'; + break; + } + + if (empty($width)) { + return; + } + + $this->column_name_width = $this->column_value_width = $width; + } + + public function setChartLabelFormatter() + { + if (count($this->tables) > 1) { + foreach ($this->tables as $table_key => $table) { + if (empty($this->chart[$table_key])) { + continue; + } + + $this->chart[$table_key]['bar']['yaxis']['labels']['formatter'] = $this->getChartLabelFormatter($this->bar_formatter_type); + $this->chart[$table_key]['donut']['yaxis']['labels']['formatter'] = $this->getChartLabelFormatter($this->donut_formatter_type); + } + } else { + $this->chart['bar']['yaxis']['labels']['formatter'] = $this->getChartLabelFormatter($this->bar_formatter_type); + $this->chart['donut']['yaxis']['labels']['formatter'] = $this->getChartLabelFormatter($this->donut_formatter_type); + } + } + + public function setYear() + { + $this->year = request()->filled('start_date') ? Date::parse(request('start_date'))->year : Date::now()->year; + } + + public function setViews() + { + $this->views = [ + 'show' => 'components.reports.show', + 'print' => 'components.reports.print', + 'filter' => 'components.reports.filter', + + 'detail' => 'components.reports.detail', + 'detail.content.header' => 'components.reports.detail.content.header', + 'detail.content.footer' => 'components.reports.detail.content.footer', + 'detail.table' => 'components.reports.detail.table', + 'detail.table.header' => 'components.reports.detail.table.header', + 'detail.table.body' => 'components.reports.detail.table.body', + 'detail.table.row' => 'components.reports.detail.table.row', + 'detail.table.footer' => 'components.reports.detail.table.footer', + + 'summary' => 'components.reports.summary', + 'summary.content.header' => 'components.reports.summary.content.header', + 'summary.content.footer' => 'components.reports.summary.content.footer', + 'summary.table' => 'components.reports.summary.table', + 'summary.table.header' => 'components.reports.summary.table.header', + 'summary.table.body' => 'components.reports.summary.table.body', + 'summary.table.row' => 'components.reports.summary.table.row', + 'summary.table.footer' => 'components.reports.summary.table.footer', + 'summary.chart' => 'components.reports.summary.chart', + ]; + } + + public function setTables() + { + $this->tables = [ + 'default' => trans_choice('general.totals', 1), + ]; + } + + public function setDates() + { + if (! $period = $this->getPeriod()) { + return; + } + + [$start, $end] = $this->getStartAndEndDates($this->year); + + $counter = match ($period) { + 'weekly' => $end->diffInWeeks($start), + 'quarterly' => $end->diffInQuarters($start), + 'yearly' => $end->diffInYears($start), + default => $end->diffInMonths($start), + }; + + for ($j = 0; $j <= $counter; $j++) { + $date = $this->getPeriodicDate($start, $this->getPeriod(), $this->year); + + $this->dates[] = $date; + + foreach ($this->tables as $table_key => $table_name) { + $this->footer_totals[$table_key][$date] = 0; + } + + match ($period) { + 'weekly' => $start->addWeek(), + 'quarterly' => $start->addQuarter(), + 'yearly' => $start->addYear(), + default => $start->addMonth(), + }; + } + } + + public function setFilters() + { + event(new FilterShowing($this)); + } + + public function setGroups() + { + event(new GroupShowing($this)); + } + + public function setRows() + { + event(new RowsShowing($this)); + } + + public function setTotals($items, $date_field, $check_type = false, $table = 'default', $with_tax = true) + { + event(new TotalCalculating($this, $items, $date_field, $check_type, $table, $with_tax)); + + $group_field = $this->getGroup() . '_id'; + + foreach ($items as $item) { + // Make groups extensible + $item = $this->applyGroups($item); + + $date = $this->getPeriodicDate(Date::parse($item->$date_field), $this->getPeriod(), $this->year); + + if (!isset($item->$group_field)) { + continue; + } + + $group = $item->$group_field; + + if ( + !isset($this->row_values[$table][$group]) + || !isset($this->row_values[$table][$group][$date]) + || !isset($this->footer_totals[$table][$date]) + ) { + continue; + } + + $amount = $item->getAmountConvertedToDefault(false, $with_tax); + + $type = ($item->type === Document::INVOICE_TYPE || $item->type === 'income') ? 'income' : 'expense'; + + if (($check_type == false) || ($type == 'income')) { + $this->row_values[$table][$group][$date] += $amount; + + $this->footer_totals[$table][$date] += $amount; + } else { + $this->row_values[$table][$group][$date] -= $amount; + + $this->footer_totals[$table][$date] -= $amount; + } + } + + event(new TotalCalculated($this, $items, $date_field, $check_type, $table, $with_tax)); + } + + public function setArithmeticTotals($items, $date_field, $operator = 'add', $table = 'default', $amount_field = 'amount') + { + $group_field = $this->getGroup() . '_id'; + + $function = $operator . 'ArithmeticAmount'; + + foreach ($items as $item) { + // Make groups extensible + $item = $this->applyGroups($item); + + $date = $this->getPeriodicDate(Date::parse($item->$date_field), $this->getPeriod(), $this->year); + + if (!isset($item->$group_field)) { + continue; + } + + $group = $item->$group_field; + + if ( + !isset($this->row_values[$table][$group]) + || !isset($this->row_values[$table][$group][$date]) + || !isset($this->footer_totals[$table][$date]) + ) { + continue; + } + + $amount = isset($item->$amount_field) ? $item->$amount_field : 1; + + $this->$function($this->row_values[$table][$group][$date], $amount); + $this->$function($this->footer_totals[$table][$date], $amount); + } + } + + public function addArithmeticAmount(&$current, $amount) + { + $current = $current + $amount; + } + + public function subArithmeticAmount(&$current, $amount) + { + $current = $current - $amount; + } + + public function mulArithmeticAmount(&$current, $amount) + { + $current = $current * $amount; + } + + public function divArithmeticAmount(&$current, $amount) + { + if ($amount == 0) { + throw new \InvalidArgumentException('Division by zero is not allowed'); + } + + $current = $current / $amount; + } + + public function modArithmeticAmount(&$current, $amount) + { + if ($amount == 0) { + throw new \InvalidArgumentException('Modulo by zero is not allowed'); + } + + $current = $current % $amount; + } + + public function expArithmeticAmount(&$current, $amount) + { + $current = $current ** $amount; + } + + public function applyFilters($model, $args = []) + { + event(new FilterApplying($this, $model, $args)); + + return $model; + } + + public function applyGroups($model, $args = []) + { + event(new GroupApplying($this, $model, $args)); + + return $model; + } + + public function flattenDocumentItems(Collection $documents): Collection + { + return $documents->flatMap(fn (Document $d) => + $d->items->map(function (DocumentItem $di) use ($d) { + $proxy = clone $di; + $proxy->type = $d->type; + $proxy->document_number = $d->document_number; + $proxy->status = $d->status; + $proxy->issued_at = $d->issued_at; + $proxy->due_at = $d->due_at; + $proxy->currency_code = $d->currency_code; + $proxy->currency_rate = $d->currency_rate; + $proxy->amount = $di->total; + $proxy->contact_id = $d->contact_id; + + return $proxy; + }) + ); + } + + public function getUrl($action = 'print') + { + $url = company_id() . '/common/reports/' . $this->model->id . '/' . $action; + + $parameters = array_filter( + request()->all(), + static function ($value, $key) { + // Skip Laravel internal params + if (in_array($key, ['_token', '_method'])) { + return false; + } + + return is_null($value) || is_scalar($value) || is_array($value); + }, + ARRAY_FILTER_USE_BOTH + ); + + $query = http_build_query($parameters, '', '&', PHP_QUERY_RFC3986); + + if (!empty($query)) { + $url .= '?' . $query; + } + + return $url; + } + + public function getSetting($name, $default = '') + { + return $this->model->settings->$name ?? $default; + } + + public function getBasis() + { + return $this->getSearchStringValue('basis', $this->getSetting('basis', $this->getDefaultFieldSelection($this->getBasisField()))); + } + + public function getPeriod() + { + return $this->getSearchStringValue('period', $this->getSetting('period', $this->getDefaultFieldSelection($this->getPeriodField()))); + } + + public function getGroup() + { + return $this->getSearchStringValue('group', $this->getSetting('group', $this->getDefaultFieldSelection($this->getGroupField()))); + } + + public function getDiscount() + { + return $this->getSearchStringValue('discount'); + } + + public function getFields() + { + return [ + $this->getGroupField(), + $this->getPeriodField(), + $this->getBasisField(), + ]; + } + + public function getGroupField() + { + $this->setGroups(); + + return [ + 'type' => 'select', + 'name' => 'group', + 'title' => trans('general.group_by'), + 'icon' => 'folder', + 'values' => $this->groups, + 'selected' => 'category', + 'attributes' => [ + 'required' => 'required', + ], + ]; + } + + protected function getDefaultFieldSelection(array $field, string $fallback = ''): string + { + return $field['selected'] ?? $fallback; + } + + public function getPeriodField() + { + return [ + 'type' => 'select', + 'name' => 'period', + 'title' => trans('general.period'), + 'icon' => 'calendar', + 'values' => [ + 'weekly' => trans('general.weekly'), + 'monthly' => trans('general.monthly'), + 'quarterly' => trans('general.quarterly'), + 'yearly' => trans('general.yearly'), + ], + 'selected' => 'quarterly', + 'attributes' => [ + 'required' => 'required', + ], + ]; + } + + public function getBasisField() + { + return [ + 'type' => 'select', + 'name' => 'basis', + 'title' => trans('general.basis'), + 'icon' => 'file', + 'values' => [ + 'accrual' => trans('general.accrual'), + 'cash' => trans('general.cash'), + ], + 'selected' => 'accrual', + 'attributes' => [ + 'required' => 'required', + ], + ]; + } + + public function randHexColorPart(): string + { + return str_pad( dechex( mt_rand( 0, 255 ) ), 2, '0', STR_PAD_LEFT); + } + + public function randHexColor(): string + { + return '#' . $this->randHexColorPart() . $this->randHexColorPart() . $this->randHexColorPart(); + } + + // @deprecated 3.1 + public function getFormattedDate($date) + { + return $this->getPeriodicDate($date, $this->getPeriod(), $this->year); + } +} diff --git a/app/Abstracts/View/Component.php b/app/Abstracts/View/Component.php new file mode 100644 index 00000000..4b1f81cc --- /dev/null +++ b/app/Abstracts/View/Component.php @@ -0,0 +1,13 @@ +getConsumableComponentData($key, $default); + } +} diff --git a/app/Abstracts/View/Components/Contacts/Form.php b/app/Abstracts/View/Components/Contacts/Form.php new file mode 100644 index 00000000..2de2c8e1 --- /dev/null +++ b/app/Abstracts/View/Components/Contacts/Form.php @@ -0,0 +1,568 @@ +type = $type; + + $this->model = ! empty($model) ? $model : $contact; + $this->contact = $this->model; + + /* -- Content Start -- */ + $this->formId = $formId; + $this->formRoute = $this->getFormRoute($type, $formRoute, $this->model); + $this->formMethod = $this->getFormMethod($type, $formMethod, $this->model); + + $this->hideSectionGeneral = $hideSectionGeneral; + $this->hideSectionBilling = $hideSectionBilling; + $this->hideSectionAddress = $hideSectionAddress; + $this->hideSectionPersons = $hideSectionPersons; + + /* -- General Start -- */ + $this->textSectionGeneralTitle = $this->getTextSectionGeneralTitle($type, $textSectionGeneralTitle); + $this->textSectionGeneralDescription = $this->getTextSectionGeneralDescription($type, $textSectionGeneralDescription); + + $this->hideName = $hideName; + $this->textName = $this->getTextName($type, $textName); + $this->classNameFromGroupClass = $this->getClassNameFormGroupClass($type, $classNameFromGroupClass); + + $this->hideEmail = $hideEmail; + $this->textEmail = $this->getTextEmail($type, $textEmail); + + $this->hidePhone = $hidePhone; + $this->textPhone = $this->getTextPhone($type, $textPhone); + + $this->hideWebsite = $hideWebsite; + $this->textWebsite = $this->getTextWebsite($type, $textWebsite); + + $this->hideReference = $hideReference; + $this->textReference = $this->getTextReference($type, $textReference); + + $this->hideCanLogin = $hideCanLogin; + $this->hideLogo = $hideLogo; + /* -- General End -- */ + + /* -- Billing Start -- */ + $this->textSectionBillingTitle = $this->getTextSectionBillingTitle($type, $textSectionBillingTitle); + $this->textSectionBillingDescription = $this->getTextSectionBillingDescription($type, $textSectionBillingDescription); + + $this->hideTaxNumber = $hideTaxNumber; + $this->textTaxNumber = $this->getTextTaxNumber($type, $textTaxNumber); + + $this->hideCurrency = $hideCurrency; + + $this->hideCategory = $hideCategory; + $this->categoryType = $this->getCategoryType($type, $categoryType); + /* -- Billing End -- */ + + /* -- Address Start -- */ + $this->textSectionAddressTitle = $this->getTextSectionAddressTitle($type, $textSectionAddressTitle); + $this->textSectionAddressDescription = $this->getTextSectionAddressDescription($type, $textSectionAddressDescription); + + $this->hideAddress = $hideAddress; + $this->textAddress = $this->getTextAddress($type, $textAddress); + + $this->hideCity = $hideCity; + $this->textCity = $this->getTextCity($type, $textCity); + + $this->hideZipCode = $hideZipCode; + $this->textZipCode = $this->getTextZipCode($type, $textZipCode); + + $this->hideState = $hideState; + $this->textState = $this->getTextState($type, $textState); + + $this->hideState = $hideTaxNumber; + /* -- Address End -- */ + + /* -- Persons Start -- */ + $this->textSectionPersonsTitle = $this->getTextSectionPersonsTitle($type, $textSectionPersonsTitle); + $this->textSectionPersonsDescription = $this->getTextSectionPersonsDescription($type, $textSectionPersonsDescription); + /* -- Persons End -- */ + + /* -- Buttons Start -- */ + $this->cancelRoute = $this->getCancelRoute($type, $cancelRoute); + /* -- Buttons End -- */ + /* -- Content End -- */ + + // Set Parent data + $this->setParentData(); + } + + /* -- Content Start -- */ + /* -- General Start -- */ + protected function getTextSectionGeneralTitle($type, $textSectionGeneralTitle) + { + if (! empty($textSectionGeneralTitle)) { + return $textSectionGeneralTitle; + } + + $translation = $this->getTextFromConfig($type, 'section_general_title', 'general'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.general'; + } + + protected function getTextSectionGeneralDescription($type, $textSectionGeneralDescription) + { + if (! empty($textSectionGeneralDescription)) { + return $textSectionGeneralDescription; + } + + $translation = $this->getTextFromConfig($type, 'section_general_description', 'form_description.general'); + + if (! empty($translation)) { + return $translation; + } + + return 'customers.form_description.general'; + } + + protected function getTextName($type, $textName) + { + if (! empty($textName)) { + return $textName; + } + + $translation = $this->getTextFromConfig($type, 'name', 'name'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.name'; + } + + protected function getClassNameFormGroupClass($type, $classNameFromGroupClass) + { + if (! empty($classNameFromGroupClass)) { + return $classNameFromGroupClass; + } + + $class = $this->getClassFromConfig($type, 'name'); + + if (! empty($class)) { + return $class; + } + + return 'sm:col-span-6'; + } + + protected function getTextEmail($type, $textEmail) + { + if (! empty($textEmail)) { + return $textEmail; + } + + $translation = $this->getTextFromConfig($type, 'email', 'email'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.email'; + } + + protected function getTextPhone($type, $textPhone) + { + if (! empty($textPhone)) { + return $textPhone; + } + + $translation = $this->getTextFromConfig($type, 'phone', 'phone'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.phone'; + } + + protected function getTextWebsite($type, $textWebsite) + { + if (! empty($textWebsite)) { + return $textWebsite; + } + + $translation = $this->getTextFromConfig($type, 'website', 'website'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.website'; + } + + protected function getTextReference($type, $textReference) + { + if (! empty($textReference)) { + return $textReference; + } + + $translation = $this->getTextFromConfig($type, 'reference', 'reference'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.reference'; + } + /* -- General End -- */ + + /* -- Billing Start -- */ + protected function getTextSectionBillingTitle($type, $textSectionBillingTitle) + { + if (! empty($textSectionBillingTitle)) { + return $textSectionBillingTitle; + } + + $translation = $this->getTextFromConfig($type, 'section_billing_title'); + + if (! empty($translation)) { + return $translation; + } + + return 'items.billing'; + } + + protected function getTextSectionBillingDescription($type, $textSectionBillingDescription) + { + if (! empty($textSectionBillingDescription)) { + return $textSectionBillingDescription; + } + + $translation = $this->getTextFromConfig($type, 'section_billing_description'); + + if (! empty($translation)) { + return $translation; + } + + return 'customers.form_description.billing'; + } + + protected function getTextTaxNumber($type, $textTaxNumber) + { + if (! empty($textTaxNumber)) { + return $textTaxNumber; + } + + $translation = $this->getTextFromConfig($type, 'tax_number', 'tax_number'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.tax_number'; + } + + protected function getCategoryType($type, $categoryType) + { + if (! empty($categoryType)) { + return $categoryType; + } + + return config('type.contact.' . $type . '.transaction_type', 'income'); + } + /* -- Billing End -- */ + + /* -- Address Start -- */ + protected function getTextSectionAddressTitle($type, $textSectionAddressTitle) + { + if (! empty($textSectionAddressTitle)) { + return $textSectionAddressTitle; + } + + $translation = $this->getTextFromConfig($type, 'section_address_title', 'address'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.address'; + } + + protected function getTextSectionAddressDescription($type, $textSectionAddressDescription) + { + if (! empty($textSectionAddressDescription)) { + return $textSectionAddressDescription; + } + + $translation = $this->getTextFromConfig($type, 'section_address_description'); + + if (! empty($translation)) { + return $translation; + } + + return 'customers.form_description.address'; + } + + /* -- Persons Start -- */ + protected function getTextSectionPersonsTitle($type, $textSectionPersonsTitle) + { + if (! empty($textSectionPersonsTitle)) { + return $textSectionPersonsTitle; + } + + $translation = $this->getTextFromConfig($type, 'section_contact_persons_title', 'contact_persons'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.contact_persons'; + } + + protected function getTextSectionPersonsDescription($type, $textSectionPersonsDescription) + { + if (! empty($textSectionPersonsDescription)) { + return $textSectionPersonsDescription; + } + + $translation = $this->getTextFromConfig($type, 'section_contact_persons_description'); + + if (! empty($translation)) { + return $translation; + } + + return 'customers.form_description.contact_persons'; + } + + protected function getTextAddress($type, $textAddress) + { + if (! empty($textAddress)) { + return $textAddress; + } + + $translation = $this->getTextFromConfig($type, 'address', 'address'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.address'; + } + + protected function getTextCity($type, $textCity) + { + if (! empty($textCity)) { + return $textCity; + } + + $translation = $this->getTextFromConfig($type, 'city', 'cities'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.cities'; + } + + protected function getTextZipCode($type, $textZipCode) + { + if (! empty($textZipCode)) { + return $textZipCode; + } + + $translation = $this->getTextFromConfig($type, 'zip_code', 'zip_code'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.zip_code'; + } + + protected function getTextState($type, $textState) + { + if (! empty($textState)) { + return $textState; + } + + $translation = $this->getTextFromConfig($type, 'state', 'state'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.state'; + } + /* -- Address End -- */ + /* -- Content End -- */ +} diff --git a/app/Abstracts/View/Components/Contacts/Index.php b/app/Abstracts/View/Components/Contacts/Index.php new file mode 100644 index 00000000..cfa0cdf1 --- /dev/null +++ b/app/Abstracts/View/Components/Contacts/Index.php @@ -0,0 +1,497 @@ +type = $type; + $this->group = $this->getGroup($type, $group); + $this->page = $this->getPage($type, $page); + $this->contacts = ($contacts) ? $contacts : collect(); + $this->textPage = $this->getTextPage($type, $textPage); + + $this->permissionCreate = $this->getPermissionCreate($type, $permissionCreate); + $this->permissionUpdate = $this->getPermissionUpdate($type, $permissionUpdate); + $this->permissionDelete = $this->getPermissionDelete($type, $permissionDelete); + /* -- Main End -- */ + + /* -- Buttons Start -- */ + $this->checkPermissionCreate = $checkPermissionCreate; + + $this->hideCreate = $hideCreate; + $this->hideImport = $hideImport; + $this->hideExport = $hideExport; + + $this->routeButtonShow = $this->getRouteButtonShow($type, $routeButtonShow); + $this->createRoute = $this->getCreateRoute($type, $createRoute); + $this->importRoute = $this->getImportRoute($importRoute); + $this->importRouteParameters = $this->getImportRouteParameters($type, $importRouteParameters); + $this->exportRoute = $this->getExportRoute($type, $exportRoute); + /* -- Buttons End -- */ + + /* -- Content Start -- */ + $this->hideEmptyPage = $hideEmptyPage; + + $this->hideSummary = $hideSummary; + $this->summaryItems = $this->getSummaryItems($type, $summaryItems); + + $this->hideSearchString = $hideSearchString; + $this->hideBulkAction = $hideBulkAction; + + $this->searchStringModel = $this->getSearchStringModel($type, $searchStringModel); + + $this->bulkActionClass = $this->getBulkActionClass($type, $bulkActionClass); + $this->bulkActionRouteParameters = $this->getBulkActionRouteParameters($type, $bulkActionRouteParameters); + + $this->searchRoute = $this->getIndexRoute($type, $searchRoute); + + $this->classBulkAction = $this->getClassBulkAction($type, $classBulkAction); + + $this->showLogo = $showLogo; + $this->hideName = $hideName; + $this->hideTaxNumber = $hideTaxNumber; + $this->classNameAndTaxNumber = $this->getClassNameAndTaxNumber($type, $classNameAndTaxNumber); + $this->textName = $this->getTextName($type, $textName); + $this->textTaxNumber = $this->getTextTaxNumber($type, $textTaxNumber); + + $this->hideEmail = $hideEmail; + $this->hidePhone = $hidePhone; + $this->classEmailAndPhone = $this->getClassEmailAndPhone($type, $classEmailAndPhone); + $this->textEmail = $this->getTextEmail($type, $textEmail); + $this->textPhone = $this->getTextPhone($type, $textPhone); + + $this->hideCountry = $hideCountry; + $this->hideCurrencyCode = $hideCurrencyCode; + $this->classCountryAndCurrencyCode = $this->getClassCountryAndCurrencyCode($type, $classCountryAndCurrencyCode); + $this->textCountry = $this->getTextCountry($type, $textCountry); + $this->textCurrencyCode = $this->getTextCurrencyCode($type, $textCurrencyCode); + + $this->hideOpen = $hideOpen; + $this->hideOverdue = $hideOverdue; + $this->classOpenAndOverdue = $this->getClassOpenAndOverdue($type, $classOpenAndOverdue); + $this->textOpen = $this->getTextOpen($type, $textOpen); + $this->textOverdue = $this->getTextOverdue($type, $textOverdue); + /* -- Content End -- */ + + /* -- Empty Start -- */ + $this->imageEmptyPage = $this->getImageEmptyPage($type, $imageEmptyPage); + $this->textEmptyPage = $this->getTextEmptyPage($type, $textEmptyPage); + $this->urlDocsPath = $this->getUrlDocsPath($type, $urlDocsPath); + /* -- Empty End -- */ + + // Set Parent data + $this->setParentData(); + } + + public function getSummaryItems($type, $summaryItems) + { + if (! empty($summaryItems)) { + return $summaryItems; + } + + $route = $this->getIndexRoute($type, null); + + $document_type = config('type.contact.' . $type . '.document_type', 'invoice'); + + $totals = $this->getTotalsForFutureDocuments($document_type); + + $items = []; + + foreach ($totals as $key => $total) { + $items[] = [ + 'title' => ($key == 'overdue') ? trans('general.overdue') : trans('documents.statuses.' . $key), + //'href' => route($route, ['search' => 'status:' . $key]), + 'amount' => money($total)->formatForHumans(), + 'tooltip' => money($total)->format(), + ]; + } + + return $items; + } + + /* -- Content Start -- */ + protected function getClassNameAndTaxNumber($type, $classNameAndTaxNumber) + { + if (! empty($classNameAndTaxNumber)) { + return $classNameAndTaxNumber; + } + + $class = $this->getClassFromConfig($type, 'name_and_tax_number'); + + if (! empty($class)) { + return $class; + } + + return 'w-6/12 sm:w-3/12 truncate'; + } + + protected function getTextName($type, $textName) + { + if (! empty($textName)) { + return $textName; + } + + $translation = $this->getTextFromConfig($type, 'name', 'name'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.name'; + } + + protected function getTextTaxNumber($type, $textTaxNumber) + { + if (! empty($textTaxNumber)) { + return $textTaxNumber; + } + + $translation = $this->getTextFromConfig($type, 'tax_number', 'tax_number'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.tax_number'; + } + + protected function getClassEmailAndPhone($type, $classEmailAndPhone) + { + if (! empty($classEmailAndPhone)) { + return $classEmailAndPhone; + } + + $class = $this->getClassFromConfig($type, 'email_and_phone'); + + if (! empty($class)) { + return $class; + } + + return 'w-3/12 hidden sm:table-cell'; + } + + protected function getTextEmail($type, $textEmail) + { + if (! empty($textEmail)) { + return $textEmail; + } + + $translation = $this->getTextFromConfig($type, 'email', 'email'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.email'; + } + + protected function getTextPhone($type, $textPhone) + { + if (! empty($textPhone)) { + return $textPhone; + } + + $translation = $this->getTextFromConfig($type, 'phone', 'phone'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.phone'; + } + + protected function getClassCountryAndCurrencyCode($type, $classCountryAndCurrencyCode) + { + if (! empty($classCountryAndCurrencyCode)) { + return $classCountryAndCurrencyCode; + } + + $class = $this->getClassFromConfig($type, 'country_and_currency_code'); + + if (! empty($class)) { + return $class; + } + + return 'w-3/12 hidden sm:table-cell'; + } + + protected function getTextCountry($type, $textCountry) + { + if (! empty($textCountry)) { + return $textCountry; + } + + $translation = $this->getTextFromConfig($type, 'country', 'countries'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.countries'; + } + + protected function getTextCurrencyCode($type, $textCurrencyCode) + { + if (! empty($textCurrencyCode)) { + return $textCurrencyCode; + } + + $translation = $this->getTextFromConfig($type, 'currency_code', 'currencies'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.currencies'; + } + + protected function getClassOpenAndOverdue($type, $classOpenAndOverdue) + { + if (! empty($classOpenAndOverdue)) { + return $classOpenAndOverdue; + } + + $class = $this->getClassFromConfig($type, 'open_and_overdue'); + + if (! empty($class)) { + return $class; + } + + return 'w-6/12 sm:w-3/12'; + } + + protected function getTextOpen($type, $textOpen) + { + if (! empty($textOpen)) { + return $textOpen; + } + + $translation = $this->getTextFromConfig($type, 'open', 'open'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.open'; + } + + protected function getTextOverdue($type, $textOverdue) + { + if (! empty($textOverdue)) { + return $textOverdue; + } + + $translation = $this->getTextFromConfig($type, 'overdue', 'overdue'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.overdue'; + } + + protected function getRouteButtonShow($type, $routeButtonShow) + { + if (!empty($routeButtonShow)) { + return $routeButtonShow; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'show', $parameter); + + if (!empty($route)) { + return $route; + } + + return 'customer.show'; + } + /* -- Content End -- */ +} diff --git a/app/Abstracts/View/Components/Contacts/Show.php b/app/Abstracts/View/Components/Contacts/Show.php new file mode 100644 index 00000000..5a614100 --- /dev/null +++ b/app/Abstracts/View/Components/Contacts/Show.php @@ -0,0 +1,319 @@ +type = $type; + + $this->model = ! empty($model) ? $model : $contact; + $this->contact = $this->model; + + $this->permissionCreate = $this->getPermissionCreate($type, $permissionCreate); + $this->permissionUpdate = $this->getPermissionUpdate($type, $permissionUpdate); + $this->permissionDelete = $this->getPermissionDelete($type, $permissionDelete); + /* -- Main End -- */ + + /* -- Buttons Start -- */ + $this->hideNewDropdown = $hideNewDropdown; + $this->hideButtonDocument = $hideButtonDocument; + $this->hideButtonTransaction = $hideButtonTransaction; + + $this->permissionCreateDocument = $this->getPermissionCreateDocument($type, $permissionCreateDocument); + $this->permissionCreateTransaction = $this->getPermissionCreateTransaction($type, $permissionCreateTransaction); + + $this->routeButtonDocument = $this->getCreateDocumentRoute($type, $routeButtonDocument); + $this->routeButtonTransaction = $this->getCreateTransactionRoute($type, $routeButtonTransaction); + + $this->textDocument = $this->getTextDocument($type, $textDocument); + $this->textTransaction = $this->getTextTransaction($type, $textTransaction); + + $this->hideButtonEdit = $hideButtonEdit; + $this->routeButtonEdit = $this->getEditRoute($type, $routeButtonEdit); + + $this->hideActionsDropdown = $hideActionsDropdown; + $this->hideButtonDuplicate = $hideButtonDuplicate; + $this->routeButtonDuplicate = $this->getDuplicateRoute($type, $routeButtonDuplicate); + + $this->hideButtonDelete = $hideButtonDelete; + $this->routeButtonDelete = $this->getDeleteRoute($type, $routeButtonDelete); + $this->textDeleteModal = $this->getTextDeleteModal($type, $textDeleteModal); + /* -- Buttons End -- */ + + /* -- Profile Start -- */ + $this->hideProfile = $hideTopLeft; + $this->hideAvatar = $hideAvatar; + $this->hideEmail = $hideEmail; + $this->hidePhone = $hidePhone; + + $this->hideDetails = $hideTopRight; + $this->hideOverdue = $hideOverdue; + $this->hideOpen = $hideOpen; + $this->hidePaid = $hidePaid; + + $this->hideSummary = $hideBottomLeft; + $this->hideAddress = $hideAddress; + $this->hideTaxNumber = $hideTaxNumber; + $this->hideWebsite = $hideWebsite; + $this->hideReference = $hideReference; + $this->hideUser = $hideUser; + $this->hidePersons = $hidePersons; + + $this->hideContent = $hideBottomRight; + /* -- Profile End -- */ + } + + protected function getPermissionCreateDocument($type, $permissionCreateDocument) + { + if (! empty($permissionCreateDocument)) { + return $permissionCreateDocument; + } + + $document_type = config('type.contact.' . $type . '.document_type', 'invoice'); + + $permission = ''; + $config_key = 'create'; + + // if set config translation config_key + if ($permission = config('type.document.' . $document_type . '.permission.' . $config_key)) { + return $permission; + } + + $alias = config('type.document.' . $document_type . '.alias'); + $group = config('type.document.' . $document_type . '.group'); + $prefix = config('type.document.' . $document_type . '.permission.prefix'); + + $permission = $config_key . '-'; + + // if use module set module alias + if (! empty($alias)) { + $permission .= $alias . '-'; + } + + // if controller in folder it must + if (! empty($group)) { + $permission .= $group . '-'; + } + + $permission .= $prefix; + + $permissionCreateDocument = $permission; + + return $permissionCreateDocument; + } + + protected function getPermissionCreateTransaction($type, $permissionCreateTransaction) + { + if (! empty($permissionCreateTransaction)) { + return $permissionCreateTransaction; + } + + $permissionCreateTransaction = 'create-banking-transactions'; + + return $permissionCreateTransaction; + } + + protected function getCreateDocumentRoute($type, $routeButtonDocument) + { + if (! empty($routeButtonDocument)) { + return $routeButtonDocument; + } + + $prefix = config('type.contact.' . $type . '.route.prefix'); + $document_type = config('type.contact.' . $type . '.document_type'); + + return $prefix . '.create-' . $document_type; + } + + protected function getCreateTransactionRoute($type, $routeButtonDocument) + { + if (! empty($routeButtonDocument)) { + return $routeButtonDocument; + } + + $prefix = config('type.contact.' . $type . '.route.prefix'); + $transaction_type = config('type.contact.' . $type . '.transaction_type'); + + return $prefix . '.create-' . $transaction_type; + } + + protected function getTextDocument($type, $textDocument) + { + if (! empty($textDocument)) { + return $textDocument; + } + + $document_type = config('type.contact.' . $type . '.document_type'); + + switch ($document_type) { + case 'invoice': + $text = 'general.invoices'; + break; + case 'bill': + $text = 'general.bills'; + break; + default: + $text = config('type.contact.' . $type . '.translation.prefix') . '.' . config('type.contact.' . $type . '.route.prefix'); + } + + return $text; + } + + protected function getTextTransaction($type, $textTransaction) + { + if (! empty($textTransaction)) { + return $textTransaction; + } + + $document_type = config('type.contact.' . $type . '.document_type'); + + switch ($document_type) { + case 'invoice': + $text = 'general.incomes'; + break; + case 'bill': + $text = 'general.expenses'; + break; + default: + $text = config('type.contact.' . $type . '.translation.prefix') . '.' . config('type.contact.' . $type . '.transaction_type') . 's'; + } + + return $text; + } + + protected function getTextDeleteModal($type, $textDeleteModal) + { + if (! empty($textDeleteModal)) { + return $textDeleteModal; + } + + $document_type = config('type.contact.' . $type . '.document_type'); + + switch ($document_type) { + case 'invoice': + $text = 'general.incomes'; + break; + case 'bill': + $text = 'general.expenses'; + break; + default: + $text = config('type.contact.' . $type . '.translation.prefix') . '.' . config('type.contact.' . $type . '.transaction_type') . 's'; + } + + return $text; + } +} diff --git a/app/Abstracts/View/Components/Documents/Form.php b/app/Abstracts/View/Components/Documents/Form.php new file mode 100644 index 00000000..dc815dd9 --- /dev/null +++ b/app/Abstracts/View/Components/Documents/Form.php @@ -0,0 +1,1395 @@ +type = $type; + + $this->model = ! empty($model) ? $model : $document; + $this->document = $this->model; + + $this->currency_code = ! empty($this->currency) ? $this->currency->code : default_currency(); + $this->currency = $this->getCurrency($document, $currency, $currency_code); + $this->currencies = $this->getCurrencies($currencies); + + $this->taxes = Tax::enabled()->orderBy('name')->get()->pluck('title', 'id'); + + /* -- Content Start -- */ + /* -- Form Start -- */ + $this->formId = $formId; + $this->formRoute = $this->getFormRoute($type, $formRoute, $this->model); + $this->formMethod = $this->getFormMethod($type, $formMethod, $this->model); + /* -- Form End -- */ + + /* -- Company Start -- */ + $this->hideCompany = $hideCompany; + $this->textSectionCompaniesTitle = $this->getTextSectionCompaniesTitle($type, $textSectionCompaniesTitle); + $this->textSectionCompaniesDescription = $this->getTextSectionCompaniesDescription($type, $textSectionCompaniesDescription); + $this->hideLogo = $hideLogo; + $this->hideCompanyEdit = $hideCompanyEdit; + /** Company End */ + + /* -- Main Start -- */ + $this->textSectionMainTitle = $this->getTextSectionMainTitle($type, $textSectionMainTitle); + $this->textSectionMainDescription = $this->getTextSectionMainDescription($type, $textSectionMainDescription); + + /* -- Metadata Start -- */ + $this->typeContact = $this->getTypeContact($type, $typeContact); + $this->contact = $this->getContact($contact, $document); + $this->contacts = $this->getContacts($type, $document, $contacts); + + $this->searchContactRoute = $this->getSearchContactRoute($type, $searchContactRoute); + $this->createContactRoute = $this->getCreateContactRoute($type, $createContactRoute); + + $this->textContact = $this->getTextContact($type, $textContact); + $this->textAddContact = $this->getTextAddContact($type, $textAddContact); + $this->textCreateNewContact = $this->getTextCreateNewContact($type, $textCreateNewContact); + $this->textEditContact = $this->getTextEditContact($type, $textEditContact); + $this->textContactInfo = $this->getTextContactInfo($type, $textContactInfo); + $this->textChooseDifferentContact = $this->getTextChooseDifferentContact($type, $textChooseDifferentContact); + + $this->hideDocumentTitle = $hideDocumentTitle; + $this->hideDocumentSubheading = $hideDocumentSubheading; + $this->title = $this->getTitleValue($type, $title); + $this->subheading = $this->getSubheadingValue($type, $subheading); + + $this->hideIssuedAt = $hideIssuedAt; + $this->textIssuedAt = $this->getTextIssuedAt($type, $textIssuedAt); + $this->issuedAt = $this->getIssuedAt($type, $document, $issuedAt); + + $this->hideDueAt = $hideDueAt; + $this->textDueAt = $this->getTextDueAt($type, $textDueAt); + $this->dueAt = $this->getDueAt($type, $document, $dueAt); + $this->periodDueAt = $this->getPeriodDueAt($type, $periodDueAt); + + $this->hideDocumentNumber = $hideDocumentNumber; + $this->textDocumentNumber = $this->getTextDocumentNumber($type, $textDocumentNumber); + $this->documentNumber = $this->getDocumentNumber($type, $document, $documentNumber); + + $this->hideOrderNumber = $hideOrderNumber; + $this->textOrderNumber = $this->getTextOrderNumber($type, $textOrderNumber); + $this->orderNumber = $this->getOrderNumber($type, $document, $orderNumber); + /* -- Metadata End -- */ + + /** Items Start */ + $this->hideEditItemColumns = $hideEditItemColumns; + + $this->hideItems = $this->getHideItems($type, $hideItems, $hideItemName, $hideItemDescription); + $this->hideItemName = $this->getHideItemName($type, $hideItemName); + $this->hideSettingItemName = $this->getHideSettingItemName($type, $hideSettingItemName); + $this->textItemName = $this->getTextItemName($type, $textItemName); + + $this->hideItemDescription = $this->getHideItemDescription($type, $hideItemDescription); + $this->hideSettingItemDescription = $this->getHideSettingItemDescription($type, $hideSettingItemDescription); + $this->textItemDescription = $this->getTextItemDescription($type, $textItemDescription); + + $this->hideItemQuantity = $this->getHideItemQuantity($type, $hideItemQuantity); + $this->hideSettingItemQuantity = $this->getHideSettingItemQuantity($type, $hideSettingItemQuantity); + $this->textItemQuantity = $this->getTextItemQuantity($type, $textItemQuantity); + + $this->hideItemPrice = $this->getHideItemPrice($type, $hideItemPrice); + $this->hideSettingItemPrice = $this->getHideSettingItemPrice($type, $hideSettingItemPrice); + $this->textItemPrice = $this->getTextItemPrice($type, $textItemPrice); + + $this->hideItemAmount = $this->getHideItemAmount($type, $hideItemAmount); + $this->hideSettingItemAmount = $this->getHideSettingItemAmount($type, $hideSettingItemAmount); + $this->textItemAmount = $this->getTextItemAmount($type, $textItemAmount); + + $this->hideDiscount = $this->getHideDiscount($type, $hideDiscount); + + $this->isSalePrice = $isSalePrice; + $this->isPurchasePrice = $isPurchasePrice; + $this->searchCharLimit = $this->getSearchCharLimit($type, $searchCharLimit); + /** Items End */ + + /** Notes Start */ + $this->notes = $this->getNotesValue($notes); + /** Notes End */ + /** Main End */ + + /* -- Recurring Start -- */ + $this->showRecurring = $showRecurring; + /* -- Recurring End -- */ + + /* -- Advanced Start -- */ + $this->hideAdvanced = $hideAdvanced; + $this->textSectionAdvancedTitle = $this->getTextSectionAdvancedTitle($type, $textSectionAdvancedTitle); + $this->textSectionAdvancedDescription = $this->getTextSectionAdvancedDescription($type, $textSectionAdvancedDescription); + + $this->hideFooter = $hideFooter; + $this->classFooter = !empty($classFooter) ? $classFooter : 'sm:col-span-3'; + $this->footer = $this->getFooterValue($footer); + + $this->hideCategory = $hideCategory; + $this->classCategory = !empty($classCategory) ? $classCategory : 'sm:col-span-4 grid gap-x-8 gap-y-3'; + $this->typeCategory = $this->getTypeCategory($type, $typeCategory); + $this->categoryId = $this->getCategoryId($type, $categoryId); + + $this->hideAttachment = $hideAttachment; + $this->classAttachment = !empty($classAttachment) ? $classAttachment : 'sm:col-span-4'; + + $this->hideTemplate = $hideTemplate; + $this->hideBackgroundColor = $hideBackgroundColor; + + $this->templates = $this->getTemplates($type, $templates); + $this->template = $this->getDocumentTemplate($type, $template); + $this->backgroundColor = $this->getBackgroundColor($type, $backgroundColor); + /** Advanced End */ + + /** Buttons Start */ + $this->hideButtons = $hideButtons; + $this->cancelRoute = $this->getCancelRoute($type, $cancelRoute); + $this->hideSendTo = $hideSendTo; + /** Buttons End */ + /* -- Content End -- */ + + // Set Parent data + $this->setParentData(); + } + + protected function getCurrencies($currencies) + { + if (!empty($currencies)) { + return $currencies; + } + + return Currency::enabled()->pluck('name', 'code'); + } + + protected function getCurrency($document, $currency, $currency_code) + { + if (! empty($currency)) { + return $currency; + } + + if (! empty($currency_code)) { + $currency = Currency::where('code', $currency_code)->first(); + } + + if (empty($currency) && ! empty($document)) { + $currency = Currency::where('code', $document->currency_code)->first(); + } + + if (empty($currency)) { + $currency = Currency::where('code', default_currency())->first(); + } + + return $currency; + } + + protected function getTextSectionCompaniesTitle($type, $textSectionCompaniesTitle) + { + if (! empty($textSectionCompaniesTitle)) { + return $textSectionCompaniesTitle; + } + + return $this->getTextSectionTitle($type, 'companies', 'general.companies'); + } + + protected function getTextSectionCompaniesDescription($type, $textSectionCompaniesDescription) + { + if (! empty($textSectionCompaniesDescription)) { + return $textSectionCompaniesDescription; + } + + return $this->getTextSectionDescription($type, 'companies', 'documents.form_description.companies'); + } + + protected function getTextSectionMainTitle($type, $textSectionMainTitle) + { + if (! empty($textSectionMainTitle)) { + return $textSectionMainTitle; + } + + return $this->getTextSectionTitle($type, 'main', 'documents.billing'); + } + + protected function getTextSectionMainDescription($type, $textSectionMainDescription) + { + if (! empty($textSectionMainDescription)) { + return $textSectionMainDescription; + } + + return $this->getTextSectionDescription($type, 'billing', 'documents.form_description.billing'); + } + + protected function getTypeContact($type, $typeContact) + { + if (! empty($typeContact)) { + return $typeContact; + } + + return config('type.' . static::OBJECT_TYPE . '.' . $type . '.contact_type', 'customer'); + } + + protected function getTextContact($type, $textContact) + { + if (! empty($textContact)) { + return $textContact; + } + + $contact_type = config('type.' . static::OBJECT_TYPE . '.' . $type . '.contact_type'); + + $default_key = config('type.contact.' . $contact_type . '.translation.prefix'); + + $translation = $this->getTextFromConfig($type, 'contact', $default_key); + + if (!empty($translation)) { + return $translation; + } + + return 'general.customers'; + } + + protected function getContact($contact, $document) + { + if (!empty($contact)) { + return $contact; + } + + $contact = new \stdClass(); + + if (!empty($document) && !empty($document->contact)) { + $contact = $document->contact; + } + + if (request()->old('contact', false)) { + $contact = request()->old('contact'); + } + + return $contact; + } + + protected function getContacts($type, $document, $contacts) + { + if (!empty($contacts)) { + return $contacts; + } + + $contact_type = $this->getTypeContact($type, null); + + if ($contact_type) { + $contacts = Contact::$contact_type()->enabled()->orderBy('name')->take(setting('default.select_limit'))->get(); + } else { + $contacts = Contact::enabled()->orderBy('name')->take(setting('default.select_limit'))->get(); + } + + if (!empty($document) && ($document->contact && !$contacts->contains('id', $document->contact_id))) { + $contacts->push($document->contact); + } + + return $contacts; + } + + protected function getSearchContactRoute($type, $searchContactRoute) + { + if (! empty($searchContactRoute)) { + return $searchContactRoute; + } + + $contact_type = config('type.' . static::OBJECT_TYPE . '.' . $type . '.contact_type'); + + $default_key = config('type.contact.' . $contact_type . '.route.prefix'); + + return route($default_key . '.index'); + } + + protected function getCreateContactRoute($type, $createContactRoute) + { + if (! empty($createContactRoute)) { + return $createContactRoute; + } + + $contact_type = config('type.' . static::OBJECT_TYPE . '.' . $type . '.contact_type'); + + $default_key = config('type.contact.' . $contact_type . '.route.prefix'); + + return route('modals.' . $default_key . '.create'); + } + + protected function getTextAddContact($type, $textAddContact) + { + if (!empty($textAddContact)) { + return $textAddContact; + } + + $default_key = Str::plural(config('type.document.' . $type . '.contact_type'), 2); + + $translation = $this->getTextFromConfig($type, 'add_contact', $default_key, 'trans_choice'); + + if (!empty($translation)) { + return [ + 'general.form.add', + $translation, + ]; + } + + return [ + 'general.form.add', + 'general.customers', + ]; + } + + protected function getTextCreateNewContact($type, $textCreateNewContact) + { + if (!empty($textCreateNewContact)) { + return $textCreateNewContact; + } + + $default_key = Str::plural(config('type.document.' . $type . '.contact_type'), 2); + + $translation = $this->getTextFromConfig($type, 'create_new_contact', $default_key, 'trans_choice'); + + if (!empty($translation)) { + return [ + 'general.title.new', + $translation, + ]; + } + + return 'general.title.add'; + } + + protected function getTextEditContact($type, $textEditContact) + { + if (!empty($textEditContact)) { + return $textEditContact; + } + + $translation = $this->getTextFromConfig($type, 'edit_contact', 'form.edit'); + + if (!empty($translation)) { + return $translation; + } + + return 'general.form.edit'; + } + + protected function getTextContactInfo($type, $textContactInfo) + { + if (!empty($textContactInfo)) { + return $textContactInfo; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'bill_from'; + break; + default: + $default_key = 'bill_to'; + break; + } + + $translation = $this->getTextFromConfig($type, 'contact_info', $default_key); + + if (!empty($translation)) { + return $translation; + } + + return 'invoices.bill_to'; + } + + protected function getTextChooseDifferentContact($type, $textChooseDifferentContact) + { + if (!empty($textChooseDifferentContact)) { + return $textChooseDifferentContact; + } + + $default_key = Str::plural(config('type.document.' . $type . '.contact_type'), 2); + + $translation = $this->getTextFromConfig($type, 'choose_different_contact', $default_key, 'trans_choice'); + + if (!empty($translation)) { + return [ + 'general.form.choose_different', + $translation, + ]; + } + + return [ + 'general.form.choose_different', + 'general.customers', + ]; + } + + protected function getTextIssuedAt($type, $textIssuedAt) + { + if (!empty($textIssuedAt)) { + return $textIssuedAt; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'bill_date'; + break; + default: + $default_key = 'invoice_date'; + break; + } + + $translation = $this->getTextFromConfig($type, 'issued_at', $default_key); + + if (!empty($translation)) { + return $translation; + } + + return 'invoices.invoice_date'; + } + + protected function getIssuedAt($type, $document, $issuedAt) + { + if (!empty($issuedAt)) { + return $issuedAt; + } + + if ($document) { + return $document->issued_at; + } + + $issued_at = $type . '_at'; + + if (request()->has($issued_at)) { + $issuedAt = request()->get($issued_at); + } else { + $issuedAt = request()->get('invoice_at', Date::now()->toDateString()); + } + + return $issuedAt; + } + + protected function getTextDueAt($type, $textDueAt) + { + if (!empty($textDueAt)) { + return $textDueAt; + } + + $translation = $this->getTextFromConfig($type, 'due_at', 'due_date'); + + if (!empty($translation)) { + return $translation; + } + + return 'invoices.due_date'; + } + + protected function getDueAt($type, $document, $dueAt) + { + if (!empty($dueAt)) { + return $dueAt; + } + + if ($document) { + return $document->due_at; + } + + $issued_at = $type . '_at'; + + if (request()->has($issued_at)) { + $issuedAt = request()->get($issued_at); + } else { + $issuedAt = Date::now()->toDateString(); + } + + $addDays = setting($this->getDocumentSettingKey($type, 'payment_terms'), 0) ?: 0; + + $dueAt = Date::parse($issuedAt)->addDays($addDays)->toDateString(); + + return $dueAt; + } + + protected function getPeriodDueAt($type, $periodDueAt) + { + if (! empty($periodDueAt)) { + return $periodDueAt; + } + + return setting($type. '.payment_terms', 0); + } + + protected function getTextDocumentNumber($type, $textDocumentNumber) + { + if (! empty($textDocumentNumber)) { + return $textDocumentNumber; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'bill_number'; + break; + default: + $default_key = 'invoice_number'; + break; + } + + $translation = $this->getTextFromConfig($type, 'document_number', $default_key); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.invoice_number'; + } + + protected function getDocumentNumber($type, $document, $documentNumber) + { + if (! empty($documentNumber)) { + return $documentNumber; + } + + if ($document) { + return $document->document_number; + } + + $contact = ($this->contact instanceof Contact) ? $this->contact : null; + + $utility = app(DocumentNumber::class); + + $document_number = $utility->getNextNumber($type, $contact); + + if (empty($document_number)) { + $document_number = $utility->getNextNumber(Document::INVOICE_TYPE, $contact); + } + + return $document_number; + } + + protected function getTextOrderNumber($type, $textOrderNumber) + { + if (! empty($textOrderNumber)) { + return $textOrderNumber; + } + + $translation = $this->getTextFromConfig($type, 'order_number'); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.order_number'; + } + + protected function getOrderNumber($type, $document, $orderNumber) + { + if (! empty($orderNumber)) { + return $orderNumber; + } + + if ($document) { + return $document->order_number; + } + + $order_number = null; + } + + protected function getHideItems($type, $hideItems, $hideItemName, $hideItemDescription) + { + if (! empty($hideItems)) { + return $hideItems; + } + + $hide = $this->getHideFromConfig($type, 'items'); + + if ($hide) { + return $hide; + } + + $hideItems = ($this->getHideItemName($type, $hideItemName) & $this->getHideItemDescription($type, $hideItemDescription)) ? true : false; + + return $hideItems; + } + + protected function getHideItemName($type, $hideItemName): bool + { + if (! empty($hideItemName)) { + return $hideItemName; + } + + $hide = $this->getHideFromConfig($type, 'name'); + + if ($hide) { + return $hide; + } + + return false; + } + + protected function getHideSettingItemName($type, $hideSettingItemName): bool + { + if (! empty($hideSettingItemName)) { + return $hideSettingItemName; + } + + $hideItemName = setting($this->getDocumentSettingKey($type, 'item_name'), false); + + // if you use setting translation + if ($hideItemName === 'hide') { + return true; + } + + return false; + } + + protected function getTextItemName($type, $textItemName) + { + if (! empty($textItemName)) { + return $textItemName; + } + + // if you use setting translation + if (setting($this->getDocumentSettingKey($type, 'item_name'), 'items') === 'custom') { + if (empty($textItemName = setting($this->getDocumentSettingKey($type, 'item_name_input')))) { + $textItemName = 'general.items'; + } + + return $textItemName; + } + + if (setting($this->getDocumentSettingKey($type, 'item_name')) !== null + && (trans(setting($this->getDocumentSettingKey($type, 'item_name'))) != setting($this->getDocumentSettingKey($type, 'item_name'))) + ) { + return setting($this->getDocumentSettingKey($type, 'item_name')); + } + + $translation = $this->getTextFromConfig($type, 'items'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.items'; + } + + protected function getHideItemDescription($type, $hideItemDescription): bool + { + if (! empty($hideItemDescription)) { + return $hideItemDescription; + } + + $hide = $this->getHideFromConfig($type, 'description'); + + if ($hide) { + return $hide; + } + + return false; + } + + protected function getHideSettingItemDescription($type, $hideSettingItemDescription): bool + { + if (! empty($hideSettingItemDescription)) { + return $hideSettingItemDescription; + } + + // if you use setting translation + if (setting($this->getDocumentSettingKey($type, 'hide_item_description'), false)) { + return true; + } + + return false; + } + + protected function getTextItemDescription($type, $textItemDescription) + { + if (! empty($textItemDescription)) { + return $textItemDescription; + } + + $translation = $this->getTextFromConfig($type, 'description'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.description'; + } + + protected function getHideItemQuantity($type, $hideItemQuantity): bool + { + if (! empty($hideItemQuantity)) { + return $hideItemQuantity; + } + + $hide = $this->getHideFromConfig($type, 'quantity'); + + if ($hide) { + return $hide; + } + + return false; + } + + protected function getHideSettingItemQuantity($type, $hideSettingItemQuantity): bool + { + if (! empty($hideSettingItemQuantity)) { + return $hideSettingItemQuantity; + } + + $hideItemQuantity = setting($this->getDocumentSettingKey($type, 'quantity_name'), false); + + // if you use setting translation + if ($hideItemQuantity === 'hide') { + return true; + } + + return false; + } + + protected function getTextItemQuantity($type, $textItemQuantity) + { + if (! empty($textItemQuantity)) { + return $textItemQuantity; + } + + // if you use setting translation + if (setting($this->getDocumentSettingKey($type, 'quantity_name'), 'quantity') === 'custom') { + if (empty($textItemQuantity = setting($this->getDocumentSettingKey($type, 'quantity_name_input')))) { + $textItemQuantity = 'invoices.quantity'; + } + + return $textItemQuantity; + } + + if (setting($this->getDocumentSettingKey($type, 'quantity_name')) !== null + && (trans(setting($this->getDocumentSettingKey($type, 'quantity_name'))) != setting($this->getDocumentSettingKey($type, 'quantity_name'))) + ) { + return setting($this->getDocumentSettingKey($type, 'quantity_name')); + } + + $translation = $this->getTextFromConfig($type, 'quantity'); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.quantity'; + } + + protected function getHideItemPrice($type, $hideItemPrice): bool + { + if (! empty($hideItemPrice)) { + return $hideItemPrice; + } + + $hide = $this->getHideFromConfig($type, 'price'); + + if ($hide) { + return $hide; + } + + return false; + } + + protected function getHideSettingItemPrice($type, $hideSettingItemPrice): bool + { + if (! empty($hideSettingItemPrice)) { + return $hideSettingItemPrice; + } + + $hideItemPrice = setting($this->getDocumentSettingKey($type, 'price_name'), false); + + // if you use setting translation + if ($hideItemPrice === 'hide') { + return true; + } + + return false; + } + + protected function getTextItemPrice($type, $textItemPrice) + { + if (! empty($textItemPrice)) { + return $textItemPrice; + } + + // if you use setting translation + if (setting($this->getDocumentSettingKey($type, 'price_name'), 'price') === 'custom') { + if (empty($textItemPrice = setting($this->getDocumentSettingKey($type, 'price_name_input')))) { + $textItemPrice = 'invoices.price'; + } + + return $textItemPrice; + } + + if (setting($this->getDocumentSettingKey($type, 'price_name')) !== null + && (trans(setting($this->getDocumentSettingKey($type, 'price_name'))) != setting($this->getDocumentSettingKey($type, 'price_name'))) + ) { + return setting($this->getDocumentSettingKey($type, 'price_name')); + } + + $translation = $this->getTextFromConfig($type, 'price'); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.price'; + } + + protected function getHideItemAmount($type, $hideItemAmount): bool + { + if (! empty($hideItemAmount)) { + return $hideItemAmount; + } + + $hide = $this->getHideFromConfig($type, 'amount'); + + if ($hide) { + return $hide; + } + + return false; + } + + protected function getHideSettingItemAmount($type, $hideSettingItemAmount): bool + { + if (! empty($hideSettingItemAmount)) { + return $hideSettingItemAmount; + } + + // if you use setting translation + if (setting($this->getDocumentSettingKey($type, 'hide_amount'), false)) { + return true; + } + + return false; + } + + protected function getTextItemAmount($type, $textItemAmount) + { + if (!empty($textItemAmount)) { + return $textItemAmount; + } + + $translation = $this->getTextFromConfig($type, 'amount'); + + if (!empty($translation)) { + return $translation; + } + + return 'general.amount'; + } + + protected function getHideDiscount($type, $hideDiscount) + { + if (! empty($hideDiscount)) { + return $hideDiscount; + } + + // if you use setting translation + if ($hideDiscount = setting($this->getDocumentSettingKey($type, 'hide_discount'), false)) { + return $hideDiscount; + } + + $hide = $this->getHideFromConfig($type, 'discount'); + + if ($hide) { + return $hide; + } + + // @todo what return value invoice or always false?? + return setting('invoice.hide_discount', $hideDiscount); + } + + protected function getSearchCharLimit($type, $searchCharLimit) + { + if (! empty($searchCharLimit)) { + return $searchCharLimit; + } + + // if you use setting translation + if ($settingCharLimit = setting($this->getDocumentSettingKey($type, 'item_search_chart_limit'), false)) { + return $settingCharLimit; + } + + $hide = $this->getHideFromConfig($type, 'item_search_char_limit'); + + if ($hide) { + return $hide; + } + + // @todo what return value invoice or always false?? + return setting('invoice.item_search_char_limit', $searchCharLimit); + } + + protected function getNotesValue($notes) + { + if (! empty($notes)) { + return $notes; + } + + if (! empty($this->document)) { + return $this->document->notes; + } + + return setting($this->getDocumentSettingKey($this->type, 'notes')); + } + + protected function getTextSectionAdvancedTitle($type, $textSectionAdvancedTitle) + { + if (! empty($textSectionAdvancedTitle)) { + return $textSectionAdvancedTitle; + } + + return $this->getTextSectionTitle($type, 'advanced', 'documents.advanced'); + } + + protected function getTextSectionAdvancedDescription($type, $textSectionAdvancedDescription) + { + if (! empty($textSectionAdvancedDescription)) { + return $textSectionAdvancedDescription; + } + + return $this->getTextSectionDescription($type, 'advanced', 'documents.form_description.advanced'); + } + + protected function getTitleValue($type, $title) + { + if (! empty($title)) { + return $title; + } + + if (! empty($this->document) && $this->document->title !== '') { + return $this->document->title; + } + + return setting($this->getDocumentSettingKey($type, 'title')); + } + + protected function getSubheadingValue($type, $subheading) + { + if (! empty($subheading)) { + return $subheading; + } + + if (! empty($this->document) && $this->document->title !== '') { + return $this->document->subheading; + } + + return setting($this->getDocumentSettingKey($type, 'subheading')); + } + + protected function getFooterValue($footer) + { + if (! empty($footer)) { + return $footer; + } + + if (! empty($this->document)) { + return $this->document->footer; + } + + return setting($this->getDocumentSettingKey($this->type, 'footer')); + } + + protected function getTypeCategory($type, $typeCategory) + { + if (!empty($typeCategory)) { + return $typeCategory; + } + + if ($category_type = config('type.' . static::OBJECT_TYPE . '.' . $type . '.category_type')) { + return $category_type; + } + + // set default type + $type = Document::INVOICE_TYPE; + + return config('type.' . static::OBJECT_TYPE .'.' . $type . '.category_type'); + } + + protected function getCategoryId($type, $categoryId) + { + if (!empty($categoryId)) { + return $categoryId; + } + + if (! empty($this->document) && ! empty($this->document->category_id)) { + return $this->document->category_id; + } + + return setting('default.' . $this->typeCategory . '_category'); + } + + protected function getTemplates($type, $templates) + { + if (! empty($templates)) { + return $templates; + } + + $templates = $this->getDocumentTemplates($type); + + return $templates; + } + + protected function getDocumentTemplate($type, $documentTemplate) + { + if (! empty($documentTemplate)) { + return $documentTemplate; + } + + if (! empty($this->document) && ! empty($this->document->template)) { + return $this->document->template; + } + + if ($template = config('type.document.' . $type . '.template', false)) { + return $template; + } + + $documentTemplate = setting($this->getDocumentSettingKey($type, 'template'), 'default'); + + return $documentTemplate; + } + + protected function getBackgroundColor($type, $backgroundColor) + { + if (! empty($backgroundColor)) { + return $backgroundColor; + } + + if (! empty($this->document) && $this->document->color !== '') { + return $this->getHexCodeOfTailwindClass($this->document->color); + } + + // checking setting color + $key = $this->getDocumentSettingKey($type, 'color'); + + if (! empty(setting($key))) { + $backgroundColor = setting($key); + } + + // checking config color + if (empty($backgroundColor) && $background_color = config('type.document.' . $type . '.color', false)) { + $backgroundColor = $background_color; + } + + // set default color + if (empty($backgroundColor)) { + $backgroundColor = '#55588b'; + } + + return $this->getHexCodeOfTailwindClass($backgroundColor); + } +} diff --git a/app/Abstracts/View/Components/Documents/Index.php b/app/Abstracts/View/Components/Documents/Index.php new file mode 100644 index 00000000..c07bb52a --- /dev/null +++ b/app/Abstracts/View/Components/Documents/Index.php @@ -0,0 +1,907 @@ +type = $type; + $this->contact_type = $this->getTypeContact($type, $contact_type); + $this->category_type = $this->getTypeCategory($type, $category_type); + $this->alias = $this->getAlias($type, $alias); + $this->documents = ($documents) ? $documents : collect(); + $this->totalDocuments = $this->getTotalDocuments($totalDocuments); + $this->group = $this->getGroup($type, $group); + $this->page = $this->getPage($type, $page); + $this->textTabDocument = $this->getTextTabDocument($type, $textTabDocument); + $this->textPage = $this->getTextPage($type, $textPage); + + $this->permissionCreate = $this->getPermissionCreate($type, $permissionCreate); + $this->permissionUpdate = $this->getPermissionUpdate($type, $permissionUpdate); + $this->permissionDelete = $this->getPermissionDelete($type, $permissionDelete); + + $this->routeTabDocument = $this->getRouteTabDocument($type, $routeTabDocument); + $this->routeParamsTabUnpaid = $this->getRouteParamsTabUnpaid($type, $routeParamsTabUnpaid); + $this->routeParamsTabDraft = $this->getRouteParamsTabDraft($type, $routeParamsTabDraft); + $this->routeTabRecurring = $this->getRouteTabRecurring($type, $routeTabRecurring); + /* -- Main End -- */ + + /* -- Buttons Start -- */ + $this->hideAcceptPayment = $hideAcceptPayment; + + $this->checkPermissionCreate = $checkPermissionCreate; + + $this->hideCreate = $hideCreate; + $this->hideImport = $hideImport; + $this->hideExport = $hideExport; + + $this->createRoute = $this->getCreateRoute($type, $createRoute); + $this->importRoute = $this->getImportRoute($importRoute); + $this->importRouteParameters = $this->getImportRouteParameters($type, $importRouteParameters); + $this->exportRoute = $this->getExportRoute($type, $exportRoute); + /* -- Buttons End -- */ + + /* -- Content Start -- */ + /* -- Empty Page Start -- */ + $this->hideEmptyPage = $this->getHideEmptyPage($hideEmptyPage); + + $this->emptyPageButtons = $this->getEmptyPageButtons($type, $emptyPageButtons); + $this->imageEmptyPage = $this->getImageEmptyPage($type, $imageEmptyPage); + $this->textEmptyPage = $this->getTextEmptyPage($type, $textEmptyPage); + $this->urlDocsPath = $this->getUrlDocsPath($type, $urlDocsPath); + /* -- Empty Page End -- */ + + /* -- Summary Start -- */ + $this->hideSummary = $hideSummary; + $this->summaryItems = $this->getSummaryItems($type, $summaryItems); + /* -- Sumamry End -- */ + + /* Container Start */ + $this->withoutTabs = $withoutTabs; + $this->tabSuffix = $this->getTabSuffix($type, $tabSuffix); + $this->tabActive = $this->getTabActive($type, $tabActive); + + $this->hideRecurringTemplates = $hideRecurringTemplates; + + $this->hideSearchString = $hideSearchString; + $this->hideBulkAction = $hideBulkAction; + + $this->searchStringModel = $this->getSearchStringModel($type, $searchStringModel); + + $this->bulkActionClass = $this->getBulkActionClass($type, $bulkActionClass); + $this->bulkActionRouteParameters = $this->getBulkActionRouteParameters($type, $bulkActionRouteParameters); + + $this->searchRoute = $this->getIndexRoute($type, $searchRoute); + + $this->classBulkAction = $this->getClassBulkAction($type, $classBulkAction); + + /* Document Start */ + $this->hideDueAt = $hideDueAt; + $this->hideIssuedAt = $hideIssuedAt; + $this->classDueAtAndIssueAt = $this->getClassDueAtAndIssueAt($type, $classDueAtAndIssueAt); + $this->textDueAt = $this->getTextDueAt($type, $textDueAt); + $this->textIssuedAt = $this->getTextIssuedAt($type, $textIssuedAt); + + $this->hideStartedAt = $hideStartedAt; + $this->hideEndedAt = $hideEndedAt; + $this->classStartedAtAndEndedAt = $this->getClassStartedAndEndedAt($type, $classStartedAtAndEndedAt); + $this->textStartedAt = $this->getTextStartedAt($type, $textStartedAt); + $this->textEndedAt = $this->getTextEndedAt($type, $textEndedAt); + + $this->hideStatus = $hideStatus; + $this->classStatus = $this->getClassStatus($type, $classStatus); + + $this->hideCategory = $hideCategory; + $this->textCategory = $this->getTextCategory($type, $textCategory); + $this->hideCurrency = $hideCurrency; + + $this->hideFrequency = $hideFrequency; + $this->hideDuration = $hideDuration; + $this->classFrequencyAndDuration = $this->getClassFrequencyAndDuration($type, $classFrequencyAndDuration); + + $this->hideContactName = $hideContactName; + $this->hideDocumentNumber = $hideDocumentNumber; + $this->classContactNameAndDocumentNumber = $this->getClassContactNameAndDocumentNumber($type, $classContactNameAndDocumentNumber); + $this->textContactName = $this->getTextContactName($type, $textContactName); + $this->showContactRoute = $this->getShowContactRoute($type, $showContactRoute); + $this->textDocumentNumber = $this->getTextDocumentNumber($type, $textDocumentNumber); + + $this->hideAmount = $hideAmount; + $this->classAmount = $this->getClassAmount($type, $classAmount); + + $this->hideShow = $hideShow; + $this->showRoute = $this->getShowRoute($type, $showRoute); + + $this->hideEdit = $hideEdit; + $this->editRoute = $this->getEditRoute($type, $editRoute); + + $this->hideDuplicate = $hideDuplicate; + $this->duplicateRoute = $this->getDuplicateRoute($type, $duplicateRoute); + + $this->textDocumentStatus = $this->getTextDocumentStatus($type, $textDocumentStatus); + + $this->checkButtonReconciled = $checkButtonReconciled; + $this->checkButtonCancelled = $checkButtonCancelled; + /* Document End */ + /* Container End */ + /* -- Content End -- */ + + // Set Parent data + $this->setParentData(); + } + + protected function getTypeContact($type, $typeContact) + { + if (! empty($typeContact)) { + return $typeContact; + } + + return config('type.' . static::OBJECT_TYPE . '.' . $type . '.contact_type', 'customer'); + } + + protected function getTypeCategory($type, $typeCategory) + { + if (!empty($typeCategory)) { + return $typeCategory; + } + + if ($category_type = config('type.' . static::OBJECT_TYPE . '.' . $type . '.category_type')) { + return $category_type; + } + + // set default type + $type = Document::INVOICE_TYPE; + + return config('type.' . static::OBJECT_TYPE .'.' . $type . '.category_type'); + } + + protected function getTotalDocuments($totalDocuments) + { + if (! is_null($totalDocuments)) { + return $totalDocuments; + } + + return $this->documents->count(); + } + + protected function getHideEmptyPage($hideEmptyPage): bool + { + if ($hideEmptyPage) { + return $hideEmptyPage; + } + + if ($this->totalDocuments > 0) { + return true; + } + + if (request()->has('search') && ($this->totalDocuments > 0)) { + return true; + } + + return false; + } + + protected function getEmptyPageButtons($type, $emptyPageButtons) + { + if (! empty($emptyPageButtons)) { + return $emptyPageButtons; + } + + $prefix = config('type.' . static::OBJECT_TYPE . '.' . $type . '.route.prefix', 'invoices'); + + $buttons = []; + + if (! $this->hideCreate) { + $route = $this->getRouteFromConfig($type, 'create'); + + $buttons[] = [ + 'permission' => $this->permissionCreate, + 'url' => route($this->createRoute), + 'text' => trans('general.title.new', ['type' => trans_choice($this->textPage ?? 'general.' . $prefix, 1)]), + 'description' => trans('general.empty.actions.new', ['type' => strtolower(trans_choice($this->textPage ?? 'general.' . $prefix, 1))]), + 'active_badge' => true, + 'stack' => 'create_button', + ]; + } + + if (! $this->hideImport) { + $route = $this->getRouteFromConfig($type, 'import'); + + $buttons[] = [ + 'permission' => $this->permissionCreate, + 'url' => route($this->importRoute, $this->importRouteParameters), + 'text' => trans('import.title', ['type' => trans_choice($this->textPage ?? 'general.' . $prefix, 2)]), + 'description' => trans('general.empty.actions.import', ['type' => strtolower(trans_choice($this->textPage ?? 'general.' . $prefix, 2))]), + 'stack' => 'import_button', + ]; + } + + return $buttons; + } + + public function getSummaryItems($type, $summaryItems) + { + if (! empty($summaryItems)) { + return $summaryItems; + } + + $route = $this->getIndexRoute($type, null); + + $totals = $this->getTotalsForFutureDocuments($type); + + $items = []; + + foreach ($totals as $key => $total) { + $title = ($key == 'overdue') ? trans('general.overdue') : trans('documents.statuses.' . $key); + $href = route($route, ['search' => 'status:' . $key]); + $amount = money($total)->formatForHumans(); + $tooltip = money($total)->format(); + + $items[] = [ + 'title' => $title, + //'href' => $href, + 'amount' => $amount, + 'tooltip' => $tooltip, + ]; + } + + return $items; + } + + protected function getTextTabDocument($type, $textTabDocument) + { + if (! empty($textTabDocument)) { + return $textTabDocument; + } + + $default_key = config('type.' . static::OBJECT_TYPE . '.' . $type . '.translation.prefix'); + + $translation = $this->getTextFromConfig($type, 'tab_document', $default_key); + + if (! empty($translation)) { + return $translation; + } + + return 'general.invoices'; + } + + public function getTabActive($type, $tabActive) + { + if (! empty($tabActive)) { + return $tabActive; + } + + return $type . '-' . $this->tabSuffix; + } + + public function getTabSuffix($type, $tabSuffix) + { + if (! empty($tabSuffix)) { + return $tabSuffix; + } + + if (request()->get('list_records') == 'all') { + return 'all'; + } + + $status = $this->getSearchStringValue('status'); + + $unpaid = str_replace('status:', '', config('type.document.' . $type . '.route.params.unpaid.search')); + + if ($status == $unpaid) { + return 'unpaid'; + } + + $draft = str_replace('status:', '', config('type.document.' . $type . '.route.params.draft.search')); + + if ($status == $draft) { + return 'draft'; + } + + $suffix = $this->getTabActiveFromSetting($type); + + if (! empty($suffix)) { + return $suffix; + } + + return 'unpaid'; + } + + protected function getRouteTabDocument($type, $routeTabDocument) + { + if (! empty($routeTabDocument)) { + return $routeTabDocument; + } + + $route = $this->getRouteFromConfig($type, 'document', 'invoices'); + + if (! empty($route)) { + return $route; + } + + return 'invoices.index'; + } + + protected function getRouteParamsTabUnpaid($type, $routeParamsTabUnpaid) + { + if (! empty($routeParamsTabUnpaid)) { + return $routeParamsTabUnpaid; + } + + $params = $this->getRouteParamsFromConfig($type, 'unpaid'); + + if (! empty($params)) { + return $params; + } + + return ['search' => 'status:sent,viewed,partial']; + } + + protected function getRouteParamsTabDraft($type, $routeParamsTabDraft) + { + if (! empty($routeParamsTabDraft)) { + return $routeParamsTabDraft; + } + + $params = $this->getRouteParamsFromConfig($type, 'draft'); + + if (! empty($params)) { + return $params; + } + + return ['search' => 'status:draft']; + } + + protected function getRouteTabRecurring($type, $routeTabDocument) + { + if (! empty($routeTabDocument)) { + return $routeTabDocument; + } + + $route = $this->getRouteFromConfig($type, 'recurring', 'recurring-invoices'); + + if (! empty($route)) { + return $route; + } + + return 'recurring-invoices.index'; + } + + protected function getClassDueAtAndIssueAt($type, $classDueAtAndIssueAt) + { + if (! empty($classDueAtAndIssueAt)) { + return $classDueAtAndIssueAt; + } + + $class = $this->getClassFromConfig($type, 'due_at_and_issue_at'); + + if (! empty($class)) { + return $class; + } + + return 'w-4/12 table-title hidden sm:table-cell'; + } + + protected function getTextDueAt($type, $textDueAt) + { + if (! empty($textDueAt)) { + return $textDueAt; + } + + $translation = $this->getTextFromConfig($type, 'due_at', 'due_date'); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.due_date'; + } + + protected function getTextIssuedAt($type, $textIssuedAt) + { + if (! empty($textIssuedAt)) { + return $textIssuedAt; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'bill_date'; + break; + default: + $default_key = 'invoice_date'; + break; + } + + $translation = $this->getTextFromConfig($type, 'issued_at', $default_key); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.invoice_date'; + } + + protected function getClassStartedAndEndedAt($type, $classStartedAtAndEndedAt) + { + if (! empty($classStartedAtAndEndedAt)) { + return $classStartedAtAndEndedAt; + } + + $class = $this->getClassFromConfig($type, 'started_at_and_end_at'); + + if (! empty($class)) { + return $class; + } + + return 'w-4/12 table-title hidden sm:table-cell'; + } + + protected function getClassFrequencyAndDuration($type, $classFrequencyAndDuration) + { + if (! empty($classFrequencyAndDuration)) { + return $classFrequencyAndDuration; + } + + $class = $this->getClassFromConfig($type, 'frequency_and_duration'); + + if (! empty($class)) { + return $class; + } + + return 'w-2/12'; + } + + protected function getTextStartedAt($type, $textStartedAt) + { + if (! empty($textStartedAt)) { + return $textStartedAt; + } + + $translation = $this->getTextFromConfig($type, 'started_at', 'started_date'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.start_date'; + } + + protected function getTextEndedAt($type, $textEndedAt) + { + if (! empty($textEndedAt)) { + return $textEndedAt; + } + + $translation = $this->getTextFromConfig($type, 'ended_at', 'ended_date'); + + if (! empty($translation)) { + return $translation; + } + + return 'recurring.last_issued'; + } + + protected function getClassStatus($type, $classStatus) + { + if (! empty($classStatus)) { + return $classStatus; + } + + $class = $this->getClassFromConfig($type, 'status'); + + if (! empty($class)) { + return $class; + } + + return 'w-3/12 table-title hidden sm:table-cell'; + } + + protected function getTextCategory($type, $textCategory) + { + if (!empty($textCategory)) { + return $textCategory; + } + + $translation = $this->getTextFromConfig($type, 'categories', 'categories', 'trans_choice'); + + if (!empty($translation)) { + return $translation; + } + + return 'general.categories'; + } + + protected function getClassContactNameAndDocumentNumber($type, $classContactNameAndDocumentNumber) + { + if (! empty($classContactNameAndDocumentNumber)) { + return $classContactNameAndDocumentNumber; + } + + $class = $this->getClassFromConfig($type, 'contact_name'); + + if (! empty($class)) { + return $class; + } + + return 'w-6/12 sm:w-3/12 table-title'; + } + + protected function getTextContactName($type, $textContactName) + { + if (! empty($textContactName)) { + return $textContactName; + } + + $default_key = Str::plural(config('type.' . static::OBJECT_TYPE . '.' . $type . '.contact_type'), 2); + + $translation = $this->getTextFromConfig($type, 'contact_name', $default_key, 'trans_choice'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.customers'; + } + + protected function getShowContactRoute($type, $showContactRoute) + { + if (! empty($showContactRoute)) { + return $showContactRoute; + } + + + if (! empty($showRoute)) { + return $showRoute; + } + + $route = $this->getRouteFromConfig($type, 'contact.show', 1); + + if (!empty($route)) { + return $route; + } + + $default_key = Str::plural(config('type.' . static::OBJECT_TYPE . '.' . $type . '.contact_type'), 2); + + return $default_key . '.show'; + } + + protected function getTextDocumentNumber($type, $textDocumentNumber) + { + if (! empty($textDocumentNumber)) { + return $textDocumentNumber; + } + + $translation = $this->getTextFromConfig($type, 'document_number', 'numbers'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.numbers'; + } + + protected function getClassAmount($type, $classAmount) + { + if (! empty($classAmount)) { + return $classAmount; + } + + $class = $this->getClassFromConfig($type, 'amount'); + + if (! empty($class)) { + return $class; + } + + return 'w-6/12 sm:w-2/12'; + } + + protected function getTextDocumentStatus($type, $textDocumentStatus) + { + if (! empty($textDocumentStatus)) { + return $textDocumentStatus; + } + + $translation = $this->getTextFromConfig($type, 'document_status', 'statuses.'); + + if (! empty($translation)) { + return $translation; + } + + $alias = config('type.' . static::OBJECT_TYPE . '.' . $type . '.alias'); + + if (! empty($alias)) { + $translation = $alias . '::' . config('type.' . static::OBJECT_TYPE . '.' . $type . '.translation.prefix') . '.statuses'; + + if (is_array(trans($translation))) { + return $translation . '.'; + } + } + + return 'documents.statuses.'; + } +} diff --git a/app/Abstracts/View/Components/Documents/Show.php b/app/Abstracts/View/Components/Documents/Show.php new file mode 100644 index 00000000..7c3c1134 --- /dev/null +++ b/app/Abstracts/View/Components/Documents/Show.php @@ -0,0 +1,1434 @@ +type = $type; + $this->document = $document; + $this->transactions = ($transactions) ? $transactions : $document->transactions; + + $this->permissionCreate = $this->getPermissionCreate($type, $permissionCreate); + $this->permissionUpdate = $this->getPermissionUpdate($type, $permissionUpdate); + $this->permissionDelete = $this->getPermissionDelete($type, $permissionDelete); + /* -- Main End -- */ + + /* -- Buttons Start -- */ + $this->textPage = $this->getTextPage($type, $textPage); + $this->hideCreate = $hideCreate; + $this->createRoute = $this->getCreateRoute($type, $createRoute); + $this->textCreate = $this->getTextCreate($type, $textCreate); + + $this->hideButtonStatuses = $this->getHideButtonStatuses($type, $hideButtonStatuses); + $this->hideEdit = $hideEdit; + $this->editRoute = $this->getEditRoute($type, $editRoute); + $this->showRoute = $this->getShowRoute($type, $showRoute); + + $this->hideMoreActions = $hideMoreActions; + $this->hideDuplicate = $hideDuplicate; + $this->duplicateRoute = $this->getDuplicateRoute($type, $duplicateRoute); + + $this->hidePrint = $hidePrint; + $this->printRoute = $this->getPrintRoute($type, $printRoute); + + $this->hideShare = $hideShare; + $this->shareRoute = $this->getShareRoute($type, $shareRoute); + $this->signedUrl = $this->getSignedUrl($type, $signedUrl); + + $this->hideEmail = $hideEmail; + $this->emailRoute = $this->getEmailRoute($type, $emailRoute); + $this->textEmail = $this->getTextEmail($type, $textEmail); + + $this->hidePdf = $hidePdf; + $this->pdfRoute = $this->getPdfRoute($type, $pdfRoute); + + $this->hideCancel = $hideCancel; + $this->cancelledRoute = $this->getCancelledRoute($type, $cancelledRoute); + $this->restoreRoute = $this->getRestoreRoute($type, $restoreRoute); + + $this->hideCustomize = $hideCustomize; + $this->permissionCustomize = $this->getPermissionCustomize($type, $permissionCustomize); + $this->customizeRoute = $this->getCustomizeRoute($type, $customizeRoute); + + $this->hideEnd = $hideEnd; + $this->endRoute = $this->getEndRoute($type, $endRoute); + + $this->hideDelete = $hideDelete; + $this->checkReconciled = $checkReconciled; + $this->deleteRoute = $this->getDeleteRoute($type, $deleteRoute); + $this->textDeleteModal = $textDeleteModal; + + $this->hideDivider1 = $hideDivider1; + $this->hideDivider2 = $hideDivider2; + $this->hideDivider3 = $hideDivider3; + $this->hideDivider4 = $hideDivider4; + /* -- Buttons End -- */ + + /* -- Content Start -- */ + $this->accordionActive = $this->getAccordionActive($type, $accordionActive); + $this->hideRecurringMessage = $hideRecurringMessage; + $this->textRecurringType = $this->getTextRecurringType($type, $textRecurringType); + + $this->hideStatusMessage = $hideStatusMessage; + $this->textStatusMessage = $this->getTextStatusMessage($type, $textStatusMessage); + + $this->hideCreated = $hideCreated; + + $this->hideSend = $hideSend; + $this->hideMarkSent = $hideMarkSent; + $this->textMarkSent = $this->getTextMarkSent($type, $textMarkSent); + $this->markSentRoute = $this->getMarkSentRoute($type, $markSentRoute); + + $this->hideReceive = $hideReceive; + $this->hideMarkReceived = $hideMarkReceived; + $this->textMarkReceived = $this->getTextMarkReceived($type, $textMarkReceived); + $this->markReceivedRoute = $this->getMarkReceivedRoute($type, $markReceivedRoute); + + $this->hideGetPaid = $hideGetPaid; + + $this->hideAddPayment = $hideAddPayment; + $this->hideAcceptPayment = $hideAcceptPayment; + + $this->transactionEmailRoute = $this->getTransactionEmailRoute($type, $transactionEmailRoute); + $this->transactionEmailTemplate = $this->getTransactionEmailTemplate($type, $transactionEmailTemplate); + + $this->hideRestore = $this->getHideRestore($hideRestore); + + $this->hideMakePayment = $hideMakePayment; + + $this->hideSchedule = $hideSchedule; + $this->hideChildren = $hideChildren; + + $this->hideAttachment = $hideAttachment; + $this->attachment = ''; + + if (! empty($attachment)) { + $this->attachment = $attachment; + } else if (! empty($document)) { + $this->attachment = $document->attachment; + } + /* -- Content End -- */ + + /* -- Template Start -- */ + $this->documentTemplate = $this->getDocumentTemplate($type, $documentTemplate); + $this->logo = $this->getLogo($logo); + $this->backgroundColor = $this->getBackgroundColor($type, $backgroundColor); + + $this->hideFooter = $hideFooter; + $this->hideCompanyLogo = $hideCompanyLogo; + $this->hideCompanyDetails = $hideCompanyDetails; + $this->hideCompanyName = $hideCompanyName; + $this->hideCompanyAddress = $hideCompanyAddress; + $this->hideCompanyTaxNumber = $hideCompanyTaxNumber; + $this->hideCompanyPhone = $hideCompanyPhone; + $this->hideCompanyEmail = $hideCompanyEmail; + $this->hideContactInfo = $hideContactInfo; + $this->hideContactName = $hideContactName; + $this->hideContactAddress = $hideContactAddress; + $this->hideContactTaxNumber = $hideContactTaxNumber; + $this->hideContactPhone = $hideContactPhone; + $this->hideContactEmail = $hideContactEmail; + $this->hideOrderNumber = $hideOrderNumber; + $this->hideDocumentNumber = $hideDocumentNumber; + $this->hideIssuedAt = $hideIssuedAt; + $this->hideDueAt = $hideDueAt; + + $this->textDocumentTitle = $this->getTextDocumentTitle($type, $textDocumentTitle); + $this->textDocumentSubheading = $this->getTextDocumentSubheading($type, $textDocumentSubheading); + $this->textContactInfo = $this->getTextContactInfo($type, $textContactInfo); + $this->textIssuedAt = $this->getTextIssuedAt($type, $textIssuedAt); + $this->textDocumentNumber = $this->getTextDocumentNumber($type, $textDocumentNumber); + $this->textDueAt = $this->getTextDueAt($type, $textDueAt); + $this->textOrderNumber = $this->getTextOrderNumber($type, $textOrderNumber); + + $this->hideItems = $this->getHideItems($type, $hideItems, $hideName, $hideDescription); + $this->hideName = $this->getHideName($type, $hideName); + $this->hideDescription = $this->getHideDescription($type, $hideDescription); + $this->hideQuantity = $this->getHideQuantity($type, $hideQuantity); + $this->hidePrice = $this->getHidePrice($type, $hidePrice); + $this->hideDiscount = $this->getHideDiscount($type, $hideDiscount); + $this->hideAmount = $this->getHideAmount($type, $hideAmount); + $this->hideNote = $hideNote; + + $this->textItems = $this->getTextItems($type, $textItems); + $this->textQuantity = $this->getTextQuantity($type, $textQuantity); + $this->textPrice = $this->getTextPrice($type, $textPrice); + $this->textAmount = $this->getTextAmount($type, $textAmount); + /* -- Template End -- */ + + // Set Parent data + $this->setParentData(); + } + + protected function getTextCreate($type, $textCreate) + { + if (! empty($textCreate)) { + return $textCreate; + } + + return trans('general.new') . ' ' . ucfirst($type); + } + + protected function getHideButtonStatuses($type, $hideButtonStatuses) + { + if (! empty($hideButtonStatuses)) { + return $hideButtonStatuses; + } + + $hideButtonStatuses = ['paid', 'cancelled']; + + if ($button_statuses = config('type.' . static::OBJECT_TYPE . '.' . $type . '.button_statuses')) { + $hideButtonStatuses = $button_statuses; + } + + return $hideButtonStatuses; + } + + protected function getPrintRoute($type, $printRoute) + { + if (! empty($printRoute)) { + return $printRoute; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'print', $parameter); + + if (! empty($route)) { + return $route; + } + + return 'invoices.print'; + } + + protected function getShareRoute($type, $shareRoute) + { + if (! empty($shareRoute)) { + return $shareRoute; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'share', $parameter, true); + + if (! empty($route)) { + return $route; + } + + return 'modals.invoices.share.create'; + } + + protected function getSignedUrl($type, $signedUrl) + { + if (! empty($signedUrl)) { + return $signedUrl; + } + + $page = config('type.' . static::OBJECT_TYPE . '.' . $type . '.route.prefix'); + $alias = config('type.' . static::OBJECT_TYPE . '.' . $type . '.alias'); + + $route = ''; + + if (! empty($alias)) { + $route .= $alias . '.'; + } + + $route .= 'signed.' . $page . '.show'; + + try { + route($route, [$this->document->id, 'company_id' => company_id()]); + + $signedUrl = URL::signedRoute($route, [$this->document->id]); + } catch (\Exception $e) { + $signedUrl = URL::signedRoute('signed.invoices.show', [$this->document->id]); + } + + return $signedUrl; + } + + protected function getEmailRoute($type, $emailRoute) + { + if (! empty($emailRoute)) { + return $emailRoute; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'emails.create', $parameter, true); + + if (! empty($route)) { + return $route; + } + + return 'modals.invoices.emails.create'; + } + + protected function getTextEmail($type, $textEmail) + { + if (! empty($textEmail)) { + return $textEmail; + } + + $translation = $this->getTextFromConfig($type, 'send_mail', 'send_mail'); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.send_mail'; + } + + protected function getPdfRoute($type, $pdfRoute) + { + if (! empty($pdfRoute)) { + return $pdfRoute; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'pdf', $parameter); + + if (! empty($route)) { + return $route; + } + + return 'invoices.pdf'; + } + + protected function getCancelledRoute($type, $cancelledRoute) + { + if (! empty($cancelledRoute)) { + return $cancelledRoute; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'cancelled', $parameter); + + if (! empty($route)) { + return $route; + } + + return 'invoices.cancelled'; + } + + protected function getRestoreRoute($type, $restoreRoute) + { + if (! empty($restoreRoute)) { + return $restoreRoute; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'restore', $parameter); + + if (! empty($route)) { + return $route; + } + + return 'invoices.restore'; + } + + protected function getPermissionCustomize($type, $permissionCustomize) + { + if (! empty($permissionCustomize)) { + return $permissionCustomize; + } + + $permissionUpdate = $this->getPermissionFromConfig($type, 'update'); + + return $permissionUpdate; + } + + protected function getCustomizeRoute($type, $customizeRoute) + { + if (!empty($customizeRoute)) { + return $customizeRoute; + } + + $route = ''; + + $alias = config('type.' . static::OBJECT_TYPE . '.' . $type . '.alias'); + + if (!empty($alias)) { + $route .= $alias . '.'; + } + + $route .= 'settings.' . $type . '.edit'; + + try { + route($route); + } catch (\Exception $e) { + $route = 'settings.invoice.edit'; + } + + return $route; + } + + protected function getEndRoute($type, $endButton) + { + if (!empty($endButton)) { + return $endButton; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'end', $parameter); + + if (!empty($route)) { + return $route; + } + + return 'recurring-invoices.end'; + } + + protected function getAccordionActive($type, $accordionActive) + { + if (! empty($accordionActive)) { + return $accordionActive; + } + + $status_workflow = $alias = config('type.' . static::OBJECT_TYPE . '.' . $type . '.status_workflow'); + + $status = false; + + if (! empty($status_workflow[$this->document->status])) { + $status = $status_workflow[$this->document->status]; + } + + return $status; + } + + protected function getTextRecurringType($type, $textRecurringType) + { + if (! empty($textRecurringType)) { + return $textRecurringType; + } + + $translation = config('type.' . static::OBJECT_TYPE . '.' . $type . '.translation.tab_document'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.invoices'; + } + + protected function getTextStatusMessage($type, $textStatusMessage) + { + if (! empty($textStatusMessage)) { + return $textStatusMessage; + } + + $default_key = 'messages.draft'; + + $translation = $this->getTextFromConfig($type, 'status_message', $default_key); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.messages.draft'; + } + + protected function getTextMarkSent($type, $textMarkSent) + { + if (! empty($textMarkSent)) { + return $textMarkSent; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'mark_received'; + break; + default: + $default_key = 'mark_sent'; + break; + } + + $translation = $this->getTextFromConfig($type, 'mark_sent', $default_key); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.mark_sent'; + } + + protected function getMarkSentRoute($type, $markSentRoute) + { + if (! empty($markSentRoute)) { + return $markSentRoute; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'sent', $parameter); + + if (!empty($route)) { + return $route; + } + + return 'invoices.sent'; + } + + protected function getTextMarkReceived($type, $textMarkReceived) + { + if (! empty($textMarkReceived)) { + return $textMarkReceived; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'mark_received'; + break; + default: + $default_key = 'mark_sent'; + break; + } + + $translation = $this->getTextFromConfig($type, 'mark_received', $default_key); + + if (! empty($translation)) { + return $translation; + } + + return 'bills.mark_received'; + } + + protected function getMarkReceivedRoute($type, $markReceivedRoute) + { + if (! empty($markReceivedRoute)) { + return $markReceivedRoute; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'received', $parameter); + + if (!empty($route)) { + return $route; + } + + return 'bills.received'; + } + + protected function getTransactionEmailRoute($type, $transactionEmailRoute) + { + if (! empty($transactionEmailRoute)) { + return $transactionEmailRoute; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'transaction_email', $parameter); + + if (! empty($route)) { + return $route; + } + + return 'modals.transactions.emails.create'; + } + + protected function getTransactionEmailTemplate($type, $transactionEmailTemplate) + { + if (! empty($transactionEmailTemplate)) { + return $transactionEmailTemplate; + } + + return config('type.' . static::OBJECT_TYPE . '.' . $type . '.transaction.email_template', false); + } + + protected function getHideRestore($hideRestore) + { + if (! empty($hideRestore)) { + return $hideRestore; + } + + $hideRestore = true; + + if ($this->document->status == 'cancelled') { + $hideRestore = false; + } + + return $hideRestore; + } + + protected function getDocumentTemplate($type, $documentTemplate) + { + if (! empty($documentTemplate)) { + return $documentTemplate; + } + + if (! empty($this->document) && ! empty($this->document->template)) { + $document_templates = $this->getDocumentTemplates($type); + + $template = 'default'; + + foreach ($document_templates as $template) { + if ($template['id'] != $this->document->template) { + continue; + } + + $template = $template['template']; + break; + } + + return $template; + } + + if ($template = config('type.' . static::OBJECT_TYPE . '.' . $type . '.template', false)) { + return $template; + } + + $documentTemplate = setting($this->getDocumentSettingKey($type, 'template'), 'default'); + + return $documentTemplate; + } + + protected function getLogo($logo) + { + if (! empty($logo)) { + return $logo; + } + + static $content; + + if (! empty($content)) { + return $content; + } + + $media_id = (! empty($this->document->contact->logo) && ! empty($this->document->contact->logo->id)) ? $this->document->contact->logo->id : setting('company.logo'); + + $media = Media::find($media_id); + + if (! empty($media)) { + $path = $media->getDiskPath(); + + if (! $media->fileExists()) { + return $logo; + } + } else { + $path = base_path('public/img/company.png'); + } + + try { + $image = Image::cache(function($image) use ($media, $path) { + $width = setting('invoice.logo_size_width'); + $height = setting('invoice.logo_size_height'); + + if ($media) { + $image->make($media->contents())->resize($width, $height)->encode(); + } else { + $image->make($path)->resize($width, $height)->encode(); + } + }); + } catch (NotReadableException | \Exception $e) { + Log::info('Company ID: ' . company_id() . ' components/documentshow.php exception.'); + Log::info($e->getMessage()); + + $path = base_path('public/img/company.png'); + + $image = Image::cache(function($image) use ($path) { + $width = setting('invoice.logo_size_width'); + $height = setting('invoice.logo_size_height'); + + $image->make($path)->resize($width, $height)->encode(); + }); + } + + if (empty($image)) { + return $logo; + } + + $extension = File::extension($path); + + $content = 'data:image/' . $extension . ';base64,' . base64_encode($image); + + return $content; + } + + protected function getBackgroundColor($type, $backgroundColor) + { + if (! empty($backgroundColor)) { + return $backgroundColor; + } + + if (! empty($this->document) && $this->document->color !== '') { + return $this->getHexCodeOfTailwindClass($this->document->color); + } + + // checking setting color + $key = $this->getDocumentSettingKey($type, 'color'); + + if (! empty(setting($key))) { + $backgroundColor = setting($key); + } + + // checking config color + if (empty($backgroundColor) && $background_color = config('type.document.' . $type . '.color', false)) { + $backgroundColor = $background_color; + } + + // set default color + if (empty($backgroundColor)) { + $backgroundColor = '#55588b'; + } + + return $this->getHexCodeOfTailwindClass($backgroundColor); + } + + protected function getTextDocumentTitle($type, $textDocumentTitle) + { + if (! empty($textDocumentTitle)) { + return $textDocumentTitle; + } + + if (! empty($this->document) && $this->document->title !== '') { + return $this->document->title; + } + + $key = $this->getDocumentSettingKey($type, 'title'); + + if (! empty(setting($key))) { + return setting($key); + } + + $translation = $this->getTextFromConfig($type, 'document_title', Str::plural($type)); + + if (! empty($translation)) { + return trans_choice($translation, 1); + } + + return setting('invoice.title'); + } + + protected function getTextDocumentSubheading($type, $textDocumentSubheading) + { + if (! empty($textDocumentSubheading)) { + return $textDocumentSubheading; + } + + if (! empty($this->document) && $this->document->subheading !== '') { + return $this->document->subheading; + } + + $key = $this->getDocumentSettingKey($type, 'subheading'); + + if (!empty(setting($key))) { + return setting($key); + } + + $translation = $this->getTextFromConfig($type, 'document_subheading', 'subheading'); + + if (! empty($translation)) { + return trans($translation); + } + + return false; + } + + protected function getTextDocumentNumber($type, $textDocumentNumber) + { + if (! empty($textDocumentNumber)) { + return $textDocumentNumber; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'bill_number'; + break; + default: + $default_key = 'invoice_number'; + break; + } + + $translation = $this->getTextFromConfig($type, 'document_number', $default_key); + + if (! empty($translation)) { + return $translation; + } + + return 'general.numbers'; + } + + protected function getTextOrderNumber($type, $textOrderNumber) + { + if (! empty($textOrderNumber)) { + return $textOrderNumber; + } + + $translation = $this->getTextFromConfig($type, 'order_number'); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.order_number'; + } + + protected function getTextContactInfo($type, $textContactInfo) + { + if (! empty($textContactInfo)) { + return $textContactInfo; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'bill_from'; + break; + default: + $default_key = 'bill_to'; + break; + } + + $translation = $this->getTextFromConfig($type, 'contact_info', $default_key); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.bill_to'; + } + + protected function getTextIssuedAt($type, $textIssuedAt) + { + if (! empty($textIssuedAt)) { + return $textIssuedAt; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'bill_date'; + break; + default: + $default_key = 'invoice_date'; + break; + } + + $translation = $this->getTextFromConfig($type, 'issued_at', $default_key); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.invoice_date'; + } + + protected function getTextDueAt($type, $textDueAt) + { + if (! empty($textDueAt)) { + return $textDueAt; + } + + $translation = $this->getTextFromConfig($type, 'due_at', 'due_date'); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.due_date'; + } + + protected function getTextItems($type, $textItems) + { + if (! empty($textItems)) { + return $textItems; + } + + // if you use settting translation + if (setting($this->getDocumentSettingKey($type, 'item_name'), 'items') == 'custom') { + if (empty($textItems = setting($this->getDocumentSettingKey($type, 'item_name_input')))) { + $textItems = 'general.items'; + } + + return $textItems; + } + + $translation = $this->getTextFromConfig($type, 'items'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.items'; + } + + protected function getTextQuantity($type, $textQuantity) + { + if (! empty($textQuantity)) { + return $textQuantity; + } + + // if you use settting translation + if (setting($this->getDocumentSettingKey($type, 'quantity_name'), 'quantity') === 'custom') { + if (empty($textQuantity = setting($this->getDocumentSettingKey($type, 'quantity_name_input')))) { + $textQuantity = 'invoices.quantity'; + } + + return $textQuantity; + } + + $translation = $this->getTextFromConfig($type, 'quantity'); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.quantity'; + } + + protected function getTextPrice($type, $textPrice) + { + if (!empty($textPrice)) { + return $textPrice; + } + + // if you use settting translation + if (setting($this->getDocumentSettingKey($type, 'price_name'), 'price') === 'custom') { + if (empty($textPrice = setting($this->getDocumentSettingKey($type, 'price_name_input')))) { + $textPrice = 'invoices.price'; + } + + return $textPrice; + } + + $translation = $this->getTextFromConfig($type, 'price'); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.price'; + } + + protected function getTextAmount($type, $textAmount) + { + if (! empty($textAmount)) { + return $textAmount; + } + + $translation = $this->getTextFromConfig($type, 'amount'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.amount'; + } + + protected function getHideItems($type, $hideItems, $hideName, $hideDescription) + { + if (! empty($hideItems)) { + return $hideItems; + } + + $hide = $this->getHideFromConfig($type, 'items'); + + if ($hide) { + return $hide; + } + + $hideItems = ($this->getHideName($type, $hideName) & $this->getHideDescription($type, $hideDescription)) ? true : false; + + return $hideItems; + } + + protected function getHideName($type, $hideName) + { + if (! empty($hideName)) { + return $hideName; + } + + $hideName = setting($this->getDocumentSettingKey($type, 'item_name'), false); + + // if you use settting translation + if ($hideName === 'hide') { + return true; + } + + $hide = $this->getHideFromConfig($type, 'name'); + + if ($hide) { + return $hide; + } + + return false; + } + + protected function getHideDescription($type, $hideDescription) + { + if (! empty($hideDescription)) { + return $hideDescription; + } + + // if you use settting translation + if (setting($this->getDocumentSettingKey($type, 'hide_item_description'), false)) { + return true; + } + + $hide = $this->getHideFromConfig($type, 'description'); + + if ($hide) { + return $hide; + } + + return false; + } + + protected function getHideQuantity($type, $hideQuantity) + { + if (! empty($hideQuantity)) { + return $hideQuantity; + } + + $hideQuantity = setting($this->getDocumentSettingKey($type, 'quantity_name'), false); + + // if you use settting translation + if ($hideQuantity === 'hide') { + return true; + } + + $hide = $this->getHideFromConfig($type, 'quantity'); + + if ($hide) { + return $hide; + } + + return false; + } + + protected function getHidePrice($type, $hidePrice) + { + if (! empty($hidePrice)) { + return $hidePrice; + } + + $hidePrice = setting($this->getDocumentSettingKey($type, 'price_name'), false); + + // if you use settting translation + if ($hidePrice === 'hide') { + return true; + } + + $hide = $this->getHideFromConfig($type, 'price'); + + if ($hide) { + return $hide; + } + + return false; + } + + protected function getHideDiscount($type, $hideDiscount) + { + if (! empty($hideDiscount)) { + return $hideDiscount; + } + + // if you use settting translation + if ($hideDiscount = setting($this->getDocumentSettingKey($type, 'hide_discount'), false)) { + return $hideDiscount; + } + + $hide = $this->getHideFromConfig($type, 'discount'); + + if ($hide) { + return $hide; + } + + // @todo what return value invoice or always false?? + return setting('invoice.hide_discount', $hideDiscount); + } + + protected function getHideAmount($type, $hideAmount) + { + if (! empty($hideAmount)) { + return $hideAmount; + } + + // if you use settting translation + if (setting($this->getDocumentSettingKey($type, 'hide_amount'), false)) { + return true; + } + + $hide = $this->getHideFromConfig($type, 'amount'); + + if ($hide) { + return $hide; + } + + return false; + } +} diff --git a/app/Abstracts/View/Components/Documents/Template.php b/app/Abstracts/View/Components/Documents/Template.php new file mode 100644 index 00000000..b0dbff80 --- /dev/null +++ b/app/Abstracts/View/Components/Documents/Template.php @@ -0,0 +1,767 @@ +type = $type; + $this->item = $item; + $this->document = $document; + $this->documentTemplate = $this->getDocumentTemplate($type, $documentTemplate); + $this->logo = $this->getLogo($logo); + $this->backgroundColor = $this->getBackgroundColor($type, $backgroundColor); + + $this->hideFooter = $hideFooter; + $this->hideCompanyLogo = $hideCompanyLogo; + $this->hideCompanyDetails = $hideCompanyDetails; + $this->hideCompanyName = $hideCompanyName; + $this->hideCompanyAddress = $hideCompanyAddress; + $this->hideCompanyTaxNumber = $hideCompanyTaxNumber; + $this->hideCompanyPhone = $hideCompanyPhone; + $this->hideCompanyEmail = $hideCompanyEmail; + $this->hideContactInfo = $hideContactInfo; + $this->hideContactName = $hideContactName; + $this->hideContactAddress = $hideContactAddress; + $this->hideContactTaxNumber = $hideContactTaxNumber; + $this->hideContactPhone = $hideContactPhone; + $this->hideContactEmail = $hideContactEmail; + $this->hideOrderNumber = $hideOrderNumber; + $this->hideDocumentNumber = $hideDocumentNumber; + $this->hideIssuedAt = $hideIssuedAt; + $this->hideDueAt = $hideDueAt; + + $this->textDocumentTitle = $this->getTextDocumentTitle($type, $textDocumentTitle); + $this->textDocumentSubheading = $this->getTextDocumentSubheading($type, $textDocumentSubheading); + $this->textContactInfo = $this->getTextContactInfo($type, $textContactInfo); + $this->showContactRoute = $this->getShowContactRoute($type, $showContactRoute); + $this->textIssuedAt = $this->getTextIssuedAt($type, $textIssuedAt); + $this->textDocumentNumber = $this->getTextDocumentNumber($type, $textDocumentNumber); + $this->textDueAt = $this->getTextDueAt($type, $textDueAt); + $this->textOrderNumber = $this->getTextOrderNumber($type, $textOrderNumber); + + $this->hideItems = $this->getHideItems($type, $hideItems, $hideName, $hideDescription); + $this->hideName = $this->getHideName($type, $hideName); + $this->hideDescription = $this->getHideDescription($type, $hideDescription); + $this->hideQuantity = $this->getHideQuantity($type, $hideQuantity); + $this->hidePrice = $this->getHidePrice($type, $hidePrice); + $this->hideDiscount = $this->getHideDiscount($type, $hideDiscount); + $this->hideAmount = $this->getHideAmount($type, $hideAmount); + $this->hideNote = $hideNote; + + $this->textItems = $this->getTextItems($type, $textItems); + $this->textQuantity = $this->getTextQuantity($type, $textQuantity); + $this->textPrice = $this->getTextPrice($type, $textPrice); + $this->textAmount = $this->getTextAmount($type, $textAmount); + + $this->print = $this->getPrint($print); + + // Set Parent data + //$this->setParentData(); + } + + protected function getDocumentTemplate($type, $documentTemplate) + { + if (! empty($documentTemplate)) { + return $documentTemplate; + } + + if (! empty($this->document) && ! empty($this->document->template)) { + $document_templates = $this->getDocumentTemplates($type); + + $template = 'default'; + + foreach ($document_templates as $template) { + if ($template['id'] != $this->document->template) { + continue; + } + + $template = $template['template']; + break; + } + + return $template; + } + + if ($template = config('type.document.' . $type . '.template', false)) { + return $template; + } + + $documentTemplate = setting($this->getDocumentSettingKey($type, 'template'), 'default'); + + return $documentTemplate; + } + + protected function getLogo($logo) + { + if (! empty($logo)) { + return $logo; + } + + static $content; + + if (! empty($content)) { + return $content; + } + + $media_id = (! empty($this->document->contact->logo) && ! empty($this->document->contact->logo->id)) ? $this->document->contact->logo->id : setting('company.logo'); + + $media = Media::find($media_id); + + if (! empty($media)) { + $path = $media->getDiskPath(); + + if (! $media->fileExists()) { + return $logo; + } + } else { + $path = base_path('public/img/company.png'); + } + + try { + $image = Image::cache(function($image) use ($media, $path) { + $width = setting('invoice.logo_size_width'); + $height = setting('invoice.logo_size_height'); + + if ($media) { + $image->make($media->contents())->resize($width, $height)->encode(); + } else { + $image->make($path)->resize($width, $height)->encode(); + } + }); + } catch (NotReadableException | \Exception $e) { + Log::info('Company ID: ' . company_id() . ' components/documentshow.php exception.'); + Log::info($e->getMessage()); + + $path = base_path('public/img/company.png'); + + $image = Image::cache(function($image) use ($path) { + $width = setting('invoice.logo_size_width'); + $height = setting('invoice.logo_size_height'); + + $image->make($path)->resize($width, $height)->encode(); + }); + } + + if (empty($image)) { + return $logo; + } + + $extension = File::extension($path); + + $content = 'data:image/' . $extension . ';base64,' . base64_encode($image); + + return $content; + } + + protected function getBackgroundColor($type, $backgroundColor) + { + if (! empty($backgroundColor)) { + return $backgroundColor; + } + + if (! empty($this->document) && $this->document->color !== '') { + return $this->getHexCodeOfTailwindClass($this->document->color); + } + + // checking setting color + $key = $this->getDocumentSettingKey($type, 'color'); + + if (! empty(setting($key))) { + $backgroundColor = setting($key); + } + + // checking config color + if (empty($backgroundColor) && $background_color = config('type.document.' . $type . '.color', false)) { + $backgroundColor = $background_color; + } + + // set default color + if (empty($backgroundColor)) { + $backgroundColor = '#55588b'; + } + + return $this->getHexCodeOfTailwindClass($backgroundColor); + } + + protected function getTextDocumentTitle($type, $textDocumentTitle) + { + if (! empty($textDocumentTitle)) { + return $textDocumentTitle; + } + + if (! empty($this->document) && $this->document->title !== '') { + return $this->document->title; + } + + $key = $this->getDocumentSettingKey($type, 'title'); + + if (! empty(setting($key))) { + return setting($key); + } + + $translation = $this->getTextFromConfig($type, 'document_title', Str::plural($type)); + + if (! empty($translation)) { + return trans_choice($translation, 1); + } + + return setting('invoice.title'); + } + + protected function getTextDocumentSubheading($type, $textDocumentSubheading) + { + if (! empty($textDocumentSubheading)) { + return $textDocumentSubheading; + } + + if (! empty($this->document) && $this->document->subheading !== '') { + return $this->document->subheading; + } + + $key = $this->getDocumentSettingKey($type, 'subheading'); + + if (! empty(setting($key))) { + return setting($key); + } + + $translation = $this->getTextFromConfig($type, 'document_subheading', 'subheading'); + + if (! empty($translation)) { + return trans($translation); + } + + return false; + } + + protected function getTextDocumentNumber($type, $textDocumentNumber) + { + if (! empty($textDocumentNumber)) { + return $textDocumentNumber; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'bill_number'; + break; + default: + $default_key = 'invoice_number'; + break; + } + + $translation = $this->getTextFromConfig($type, 'document_number', $default_key); + + if (! empty($translation)) { + return $translation; + } + + return 'general.numbers'; + } + + protected function getTextOrderNumber($type, $textOrderNumber) + { + if (! empty($textOrderNumber)) { + return $textOrderNumber; + } + + $translation = $this->getTextFromConfig($type, 'order_number'); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.order_number'; + } + + protected function getTextContactInfo($type, $textContactInfo) + { + if (! empty($textContactInfo)) { + return $textContactInfo; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'bill_from'; + break; + default: + $default_key = 'bill_to'; + break; + } + + $translation = $this->getTextFromConfig($type, 'contact_info', $default_key); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.bill_to'; + } + + protected function getTextIssuedAt($type, $textIssuedAt) + { + if (! empty($textIssuedAt)) { + return $textIssuedAt; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'bill_date'; + break; + default: + $default_key = 'invoice_date'; + break; + } + + $translation = $this->getTextFromConfig($type, 'issued_at', $default_key); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.invoice_date'; + } + + protected function getTextDueAt($type, $textDueAt) + { + if (! empty($textDueAt)) { + return $textDueAt; + } + + $translation = $this->getTextFromConfig($type, 'due_at', 'due_date'); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.due_date'; + } + + protected function getTextItems($type, $textItems) + { + if (! empty($textItems)) { + return $textItems; + } + + // if you use settting translation + if (setting($this->getDocumentSettingKey($type, 'item_name'), 'items') === 'custom') { + if (empty($textItems = setting($this->getDocumentSettingKey($type, 'item_name_input')))) { + $textItems = 'general.items'; + } + + return $textItems; + } + + if (setting($this->getDocumentSettingKey($type, 'item_name')) !== null + && (trans(setting($this->getDocumentSettingKey($type, 'item_name'))) != setting($this->getDocumentSettingKey($type, 'item_name'))) + ) { + return setting($this->getDocumentSettingKey($type, 'item_name')); + } + + $translation = $this->getTextFromConfig($type, 'items'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.items'; + } + + protected function getTextQuantity($type, $textQuantity) + { + if (! empty($textQuantity)) { + return $textQuantity; + } + + // if you use settting translation + if (setting($this->getDocumentSettingKey($type, 'quantity_name'), 'quantity') === 'custom') { + if (empty($textQuantity = setting($this->getDocumentSettingKey($type, 'quantity_name_input')))) { + $textQuantity = 'invoices.quantity'; + } + + return $textQuantity; + } + + if (setting($this->getDocumentSettingKey($type, 'quantity_name')) !== null + && (trans(setting($this->getDocumentSettingKey($type, 'quantity_name'))) != setting($this->getDocumentSettingKey($type, 'quantity_name'))) + ) { + return setting($this->getDocumentSettingKey($type, 'quantity_name')); + } + + $translation = $this->getTextFromConfig($type, 'quantity'); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.quantity'; + } + + protected function getTextPrice($type, $textPrice) + { + if (! empty($textPrice)) { + return $textPrice; + } + + // if you use settting translation + if (setting($this->getDocumentSettingKey($type, 'price_name'), 'price') === 'custom') { + if (empty($textPrice = setting($this->getDocumentSettingKey($type, 'price_name_input')))) { + $textPrice = 'invoices.price'; + } + + return $textPrice; + } + + if (setting($this->getDocumentSettingKey($type, 'price_name')) !== null + && (trans(setting($this->getDocumentSettingKey($type, 'price_name'))) != setting($this->getDocumentSettingKey($type, 'price_name'))) + ) { + return setting($this->getDocumentSettingKey($type, 'price_name')); + } + + $translation = $this->getTextFromConfig($type, 'price'); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.price'; + } + + protected function getTextAmount($type, $textAmount) + { + if (! empty($textAmount)) { + return $textAmount; + } + + $translation = $this->getTextFromConfig($type, 'amount'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.amount'; + } + + protected function getShowContactRoute($type, $showContactRoute) + { + if (! empty($showContactRoute)) { + return $showContactRoute; + } + + $route = $this->getRouteFromConfig($type, 'contact.show', 1); + + if (!empty($route)) { + return $route; + } + + $default_key = Str::plural(config('type.' . static::OBJECT_TYPE . '.' . $type . '.contact_type'), 2); + + return $default_key . '.show'; + } + + protected function getHideItems($type, $hideItems, $hideName, $hideDescription) + { + if (! empty($hideItems)) { + return $hideItems; + } + + $hide = $this->getHideFromConfig($type, 'items'); + + if ($hide) { + return $hide; + } + + $hideItems = ($this->getHideName($type, $hideName) & $this->getHideDescription($type, $hideDescription)) ? true : false; + + return $hideItems; + } + + protected function getHideName($type, $hideName) + { + if (! empty($hideName)) { + return $hideName; + } + + $hideName = setting($this->getDocumentSettingKey($type, 'item_name'), false); + + // if you use settting translation + if ($hideName === 'hide') { + return true; + } + + $hide = $this->getHideFromConfig($type, 'name'); + + if ($hide) { + return $hide; + } + + return false; + } + + protected function getHideDescription($type, $hideDescription) + { + if (! empty($hideDescription)) { + return $hideDescription; + } + + // if you use settting translation + if (setting($this->getDocumentSettingKey($type, 'hide_item_description'), false)) { + return true; + } + + $hide = $this->getHideFromConfig($type, 'description'); + + if ($hide) { + return $hide; + } + + return false; + } + + protected function getHideQuantity($type, $hideQuantity) + { + if (! empty($hideQuantity)) { + return $hideQuantity; + } + + $hideQuantity = setting($this->getDocumentSettingKey($type, 'quantity_name'), false); + + // if you use settting translation + if ($hideQuantity === 'hide') { + return true; + } + + $hide = $this->getHideFromConfig($type, 'quantity'); + + if ($hide) { + return $hide; + } + + return false; + } + + protected function getHidePrice($type, $hidePrice) + { + if (! empty($hidePrice)) { + return $hidePrice; + } + + $hidePrice = setting($this->getDocumentSettingKey($type, 'price_name'), false); + + // if you use settting translation + if ($hidePrice === 'hide') { + return true; + } + + $hide = $this->getHideFromConfig($type, 'price'); + + if ($hide) { + return $hide; + } + + return false; + } + + protected function getHideDiscount($type, $hideDiscount) + { + if (! empty($hideDiscount)) { + return $hideDiscount; + } + + // if you use settting translation + if ($hideDiscount = setting($this->getDocumentSettingKey($type, 'hide_discount'), false)) { + return $hideDiscount; + } + + $hide = $this->getHideFromConfig($type, 'discount'); + + if ($hide) { + return $hide; + } + + // @todo what return value invoice or always false?? + return setting('invoice.hide_discount', $hideDiscount); + } + + protected function getHideAmount($type, $hideAmount) + { + if (! empty($hideAmount)) { + return $hideAmount; + } + + // if you use settting translation + if (setting($this->getDocumentSettingKey($type, 'hide_amount'), false)) { + return true; + } + + $hide = $this->getHideFromConfig($type, 'amount'); + + if ($hide) { + return $hide; + } + + return false; + } + + protected function getPrint($print) + { + if (! empty($print)) { + return $print; + } + + $self = new ReflectionProperty($this::class, 'methodCache'); + $self->setAccessible(true); + + $values = $self->getValue(); + + if (array_key_exists('App\View\Components\Layouts\Admin', $values)) { + return false; + } + + return true; + } +} diff --git a/app/Abstracts/View/Components/Form.php b/app/Abstracts/View/Components/Form.php new file mode 100644 index 00000000..ea04803f --- /dev/null +++ b/app/Abstracts/View/Components/Form.php @@ -0,0 +1,362 @@ +type = $this->getType($type); + $this->name = $this->getName($name); + $this->label = $label; + $this->id = $id ?? $name; + $this->value = $this->getValue($value, $valueKey); + $this->placeholder = $this->getPlaceholder($placeholder); + $this->searchText = $this->getSearchText($searchText); + $this->rows = $rows; + + $this->remote = $remote; + $this->multiple = $multiple; + $this->addNew = $addNew; + $this->group = $group; + + $this->searchable = $searchable; + $this->disabled = $disabled; + $this->readonly = $readonly; + $this->required = $this->getRequired($required, $notRequired); + + $this->options = $this->getOptions($options); + $this->option = $option; + $this->optionKey = $optionKey; + $this->optionValue = $optionValue; + $this->fullOptions = $this->getFullOptions($fullOptions, $options, $searchable); + $this->checked = $this->getChecked($checked, $checkedKey); + $this->selected = $this->getSelected($selected, $selectedKey); + + $this->formGroupClass = $this->getFromGroupClass($formGroupClass); + $this->inputGroupClass = $this->getInputGroupClass($inputGroupClass); + + $this->icon = $icon; + $this->trailing = $trailing; + + $this->custom_attributes = $this->getCustomAttributes(); + + $this->setDynamicAttributes($dynamicAttributes); + + $this->hideCurrency = $hideCurrency; + } + + protected function getType($type) + { + if (! empty($type) && (! empty($this->type) && $type != 'text')) { + return $type; + } + + if (! empty($this->type)) { + return $this->type; + } + } + + protected function getName($name) + { + if (! empty($name)) { + return $name; + } + + return $this->name; + } + + protected function getValue($value, $valueKey) + { + if ($value != '') { + return $value; + } + + if (empty($valueKey)) { + $valueKey = $this->name; + } + + if (empty($valueKey)) { + return $value; + } + + // set model value. + $model = $this->getParentData('model'); + + $value_keys = explode('.', $valueKey); + + if (count($value_keys) > 1) { + $valueKey = $value_keys[0]; + } + + if (! empty($model->{$valueKey})) { + $value = $model->{$valueKey}; + } + + if ($model instanceof Collection) { + $value = $model->get($valueKey); + } + + if (count($value_keys) > 1) { + $value = $value[0]->{$value_keys[1]}; + } + + if (empty($value) && request()->has($valueKey)) { + $value = request()->get($valueKey); + } + + return old($valueKey, $value); + } + + protected function getPlaceholder($placeholder) + { + if (! empty($placeholder)) { + return $placeholder; + } + + $label = $this->label; + + if (! empty($this->label) && ! empty($this->label->contents)) { + $label = $this->name; + } + + if ($this->type == 'select') { + return trans('general.form.select.field', ['field' => $label]); + } + + return trans('general.form.enter', ['field' => $label]); + } + + protected function getSearchText($searchText) + { + if (! empty($searchText)) { + return $searchText; + } + + return trans('general.search_placeholder'); + } + + protected function getOptions($options) + { + if (! empty($options)) { + if (is_array($options) && ! $this->group) { + $o = collect(); + + foreach ($options as $key => $value) { + if (is_array($value)) { + $o->push((object) $value); + } else { + $o->push((object) [ + 'id' => $key, + 'name' => $value, + ]); + } + } + + $options = $o; + } + + return $options; + } + + return []; + } + + protected function getFullOptions($fullOptions, $options, $searchable) + { + if (! empty($fullOptions)) { + return $fullOptions; + } + + if ($searchable && empty($fullOptions)) { + $this->options = $this->options->take(setting('default.select_limit')); + + return $options; + } + + return []; + } + + protected function getChecked($checked, $checkedKey) + { + return $this->getValue($checked, $checkedKey); + } + + protected function getSelected($selected, $selectedKey) + { + return $this->getValue($selected, $selectedKey); + } + + protected function getRequired($required, $notRequired) + { + if (! empty($notRequired)) { + return false; + } + + return $required; + } + + protected function getFromGroupClass($formGroupClass) + { + if (! empty($formGroupClass)) { + return $formGroupClass; + } + + return $this->formGroupClass; + } + + protected function getInputGroupClass($inputGroupClass) + { + if (! empty($inputGroupClass)) { + return $inputGroupClass; + } + + return $this->inputGroupClass; + } + + protected function getCustomAttributes() + { + $attributes = []; + + if (! empty($this->required)) { + $attributes['required'] = $this->required; + } + + if (! empty($this->disabled)) { + $attributes['disabled'] = $this->disabled; + } + + if (! empty($this->readonly)) { + $attributes['readonly'] = $this->readonly; + } + + foreach ($this->custom_attributes as $key => $value) { + $attributes[$key] = $value; + } + + return $attributes; + } + + protected function setDynamicAttributes($dynamicAttributes) + { + if (! empty($dynamicAttributes)) { + $this->dynamicAttributes = $dynamicAttributes; + } + } +} diff --git a/app/Abstracts/View/Components/Transactions/Index.php b/app/Abstracts/View/Components/Transactions/Index.php new file mode 100644 index 00000000..0a299fb1 --- /dev/null +++ b/app/Abstracts/View/Components/Transactions/Index.php @@ -0,0 +1,760 @@ +type = $type; + $this->transactions = $transactions; + /* -- Main Start -- */ + $this->type = $type; + $this->alias = $this->getAlias($type, $alias); + $this->transactions = ($transactions) ? $transactions : collect(); + $this->totalTransactions = $this->getTotalTransactions($totalTransactions); + + $this->permissionCreate = $this->getPermissionCreate($type, $permissionCreate); + $this->permissionUpdate = $this->getPermissionUpdate($type, $permissionUpdate); + $this->permissionDelete = $this->getPermissionDelete($type, $permissionDelete); + + /* -- Main End -- */ + + /* -- Buttons Start -- */ + $this->checkPermissionCreate = $checkPermissionCreate; + + $this->hideIncomeCreate = $hideIncomeCreate; + $this->routeIncomeCreate = $this->getRouteIncomeCreate($type, $routeIncomeCreate); + $this->textIncomeCreate = $this->getTextIncomeCreate($type, $textIncomeCreate); + + $this->hideExpenseCreate = $hideExpenseCreate; + $this->routeExpenseCreate = $this->getRouteExpenseCreate($type, $routeExpenseCreate); + $this->textExpenseCreate = $this->getTextExpenseCreate($type, $textExpenseCreate); + + $this->hideImport = $hideImport; + $this->routeImport = $this->getRouteImport($type, $routeImport); + + $this->hideExport = $hideExport; + $this->routeExport = $this->getRouteExport($type, $routeExport); + /* -- Buttons End -- */ + + /* -- Content Start -- */ + + /* -- Empty Page Start -- */ + $this->hideEmptyPage = $this->getHideEmptyPage($hideEmptyPage); + /* -- Empty Page End -- */ + + /* -- Summary Start -- */ + $this->hideSummary = $hideSummary; + $this->summaryItems = $this->getSummaryItems($type, $summaryItems); + /* -- Summary End -- */ + + /* Container Start */ + $this->hideSearchString = $hideSearchString; + $this->hideBulkAction = $hideBulkAction; + + $this->searchStringModel = $this->getSearchStringModel($type, $searchStringModel); + + $this->bulkActionClass = $this->getBulkActionClass($type, $bulkActionClass); + $this->bulkActionRouteParameters = $this->getBulkActionRouteParameters($type, $bulkActionRouteParameters); + + $this->searchRoute = $this->getIndexRoute($type, $searchRoute); + + $this->classBulkAction = $this->getClassBulkAction($type, $classBulkAction); + + $this->tabSuffix = $this->getTabSuffix($type, $tabSuffix); + $this->tabActive = $this->getTabActive($type, $tabActive); + + $this->hidePaymentMethod = $hidePaymentMethod; + + /* Document Start */ + $this->hidePaidAt = $hidePaidAt; + $this->hideNumber = $hideNumber; + $this->classPaidAtAndNumber = $this->getClassPaidAtAndNumber($type, $classPaidAtAndNumber); + $this->textPaidAt = $this->getTextPaidAt($type, $textPaidAt); + $this->textNumber = $this->getTextNumber($type, $textNumber); + + $this->hideStartedAt = $hideStartedAt; + $this->hideEndedAt = $hideEndedAt; + $this->classStartedAtAndEndedAt = $this->getClassStartedAndEndedAt($type, $classStartedAtAndEndedAt); + $this->textStartedAt = $this->getTextStartedAt($type, $textStartedAt); + $this->textEndedAt = $this->getTextEndedAt($type, $textEndedAt); + + $this->hideType = $hideType; + $this->hideCategory = $hideCategory; + $this->classTypeAndCategory = $this->getClassTypeAndCategory($type, $classTypeAndCategory); + $this->textType = $this->getTextType($type, $textType); + $this->textCategory = $this->getTextCategory($type, $textCategory); + + $this->hideStatus = $hideStatus; + $this->classStatus = $this->getClassStatus($type, $classStatus); + + $this->hideFrequency = $hideFrequency; + $this->hideDuration = $hideDuration; + $this->classFrequencyAndDuration = $this->getClassFrequencyAndDuration($type, $classFrequencyAndDuration); + + $this->hideAccount = $hideAccount; + $this->classAccount = $this->getClassAccount($type, $classAccount); + $this->textAccount = $this->getTextAccount($type, $textAccount); + + $this->hideContact = $hideContact; + $this->hideDocument = $hideDocument; + $this->classContactAndDocument = $this->getClassContactAndDocument($type, $classContactAndDocument); + $this->textContact = $this->getTextContact($type, $textContact); + $this->textDocument = $this->getTextDocument($type, $textDocument); + + $this->hideAmount = $hideAmount; + $this->classAmount = $this->getClassAmount($type, $classAmount); + $this->textAmount = $this->getTextAmount($type, $textAmount); + + /* Document End */ + + /* Container End */ + + /* -- Content End -- */ + + // Set Parent data + $this->setParentData(); + } + + protected function getTotalTransactions($totalTransactions) + { + if (! is_null($totalTransactions)) { + return $totalTransactions; + } + + return $this->transactions->count(); + } + + protected function getRouteIncomeCreate ($type, $routeIncomeCreate) + { + if (! empty($routeIncomeCreate)) { + return $routeIncomeCreate; + } + + $route = $this->getRouteFromConfig($type, 'income_create'); + + if (! empty($route)) { + return $route; + } + + if (Str::contains($type, 'recurring')) { + return ['recurring-transactions.create', ['type' => 'income-recurring']]; + } + + return ['transactions.create', ['type' => 'income']]; + } + + protected function getTextIncomeCreate($type, $textIncomeCreate) + { + if (! empty($textIncomeCreate)) { + return $textIncomeCreate; + } + + $translation = $this->getTextFromConfig($type, 'income_create', 'income_create'); + + if (! empty($translation)) { + return $translation; + } + + if (Str::contains($type, 'recurring')) { + return 'general.recurring_incomes'; + } + + return 'general.incomes'; + } + + protected function getRouteExpenseCreate ($type, $routeExpenseCreate) + { + if (! empty($routeExpenseCreate)) { + return $routeExpenseCreate; + } + + $route = $this->getRouteFromConfig($type, 'expense_create'); + + if (! empty($route)) { + return $route; + } + + if (Str::contains($type, 'recurring')) { + return ['recurring-transactions.create', ['type' => 'expense-recurring']]; + } + + return ['transactions.create', ['type' => 'expense']]; + } + + protected function getTextExpenseCreate($type, $textExpenseCreate) + { + if (! empty($textExpenseCreate)) { + return $textExpenseCreate; + } + + $translation = $this->getTextFromConfig($type, 'expense_create', 'expense_create'); + + if (! empty($translation)) { + return $translation; + } + + if (Str::contains($type, 'recurring')) { + return 'general.recurring_expenses'; + } + + return 'general.expenses'; + } + + protected function getRouteImport($type, $routeImport) + { + if (! empty($routeImport)) { + return $routeImport; + } + + $route = $this->getRouteFromConfig($type, 'import'); + + if (! empty($route)) { + //return $route; + } + + if (Str::contains($type, 'recurring')) { + return ['import.create', ['banking', 'recurring-transactions']]; + } + + return ['import.create', ['banking', 'transactions']]; + } + + protected function getRouteExport($type, $routeExport) + { + if (! empty($routeExport)) { + return $routeExport; + } + + $route = $this->getRouteFromConfig($type, 'export'); + + if (! empty($route)) { + return $route; + } + + if (Str::contains($type, 'recurring')) { + return 'recurring-transactions.export'; + } + + return 'transactions.export'; + } + + protected function getHideEmptyPage($hideEmptyPage): bool + { + if ($hideEmptyPage) { + return $hideEmptyPage; + } + + if ($this->totalTransactions > 0) { + return true; + } + + if (request()->has('search') && ($this->totalTransactions > 0)) { + return true; + } + + return false; + } + + public function getSummaryItems($type, $summaryItems) + { + if (! empty($summaryItems)) { + return $summaryItems; + } + + #todo this lines + return []; + } + + public function getTabActive($type, $tabActive) + { + if (! empty($tabActive)) { + return $tabActive; + } + + $search_type = $type == 'income-recurring' ? 'recurring-transactions' : search_string_value('type'); + + return ($this->tabSuffix) ? 'transactions-' . $this->tabSuffix : $search_type; + } + + public function getTabSuffix($type, $tabSuffix) + { + if (! empty($tabSuffix)) { + return $tabSuffix; + } + + $search_type = $type == 'income-recurring' ? 'recurring-transactions' : search_string_value('type'); + + if ($search_type == 'income') { + return 'income'; + } + + if ($search_type == 'expense') { + return 'expense'; + } + + $suffix = $this->getTabActiveFromSetting($type); + + if (! empty($suffix)) { + return $suffix; + } + + return 'all'; + } + + protected function getClassPaidAtAndNumber($type, $classPaidAtAndNumber) + { + if (! empty($classPaidAtAndNumber)) { + return $classPaidAtAndNumber; + } + + $class = $this->getClassFromConfig($type, 'paid_at_and_number'); + + if (! empty($class)) { + return $class; + } + + return 'w-4/12 sm:w-3/12'; + } + + protected function getTextPaidAt($type, $textPaidAt) + { + if (! empty($textPaidAt)) { + return $textPaidAt; + } + + $translation = $this->getTextFromConfig($type, 'paid_at', 'paid_date'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.date'; + } + + protected function getTextNumber($type, $textNumber) + { + if (! empty($textNumber)) { + return $textNumber; + } + + $translation = $this->getTextFromConfig($type, 'number', 'number'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.numbers'; + } + + protected function getClassStartedAndEndedAt($type, $classStartedAtAndEndedAt) + { + if (! empty($classStartedAtAndEndedAt)) { + return $classStartedAtAndEndedAt; + } + + $class = $this->getClassFromConfig($type, 'started_at_and_end_at'); + + if (! empty($class)) { + return $class; + } + + return '"w-4/12 sm:w-3/12'; + } + + protected function getTextStartedAt($type, $textStartedAt) + { + if (! empty($textStartedAt)) { + return $textStartedAt; + } + + $translation = $this->getTextFromConfig($type, 'started_at', 'started_date'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.start_date'; + } + + protected function getTextEndedAt($type, $textEndedAt) + { + if (! empty($textEndedAt)) { + return $textEndedAt; + } + + $translation = $this->getTextFromConfig($type, 'ended_at', 'ended_date'); + + if (! empty($translation)) { + return $translation; + } + + return 'recurring.last_issued'; + } + + protected function getClassTypeAndCategory($type, $classTypeAndCategory) + { + if (! empty($classTypeAndCategory)) { + return $classTypeAndCategory; + } + + $class = $this->getClassFromConfig($type, 'type_and_category'); + + if (! empty($class)) { + return $class; + } + + return 'w-2/12'; + } + + protected function getTextType($type, $textType) + { + if (! empty($textType)) { + return $textType; + } + + $translation = $this->getTextFromConfig($type, 'type', 'type'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.types'; + } + + protected function getTextCategory($type, $textCategory) + { + if (! empty($textCategory)) { + return $textCategory; + } + + $translation = $this->getTextFromConfig($type, 'category', 'category'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.categories'; + } + + protected function getClassStatus($type, $classStatus) + { + if (! empty($classStatus)) { + return $classStatus; + } + + $class = $this->getClassFromConfig($type, 'status'); + + if (! empty($class)) { + return $class; + } + + return 'w-4/12 sm:w-3/12'; + } + + protected function getClassFrequencyAndDuration($type, $classFrequencyAndDuration) + { + if (! empty($classFrequencyAndDuration)) { + return $classFrequencyAndDuration; + } + + $class = $this->getClassFromConfig($type, 'frequency_and_duration'); + + if (! empty($class)) { + return $class; + } + + return 'w-2/12'; + } + + protected function getClassAccount($type, $classAccount) + { + if (! empty($classAccount)) { + return $classAccount; + } + + $class = $this->getClassFromConfig($type, 'account'); + + if (! empty($class)) { + return $class; + } + + return 'w-4/12 sm:w-3/12'; + } + + protected function getTextAccount($type, $textAccount) + { + if (! empty($textAccount)) { + return $textAccount; + } + + $translation = $this->getTextFromConfig($type, 'account', 'account'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.accounts'; + } + + protected function getClassContactAndDocument($type, $classContactAndDocument) + { + if (! empty($classContactAndDocument)) { + return $classContactAndDocument; + } + + $class = $this->getClassFromConfig($type, 'contact_and_document'); + + if (! empty($class)) { + return $class; + } + + return 'w-2/12'; + } + + protected function getTextContact($type, $textContact) + { + if (! empty($textContact)) { + return $textContact; + } + + $translation = $this->getTextFromConfig($type, 'contact', 'contact'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.contacts'; + } + + protected function getTextDocument($type, $textDocument) + { + if (! empty($textDocument)) { + return $textDocument; + } + + $translation = $this->getTextFromConfig($type, 'document', 'document'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.documents'; + } + + protected function getClassAmount($type, $classAmount) + { + if (! empty($classAmount)) { + return $classAmount; + } + + $class = $this->getClassFromConfig($type, 'amount'); + + if (! empty($class)) { + return $class; + } + + return 'w-4/12 sm:w-2/12'; + } + + protected function getTextAmount($type, $textAmount) + { + if (! empty($textAmount)) { + return $textAmount; + } + + $translation = $this->getTextFromConfig($type, 'amount', 'amount'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.amount'; + } +} diff --git a/app/Abstracts/View/Components/Transactions/Show.php b/app/Abstracts/View/Components/Transactions/Show.php new file mode 100644 index 00000000..629bdd1c --- /dev/null +++ b/app/Abstracts/View/Components/Transactions/Show.php @@ -0,0 +1,1142 @@ +type = $type; + $this->transaction = $transaction; + $this->transactionTemplate = $this->getTransactionTemplate($type, $transactionTemplate); + $this->logo = $this->getLogo($logo); + $this->payment_methods = ($payment_methods) ?: Modules::getPaymentMethods('all'); + $this->date_format = $this->getCompanyDateFormat(); + + // Navbar Hide + $this->hideButtonAddNew = $hideButtonAddNew; + $this->hideButtonMoreActions = $hideButtonMoreActions; + $this->hideButtonEdit = $hideButtonEdit; + $this->hideButtonDuplicate = $hideButtonDuplicate; + $this->hideButtonConnect = $hideButtonConnect; + $this->hideButtonPrint = $hideButtonPrint; + $this->hideButtonShare = $hideButtonShare; + $this->hideButtonEmail = $hideButtonEmail; + $this->hideButtonPdf = $hideButtonPdf; + $this->hideButtonEnd = $hideButtonEnd; + $this->hideButtonDelete = $hideButtonDelete; + $this->checkButtonReconciled = $checkButtonReconciled; + + $this->hideDivider1 = $hideDivider1; + $this->hideDivider2 = $hideDivider2; + $this->hideDivider3 = $hideDivider3; + $this->hideDivider4 = $hideDivider4; + + // Navbar Permission + $this->permissionCreate = $this->getPermissionCreate($type, $permissionCreate); + $this->permissionUpdate = $this->getPermissionUpdate($type, $permissionUpdate); + $this->permissionDelete = $this->getPermissionDelete($type, $permissionDelete); + + // Navbar route + $this->routeButtonAddNew = $this->getRouteButtonAddNew($type, $routeButtonAddNew); + $this->routeButtonEdit = $this->getRouteButtonEdit($type, $routeButtonEdit); + $this->routeButtonDuplicate = $this->getRouteButtonDuplicate($type, $routeButtonDuplicate); + $this->routeButtonPrint = $this->getRouteButtonPrint($type, $routeButtonPrint); + $this->shareRoute = $this->getShareRoute($type, $shareRoute); + $this->signedUrl = $this->getSignedUrl($type, $signedUrl); + $this->routeButtonEmail = $this->getRouteButtonEmail($type, $routeButtonEmail); + $this->routeButtonPdf = $this->getRouteButtonPdf($type, $routeButtonPdf); + $this->routeButtonEnd = $this->getRouteButtonEnd($type, $routeButtonEnd); + $this->routeButtonDelete = $this->getRouteButtonDelete($type, $routeButtonDelete); + $this->routeContactShow = $this->getRouteContactShow($type, $routeContactShow); + + // Navbar Text + $this->textButtonAddNew = $this->getTextButtonAddNew($type, $textButtonAddNew); + $this->textDeleteModal = $textDeleteModal; + + // Hide Schedule + $this->hideSchedule = $hideSchedule; + + // Hide Children + $this->hideChildren = $hideChildren; + + // Hide Connect + $this->hideConnect = $hideConnect; + + // Hide Transfer + $this->hideTransfer = $hideTransfer; + + // Hide Attachment + $this->hideAttachment = $hideAttachment; + + // Company Information Hide checker + $this->hideCompany = $hideCompany; + $this->hideCompanyLogo = $hideCompanyLogo; + $this->hideCompanyDetails = $hideCompanyDetails; + $this->hideCompanyName = $hideCompanyName; + $this->hideCompanyAddress = $hideCompanyAddress; + $this->hideCompanyTaxNumber = $hideCompanyTaxNumber; + $this->hideCompanyPhone = $hideCompanyPhone; + $this->hideCompanyEmail = $hideCompanyEmail; + + // Transaction Information Hide checker + $this->hideContentTitle = $hideContentTitle; + $this->hideNumber = $hideNumber; + $this->hidePaidAt = $hidePaidAt; + $this->hideAccount = $hideAccount; + $this->hideCategory = $hideCategory; + $this->hidePaymentMethods = $hidePaymentMethods; + $this->hideReference = $hideReference; + $this->hideDescription = $hideDescription; + $this->hideAmount = $hideAmount; + + // Transaction Information Text + $this->textContentTitle = $this->getTextContentTitle($type, $textContentTitle); + $this->textNumber = $this->getTextNumber($type, $textNumber); + $this->textPaidAt = $this->getTextPaidAt($type, $textPaidAt); + $this->textAccount = $this->getTextAccount($type, $textAccount); + $this->textCategory = $this->getTextCategory($type, $textCategory); + $this->textPaymentMethods = $this->getTextPaymentMethods($type, $textPaymentMethods); + $this->textReference = $this->getTextReference($type, $textReference); + $this->textDescription = $this->getTextDescription($type, $textDescription); + $this->textAmount = $this->getTextAmount($type, $textAmount); + $this->textPaidBy = $this->getTextPaidBy($type, $textPaidBy); + + // Contact Information Hide checker + $this->hideContact = $hideContact; + $this->hideContactInfo = $hideContactInfo; + $this->hideContactName = $hideContactName; + $this->hideContactAddress = $hideContactAddress; + $this->hideContactTaxNumber = $hideContactTaxNumber; + $this->hideContactPhone = $hideContactPhone; + $this->hideContactEmail = $hideContactEmail; + + // Related Information Hide checker + $this->hideRelated = $hideRelated; + $this->hideRelatedDocumentNumber = $hideRelatedDocumentNumber; + $this->hideRelatedContact = $hideRelatedContact; + $this->hideRelatedDocumentDate = $hideRelatedDocumentDate; + $this->hideRelatedDocumentAmount = $hideRelatedDocumentAmount; + $this->hideRelatedAmount = $hideRelatedAmount; + + // Related Information Text + $this->textRelatedTransansaction = $this->getTextRelatedTransansaction($type, $textRelatedTransansaction); + $this->textRelatedDocumentNumber = $this->getTextRelatedDocumentNumber($type, $textRelatedDocumentNumber); + $this->textRelatedContact = $this->getTextRelatedContact($type, $textRelatedContact); + $this->textRelatedDocumentDate = $this->getTextRelatedDocumentDate($type, $textRelatedDocumentDate); + $this->textRelatedDocumentAmount = $this->getTextRelatedDocumentAmount($type, $textRelatedDocumentAmount); + $this->textRelatedAmount = $this->getTextRelatedAmount($type, $textRelatedAmount); + + $this->routeDocumentShow = $this->routeDocumentShow($type, $routeDocumentShow); + $this->routeTransactionShow = $this->routeTransactionShow($type, $routeTransactionShow); + + // Attachment data.. + $this->attachment = ''; + + if (! empty($attachment)) { + $this->attachment = $attachment; + } else if (! empty($transaction)) { + $this->attachment = $transaction->attachment; + } + + // Connect translations + $this->connectTranslations = $this->getTranslationsForConnect($type); + $this->hideConnectMessage = $hideConnectMessage; + + $this->textRecurringType = $this->getTextRecurringType($type, $textRecurringType); + $this->hideRecurringMessage = $hideRecurringMessage; + $this->hideCreated = $hideCreated; + } + + protected function getTransactionTemplate($type, $transactionTemplate) + { + if (! empty($transactionTemplate)) { + return $transactionTemplate; + } + + if ($template = config('type.transaction.' . $type . '.template', false)) { + return $template; + } + + $transactionTemplate = setting($this->getTransactionSettingKey($type, 'template')) ?: 'default'; + + return $transactionTemplate; + } + + protected function getLogo($logo) + { + if (! empty($logo)) { + return $logo; + } + + static $content; + + if (! empty($content)) { + return $content; + } + + $media_id = (!empty($this->transaction->contact->logo) && !empty($this->transaction->contact->logo->id)) ? $this->transaction->contact->logo->id : setting('company.logo'); + + $media = Media::find($media_id); + + if (! empty($media)) { + $path = $media->getDiskPath(); + + if (! $media->fileExists()) { + return $logo; + } + } else { + $path = base_path('public/img/company.png'); + } + + try { + $image = Image::cache(function($image) use ($media, $path) { + $width = setting('invoice.logo_size_width'); + $height = setting('invoice.logo_size_height'); + + if ($media) { + $image->make($media->contents())->resize($width, $height)->encode(); + } else { + $image->make($path)->resize($width, $height)->encode(); + } + }); + } catch (NotReadableException | \Exception $e) { + Log::info('Company ID: ' . company_id() . ' components/transactionshow.php exception.'); + Log::info($e->getMessage()); + + $path = base_path('public/img/company.png'); + + $image = Image::cache(function($image) use ($path) { + $width = setting('invoice.logo_size_width'); + $height = setting('invoice.logo_size_height'); + + $image->make($path)->resize($width, $height)->encode(); + }); + } + + if (empty($image)) { + return $logo; + } + + $extension = File::extension($path); + + $content = 'data:image/' . $extension . ';base64,' . base64_encode($image); + + return $content; + } + + protected function getRouteButtonAddNew($type, $routeButtonAddNew) + { + if (! empty($routeButtonAddNew)) { + return $routeButtonAddNew; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'create', $parameter); + + if (! empty($route)) { + return $route; + } + + return 'transactions.create'; + } + + protected function getRouteButtonEdit($type, $routeButtonEdit) + { + if (! empty($routeButtonEdit)) { + return $routeButtonEdit; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'edit', $parameter); + + if (! empty($route)) { + return $route; + } + + return 'transactions.edit'; + } + + protected function getRouteButtonDuplicate($type, $routeButtonDuplicate) + { + if (! empty($routeButtonDuplicate)) { + return $routeButtonDuplicate; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'duplicate', $parameter); + + if (! empty($route)) { + return $route; + } + + return 'transactions.duplicate'; + } + + protected function getRouteButtonPrint($type, $routeButtonPrint) + { + if (! empty($routeButtonPrint)) { + return $routeButtonPrint; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'print', $parameter); + + if (! empty($route)) { + return $route; + } + + return 'transactions.print'; + } + + protected function getShareRoute($type, $shareRoute) + { + if (! empty($shareRoute)) { + return $shareRoute; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'share', $parameter); + + if (! empty($route)) { + return $route; + } + + return 'modals.transactions.share.create'; + } + + protected function getSignedUrl($type, $signedUrl) + { + if (! empty($signedUrl)) { + return $signedUrl; + } + + $page = config('type.transaction.' . $type . '.route.prefix'); + $alias = config('type.transaction.' . $type . '.alias'); + + $route = ''; + + if (! empty($alias)) { + $route .= $alias . '.'; + } + + $route .= 'signed.' . $page . '.show'; + + try { + route($route, [$this->transaction->id, 'company_id' => company_id()]); + + $signedUrl = URL::signedRoute($route, [$this->transaction->id]); + } catch (\Exception $e) { + $signedUrl = URL::signedRoute('signed.payments.show', [$this->transaction->id]); + } + + return $signedUrl; + } + + protected function getRouteButtonEmail($type, $routeButtonEmail) + { + if (! empty($routeButtonEmail)) { + return $routeButtonEmail; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'emails.create', $parameter, true); + + if (! empty($route)) { + return $route; + } + + return 'modals.transactions.emails.create'; + } + + protected function getRouteButtonPdf($type, $routeButtonPdf) + { + if (! empty($routeButtonPdf)) { + return $routeButtonPdf; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'pdf', $parameter); + + if (! empty($route)) { + return $route; + } + + return 'transactions.pdf'; + } + + protected function getRouteButtonEnd($type, $routeButtonEnd) + { + if (! empty($routeButtonEnd)) { + return $routeButtonEnd; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'end', $parameter); + + if (! empty($route)) { + return $route; + } + + return 'recurring-transactions.end'; + } + + protected function getRouteButtonDelete($type, $routeButtonDelete) + { + if (! empty($routeButtonDelete)) { + return $routeButtonDelete; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'destroy', $parameter); + + if (! empty($route)) { + return $route; + } + + return 'transactions.destroy'; + } + + protected function getRouteContactShow($type, $routeContactShow) + { + if (! empty($routeContactShow)) { + return $routeContactShow; + } + + //example route parameter. + $parameter = 1; + + $route = Str::plural(config('type.transaction.' . $type . '.contact_type'), 2) . '.show'; + + try { + route($route, $parameter); + } catch (\Exception $e) { + try { + $route = Str::plural($type, 2) . '.' . $config_key; + + route($route, $parameter); + } catch (\Exception $e) { + $route = ''; + } + } + + if (! empty($route)) { + return $route; + } + + return 'customers.show'; + } + + protected function getTextButtonAddNew($type, $textButtonAddNew) + { + if (! empty($textButtonAddNew)) { + return $textButtonAddNew; + } + + $translation = $this->getTextFromConfig($type, 'transactions'); + + if (! empty($translation)) { + return trans('general.title.new', ['type' => trans_choice($translation, 1)]); + } + + return trans('general.title.new', ['type' => trans_choice('general.' . Str::plural($type), 1)]); + } + + protected function getTextContentTitle($type, $textContentTitle) + { + if (! empty($textContentTitle)) { + return $textContentTitle; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'payment_made'; + break; + default: + $default_key = 'receipts'; + break; + } + + $translation = $this->getTextFromConfig($type, $type . '_made', $default_key); + + if (! empty($translation)) { + return $translation; + } + + return 'general.receipts'; + } + + protected function getTextNumber($type, $textNumber) + { + if (! empty($textNumber)) { + return $textNumber; + } + + return 'general.numbers'; + } + + protected function getTextPaidAt($type, $textPaidAt) + { + if (! empty($textPaidAt)) { + return $textPaidAt; + } + + $translation = $this->getTextFromConfig($type, 'paid_at', 'date'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.date'; + } + + protected function getTextAccount($type, $textAccount) + { + if (! empty($textAccount)) { + return $textAccount; + } + + $translation = $this->getTextFromConfig($type, 'accounts', 'accounts', 'trans_choice'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.accounts'; + } + + protected function getTextCategory($type, $textCategory) + { + if (! empty($textCategory)) { + return $textCategory; + } + + $translation = $this->getTextFromConfig($type, 'categories', 'categories', 'trans_choice'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.categories'; + } + + protected function getTextPaymentMethods($type, $textPaymentMethods) + { + if (! empty($textPaymentMethods)) { + return $textPaymentMethods; + } + + $translation = $this->getTextFromConfig($type, 'payment_methods', 'payment_methods', 'trans_choice'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.payment_methods'; + } + + protected function getTextReference($type, $textReference) + { + if (! empty($textReference)) { + return $textReference; + } + + $translation = $this->getTextFromConfig($type, 'reference', 'reference'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.reference'; + } + + protected function getTextDescription($type, $textDescription) + { + if (! empty($textDescription)) { + return $textDescription; + } + + $translation = $this->getTextFromConfig($type, 'description', 'description'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.description'; + } + + protected function getTextAmount($type, $textAmount) + { + if (! empty($textAmount)) { + return $textAmount; + } + + $translation = $this->getTextFromConfig($type, 'amount', 'amount'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.amount'; + } + + protected function getTextPaidBy($type, $textPaidBy) + { + if (! empty($textPaidBy)) { + return $textPaidBy; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'paid_to'; + break; + default: + $default_key = 'paid_by'; + break; + } + + $translation = $this->getTextFromConfig($type, 'paid_to_by', $default_key); + + if (! empty($translation)) { + return $translation; + } + + return 'transactions.paid_by'; + } + + protected function getTextRelatedTransansaction($type, $textRelatedTransansaction) + { + if (! empty($textRelatedTransansaction)) { + return $textRelatedTransansaction; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'related_bill'; + break; + default: + $default_key = 'related_invoice'; + break; + } + + $translation = $this->getTextFromConfig($type, 'related_type', $default_key); + + if (! empty($translation)) { + return $translation; + } + + return 'transactions.related_invoice'; + } + + protected function getTextRelatedDocumentNumber($type, $textRelatedDocumentNumber) + { + if (! empty($textRelatedDocumentNumber)) { + return $textRelatedDocumentNumber; + } + + $translation = $this->getTextFromConfig($type, 'related_document_number', 'numbers'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.numbers'; + } + + protected function getTextRelatedContact($type, $textRelatedContact) + { + if (! empty($textRelatedContact)) { + return $textRelatedContact; + } + + $default_key = Str::plural(config('type.transaction.' . $type . '.contact_type'), 2); + + $translation = $this->getTextFromConfig($type, 'related_contact', $default_key, 'trans_choice'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.customers'; + } + + protected function getTextRelatedDocumentDate($type, $textRelatedDocumentDate) + { + if (! empty($textRelatedDocumentDate)) { + return $textRelatedDocumentDate; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'bill_date'; + break; + default: + $default_key = 'invoice_date'; + break; + } + + $translation = $this->getTextFromConfig($type, 'related_document_date', $default_key); + + if (! empty($translation)) { + return $translation; + } + + return 'invoices.invoice_date'; + } + + protected function getTextRelatedDocumentAmount($type, $textRelatedDocumentAmount) + { + if (! empty($textRelatedDocumentAmount)) { + return $textRelatedDocumentAmount; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'bill_amount'; + break; + default: + $default_key = 'invoice_amount'; + break; + } + + $translation = $this->getTextFromConfig($type, 'related_document_amount', $default_key); + + if (! empty($translation)) { + return $translation; + } + + return 'general.amount'; + } + + protected function getTextRelatedAmount($type, $textRelatedAmount) + { + if (! empty($textRelatedAmount)) { + return $textRelatedAmount; + } + + $translation = $this->getTextFromConfig($type, 'related_amount', 'amount'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.amount'; + } + + protected function routeDocumentShow($type, $routeDocumentShow) + { + if (! empty($routeDocumentShow)) { + return $routeDocumentShow; + } + + if (! $this->transaction->document) { + return $routeDocumentShow; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($this->transaction->document->type, 'show', $parameter); + + if (! empty($route)) { + return $route; + } + + return 'invoices.show'; + } + + protected function routeTransactionShow($type, $routeTransactionShow) + { + if (! empty($routeTransactionShow)) { + return $routeTransactionShow; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($type, 'show', $parameter); + + if (! empty($route)) { + return $route; + } + + return 'transactions.show'; + } + + protected function getTextRecurringType($type, $textRecurringType) + { + if (! empty($textRecurringType)) { + return $textRecurringType; + } + + $translation = config('type.transaction.' . $type . '.translation.transactions'); + + if (! empty($translation)) { + return $translation; + } + + return 'general.incomes'; + } +} diff --git a/app/Abstracts/View/Components/Transactions/Template.php b/app/Abstracts/View/Components/Transactions/Template.php new file mode 100644 index 00000000..5f96a6a6 --- /dev/null +++ b/app/Abstracts/View/Components/Transactions/Template.php @@ -0,0 +1,660 @@ +type = $type; + $this->transaction = $transaction; + + $this->logo = $this->getLogo($logo); + $this->payment_methods = ($payment_methods) ?: Modules::getPaymentMethods('all'); + + // Company Information Hide checker + $this->hideCompany = $hideCompany; + $this->hideCompanyLogo = $hideCompanyLogo; + $this->hideCompanyDetails = $hideCompanyDetails; + $this->hideCompanyName = $hideCompanyName; + $this->hideCompanyAddress = $hideCompanyAddress; + $this->hideCompanyTaxNumber = $hideCompanyTaxNumber; + $this->hideCompanyPhone = $hideCompanyPhone; + $this->hideCompanyEmail = $hideCompanyEmail; + + // Transaction Information Hide checker + $this->hideContentTitle = $hideContentTitle; + $this->hideNumber = $hideNumber; + $this->hidePaidAt = $hidePaidAt; + $this->hideAccount = $hideAccount; + $this->hideCategory = $hideCategory; + $this->hidePaymentMethods = $hidePaymentMethods; + $this->hideReference = $hideReference; + $this->hideDescription = $hideDescription; + $this->hideAmount = $hideAmount; + + // Transaction Information Text + $this->textContentTitle = $this->getTextContentTitle($type, $textContentTitle); + $this->textNumber = $this->getTextNumber($type, $textNumber); + $this->textPaidAt = $this->getTextPaidAt($type, $textPaidAt); + $this->textAccount = $this->getTextAccount($type, $textAccount); + $this->textCategory = $this->getTextCategory($type, $textCategory); + $this->textPaymentMethods = $this->getTextPaymentMethods($type, $textPaymentMethods); + $this->textReference = $this->getTextReference($type, $textReference); + $this->textDescription = $this->getTextDescription($type, $textDescription); + $this->textAmount = $this->getTextAmount($type, $textAmount); + $this->textPaidBy = $this->getTextPaidBy($type, $textPaidBy); + $this->textContactInfo = $this->getTextContactInfo($type, $textContactInfo); + + // Contact Information Hide checker + $this->hideContact = $hideContact; + $this->hideContactInfo = $hideContactInfo; + $this->hideContactName = $hideContactName; + $this->hideContactAddress = $hideContactAddress; + $this->hideContactTaxNumber = $hideContactTaxNumber; + $this->hideContactPhone = $hideContactPhone; + $this->hideContactEmail = $hideContactEmail; + + // Related Information Hide checker + $this->hideRelated = $hideRelated; + $this->hideRelatedDocumentNumber = $hideRelatedDocumentNumber; + $this->hideRelatedContact = $hideRelatedContact; + $this->hideRelatedDocumentDate = $hideRelatedDocumentDate; + $this->hideRelatedDocumentAmount = $hideRelatedDocumentAmount; + $this->hideRelatedAmount = $hideRelatedAmount; + + // Related Information Text + $this->textRelatedTransansaction = $this->getTextRelatedTransansaction($type, $textRelatedTransansaction); + $this->textRelatedDocumentNumber = $this->getTextRelatedDocumentNumber($type, $textRelatedDocumentNumber); + $this->textRelatedContact = $this->getTextRelatedContact($type, $textRelatedContact); + $this->textRelatedDocumentDate = $this->getTextRelatedDocumentDate($type, $textRelatedDocumentDate); + $this->textRelatedDocumentAmount = $this->getTextRelatedDocumentAmount($type, $textRelatedDocumentAmount); + $this->textRelatedAmount = $this->getTextRelatedAmount($type, $textRelatedAmount); + + $this->routeDocumentShow = $this->routeDocumentShow($type, $routeDocumentShow); + } + + protected function getLogo($logo) + { + if (!empty($logo)) { + return $logo; + } + + static $content; + + if (! empty($content)) { + return $content; + } + + $media_id = (!empty($this->transaction->contact->logo) && !empty($this->transaction->contact->logo->id)) ? $this->transaction->contact->logo->id : setting('company.logo'); + + $media = Media::find($media_id); + + if (!empty($media)) { + $path = $media->getDiskPath(); + + if (! $media->fileExists()) { + return $logo; + } + } else { + $path = base_path('public/img/company.png'); + } + + try { + $image = Image::cache(function($image) use ($media, $path) { + $width = setting('invoice.logo_size_width'); + $height = setting('invoice.logo_size_height'); + + if ($media) { + $image->make($media->contents())->resize($width, $height)->encode(); + } else { + $image->make($path)->resize($width, $height)->encode(); + } + }); + } catch (NotReadableException | \Exception $e) { + Log::info('Company ID: ' . company_id() . ' components/transactionshow.php exception.'); + Log::info($e->getMessage()); + + $path = base_path('public/img/company.png'); + + $image = Image::cache(function($image) use ($path) { + $width = setting('invoice.logo_size_width'); + $height = setting('invoice.logo_size_height'); + + $image->make($path)->resize($width, $height)->encode(); + }); + } + + if (empty($image)) { + return $logo; + } + + $extension = File::extension($path); + + $content = 'data:image/' . $extension . ';base64,' . base64_encode($image); + + return $content; + } + + protected function getTextContentTitle($type, $textContentTitle) + { + if (!empty($textContentTitle)) { + return $textContentTitle; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'payment_made'; + break; + default: + $default_key = 'receipts'; + break; + } + + $translation = $this->getTextFromConfig($type, $type . '_made', $default_key); + + if (!empty($translation)) { + return $translation; + } + + return 'general.receipts'; + } + + protected function getTextNumber($type, $textNumber) + { + if (!empty($textNumber)) { + return $textNumber; + } + + return 'general.numbers'; + } + + protected function getTextPaidAt($type, $textPaidAt) + { + if (!empty($textPaidAt)) { + return $textPaidAt; + } + + $translation = $this->getTextFromConfig($type, 'paid_at', 'date'); + + if (!empty($translation)) { + return $translation; + } + + return 'general.date'; + } + + protected function getTextAccount($type, $textAccount) + { + if (!empty($textAccount)) { + return $textAccount; + } + + $translation = $this->getTextFromConfig($type, 'accounts', 'accounts', 'trans_choice'); + + if (!empty($translation)) { + return $translation; + } + + return 'general.accounts'; + } + + protected function getTextCategory($type, $textCategory) + { + if (!empty($textCategory)) { + return $textCategory; + } + + $translation = $this->getTextFromConfig($type, 'categories', 'categories', 'trans_choice'); + + if (!empty($translation)) { + return $translation; + } + + return 'general.categories'; + } + + protected function getTextPaymentMethods($type, $textPaymentMethods) + { + if (!empty($textPaymentMethods)) { + return $textPaymentMethods; + } + + $translation = $this->getTextFromConfig($type, 'payment_methods', 'payment_methods', 'trans_choice'); + + if (!empty($translation)) { + return $translation; + } + + return 'general.payment_methods'; + } + + protected function getTextReference($type, $textReference) + { + if (!empty($textReference)) { + return $textReference; + } + + $translation = $this->getTextFromConfig($type, 'reference', 'reference'); + + if (!empty($translation)) { + return $translation; + } + + return 'general.reference'; + } + + protected function getTextDescription($type, $textDescription) + { + if (!empty($textDescription)) { + return $textDescription; + } + + $translation = $this->getTextFromConfig($type, 'description', 'description'); + + if (!empty($translation)) { + return $translation; + } + + return 'general.description'; + } + + protected function getTextAmount($type, $textAmount) + { + if (!empty($textAmount)) { + return $textAmount; + } + + $translation = $this->getTextFromConfig($type, 'amount', 'amount'); + + if (!empty($translation)) { + return $translation; + } + + return 'general.amount'; + } + + protected function getTextPaidBy($type, $textPaidBy) + { + if (!empty($textPaidBy)) { + return $textPaidBy; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'paid_to'; + break; + default: + $default_key = 'paid_by'; + break; + } + + $translation = $this->getTextFromConfig($type, 'paid_to_by', $default_key); + + if (!empty($translation)) { + return $translation; + } + + return 'transactions.paid_by'; + } + + protected function getTextContactInfo($type, $textContactInfo) + { + if (!empty($textContactInfo)) { + return $textContactInfo; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $textContactInfo = 'bills.bill_from'; + break; + default: + $textContactInfo = 'invoices.bill_to'; + break; + } + + return $textContactInfo; + } + + protected function getTextRelatedTransansaction($type, $textRelatedTransansaction) + { + if (!empty($textRelatedTransansaction)) { + return $textRelatedTransansaction; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'related_bill'; + break; + default: + $default_key = 'related_invoice'; + break; + } + + $translation = $this->getTextFromConfig($type, 'related_type', $default_key); + + if (!empty($translation)) { + return $translation; + } + + return 'transactions.related_invoice'; + } + + protected function getTextRelatedDocumentNumber($type, $textRelatedDocumentNumber) + { + if (!empty($textRelatedDocumentNumber)) { + return $textRelatedDocumentNumber; + } + + $translation = $this->getTextFromConfig($type, 'related_document_number', 'numbers'); + + if (!empty($translation)) { + return $translation; + } + + return 'general.numbers'; + } + + protected function getTextRelatedContact($type, $textRelatedContact) + { + if (!empty($textRelatedContact)) { + return $textRelatedContact; + } + + $default_key = Str::plural(config('type.transaction.' . $type . '.contact_type'), 2); + + $translation = $this->getTextFromConfig($type, 'related_contact', $default_key, 'trans_choice'); + + if (!empty($translation)) { + return $translation; + } + + return 'general.customers'; + } + + protected function getTextRelatedDocumentDate($type, $textRelatedDocumentDate) + { + if (!empty($textRelatedDocumentDate)) { + return $textRelatedDocumentDate; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'bill_date'; + break; + default: + $default_key = 'invoice_date'; + break; + } + + $translation = $this->getTextFromConfig($type, 'related_document_date', $default_key); + + if (!empty($translation)) { + return $translation; + } + + return 'invoices.invoice_date'; + } + + protected function getTextRelatedDocumentAmount($type, $textRelatedDocumentAmount) + { + if (!empty($textRelatedDocumentAmount)) { + return $textRelatedDocumentAmount; + } + + switch ($type) { + case 'bill': + case 'expense': + case 'purchase': + $default_key = 'bill_amount'; + break; + default: + $default_key = 'invoice_amount'; + break; + } + + $translation = $this->getTextFromConfig($type, 'related_document_amount', $default_key); + + if (!empty($translation)) { + return $translation; + } + + return 'general.amount'; + } + + protected function getTextRelatedAmount($type, $textRelatedAmount) + { + if (!empty($textRelatedAmount)) { + return $textRelatedAmount; + } + + $translation = $this->getTextFromConfig($type, 'related_amount', 'amount'); + + if (!empty($translation)) { + return $translation; + } + + return 'general.amount'; + } + + protected function routeDocumentShow($type, $routeDocumentShow) + { + if (!empty($routeDocumentShow)) { + return $routeDocumentShow; + } + + if (!$this->transaction->document) { + return $routeDocumentShow; + } + + //example route parameter. + $parameter = 1; + + $route = $this->getRouteFromConfig($this->transaction->document->type, 'show', $parameter); + + if (!empty($route)) { + return $route; + } + + return 'invoices.show'; + } +} diff --git a/app/Abstracts/View/Components/Transfers/Show.php b/app/Abstracts/View/Components/Transfers/Show.php new file mode 100644 index 00000000..6c5eef82 --- /dev/null +++ b/app/Abstracts/View/Components/Transfers/Show.php @@ -0,0 +1,50 @@ +model = $model; + $this->transfer = $this->getTransfer($model, $transfer); + $this->payment_methods = ($payment_methods) ?: Modules::getPaymentMethods('all'); + $this->template = ! empty($template) ? $template : setting('transfer.template'); + + // Set Parent data + $this->setParentData(); + } + + protected function getTransfer($model, $transfer) + { + if (! empty($model)) { + return $model; + } + + if (! empty($transfer)) { + return $transfer; + } + + return false; + } +} diff --git a/app/Abstracts/View/Components/Transfers/Template.php b/app/Abstracts/View/Components/Transfers/Template.php new file mode 100644 index 00000000..040a4282 --- /dev/null +++ b/app/Abstracts/View/Components/Transfers/Template.php @@ -0,0 +1,50 @@ +model = $model; + $this->transfer = $this->getTransfer($model, $transfer); + $this->payment_methods = ($payment_methods) ?: Modules::getPaymentMethods('all'); + $this->template = ! empty($template) ? $template : setting('transfer.template'); + + // Set Parent data + $this->setParentData(); + } + + protected function getTransfer($model, $transfer) + { + if (! empty($model)) { + return $model; + } + + if (! empty($transfer)) { + return $transfer; + } + + return false; + } +} diff --git a/app/Abstracts/Widget.php b/app/Abstracts/Widget.php new file mode 100644 index 00000000..14d38b94 --- /dev/null +++ b/app/Abstracts/Widget.php @@ -0,0 +1,143 @@ + '50', + ]; + + public $description = ''; + + public $report_class = ''; + + public $views = [ + 'header' => 'components.widgets.header', + ]; + + /** + * Methods that may be invoked via the Widgets@getData endpoint. + * Subclasses can append additional safe, read-only methods. + * + * @var array + */ + public array $allowed_methods = ['show']; + + public array $data = []; + + public function __construct($model = null) + { + $this->model = $model; + } + + public function getDefaultName() + { + return trans($this->default_name); + } + + public function getDefaultSettings() + { + return $this->default_settings; + } + + public function getDescription() + { + return trans($this->description); + } + + public function getReportUrl(): string + { + $empty_url = ''; + + if (empty($this->report_class)) { + return $empty_url; + } + + if (Reports::isModule($this->report_class) && Reports::isModuleDisabled($this->report_class)) { + $alias = Reports::getModuleAlias($this->report_class); + + return route('apps.app.show', [ + 'alias' => $alias, + 'utm_source' => 'widget', + 'utm_medium' => 'app', + 'utm_campaign' => str_replace('-', '_', $alias), + ]); + } + + if (! class_exists($this->report_class)) { + return $empty_url; + } + + if (Reports::cannotRead($this->report_class)) { + return $empty_url; + } + + $model = Report::where('class', $this->report_class)->first(); + + if (! $model instanceof Report) { + return route('reports.create'); + } + + return route('reports.show', $model->id); + } + + public function getViews() + { + return $this->views; + } + + public function view($name, $data = []) + { + if (request_is_api()) { + return $data; + } + + return view($name, array_merge(['class' => $this], (array) $data)); + } + + public function applyFilters($query, $args = ['date_field' => 'paid_at']) + { + return $this->scopeDateFilter($query, $args['date_field']); + } + + public function calculateDocumentTotals($model) + { + $open = $overdue = 0; + + $today = Date::today()->toDateString(); + + if ($model->status == 'paid') { + return [$open, $overdue]; + } + + $payments = 0; + + if ($model->status == 'partial') { + foreach ($model->transactions as $transaction) { + $payments += $transaction->getAmountConvertedToDefault(); + } + } + + // Check if the invoice/bill is open or overdue + if ($model->due_at > $today) { + $open += $model->getAmountConvertedToDefault() - $payments; + } else { + $overdue += $model->getAmountConvertedToDefault() - $payments; + } + + return [$open, $overdue]; + } +} diff --git a/app/Builders/Category.php b/app/Builders/Category.php new file mode 100644 index 00000000..3188110a --- /dev/null +++ b/app/Builders/Category.php @@ -0,0 +1,74 @@ +withChildren('sub_categories', function ($list, $parent, $relation, $level, $addChildren) { + $parent->load($relation); + $parent->level = $level; + $parent->syncOriginalAttribute('level'); + + $list->push($parent); + + if ($parent->$relation->count() == 0) { + return; + } + + foreach ($parent->$relation as $item) { + $addChildren($list, $item, $relation, $level + 1, $addChildren); + } + }); + } + + /** + * Get the categories excluding their children. + * + * @param array|string $columns + * @return \Illuminate\Support\Collection + */ + public function getWithoutChildren($columns = ['*']) + { + return parent::get($columns); + } + + /** + * Paginate the given query. + * + * @param int|null $perPage + * @param array $columns + * @param string $pageName + * @param int|null $page + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + * + * @throws \InvalidArgumentException + */ + public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) + { + $page = $page ?: Paginator::resolveCurrentPage($pageName); + + $perPage = $perPage ?: $this->model->getPerPage(); + + $results = ($total = $this->toBase()->getCountForPagination()) + ? $this->forPage($page, $perPage)->getWithoutChildren($columns) + : $this->model->newCollection(); + + return $this->paginator($results, $total, $perPage, $page, [ + 'path' => Paginator::resolveCurrentPath(), + 'pageName' => $pageName, + ]); + } +} diff --git a/app/BulkActions/Auth/Users.php b/app/BulkActions/Auth/Users.php new file mode 100644 index 00000000..58c9512c --- /dev/null +++ b/app/BulkActions/Auth/Users.php @@ -0,0 +1,123 @@ +model = user_model_class(); + } + + public $text = 'general.users'; + + public $path = [ + 'group' => 'auth', + 'type' => 'users', + ]; + + public $actions = [ + 'edit' => [ + 'icon' => 'edit', + 'name' => 'general.edit', + 'message' => '', + 'permission' => 'update-auth-users', + 'type' => 'modal', + 'handle' => 'update', + ], + 'enable' => [ + 'icon' => 'check_circle', + 'name' => 'general.enable', + 'message' => 'bulk_actions.message.enable', + 'permission' => 'update-auth-users', + ], + 'disable' => [ + 'icon' => 'hide_source', + 'name' => 'general.disable', + 'message' => 'bulk_actions.message.disable', + 'permission' => 'update-auth-users', + ], + 'delete' => [ + 'icon' => 'delete', + 'name' => 'general.delete', + 'message' => 'bulk_actions.message.delete', + 'permission' => 'delete-auth-users', + ], + ]; + + public function edit($request) + { + $selected = $this->getSelectedInput($request); + + $roles = role_model_class()::all()->reject(function ($r) { + $status = $r->hasPermission('read-client-portal'); + + if ($r->name == 'employee') { + $status = true; + } + + return $status; + })->pluck('display_name', 'id'); + + $u = new \stdClass(); + $u->landing_pages = []; + + event(new LandingPageShowing($u)); + + $landing_pages = $u->landing_pages; + + return $this->response('bulk-actions.auth.users.edit', compact('selected', 'roles', 'landing_pages')); + } + + public function update($request) + { + $users = $this->getSelectedRecords($request, 'contact'); + + foreach ($users as $user) { + try { + $request->merge([ + 'enabled' => $user->enabled, + 'uploaded_logo' => $user->logo, + ]); // + + $this->dispatch(new UpdateUser($user, $this->getUpdateRequest($request))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function disable($request) + { + $users = $this->getSelectedRecords($request, 'contact'); + + foreach ($users as $user) { + try { + $this->dispatch(new UpdateUser($user, $request->merge(['enabled' => 0]))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function destroy($request) + { + $users = $this->getSelectedRecords($request); + + foreach ($users as $user) { + try { + $this->dispatch(new DeleteUser($user)); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } +} diff --git a/app/BulkActions/Banking/Accounts.php b/app/BulkActions/Banking/Accounts.php new file mode 100644 index 00000000..37196bb7 --- /dev/null +++ b/app/BulkActions/Banking/Accounts.php @@ -0,0 +1,100 @@ + 'banking', + 'type' => 'accounts', + ]; + + public $actions = [ + 'edit' => [ + 'icon' => 'edit', + 'name' => 'general.edit', + 'message' => '', + 'permission' => 'update-banking-accounts', + 'type' => 'modal', + 'handle' => 'update', + ], + 'enable' => [ + 'icon' => 'check_circle', + 'name' => 'general.enable', + 'message' => 'bulk_actions.message.enable', + 'permission' => 'update-banking-accounts', + ], + 'disable' => [ + 'icon' => 'hide_source', + 'name' => 'general.disable', + 'message' => 'bulk_actions.message.disable', + 'permission' => 'update-banking-accounts', + ], + 'delete' => [ + 'icon' => 'delete', + 'name' => 'general.delete', + 'message' => 'bulk_actions.message.delete', + 'permission' => 'delete-banking-accounts', + ], + ]; + + public function edit($request) + { + $selected = $this->getSelectedInput($request); + + return $this->response('bulk-actions.banking.accounts.edit', compact('selected')); + } + + public function update($request) + { + $accounts = $this->getSelectedRecords($request); + + foreach ($accounts as $account) { + try { + $request->merge([ + 'enabled' => $account->enabled, + 'currency_code' => ($request->get('currency_code')) ?? $account->currency_code, + ]); // for update job authorize.. + + $this->dispatch(new UpdateAccount($account, $this->getUpdateRequest($request))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function disable($request) + { + $accounts = $this->getSelectedRecords($request); + + foreach ($accounts as $account) { + try { + $this->dispatch(new UpdateAccount($account, $request->merge(['enabled' => 0]))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function destroy($request) + { + $accounts = $this->getSelectedRecords($request); + + foreach ($accounts as $account) { + try { + $this->dispatch(new DeleteAccount($account)); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } +} diff --git a/app/BulkActions/Banking/Reconciliations.php b/app/BulkActions/Banking/Reconciliations.php new file mode 100644 index 00000000..ad1e70c2 --- /dev/null +++ b/app/BulkActions/Banking/Reconciliations.php @@ -0,0 +1,96 @@ + 'banking', + 'type' => 'reconciliations', + ]; + + public $actions = [ + 'reconcile' => [ + 'icon' => 'published_with_changes', + 'name' => 'reconciliations.reconcile', + 'message' => 'bulk_actions.message.reconcile', + 'permission' => 'update-banking-reconciliations', + ], + 'unreconcile' => [ + 'icon' => 'layers_clear', + 'name' => 'reconciliations.unreconcile', + 'message' => 'bulk_actions.message.unreconcile', + 'permission' => 'update-banking-reconciliations', + ], + 'delete' => [ + 'icon' => 'delete', + 'name' => 'general.delete', + 'message' => 'bulk_actions.message.delete', + 'permission' => 'delete-banking-reconciliations', + ], + ]; + + public function reconcile($request) + { + $reconciliations = $this->getSelectedRecords($request); + + foreach ($reconciliations as $reconciliation) { + \DB::transaction(function () use ($reconciliation) { + $reconciliation->reconciled = 1; + $reconciliation->save(); + + Transaction::where('account_id', $reconciliation->account_id) + ->isNotReconciled() + ->whereBetween('paid_at', [$reconciliation->started_at, $reconciliation->ended_at])->each(function ($item) { + $item->reconciled = 1; + $item->save(); + }); + }); + } + } + + public function unreconcile($request) + { + $reconciliations = $this->getSelectedRecords($request); + + foreach ($reconciliations as $reconciliation) { + \DB::transaction(function () use ($reconciliation) { + $reconciliation->reconciled = 0; + $reconciliation->save(); + + Transaction::where('account_id', $reconciliation->account_id) + ->isReconciled() + ->whereBetween('paid_at', [$reconciliation->started_at, $reconciliation->ended_at])->each(function ($item) { + $item->reconciled = 0; + $item->save(); + }); + }); + } + } + + public function destroy($request) + { + $reconciliations = $this->getSelectedRecords($request); + + foreach ($reconciliations as $reconciliation) { + \DB::transaction(function () use ($reconciliation) { + $reconciliation->delete(); + + Transaction::where('account_id', $reconciliation->account_id) + ->isReconciled() + ->whereBetween('paid_at', [$reconciliation->started_at, $reconciliation->ended_at])->each(function ($item) { + $item->reconciled = 0; + $item->save(); + }); + }); + } + } +} diff --git a/app/BulkActions/Banking/Transactions.php b/app/BulkActions/Banking/Transactions.php new file mode 100644 index 00000000..fc270e26 --- /dev/null +++ b/app/BulkActions/Banking/Transactions.php @@ -0,0 +1,122 @@ + 'banking', + 'type' => 'transactions', + ]; + + public $actions = [ + 'edit' => [ + 'icon' => 'edit', + 'name' => 'general.edit', + 'message' => '', + 'permission' => 'update-banking-transactions', + 'type' => 'modal', + 'handle' => 'update', + ], + 'delete' => [ + 'icon' => 'delete', + 'name' => 'general.delete', + 'message' => 'bulk_actions.message.delete', + 'permission' => 'delete-banking-transactions', + ], + 'export' => [ + 'icon' => 'file_download', + 'name' => 'general.export', + 'message' => 'bulk_actions.message.export', + 'type' => 'download', + ], + ]; + + public function edit($request) + { + $selected = $this->getSelectedInput($request); + + $real_type = 'income'; + $contact_type = 'customer'; + $account_currency_code = 'USD'; + + $types = []; + $transactions = $this->getSelectedRecords($request); + + foreach ($transactions as $transaction) { + $r_type = $this->getRealTypeTransaction($transaction->type); + + if (in_array($r_type, $types)) { + continue; + } + + $types[] = $r_type; + + $real_type = $r_type; + + $contact_type = $transaction->contact->type; + + if (! $contact_type) { + if ($real_type == Transaction::INCOME_TYPE + || $real_type == Transaction::INCOME_TRANSFER_TYPE + || $real_type == Transaction::INCOME_SPLIT_TYPE + || $real_type == Transaction::INCOME_RECURRING_TYPE + ) { + $contact_type = 'customer'; + } else { + $contact_type = 'vendor'; + } + } + + $account_currency_code = $transaction->account->currency_code; + } + + $category_and_contact = count($types) > 1 ? false : true; + + return $this->response('bulk-actions.banking.transactions.edit', compact('selected', 'category_and_contact', 'real_type', 'contact_type', 'account_currency_code')); + } + + public function update($request) + { + $transactions = $this->getSelectedRecords($request); + + foreach ($transactions as $transaction) { + try { + $request->merge([ + 'type' => $transaction->type, + 'uploaded_attachment' => $transaction->attachment, + ])->except([ + + ]); + + $this->dispatch(new UpdateTransaction($transaction, $this->getUpdateRequest($request))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function destroy($request) + { + $this->deleteTransactions($request); + } + + public function export($request) + { + $selected = $this->getSelectedInput($request); + + return $this->exportExcel(new Export($selected), trans_choice('general.transactions', 2)); + } +} diff --git a/app/BulkActions/Banking/Transfers.php b/app/BulkActions/Banking/Transfers.php new file mode 100644 index 00000000..8c8f912e --- /dev/null +++ b/app/BulkActions/Banking/Transfers.php @@ -0,0 +1,57 @@ + 'banking', + 'type' => 'transfers', + ]; + + public $actions = [ + 'delete' => [ + 'icon' => 'delete', + 'name' => 'general.delete', + 'message' => 'bulk_actions.message.delete', + 'permission' => 'delete-banking-transfers', + ], + 'export' => [ + 'icon' => 'file_download', + 'name' => 'general.export', + 'message' => 'bulk_actions.message.export', + 'type' => 'download', + ], + ]; + + public function destroy($request) + { + $transfers = $this->getSelectedRecords($request, [ + 'expense_transaction', 'income_transaction' + ]); + + foreach ($transfers as $transfer) { + try { + $this->dispatch(new DeleteTransfer($transfer)); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function export($request) + { + $selected = $this->getSelectedInput($request); + + return $this->exportExcel(new Export($selected), trans_choice('general.transfers', 2)); + } +} diff --git a/app/BulkActions/Common/Companies.php b/app/BulkActions/Common/Companies.php new file mode 100644 index 00000000..c895cdee --- /dev/null +++ b/app/BulkActions/Common/Companies.php @@ -0,0 +1,122 @@ + 'common', + 'type' => 'companies', + ]; + + public $actions = [ + 'edit' => [ + 'icon' => 'edit', + 'name' => 'general.edit', + 'message' => '', + 'permission' => 'update-common-companies', + 'type' => 'modal', + 'handle' => 'update', + ], + 'enable' => [ + 'icon' => 'check_circle', + 'name' => 'general.enable', + 'message' => 'bulk_actions.message.enable', + 'permission' => 'update-common-companies', + ], + 'disable' => [ + 'icon' => 'hide_source', + 'name' => 'general.disable', + 'message' => 'bulk_actions.message.disable', + 'permission' => 'update-common-companies', + ], + ]; + + public function edit($request) + { + $selected = $this->getSelectedInput($request); + + $money_currencies = MoneyCurrency::getCurrencies(); + + $currencies = []; + + foreach ($money_currencies as $key => $item) { + $currencies[$key] = $key . ' - ' . $item['name']; + } + + return $this->response('bulk-actions.common.companies.edit', compact('selected', 'currencies')); + } + + public function update($request) + { + $companies = $this->getSelectedRecords($request); + + foreach ($companies as $company) { + try { + if ($this->isNotUserCompany($company->id)) { + continue; + } + + $request->merge([ + 'enabled' => $company->enabled, + ]); // for update job authorize.. + + $this->dispatch(new UpdateCompany($company, $this->getUpdateRequest($request), company_id())); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function enable($request) + { + $companies = $this->getSelectedRecords($request); + + foreach ($companies as $company) { + try { + $this->dispatch(new UpdateCompany($company, $request->merge(['enabled' => 1]))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function disable($request) + { + $companies = $this->getSelectedRecords($request); + + foreach ($companies as $company) { + try { + $this->dispatch(new UpdateCompany($company, $request->merge(['enabled' => 0]))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function destroy($request) + { + $companies = $this->getSelectedRecords($request); + + foreach ($companies as $company) { + try { + $this->dispatch(new DeleteCompany($company)); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } +} diff --git a/app/BulkActions/Common/Dashboards.php b/app/BulkActions/Common/Dashboards.php new file mode 100644 index 00000000..e0644d05 --- /dev/null +++ b/app/BulkActions/Common/Dashboards.php @@ -0,0 +1,67 @@ + 'common', + 'type' => 'dashboards', + ]; + + public $actions = [ + 'enable' => [ + 'icon' => 'check_circle', + 'name' => 'general.enable', + 'message' => 'bulk_actions.message.enable', + 'permission' => 'update-common-dashboards', + ], + 'disable' => [ + 'icon' => 'hide_source', + 'name' => 'general.disable', + 'message' => 'bulk_actions.message.disable', + 'permission' => 'update-common-dashboards', + ], + 'delete' => [ + 'icon' => 'delete', + 'name' => 'general.delete', + 'message' => 'bulk_actions.message.delete', + 'permission' => 'delete-common-dashboards', + ], + ]; + + public function disable($request) + { + $dashboards = $this->getSelectedRecords($request); + + foreach ($dashboards as $dashboard) { + try { + $this->dispatch(new UpdateDashboard($dashboard, $request->merge(['enabled' => 0]))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function destroy($request) + { + $dashboards = $this->getSelectedRecords($request); + + foreach ($dashboards as $dashboard) { + try { + $this->dispatch(new DeleteDashboard($dashboard)); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } +} diff --git a/app/BulkActions/Common/Items.php b/app/BulkActions/Common/Items.php new file mode 100644 index 00000000..3a88d404 --- /dev/null +++ b/app/BulkActions/Common/Items.php @@ -0,0 +1,100 @@ + 'common', + 'type' => 'items', + ]; + + public $actions = [ + 'edit' => [ + 'icon' => 'edit', + 'name' => 'general.edit', + 'message' => '', + 'permission' => 'update-common-items', + 'type' => 'modal', + 'handle' => 'update', + ], + 'enable' => [ + 'icon' => 'check_circle', + 'name' => 'general.enable', + 'message' => 'bulk_actions.message.enable', + 'path' => ['group' => 'common', 'type' => 'items'], + 'type' => '*', + 'permission' => 'update-common-items', + ], + 'disable' => [ + 'icon' => 'hide_source', + 'name' => 'general.disable', + 'message' => 'bulk_actions.message.disable', + 'path' => ['group' => 'common', 'type' => 'items'], + 'type' => '*', + 'permission' => 'update-common-items', + ], + 'delete' => [ + 'icon' => 'delete', + 'name' => 'general.delete', + 'message' => 'bulk_actions.message.delete', + 'permission' => 'delete-common-items', + ], + 'export' => [ + 'icon' => 'file_download', + 'name' => 'general.export', + 'message' => 'bulk_actions.message.export', + 'type' => 'download', + ], + ]; + + public function edit($request) + { + $selected = $this->getSelectedInput($request); + + return $this->response('bulk-actions.common.items.edit', compact('selected')); + } + + public function update($request) + { + $items = $this->getSelectedRecords($request, 'taxes'); + + foreach ($items as $item) { + try { + $this->dispatch(new UpdateItem($item, $this->getUpdateRequest($request))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function destroy($request) + { + $items = $this->getSelectedRecords($request, 'taxes'); + + foreach ($items as $item) { + try { + $this->dispatch(new DeleteItem($item)); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function export($request) + { + $selected = $this->getSelectedInput($request); + + return $this->exportExcel(new Export($selected), trans_choice('general.items', 2)); + } +} diff --git a/app/BulkActions/Purchases/Bills.php b/app/BulkActions/Purchases/Bills.php new file mode 100644 index 00000000..ca04533a --- /dev/null +++ b/app/BulkActions/Purchases/Bills.php @@ -0,0 +1,156 @@ + 'purchases', + 'type' => 'bills', + ]; + + public $actions = [ + 'edit' => [ + 'icon' => 'edit', + 'name' => 'general.edit', + 'message' => '', + 'permission' => 'update-purchases-bills', + 'type' => 'modal', + 'handle' => 'update', + ], + 'received' => [ + 'icon' => 'send', + 'name' => 'bills.mark_received', + 'message' => 'bulk_actions.message.received', + 'permission' => 'update-purchases-bills', + ], + 'cancelled' => [ + 'icon' => 'cancel', + 'name' => 'documents.actions.cancel', + 'message' => 'bulk_actions.message.cancelled', + 'permission' => 'update-purchases-bills', + ], + 'delete' => [ + 'icon' => 'delete', + 'name' => 'general.delete', + 'message' => 'bulk_actions.message.delete', + 'permission' => 'delete-purchases-bills', + ], + 'export' => [ + 'icon' => 'file_download', + 'name' => 'general.export', + 'message' => 'bulk_actions.message.export', + 'type' => 'download', + ], + ]; + + public function edit($request) + { + $selected = $this->getSelectedInput($request); + + return $this->response('bulk-actions.purchases.bills.edit', compact('selected')); + } + + public function update($request) + { + $bills = $this->getSelectedRecords($request); + + foreach ($bills as $bill) { + try { + $discount = $bill->totals->where('code', 'discount')->makeHidden('title')->pluck('amount')->first(); + + // for extra total rows.. + $totals = $bill->totals()->whereNotIn('code', ['sub_total', 'total', 'tax', 'discount'])->get()->toArray(); + + $request->merge([ + 'items' => $bill->items->toArray(), + 'uploaded_attachment' => $bill->attachment, + 'category_id' => ($request->get('category_id')) ?? $bill->category_id, + 'discount' => $discount, + 'totals' => $totals, + ])->except([ + + ]); + + $this->dispatch(new UpdateDocument($bill, $this->getUpdateRequest($request))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function received($request) + { + $bills = $this->getSelectedRecords($request); + + foreach ($bills as $bill) { + if ($bill->status == 'received') { + continue; + } + + event(new DocumentReceived($bill)); + } + } + + public function cancelled($request) + { + $bills = $this->getSelectedRecords($request); + + foreach ($bills as $bill) { + if (in_array($bill->status, ['cancelled', 'draft'])) { + continue; + } + + event(new DocumentCancelled($bill)); + } + } + + public function duplicate($request) + { + $bills = $this->getSelectedRecords($request); + + foreach ($bills as $bill) { + $clone = $bill->duplicate(); + + $description = trans('messages.success.added', ['type' => $clone->document_number]); + + $this->dispatch(new CreateDocumentHistory($clone, 0, $description)); + } + } + + public function destroy($request) + { + $bills = $this->getSelectedRecords($request, [ + 'items', 'item_taxes', 'histories', 'transactions', 'recurring', 'totals' + ]); + + foreach ($bills as $bill) { + try { + $this->dispatch(new DeleteDocument($bill)); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function export($request) + { + $selected = $this->getSelectedInput($request); + + return $this->exportExcel(new Export($selected), trans_choice('general.bills', 2)); + } +} diff --git a/app/BulkActions/Purchases/Vendors.php b/app/BulkActions/Purchases/Vendors.php new file mode 100644 index 00000000..b892ac20 --- /dev/null +++ b/app/BulkActions/Purchases/Vendors.php @@ -0,0 +1,97 @@ + 'purchases', + 'type' => 'vendors', + ]; + + public $actions = [ + 'edit' => [ + 'icon' => 'edit', + 'name' => 'general.edit', + 'message' => '', + 'permission' => 'update-purchases-vendors', + 'type' => 'modal', + 'handle' => 'update', + ], + 'enable' => [ + 'icon' => 'check_circle', + 'name' => 'general.enable', + 'message' => 'bulk_actions.message.enable', + 'permission' => 'update-purchases-vendors', + ], + 'disable' => [ + 'icon' => 'hide_source', + 'name' => 'general.disable', + 'message' => 'bulk_actions.message.disable', + 'permission' => 'update-purchases-vendors', + ], + 'delete' => [ + 'icon' => 'delete', + 'name' => 'general.delete', + 'message' => 'bulk_actions.message.delete', + 'permission' => 'delete-purchases-vendors', + ], + 'export' => [ + 'icon' => 'file_download', + 'name' => 'general.export', + 'message' => 'bulk_actions.message.export', + 'type' => 'download', + ], + ]; + + public function edit($request) + { + $selected = $this->getSelectedInput($request); + + return $this->response('bulk-actions.purchases.vendors.edit', compact('selected')); + } + + public function update($request) + { + $vendors = $this->getSelectedRecords($request); + + foreach ($vendors as $vendor) { + try { + $request->merge([ + 'enabled' => $vendor->enabled, + 'uploaded_logo' => $vendor->logo, + ]); // for update job authorize.. + + $this->dispatch(new UpdateContact($vendor, $this->getUpdateRequest($request))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function disable($request) + { + $this->disableContacts($request); + } + + public function destroy($request) + { + $this->deleteContacts($request); + } + + public function export($request) + { + $selected = $this->getSelectedInput($request); + + return $this->exportExcel(new Export($selected), trans_choice('general.vendors', 2)); + } +} diff --git a/app/BulkActions/Sales/Customers.php b/app/BulkActions/Sales/Customers.php new file mode 100644 index 00000000..56da1056 --- /dev/null +++ b/app/BulkActions/Sales/Customers.php @@ -0,0 +1,97 @@ + 'sales', + 'type' => 'customers', + ]; + + public $actions = [ + 'edit' => [ + 'icon' => 'edit', + 'name' => 'general.edit', + 'message' => '', + 'permission' => 'update-sales-customers', + 'type' => 'modal', + 'handle' => 'update', + ], + 'enable' => [ + 'icon' => 'check_circle', + 'name' => 'general.enable', + 'message' => 'bulk_actions.message.enable', + 'permission' => 'update-sales-customers', + ], + 'disable' => [ + 'icon' => 'hide_source', + 'name' => 'general.disable', + 'message' => 'bulk_actions.message.disable', + 'permission' => 'update-sales-customers', + ], + 'delete' => [ + 'icon' => 'delete', + 'name' => 'general.delete', + 'message' => 'bulk_actions.message.delete', + 'permission' => 'delete-sales-customers', + ], + 'export' => [ + 'icon' => 'file_download', + 'name' => 'general.export', + 'message' => 'bulk_actions.message.export', + 'type' => 'download', + ], + ]; + + public function edit($request) + { + $selected = $this->getSelectedInput($request); + + return $this->response('bulk-actions.sales.customers.edit', compact('selected')); + } + + public function update($request) + { + $customers = $this->getSelectedRecords($request); + + foreach ($customers as $customer) { + try { + $request->merge([ + 'enabled' => $customer->enabled, + 'uploaded_logo' => $customer->logo, + ]); // for update job authorize.. + + $this->dispatch(new UpdateContact($customer, $this->getUpdateRequest($request))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function disable($request) + { + $this->disableContacts($request); + } + + public function destroy($request) + { + $this->deleteContacts($request); + } + + public function export($request) + { + $selected = $this->getSelectedInput($request); + + return $this->exportExcel(new Export($selected), trans_choice('general.customers', 2)); + } +} diff --git a/app/BulkActions/Sales/Invoices.php b/app/BulkActions/Sales/Invoices.php new file mode 100644 index 00000000..512af538 --- /dev/null +++ b/app/BulkActions/Sales/Invoices.php @@ -0,0 +1,172 @@ + 'sales', + 'type' => 'invoices', + ]; + + public $actions = [ + 'edit' => [ + 'icon' => 'edit', + 'name' => 'general.edit', + 'message' => '', + 'permission' => 'update-sales-invoices', + 'type' => 'modal', + 'handle' => 'update', + ], + 'sent' => [ + 'icon' => 'send', + 'name' => 'invoices.mark_sent', + 'message' => 'bulk_actions.message.sent', + 'permission' => 'update-sales-invoices', + ], + 'cancelled' => [ + 'icon' => 'cancel', + 'name' => 'documents.actions.cancel', + 'message' => 'bulk_actions.message.cancelled', + 'permission' => 'update-sales-invoices', + ], + 'delete' => [ + 'icon' => 'delete', + 'name' => 'general.delete', + 'message' => 'bulk_actions.message.delete', + 'permission' => 'delete-sales-invoices', + ], + 'export' => [ + 'icon' => 'file_download', + 'name' => 'general.export', + 'message' => 'bulk_actions.message.export', + 'type' => 'download', + ], + 'download' => [ + 'icon' => 'download', + 'name' => 'general.download', + 'message' => 'bulk_actions.message.download', + 'type' => 'download', + ], + ]; + + public function edit($request) + { + $selected = $this->getSelectedInput($request); + + return $this->response('bulk-actions.sales.invoices.edit', compact('selected')); + } + + public function update($request) + { + $invoices = $this->getSelectedRecords($request); + + foreach ($invoices as $invoice) { + try { + $discount = $invoice->totals->where('code', 'discount')->makeHidden('title')->pluck('amount')->first(); + + // for extra total rows.. + $totals = $invoice->totals()->whereNotIn('code', ['sub_total', 'total', 'tax', 'discount'])->get()->toArray(); + + $request->merge([ + 'items' => $invoice->items->toArray(), + 'uploaded_attachment' => $invoice->attachment, + 'category_id' => ($request->get('category_id')) ?? $invoice->category_id, + 'discount' => $discount, + 'totals' => $totals, + ])->except([ + + ]); + + $this->dispatch(new UpdateDocument($invoice, $this->getUpdateRequest($request))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function sent($request) + { + $invoices = $this->getSelectedRecords($request); + + foreach ($invoices as $invoice) { + if ($invoice->status == 'sent') { + continue; + } + + event(new DocumentMarkedSent($invoice)); + } + } + + public function cancelled($request) + { + $invoices = $this->getSelectedRecords($request); + + foreach ($invoices as $invoice) { + if (in_array($invoice->status, ['cancelled', 'draft'])) { + continue; + } + + event(new DocumentCancelled($invoice)); + } + } + + public function duplicate($request) + { + $invoices = $this->getSelectedRecords($request); + + foreach ($invoices as $invoice) { + $clone = $invoice->duplicate(); + + event(new DocumentCreated($clone, $request)); + } + } + + public function destroy($request) + { + $invoices = $this->getSelectedRecords($request, [ + 'items', 'item_taxes', 'histories', 'transactions', 'recurring', 'totals' + ]); + + foreach ($invoices as $invoice) { + try { + $this->dispatch(new DeleteDocument($invoice)); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function export($request) + { + $selected = $this->getSelectedInput($request); + + return $this->exportExcel(new Export($selected), trans_choice('general.invoices', 2)); + } + + public function download($request) + { + $selected = $this->getSelectedRecords($request); + + $file_name = Document::INVOICE_TYPE . '-'. date('Y-m-d-H-i-s'); + + $class = '\App\Jobs\Document\DownloadDocument'; + + return $this->downloadPdf($selected, $class, $file_name, trans_choice('general.invoices', 2)); + } +} + \ No newline at end of file diff --git a/app/BulkActions/Settings/Categories.php b/app/BulkActions/Settings/Categories.php new file mode 100644 index 00000000..df34e14a --- /dev/null +++ b/app/BulkActions/Settings/Categories.php @@ -0,0 +1,117 @@ + 'settings', + 'type' => 'categories', + ]; + + public $actions = [ + 'edit' => [ + 'icon' => 'edit', + 'name' => 'general.edit', + 'message' => '', + 'permission' => 'update-settings-categories', + 'type' => 'modal', + 'handle' => 'update', + ], + 'enable' => [ + 'icon' => 'check_circle', + 'name' => 'general.enable', + 'message' => 'bulk_actions.message.enable', + 'permission' => 'update-settings-categories', + ], + 'disable' => [ + 'icon' => 'hide_source', + 'name' => 'general.disable', + 'message' => 'bulk_actions.message.disable', + 'permission' => 'update-settings-categories', + ], + 'delete' => [ + 'icon' => 'delete', + 'name' => 'general.delete', + 'message' => 'bulk_actions.message.delete', + 'permission' => 'delete-settings-categories', + ], + ]; + + public function edit($request) + { + $selected = $this->getSelectedInput($request); + + $types = $this->getCategoryTypes(); + + return $this->response('bulk-actions.settings.categories.edit', compact('selected', 'types')); + } + + public function update($request) + { + $categories = $this->getSelectedRecords($request); + + foreach ($categories as $category) { + try { + $request->merge([ + 'enabled' => $category->enabled, + ]); // for update job authorize.. + + $this->dispatch(new UpdateCategory($category, $this->getUpdateRequest($request))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function disable($request) + { + $categories = $this->getSelectedRecords($request); + + foreach ($categories as $category) { + try { + $this->dispatch(new UpdateCategory($category, $request->merge(['enabled' => 0]))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function destroy($request) + { + $categories = $this->getSelectedRecords($request); + + foreach ($categories as $category) { + try { + $this->dispatch(new DeleteCategory($category)); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function getSelectedRecords($request, $relationships = null) + { + if (empty($relationships)) { + $model = $this->model::query(); + } else { + $relationships = Arr::wrap($relationships); + + $model = $this->model::with($relationships); + } + + return $model->getWithoutChildren()->find($this->getSelectedInput($request)); + } +} diff --git a/app/BulkActions/Settings/Currencies.php b/app/BulkActions/Settings/Currencies.php new file mode 100644 index 00000000..6b5f51aa --- /dev/null +++ b/app/BulkActions/Settings/Currencies.php @@ -0,0 +1,108 @@ + 'settings', + 'type' => 'currencies', + ]; + + public $actions = [ + 'edit' => [ + 'icon' => 'edit', + 'name' => 'general.edit', + 'message' => '', + 'permission' => 'update-settings-currencies', + 'type' => 'modal', + 'handle' => 'update', + ], + 'enable' => [ + 'icon' => 'check_circle', + 'name' => 'general.enable', + 'message' => 'bulk_actions.message.enable', + 'permission' => 'update-settings-currencies', + ], + 'disable' => [ + 'icon' => 'hide_source', + 'name' => 'general.disable', + 'message' => 'bulk_actions.message.disable', + 'permission' => 'update-settings-currencies', + ], + 'delete' => [ + 'icon' => 'delete', + 'name' => 'general.delete', + 'message' => 'bulk_actions.message.delete', + 'permission' => 'delete-settings-currencies', + ], + ]; + + public function edit($request) + { + $selected = $this->getSelectedInput($request); + + $precisions = (object) [ + '0' => '0', + '1' => '1', + '2' => '2', + '3' => '3', + '4' => '4', + ]; + + return $this->response('bulk-actions.settings.currencies.edit', compact('selected', 'precisions')); + } + + public function update($request) + { + $currencies = $this->getSelectedRecords($request); + + foreach ($currencies as $currency) { + try { + $request->merge([ + 'code' => $currency->code, + 'enabled' => $currency->enabled, + ]); // for update job authorize.. + + $this->dispatch(new UpdateCurrency($currency, $this->getUpdateRequest($request))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function disable($request) + { + $currencies = $this->getSelectedRecords($request); + + foreach ($currencies as $currency) { + try { + $this->dispatch(new UpdateCurrency($currency, $request->merge(['enabled' => 0]))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function destroy($request) + { + $currencies = $this->getSelectedRecords($request); + + foreach ($currencies as $currency) { + try { + $this->dispatch(new DeleteCurrency($currency)); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } +} diff --git a/app/BulkActions/Settings/Taxes.php b/app/BulkActions/Settings/Taxes.php new file mode 100644 index 00000000..e7663bea --- /dev/null +++ b/app/BulkActions/Settings/Taxes.php @@ -0,0 +1,109 @@ + 'settings', + 'type' => 'taxes', + ]; + + public $actions = [ + 'edit' => [ + 'icon' => 'edit', + 'name' => 'general.edit', + 'message' => '', + 'permission' => 'update-settings-taxes', + 'type' => 'modal', + 'handle' => 'update', + ], + 'enable' => [ + 'icon' => 'check_circle', + 'name' => 'general.enable', + 'message' => 'bulk_actions.message.enable', + 'permission' => 'update-settings-taxes', + ], + 'disable' => [ + 'icon' => 'hide_source', + 'name' => 'general.disable', + 'message' => 'bulk_actions.message.disable', + 'permission' => 'update-settings-taxes', + ], + 'delete' => [ + 'icon' => 'delete', + 'name' => 'general.delete', + 'message' => 'bulk_actions.message.delete', + 'permission' => 'delete-settings-taxes', + ], + ]; + + public function edit($request) + { + $selected = $this->getSelectedInput($request); + + $types = [ + 'fixed' => trans('taxes.fixed'), + 'normal' => trans('taxes.normal'), + 'inclusive' => trans('taxes.inclusive'), + 'withholding' => trans('taxes.withholding'), + 'compound' => trans('taxes.compound'), + ]; + + $disable_options = []; + + if ($compound = Tax::compound()->first()) { + $disable_options = ['compound']; + } + + return $this->response('bulk-actions.settings.taxes.edit', compact('selected', 'types', 'disable_options')); + } + + public function update($request) + { + $taxes = $this->getSelectedRecords($request); + + foreach ($taxes as $tax) { + try { + $this->dispatch(new UpdateTax($tax, $this->getUpdateRequest($request))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function disable($request) + { + $taxes = $this->getSelectedRecords($request); + + foreach ($taxes as $tax) { + try { + $this->dispatch(new UpdateTax($tax, $request->merge(['enabled' => 0]))); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } + + public function destroy($request) + { + $taxes = $this->getSelectedRecords($request); + + foreach ($taxes as $tax) { + try { + $this->dispatch(new DeleteTax($tax)); + } catch (\Exception $e) { + flash($e->getMessage())->error()->important(); + } + } + } +} diff --git a/app/Classifiers/BulkAction.php b/app/Classifiers/BulkAction.php new file mode 100644 index 00000000..2c945539 --- /dev/null +++ b/app/Classifiers/BulkAction.php @@ -0,0 +1,29 @@ +isSubclassOf(\App\Abstracts\BulkAction::class); + } + + public function countsTowardsApplicationCode(): bool + { + return true; + } + + public function countsTowardsTests(): bool + { + return false; + } +} diff --git a/app/Classifiers/Event.php b/app/Classifiers/Event.php new file mode 100644 index 00000000..bfbd5b0e --- /dev/null +++ b/app/Classifiers/Event.php @@ -0,0 +1,29 @@ +isSubclassOf(\App\Abstracts\Event::class); + } + + public function countsTowardsApplicationCode(): bool + { + return true; + } + + public function countsTowardsTests(): bool + { + return false; + } +} diff --git a/app/Classifiers/Export.php b/app/Classifiers/Export.php new file mode 100644 index 00000000..cba1248e --- /dev/null +++ b/app/Classifiers/Export.php @@ -0,0 +1,29 @@ +isSubclassOf(\App\Abstracts\Export::class); + } + + public function countsTowardsApplicationCode(): bool + { + return true; + } + + public function countsTowardsTests(): bool + { + return false; + } +} diff --git a/app/Classifiers/Import.php b/app/Classifiers/Import.php new file mode 100644 index 00000000..cabf8541 --- /dev/null +++ b/app/Classifiers/Import.php @@ -0,0 +1,29 @@ +isSubclassOf(\App\Abstracts\Import::class); + } + + public function countsTowardsApplicationCode(): bool + { + return true; + } + + public function countsTowardsTests(): bool + { + return false; + } +} diff --git a/app/Classifiers/Job.php b/app/Classifiers/Job.php new file mode 100644 index 00000000..b83f4b07 --- /dev/null +++ b/app/Classifiers/Job.php @@ -0,0 +1,29 @@ +isSubclassOf(\App\Abstracts\Job::class); + } + + public function countsTowardsApplicationCode(): bool + { + return true; + } + + public function countsTowardsTests(): bool + { + return false; + } +} diff --git a/app/Classifiers/Observer.php b/app/Classifiers/Observer.php new file mode 100644 index 00000000..835e264c --- /dev/null +++ b/app/Classifiers/Observer.php @@ -0,0 +1,29 @@ +isSubclassOf(\App\Abstracts\Observer::class); + } + + public function countsTowardsApplicationCode(): bool + { + return true; + } + + public function countsTowardsTests(): bool + { + return false; + } +} diff --git a/app/Classifiers/Report.php b/app/Classifiers/Report.php new file mode 100644 index 00000000..b8ff53b7 --- /dev/null +++ b/app/Classifiers/Report.php @@ -0,0 +1,29 @@ +isSubclassOf(\App\Abstracts\Report::class); + } + + public function countsTowardsApplicationCode(): bool + { + return true; + } + + public function countsTowardsTests(): bool + { + return false; + } +} diff --git a/app/Classifiers/Scope.php b/app/Classifiers/Scope.php new file mode 100644 index 00000000..697013ab --- /dev/null +++ b/app/Classifiers/Scope.php @@ -0,0 +1,29 @@ +isSubclassOf(\Illuminate\Database\Eloquent\Scope::class); + } + + public function countsTowardsApplicationCode(): bool + { + return true; + } + + public function countsTowardsTests(): bool + { + return false; + } +} diff --git a/app/Classifiers/Widget.php b/app/Classifiers/Widget.php new file mode 100644 index 00000000..d29d04f2 --- /dev/null +++ b/app/Classifiers/Widget.php @@ -0,0 +1,29 @@ +isSubclassOf(\App\Abstracts\Widget::class); + } + + public function countsTowardsApplicationCode(): bool + { + return true; + } + + public function countsTowardsTests(): bool + { + return false; + } +} diff --git a/app/Console/Commands/BillReminder.php b/app/Console/Commands/BillReminder.php new file mode 100644 index 00000000..85c69e6e --- /dev/null +++ b/app/Console/Commands/BillReminder.php @@ -0,0 +1,99 @@ + false]); + + $today = Date::today(); + + $start_date = $today->copy()->subWeek()->toDateString() . ' 00:00:00'; + $end_date = $today->copy()->addMonth()->toDateString() . ' 23:59:59'; + + // Get all companies + $companies = Company::whereHas('bills', function (Builder $query) use ($start_date, $end_date) { + $query->allCompanies(); + $query->whereBetween('due_at', [$start_date, $end_date]); + $query->accrued(); + $query->notPaid(); + }) + ->enabled() + ->cursor(); + + foreach ($companies as $company) { + $this->info('Sending bill reminders for ' . $company->name . ' company.'); + + // Set company + $company->makeCurrent(); + + // Don't send reminders if disabled + if (! setting('schedule.send_bill_reminder')) { + $this->info('Bill reminders disabled by ' . $company->name . '.'); + + continue; + } + + $days = explode(',', setting('schedule.bill_days')); + + foreach ($days as $day) { + $day = (int) trim($day); + + $this->remind($day); + } + } + + Company::forgetCurrent(); + } + + protected function remind($day) + { + // Get due date + $date = Date::today()->addDays($day)->toDateString(); + + // Get upcoming bills + $bills = Document::with('contact')->bill()->accrued()->notPaid()->due($date)->cursor(); + + foreach ($bills as $bill) { + $this->info($bill->document_number . ' bill reminded.'); + + try { + event(new DocumentReminded($bill, Notification::class)); + } catch (\Throwable $e) { + $this->error($e->getMessage()); + + report($e); + } + } + } +} diff --git a/app/Console/Commands/CompanySeed.php b/app/Console/Commands/CompanySeed.php new file mode 100644 index 00000000..98eae12e --- /dev/null +++ b/app/Console/Commands/CompanySeed.php @@ -0,0 +1,50 @@ +input->getOption('class') ?? 'Database\Seeds\Company'; + + $class = $this->laravel->make($class_name); + + $class->setContainer($this->laravel)->setCommand($this)->__invoke(); + } + + /** + * Get the console command options. + * + * @return array + */ + protected function getOptions() + { + return [ + ['class', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder', 'Database\Seeds\Company'], + ]; + } +} diff --git a/app/Console/Commands/DownloadModule.php b/app/Console/Commands/DownloadModule.php new file mode 100644 index 00000000..dad68381 --- /dev/null +++ b/app/Console/Commands/DownloadModule.php @@ -0,0 +1,130 @@ +alias = $this->argument('alias'); + $this->company = $this->argument('company'); + + company($this->company)->makeCurrent(); + + if (!$path = $this->download()) { + return self::CMD_ERROR; + } + + if (!$this->unzip($path)) { + return self::CMD_ERROR; + } + + if (!$this->copyFiles($path)) { + return self::CMD_ERROR; + } + + $this->info("Module [{$this->alias}] downloaded!"); + + return self::CMD_SUCCESS; + } + + public function download() + { + $this->info('Downloading module...'); + + try { + $path = $this->dispatch(new DownloadFile($this->alias, $this->getVersion())); + } catch (\Exception $e) { + $this->error($e->getMessage()); + + return false; + } + + return $path; + } + + public function unzip($path) + { + $this->info('Unzipping module...'); + + try { + $this->dispatch(new UnzipFile($this->alias, $path)); + } catch (\Exception $e) { + $this->error($e->getMessage()); + + return false; + } + + return true; + } + + public function copyFiles($path) + { + $this->info('Copying module files...'); + + try { + $this->dispatch(new CopyFiles($this->alias, $path)); + + event(new \App\Events\Module\Copied($this->alias, $this->company)); + } catch (\Exception $e) { + $this->error($e->getMessage()); + + return false; + } + + return true; + } + + protected function getVersion() + { + $version = Versions::latest($this->alias); + + if (empty($version)) { + $current = '1.0.0'; + + $url = 'apps/' . $this->alias . '/version/' . $current . '/' . version('short'); + + $version = Versions::getLatestVersion($url, $current); + } + + return $version?->latest; + } +} diff --git a/app/Console/Commands/FinishUpdate.php b/app/Console/Commands/FinishUpdate.php new file mode 100644 index 00000000..93dd42e3 --- /dev/null +++ b/app/Console/Commands/FinishUpdate.php @@ -0,0 +1,68 @@ +info('Finishing update...'); + + $this->call('cache:clear'); + + $alias = $this->argument('alias'); + $company_id = $this->argument('company'); + $new = $this->argument('new'); + $old = $this->argument('old'); + + // Check if file mirror was successful + $version = ($alias == 'core') ? version('short') : module($alias)->get('version'); + if ($version != $new) { + logger($alias . ' update failed:: file version > ' . $version . ' -vs- ' . 'request version > ' . $new); + + throw new \Exception(trans('modules.errors.finish', ['module' => $alias])); + } + + $company = company($company_id); + + if (empty($company)) { + return; + } + + $company->makeCurrent(); + + // Set locale for modules + if (($alias != 'core') && !empty($company->locale)) { + app()->setLocale($company->locale); + } + + // Disable model cache during update + config(['laravel-model-caching.enabled' => false]); + + event(new UpdateFinished($alias, $new, $old)); + } +} diff --git a/app/Console/Commands/Install.php b/app/Console/Commands/Install.php new file mode 100644 index 00000000..a1d83e68 --- /dev/null +++ b/app/Console/Commands/Install.php @@ -0,0 +1,201 @@ +getMissingOptions()) && $this->option(self::OPT_NO_INTERACTION)) { + $this->line('❌ Some options are missing and --no-interaction is present. Please run the following command for more information :'); + $this->line('❌ php artisan help install'); + $this->line('❌ Missing options are : ' . implode(', ', $missing_options)); + + return self::CMD_ERROR; + } + + $this->line('Setting locale ' . $this->locale); + Session::put(self::OPT_LOCALE, $this->locale); + app()->setLocale($this->locale); + + $this->prompt(); + + // Create the .env file + Installer::createDefaultEnvFile(); + + $this->line('Creating database tables'); + if (! $this->createDatabaseTables()) { + return self::CMD_ERROR; + } + + DB::transaction(function () { + $this->line('Creating company'); + Installer::createCompany($this->company_name, $this->company_email, $this->locale); + + $this->line('Creating admin'); + Installer::createUser($this->admin_email, $this->admin_password, $this->locale); + }); + + $this->line('Applying the final touches'); + Installer::finalTouches(); + + return self::CMD_SUCCESS; + } + + /** + * Check that all options are presents. otherwise returns an array of the missing options + */ + private function getMissingOptions() + { + $missing_options = []; + + $constants = [ + 'OPT_LOCALE', + 'OPT_DB_PORT', + 'OPT_DB_HOST', + 'OPT_DB_NAME', + 'OPT_DB_USERNAME', + 'OPT_DB_PASSWORD', + 'OPT_DB_PREFIX', + 'OPT_COMPANY_NAME', + 'OPT_COMPANY_EMAIL', + 'OPT_ADMIN_EMAIL', + 'OPT_ADMIN_PASSWORD' + ]; + + $allowed_empty = ['db_password', 'db_prefix']; + + foreach ($constants as $const) { + $option = constant("self::$const"); + + $property = str_replace('-', '_', $option); + + $this->$property = $this->option($option); + + if (!empty($this->$property)) { + continue; + } + + if (in_array($property, $allowed_empty)) { + continue; + } + + $missing_options[] = $option; + } + + return $missing_options; + } + + /** + * Ask the user for data if some options are missing. + */ + private function prompt() + { + if (empty($this->db_host)) { + $this->db_host = $this->ask('What is the database host?', 'localhost'); + } + + if (empty($this->db_port)) { + $this->db_port = $this->ask('What is the database port?', '3306'); + } + + if (empty($this->db_name)) { + $this->db_name = $this->ask('What is the database name?'); + } + + if (empty($this->db_username)) { + $this->db_username = $this->ask('What is the database username?', 'root'); + } + + if (!isset($this->db_password)) { + $this->db_password = $this->secret('What is the database password?', ''); + } + + if (empty($this->company_name)) { + $this->company_name = $this->ask('What is the company name?', 'My Company'); + } + + if (empty($this->company_email)) { + $this->company_email = $this->ask('What is the company contact email?', 'my@company.com'); + } + + if (empty($this->admin_email)) { + $this->admin_email = $this->ask('What is the admin email?', $this->company_email); + } + + if (empty($this->admin_password)) { + $this->admin_password = $this->secret('What is the admin password?'); + } + } + + private function createDatabaseTables() + { + $this->db_host = $this->option(self::OPT_DB_HOST); + $this->db_port = $this->option(self::OPT_DB_PORT); + $this->db_name = $this->option(self::OPT_DB_NAME); + $this->db_username = $this->option(self::OPT_DB_USERNAME); + $this->db_password = $this->option(self::OPT_DB_PASSWORD); + $this->db_prefix = $this->option(self::OPT_DB_PREFIX); + + $this->line('Connecting to database ' . $this->db_name . '@' . $this->db_host . ':' . $this->db_port); + + if (!Installer::createDbTables($this->db_host, $this->db_port, $this->db_name, $this->db_username, $this->db_password, $this->db_prefix)) { + $this->error('Error: Could not connect to the database! Please, make sure the details are correct.'); + + return false; + } + + return true; + } +} diff --git a/app/Console/Commands/InstallRefresh.php b/app/Console/Commands/InstallRefresh.php new file mode 100644 index 00000000..5927a4ef --- /dev/null +++ b/app/Console/Commands/InstallRefresh.php @@ -0,0 +1,57 @@ +info('Resetting migrations'); + $this->callSilent('migrate:reset', [ + '--force' => true, + ]); + + $this->info('Installing Akaunting'); + $this->callSilent('install', [ + '--db-host' => env('DB_HOST'), + '--db-port' => env('DB_PORT'), + '--db-name' => env('DB_DATABASE'), + '--db-username' => env('DB_USERNAME'), + '--db-password' => env('DB_PASSWORD'), + '--db-prefix' => env('DB_PREFIX'), + '--company-name' => $company->name, + '--company-email' => $company->email, + '--admin-email' => $user->email, + '--admin-password' => $this->option('admin-password'), + '--locale' => $company->locale, + '--no-interaction' => true, + ]); + + $this->info('Installation refreshed'); + } +} diff --git a/app/Console/Commands/InvoiceReminder.php b/app/Console/Commands/InvoiceReminder.php new file mode 100644 index 00000000..283bd43f --- /dev/null +++ b/app/Console/Commands/InvoiceReminder.php @@ -0,0 +1,99 @@ + false]); + + $today = Date::today(); + + $start_date = $today->copy()->subMonth()->toDateString() . ' 00:00:00'; + $end_date = $today->copy()->addWeek()->toDateString() . ' 23:59:59'; + + // Get all companies + $companies = Company::whereHas('invoices', function (Builder $query) use ($start_date, $end_date) { + $query->allCompanies(); + $query->whereBetween('due_at', [$start_date, $end_date]); + $query->accrued(); + $query->notPaid(); + }) + ->enabled() + ->cursor(); + + foreach ($companies as $company) { + $this->info('Sending invoice reminders for ' . $company->name . ' company.'); + + // Set company + $company->makeCurrent(); + + // Don't send reminders if disabled + if (! setting('schedule.send_invoice_reminder')) { + $this->info('Invoice reminders disabled by ' . $company->name . '.'); + + continue; + } + + $days = explode(',', setting('schedule.invoice_days')); + + foreach ($days as $day) { + $day = (int) trim($day); + + $this->remind($day); + } + } + + Company::forgetCurrent(); + } + + protected function remind($day) + { + // Get due date + $date = Date::today()->subDays($day)->toDateString(); + + // Get upcoming invoices + $invoices = Document::with('contact')->invoice()->accrued()->notPaid()->due($date)->cursor(); + + foreach ($invoices as $invoice) { + $this->info($invoice->document_number . ' invoice reminded.'); + + try { + event(new DocumentReminded($invoice, Notification::class)); + } catch (\Throwable $e) { + $this->error($e->getMessage()); + + report($e); + } + } + } +} diff --git a/app/Console/Commands/RecurringCheck.php b/app/Console/Commands/RecurringCheck.php new file mode 100644 index 00000000..1e8e56c1 --- /dev/null +++ b/app/Console/Commands/RecurringCheck.php @@ -0,0 +1,284 @@ +info('Checking for recurring...'); + + // Bind to container + app()->instance(static::class, $this); + + // Disable model cache + config(['laravel-model-caching.enabled' => false]); + + // Get all recurring + $recurring = Recurring::with('company') + /*->whereHas('recurable', function (Builder $query) { + $query->allCompanies(); + })*/ + ->active() + ->allCompanies() + ->cursor(); + + //$this->info('Total recurring: ' . $recurring->count()); + + foreach ($recurring as $recur) { + if (empty($recur->company)) { + $this->info('Missing company.'); + + $recur->delete(); + + continue; + } + + $this->info('Recurring ID: ' . $recur->id); + + $company_name = !empty($recur->company->name) ? $recur->company->name : 'Missing Company Name : ' . $recur->company->id; + + $template = $recur->recurable()->where('company_id', $recur->company_id)->first(); + + // Check if company is disabled + if (! $recur->company->enabled) { + $this->info($company_name . ' company is disabled. Skipping...'); + + if (Date::parse($recur->company->updated_at)->format('Y-m-d') > Date::now()->subMonth(3)->format('Y-m-d')) { + $recur->delete(); + + if ($template) { + $template->delete(); + } + } + + continue; + } + + // Check if company has any active user + $has_active_users = false; + + foreach ($recur->company->users as $company_user) { + if (Date::parse($company_user->last_logged_in_at)->format('Y-m-d') > Date::now()->subMonth(3)->format('Y-m-d')) { + $has_active_users = true; + + break; + } + } + + if (! $has_active_users) { + $this->info('No active users for ' . $company_name . ' company. Skipping...'); + + $recur->delete(); + + if ($template) { + $template->delete(); + } + + continue; + } + + company($recur->company_id)->makeCurrent(); + + if (! $template) { + $this->info('Missing model.'); + + $recur->delete(); + + continue; + } + + $this->info('Template ID: ' . $template->id); + + // Get the remaining schedules, including the previously failed ones + $schedules = $this->getRemainingSchedules($template, $recur); + + // Check if all schedules created + if ($schedules->count() == 0) { + $this->info('All schedules created.'); + + $recur->update(['status' => Recurring::COMPLETE_STATUS]); + + continue; + } + + // Don't create schedules for the future + $schedules = $schedules->endsBefore($recur->getRecurringRuleTomorrowDate()); + + if ($schedules->count() == 0) { + $this->info('No schedules for today.'); + + continue; + } + + foreach ($schedules as $schedule) { + $schedule_date = Date::parse($schedule->getStart()->format('Y-m-d')); + + $this->info('Schedule date: ' . $schedule_date->format('Y-m-d')); + + $this->recur($template, $schedule_date); + } + } + + Company::forgetCurrent(); + + // Remove from container + app()->forgetInstance(static::class); + + $this->info('Recurring check done!'); + } + + protected function recur(Document|Transaction $template, Date $schedule_date): void + { + DB::transaction(function () use ($template, $schedule_date) { + if (! $model = $this->getModel($template, $schedule_date)) { + return; + } + + $this->info('Model created: ' . $model->id); + + switch ($template::class) { + case Document::class: + event(new DocumentCreated($model, request())); + + event(new DocumentRecurring($model)); + + break; + case Transaction::class: + event(new TransactionCreated($model)); + + event(new TransactionRecurring($model)); + + break; + } + }); + } + + protected function getModel(Document|Transaction $template, Date $schedule_date): Document|Transaction + { + $function = ($template instanceof Transaction) ? 'getTransactionModel' : 'getDocumentModel'; + + try { + return $this->$function($template, $schedule_date); + } catch (\Throwable $e) { + $this->error($e->getMessage()); + + report($e); + + return false; + } + } + + protected function getDocumentModel(Document $template, Date $schedule_date): Document + { + $template->cloneable_relations = ['items', 'totals']; + + $model = $template->duplicate(); + + // Days between issued and due date + $diff_days = Date::parse($template->due_at)->diffInDays(Date::parse($template->issued_at)); + + $model->type = $this->getRealType($template->type); + $model->parent_id = $template->id; + $model->issued_at = $schedule_date->format('Y-m-d'); + $model->due_at = $schedule_date->copy()->addDays($diff_days)->format('Y-m-d'); + $model->created_from = 'core::recurring'; + $model->save(); + + $this->updateRelationTypes($model, $template->cloneable_relations); + + return $model; + } + + protected function getTransactionModel(Transaction $template, Date $schedule_date): Transaction + { + $template->cloneable_relations = ['taxes']; + + $model = $template->duplicate(); + + $model->type = $this->getRealType($template->type); + $model->parent_id = $template->id; + $model->paid_at = $schedule_date->format('Y-m-d'); + $model->created_from = 'core::recurring'; + $model->save(); + + $this->updateRelationTypes($model, $template->cloneable_relations); + + return $model; + } + + protected function getRemainingSchedules(Document|Transaction $template, Recurring $recur): RecurrenceCollection + { + $date_field = $this->getDateField($template); + + $created_schedules = DB::table($template->getTable()) + ->where('type', $this->getRealType($template->type)) + ->where('parent_id', $template->id) + ->get($date_field) + ->transform(function ($item, $key) use ($date_field) { + return Date::parse($item->$date_field)->format('Y-m-d'); + }) + ->toArray(); + + // Skip already created schedules + $schedules = $recur->getRecurringSchedule()->filter(function ($recurrence) use ($created_schedules) { + return ! in_array($recurrence->getStart()->format('Y-m-d'), $created_schedules); + }); + + return $schedules; + } + + protected function getDateField(Document|Transaction $template): string + { + return ($template instanceof Transaction) ? 'paid_at' : 'issued_at'; + } + + public function getRealType(string $recurring_type): string + { + return Str::replace('-recurring', '', $recurring_type); + } + + public function updateRelationTypes($model, $relations) + { + foreach ($relations as $relation) { + if (! method_exists($model, $relation)) { + continue; + } + + $model->$relation()->update(['type' => $model->type]); + } + } +} diff --git a/app/Console/Commands/SampleData.php b/app/Console/Commands/SampleData.php new file mode 100644 index 00000000..37e26387 --- /dev/null +++ b/app/Console/Commands/SampleData.php @@ -0,0 +1,37 @@ +laravel->make(SampleDataSeeder::class); + + $seeder = $class->setContainer($this->laravel)->setCommand($this); + + $seeder->__invoke(); + } +} diff --git a/app/Console/Commands/StorageTempClear.php b/app/Console/Commands/StorageTempClear.php new file mode 100644 index 00000000..bb296b2d --- /dev/null +++ b/app/Console/Commands/StorageTempClear.php @@ -0,0 +1,42 @@ +glob("{$path}/*") as $file) { + $filesystem->delete($file); + } + + $this->info('Temporary files cleared!'); + } +} diff --git a/app/Console/Commands/UninstallModule.php b/app/Console/Commands/UninstallModule.php new file mode 100644 index 00000000..dff9f361 --- /dev/null +++ b/app/Console/Commands/UninstallModule.php @@ -0,0 +1,54 @@ +prepare(); + + if (!$this->getModel()) { + $this->info("Module [{$this->alias}] not found."); + return; + } + + $this->changeRuntime(); + + // Delete db + $this->model->delete(); + + $this->createHistory('uninstalled'); + + event(new Uninstalled($this->alias, $this->company_id)); + + // Delete files + module($this->alias)->delete(); + + $this->revertRuntime(); + + $this->info("Module [{$this->alias}] uninstalled."); + } +} diff --git a/app/Console/Commands/Update.php b/app/Console/Commands/Update.php new file mode 100644 index 00000000..3a11730c --- /dev/null +++ b/app/Console/Commands/Update.php @@ -0,0 +1,199 @@ +alias = $this->argument('alias'); + $this->company = $this->argument('company'); + + if (false === $this->new = $this->getNewVersion()) { + $message = 'Not able to get the latest version of ' . $this->alias . '!'; + + $this->error($message); + + event(new UpdateFailed($this->alias, $this->new, $this->old, 'Version', $message)); + + return self::CMD_ERROR; + } + + $this->old = $this->getOldVersion(); + + if (version_compare($this->old, $this->new, '>=')) { + $message = 'The current version for the ' . $this->alias . ' is the latest version!'; + + $this->info($message); + + return self::CMD_SUCCESS; + } + + company($this->company)->makeCurrent(); + + if (!$path = $this->download()) { + return self::CMD_ERROR; + } + + if (!$this->unzip($path)) { + return self::CMD_ERROR; + } + + if (!$this->copyFiles($path)) { + return self::CMD_ERROR; + } + + if (!$this->finish()) { + return self::CMD_ERROR; + } + + return self::CMD_SUCCESS; + } + + public function getNewVersion() + { + return ($this->argument('new') == 'latest') ? Versions::latest($this->alias)?->latest : $this->argument('new'); + } + + public function getOldVersion() + { + if ($this->alias == 'core') { + return version('short'); + } + + if ($module = module($this->alias)) { + return $module->get('version'); + } + + return '1.0.0'; + } + + public function download() + { + $this->info('Downloading update...'); + + try { + $path = $this->dispatch(new DownloadFile($this->alias, $this->new)); + + event(new UpdateDownloaded($this->alias, $this->new, $this->old)); + } catch (\Exception $e) { + $message = $e->getMessage(); + + $this->error($message); + + event(new UpdateFailed($this->alias, $this->new, $this->old, 'Download', $message)); + + return false; + } + + return $path; + } + + public function unzip($path) + { + $this->info('Unzipping update...'); + + try { + $this->dispatch(new UnzipFile($this->alias, $path)); + + event(new UpdateUnzipped($this->alias, $this->new, $this->old)); + } catch (\Exception $e) { + $message = $e->getMessage(); + + $this->error($message); + + event(new UpdateFailed($this->alias, $this->new, $this->old, 'Unzip', $message)); + + return false; + } + + return true; + } + + public function copyFiles($path) + { + $this->info('Copying update files...'); + + try { + $this->dispatch(new CopyFiles($this->alias, $path)); + + event(new UpdateCopied($this->alias, $this->new, $this->old)); + } catch (\Exception $e) { + $message = $e->getMessage(); + + $this->error($message); + + event(new UpdateFailed($this->alias, $this->new, $this->old, 'Copy Files', $message)); + + return false; + } + + return true; + } + + public function finish() + { + $this->info('Finishing update...'); + + try { + $this->dispatch(new FinishUpdate($this->alias, $this->new, $this->old, $this->company)); + } catch (\Exception $e) { + $message = $e->getMessage(); + + $this->error($message); + + event(new UpdateFailed($this->alias, $this->new, $this->old, 'Finish', $message)); + + return false; + } + + return true; + } +} diff --git a/app/Console/Commands/UpdateAll.php b/app/Console/Commands/UpdateAll.php new file mode 100644 index 00000000..e69419be --- /dev/null +++ b/app/Console/Commands/UpdateAll.php @@ -0,0 +1,74 @@ +info('Starting update...'); + + // Update core + if ($this->runUpdate('core') !== true) { + $this->error('Not able to update core!'); + + return; + } + + // Update modules + $modules = module()->all(); + + foreach ($modules as $module) { + $alias = $module->get('alias'); + + if ($this->runUpdate($alias) !== true) { + $this->error('Not able to update ' . $alias . '!'); + } + } + + $this->info('Update finished.'); + } + + protected function runUpdate($alias) + { + $this->info('Updating ' . $alias . '...'); + + $company_id = $this->argument('company'); + + $command = "update {$alias} {$company_id}"; + + if (true !== $result = Console::run($command)) { + $message = !empty($result) ? $result : trans('modules.errors.finish', ['module' => $alias]); + + $this->error($message); + + return false; + } + + return true; + } +} diff --git a/app/Console/Commands/UpdateDb.php b/app/Console/Commands/UpdateDb.php new file mode 100644 index 00000000..3ed80531 --- /dev/null +++ b/app/Console/Commands/UpdateDb.php @@ -0,0 +1,43 @@ +argument('alias'); + $company_id = $this->argument('company'); + $new = $this->argument('new'); + $old = $this->argument('old'); + + company($company_id)->makeCurrent(); + + // Disable model cache during update + config(['laravel-model-caching.enabled' => false]); + + event(new UpdateFinished($alias, $new, $old)); + } +} diff --git a/app/Console/Commands/UserSeed.php b/app/Console/Commands/UserSeed.php new file mode 100644 index 00000000..d7e63618 --- /dev/null +++ b/app/Console/Commands/UserSeed.php @@ -0,0 +1,34 @@ +laravel->make('Database\Seeds\User'); + + $class->setContainer($this->laravel)->setCommand($this)->__invoke(); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php new file mode 100644 index 00000000..9b09ec25 --- /dev/null +++ b/app/Console/Kernel.php @@ -0,0 +1,60 @@ +command('reminder:invoice')->dailyAt($schedule_time); + $schedule->command('reminder:bill')->dailyAt($schedule_time); + $schedule->command('recurring:check')->dailyAt($schedule_time)->runInBackground(); + $schedule->command('storage-temp:clear')->dailyAt('17:00'); + $schedule->command('model:prune')->dailyAt('17:00'); + } + + /** + * Register the Closure based commands for the application. + * + * @return void + */ + protected function commands() + { + require base_path('routes/console.php'); + + $this->load(__DIR__ . '/Commands'); + } + + /** + * Get the timezone that should be used by default for scheduled events. + * + * @return \DateTimeZone|string|null + */ + protected function scheduleTimezone() + { + return config('app.timezone'); + } +} diff --git a/app/Console/Stubs/Modules/assets/js/app.stub b/app/Console/Stubs/Modules/assets/js/app.stub new file mode 100644 index 00000000..e69de29b diff --git a/app/Console/Stubs/Modules/assets/sass/app.stub b/app/Console/Stubs/Modules/assets/sass/app.stub new file mode 100644 index 00000000..e69de29b diff --git a/app/Console/Stubs/Modules/command.stub b/app/Console/Stubs/Modules/command.stub new file mode 100644 index 00000000..6dbc129e --- /dev/null +++ b/app/Console/Stubs/Modules/command.stub @@ -0,0 +1,32 @@ +response('$ALIAS$::index'); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + return view('$ALIAS$::create'); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * @return Response + */ + public function store(Request $request) + { + // + } + + /** + * Show the specified resource. + * + * @param int $id + * @return Response + */ + public function show($id) + { + return view('$ALIAS$::show'); + } + + /** + * Show the form for editing the specified resource. + * + * @param int $id + * @return Response + */ + public function edit($id) + { + return view('$ALIAS$::edit'); + } + + /** + * Update the specified resource in storage. + * + * @param Request $request + * @param int $id + * @return Response + */ + public function update(Request $request, $id) + { + // + } + + /** + * Remove the specified resource from storage. + * + * @param int $id + * @return Response + */ + public function destroy($id) + { + // + } +} diff --git a/app/Console/Stubs/Modules/event.stub b/app/Console/Stubs/Modules/event.stub new file mode 100644 index 00000000..5b1798f7 --- /dev/null +++ b/app/Console/Stubs/Modules/event.stub @@ -0,0 +1,28 @@ + '$STUDLY_NAME$', + 'description' => 'This is my awesome module', + +]; \ No newline at end of file diff --git a/app/Console/Stubs/Modules/listener-duck.stub b/app/Console/Stubs/Modules/listener-duck.stub new file mode 100644 index 00000000..5d700db5 --- /dev/null +++ b/app/Console/Stubs/Modules/listener-duck.stub @@ -0,0 +1,30 @@ +alias != $this->alias) { + return; + } + + $this->updatePermissions(); + + //$this->callSeeds(); + } + + protected function updatePermissions() + { + // c=create, r=read, u=update, d=delete + $this->attachPermissionsToAdminRoles([ + $this->alias . '-main' => 'c,r,u,d', + ]); + } + + protected function callSeeds() + { + Artisan::call('company:seed', [ + 'company' => company_id(), + '--class' => 'Modules\$STUDLY_NAME$\Database\Seeds\$STUDLY_NAME$DatabaseSeeder', + ]); + } +} diff --git a/app/Console/Stubs/Modules/mail.stub b/app/Console/Stubs/Modules/mail.stub new file mode 100644 index 00000000..7e9a47b8 --- /dev/null +++ b/app/Console/Stubs/Modules/mail.stub @@ -0,0 +1,33 @@ +view('view.name'); + } +} diff --git a/app/Console/Stubs/Modules/middleware.stub b/app/Console/Stubs/Modules/middleware.stub new file mode 100644 index 00000000..954583ed --- /dev/null +++ b/app/Console/Stubs/Modules/middleware.stub @@ -0,0 +1,21 @@ +bigIncrements('id'); +$FIELDS$ + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('$TABLE$'); + } +} diff --git a/app/Console/Stubs/Modules/migration/delete.stub b/app/Console/Stubs/Modules/migration/delete.stub new file mode 100644 index 00000000..53ec1b32 --- /dev/null +++ b/app/Console/Stubs/Modules/migration/delete.stub @@ -0,0 +1,32 @@ +bigIncrements('id'); +$FIELDS$ + $table->timestamps(); + }); + } +} diff --git a/app/Console/Stubs/Modules/migration/plain.stub b/app/Console/Stubs/Modules/migration/plain.stub new file mode 100644 index 00000000..cc014c60 --- /dev/null +++ b/app/Console/Stubs/Modules/migration/plain.stub @@ -0,0 +1,28 @@ +line('The introduction to the notification.') + ->action('Notification Action', 'https://laravel.com') + ->line('Thank you for using our application!'); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + // + ]; + } +} diff --git a/app/Console/Stubs/Modules/package.stub b/app/Console/Stubs/Modules/package.stub new file mode 100644 index 00000000..bbbc4984 --- /dev/null +++ b/app/Console/Stubs/Modules/package.stub @@ -0,0 +1,17 @@ +{ + "private": true, + "scripts": { + "dev": "npm run development", + "development": "mix", + "watch": "mix watch", + "watch-poll": "mix watch -- --watch-options-poll=1000", + "hot": "mix watch --hot", + "prod": "npm run production", + "production": "mix --production" + }, + "devDependencies": { + "cross-env": "^5.2.1", + "laravel-mix": "^6.0.39", + "resolve-url-loader": "^5.0.0" + } +} diff --git a/app/Console/Stubs/Modules/policy.plain.stub b/app/Console/Stubs/Modules/policy.plain.stub new file mode 100644 index 00000000..02e16df3 --- /dev/null +++ b/app/Console/Stubs/Modules/policy.plain.stub @@ -0,0 +1,20 @@ +name('invoices.show'); + // Route::post('invoices/{invoice}/confirm', 'Main@confirm')->name('invoices.confirm'); +}); diff --git a/app/Console/Stubs/Modules/rule.stub b/app/Console/Stubs/Modules/rule.stub new file mode 100644 index 00000000..9b716321 --- /dev/null +++ b/app/Console/Stubs/Modules/rule.stub @@ -0,0 +1,40 @@ + '$STUDLY_NAME$', + +]; diff --git a/app/Console/Stubs/Modules/scaffold/provider.stub b/app/Console/Stubs/Modules/scaffold/provider.stub new file mode 100644 index 00000000..b5775bb5 --- /dev/null +++ b/app/Console/Stubs/Modules/scaffold/provider.stub @@ -0,0 +1,103 @@ +loadConfig(); + $this->loadRoutes(); + } + + /** + * Boot the application events. + * + * @return void + */ + public function boot() + { + $this->loadViews(); + $this->loadViewComponents(); + $this->loadMigrations(); + } + + /** + * Load views. + * + * @return void + */ + public function loadViews() + { + $this->loadViewsFrom(__DIR__ . '/../Resources/views', '$ALIAS$'); + } + + /** + * Load view components. + * + * @return void + */ + public function loadViewComponents() + { + Blade::componentNamespace('$COMPONENT_NAMESPACE$', '$ALIAS$'); + } + + /** + * Load migrations. + * + * @return void + */ + public function loadMigrations() + { + $this->loadMigrationsFrom(__DIR__ . '/../$MIGRATIONS_PATH$'); + } + + /** + * Load config. + * + * @return void + */ + public function loadConfig() + { + $this->mergeConfigFrom(__DIR__ . '/../$PATH_CONFIG$/config.php', '$ALIAS$'); + } + + /** + * Load routes. + * + * @return void + */ + public function loadRoutes() + { + if (app()->routesAreCached()) { + return; + } + + $routes = [ + 'admin.php', + 'portal.php', + ]; + + foreach ($routes as $route) { + $this->loadRoutesFrom(__DIR__ . '/../$ROUTES_PATH$/' . $route); + } + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return []; + } +} diff --git a/app/Console/Stubs/Modules/seeder.stub b/app/Console/Stubs/Modules/seeder.stub new file mode 100644 index 00000000..4c8b9434 --- /dev/null +++ b/app/Console/Stubs/Modules/seeder.stub @@ -0,0 +1,23 @@ +call("OthersTableSeeder"); + + Model::reguard(); + } +} diff --git a/app/Console/Stubs/Modules/unit-test.stub b/app/Console/Stubs/Modules/unit-test.stub new file mode 100644 index 00000000..d21c296a --- /dev/null +++ b/app/Console/Stubs/Modules/unit-test.stub @@ -0,0 +1,19 @@ +assertTrue(true); + } +} diff --git a/app/Console/Stubs/Modules/views/index.stub b/app/Console/Stubs/Modules/views/index.stub new file mode 100644 index 00000000..ad091aa3 --- /dev/null +++ b/app/Console/Stubs/Modules/views/index.stub @@ -0,0 +1,9 @@ + + +

Hello World

+ +

+ This view is loaded from module: {!! config('$ALIAS$.name') !!} +

+
+
diff --git a/app/Console/Stubs/Modules/webpack.stub b/app/Console/Stubs/Modules/webpack.stub new file mode 100644 index 00000000..cd90d258 --- /dev/null +++ b/app/Console/Stubs/Modules/webpack.stub @@ -0,0 +1,20 @@ +const mix = require('laravel-mix'); + +/* + |-------------------------------------------------------------------------- + | Mix Asset Management + |-------------------------------------------------------------------------- + | + | Mix provides a clean, fluent API for defining some Webpack build steps + | for your Laravel application. By default, we are compiling the Sass + | file for the application as well as bundling up all the JS files. + | + */ + +mix.options({ + terser: { + extractComments: false, + } + }) + .js('Resources/assets/js/$ALIAS$.js', 'Resources/assets/js/$ALIAS$.min.js') + .vue(); diff --git a/app/Events/Auth/Attempting.php b/app/Events/Auth/Attempting.php new file mode 100644 index 00000000..39bbcf1e --- /dev/null +++ b/app/Events/Auth/Attempting.php @@ -0,0 +1,28 @@ +alias = $alias; + $this->company_id = $company_id; + $this->protocol = $protocol; + } +} diff --git a/app/Events/Auth/Authenticated.php b/app/Events/Auth/Authenticated.php new file mode 100644 index 00000000..b526a1a1 --- /dev/null +++ b/app/Events/Auth/Authenticated.php @@ -0,0 +1,28 @@ +alias = $alias; + $this->company_id = $company_id; + $this->protocol = $protocol; + } +} diff --git a/app/Events/Auth/Failed.php b/app/Events/Auth/Failed.php new file mode 100644 index 00000000..ac002456 --- /dev/null +++ b/app/Events/Auth/Failed.php @@ -0,0 +1,28 @@ +alias = $alias; + $this->company_id = $company_id; + $this->protocol = $protocol; + } +} diff --git a/app/Events/Auth/LandingPageShowing.php b/app/Events/Auth/LandingPageShowing.php new file mode 100644 index 00000000..a6b67f47 --- /dev/null +++ b/app/Events/Auth/LandingPageShowing.php @@ -0,0 +1,20 @@ +user = $user; + } +} diff --git a/app/Events/Auth/RoleCreated.php b/app/Events/Auth/RoleCreated.php new file mode 100644 index 00000000..5648d452 --- /dev/null +++ b/app/Events/Auth/RoleCreated.php @@ -0,0 +1,24 @@ +role = $role; + $this->request = $request; + } +} diff --git a/app/Events/Auth/RoleCreating.php b/app/Events/Auth/RoleCreating.php new file mode 100644 index 00000000..1cb3bf75 --- /dev/null +++ b/app/Events/Auth/RoleCreating.php @@ -0,0 +1,22 @@ +request = $request; + } +} diff --git a/app/Events/Auth/RoleDeleted.php b/app/Events/Auth/RoleDeleted.php new file mode 100644 index 00000000..fe7fc0c6 --- /dev/null +++ b/app/Events/Auth/RoleDeleted.php @@ -0,0 +1,20 @@ +role = $role; + } +} diff --git a/app/Events/Auth/RoleDeleting.php b/app/Events/Auth/RoleDeleting.php new file mode 100644 index 00000000..b885c836 --- /dev/null +++ b/app/Events/Auth/RoleDeleting.php @@ -0,0 +1,20 @@ +role = $role; + } +} diff --git a/app/Events/Auth/RoleUpdated.php b/app/Events/Auth/RoleUpdated.php new file mode 100644 index 00000000..37638406 --- /dev/null +++ b/app/Events/Auth/RoleUpdated.php @@ -0,0 +1,24 @@ +role = $role; + $this->request = $request; + } +} diff --git a/app/Events/Auth/RoleUpdating.php b/app/Events/Auth/RoleUpdating.php new file mode 100644 index 00000000..a86580e2 --- /dev/null +++ b/app/Events/Auth/RoleUpdating.php @@ -0,0 +1,24 @@ +role = $role; + $this->request = $request; + } +} diff --git a/app/Events/Auth/UserCreated.php b/app/Events/Auth/UserCreated.php new file mode 100644 index 00000000..52aa52df --- /dev/null +++ b/app/Events/Auth/UserCreated.php @@ -0,0 +1,24 @@ +user = $user; + $this->request = $request; + } +} diff --git a/app/Events/Auth/UserCreating.php b/app/Events/Auth/UserCreating.php new file mode 100644 index 00000000..6e613b5d --- /dev/null +++ b/app/Events/Auth/UserCreating.php @@ -0,0 +1,22 @@ +request = $request; + } +} diff --git a/app/Events/Auth/UserDeleted.php b/app/Events/Auth/UserDeleted.php new file mode 100644 index 00000000..d6676b79 --- /dev/null +++ b/app/Events/Auth/UserDeleted.php @@ -0,0 +1,20 @@ +user = $user; + } +} diff --git a/app/Events/Auth/UserDeleting.php b/app/Events/Auth/UserDeleting.php new file mode 100644 index 00000000..01e71251 --- /dev/null +++ b/app/Events/Auth/UserDeleting.php @@ -0,0 +1,20 @@ +user = $user; + } +} diff --git a/app/Events/Auth/UserUpdated.php b/app/Events/Auth/UserUpdated.php new file mode 100644 index 00000000..c5766fb6 --- /dev/null +++ b/app/Events/Auth/UserUpdated.php @@ -0,0 +1,24 @@ +user = $user; + $this->request = $request; + } +} diff --git a/app/Events/Auth/UserUpdating.php b/app/Events/Auth/UserUpdating.php new file mode 100644 index 00000000..ebf0b8b3 --- /dev/null +++ b/app/Events/Auth/UserUpdating.php @@ -0,0 +1,24 @@ +user = $user; + $this->request = $request; + } +} diff --git a/app/Events/Banking/AccountCreated.php b/app/Events/Banking/AccountCreated.php new file mode 100644 index 00000000..0b905125 --- /dev/null +++ b/app/Events/Banking/AccountCreated.php @@ -0,0 +1,21 @@ +account = $account; + } +} diff --git a/app/Events/Banking/AccountCreating.php b/app/Events/Banking/AccountCreating.php new file mode 100644 index 00000000..b2005537 --- /dev/null +++ b/app/Events/Banking/AccountCreating.php @@ -0,0 +1,20 @@ +request = $request; + } +} diff --git a/app/Events/Banking/AccountDeleted.php b/app/Events/Banking/AccountDeleted.php new file mode 100644 index 00000000..9374a2c6 --- /dev/null +++ b/app/Events/Banking/AccountDeleted.php @@ -0,0 +1,20 @@ +account = $account; + } +} diff --git a/app/Events/Banking/AccountDeleting.php b/app/Events/Banking/AccountDeleting.php new file mode 100644 index 00000000..f2d81ec6 --- /dev/null +++ b/app/Events/Banking/AccountDeleting.php @@ -0,0 +1,20 @@ +account = $account; + } +} diff --git a/app/Events/Banking/AccountUpdated.php b/app/Events/Banking/AccountUpdated.php new file mode 100644 index 00000000..5a666f1a --- /dev/null +++ b/app/Events/Banking/AccountUpdated.php @@ -0,0 +1,25 @@ +account = $account; + $this->request = $request; + } +} diff --git a/app/Events/Banking/AccountUpdating.php b/app/Events/Banking/AccountUpdating.php new file mode 100644 index 00000000..a76ebfae --- /dev/null +++ b/app/Events/Banking/AccountUpdating.php @@ -0,0 +1,25 @@ +account = $account; + $this->request = $request; + } +} diff --git a/app/Events/Banking/DocumentTransactionCreated.php b/app/Events/Banking/DocumentTransactionCreated.php new file mode 100644 index 00000000..58fb543b --- /dev/null +++ b/app/Events/Banking/DocumentTransactionCreated.php @@ -0,0 +1,26 @@ +document = $document; + $this->transaction = $transaction; + } +} diff --git a/app/Events/Banking/DocumentTransactionCreating.php b/app/Events/Banking/DocumentTransactionCreating.php new file mode 100644 index 00000000..c5a366bc --- /dev/null +++ b/app/Events/Banking/DocumentTransactionCreating.php @@ -0,0 +1,25 @@ +document = $document; + $this->request = $request; + } +} diff --git a/app/Events/Banking/DocumentTransactionUpdated.php b/app/Events/Banking/DocumentTransactionUpdated.php new file mode 100644 index 00000000..3a822fe1 --- /dev/null +++ b/app/Events/Banking/DocumentTransactionUpdated.php @@ -0,0 +1,30 @@ +document = $document; + $this->transaction = $transaction; + $this->request = $request; + } +} diff --git a/app/Events/Banking/DocumentTransactionUpdating.php b/app/Events/Banking/DocumentTransactionUpdating.php new file mode 100644 index 00000000..7bc2a665 --- /dev/null +++ b/app/Events/Banking/DocumentTransactionUpdating.php @@ -0,0 +1,30 @@ +document = $document; + $this->transaction = $transaction; + $this->request = $request; + } +} diff --git a/app/Events/Banking/TransactionCreated.php b/app/Events/Banking/TransactionCreated.php new file mode 100644 index 00000000..70a7cc27 --- /dev/null +++ b/app/Events/Banking/TransactionCreated.php @@ -0,0 +1,21 @@ +transaction = $transaction; + } +} diff --git a/app/Events/Banking/TransactionCreating.php b/app/Events/Banking/TransactionCreating.php new file mode 100644 index 00000000..4a709a86 --- /dev/null +++ b/app/Events/Banking/TransactionCreating.php @@ -0,0 +1,20 @@ +request = $request; + } +} diff --git a/app/Events/Banking/TransactionDeleted.php b/app/Events/Banking/TransactionDeleted.php new file mode 100644 index 00000000..d06529be --- /dev/null +++ b/app/Events/Banking/TransactionDeleted.php @@ -0,0 +1,20 @@ +transaction = $transaction; + } +} diff --git a/app/Events/Banking/TransactionDeleting.php b/app/Events/Banking/TransactionDeleting.php new file mode 100644 index 00000000..8a74a515 --- /dev/null +++ b/app/Events/Banking/TransactionDeleting.php @@ -0,0 +1,20 @@ +transaction = $transaction; + } +} diff --git a/app/Events/Banking/TransactionPrinting.php b/app/Events/Banking/TransactionPrinting.php new file mode 100644 index 00000000..0fde309b --- /dev/null +++ b/app/Events/Banking/TransactionPrinting.php @@ -0,0 +1,22 @@ +transaction = $transaction; + } +} diff --git a/app/Events/Banking/TransactionRecurring.php b/app/Events/Banking/TransactionRecurring.php new file mode 100644 index 00000000..bf14cba2 --- /dev/null +++ b/app/Events/Banking/TransactionRecurring.php @@ -0,0 +1,20 @@ +transaction = $transaction; + } +} diff --git a/app/Events/Banking/TransactionSending.php b/app/Events/Banking/TransactionSending.php new file mode 100644 index 00000000..99aa7cfa --- /dev/null +++ b/app/Events/Banking/TransactionSending.php @@ -0,0 +1,20 @@ +transaction = $transaction; + } +} diff --git a/app/Events/Banking/TransactionSent.php b/app/Events/Banking/TransactionSent.php new file mode 100644 index 00000000..3fa443ea --- /dev/null +++ b/app/Events/Banking/TransactionSent.php @@ -0,0 +1,20 @@ +transaction = $transaction; + } +} diff --git a/app/Events/Banking/TransactionSplitted.php b/app/Events/Banking/TransactionSplitted.php new file mode 100644 index 00000000..032f88ab --- /dev/null +++ b/app/Events/Banking/TransactionSplitted.php @@ -0,0 +1,23 @@ +request = $request; + $this->transaction = $transaction; + } +} diff --git a/app/Events/Banking/TransactionSplitting.php b/app/Events/Banking/TransactionSplitting.php new file mode 100644 index 00000000..2d975713 --- /dev/null +++ b/app/Events/Banking/TransactionSplitting.php @@ -0,0 +1,23 @@ +request = $request; + $this->transaction = $transaction; + } +} diff --git a/app/Events/Banking/TransactionUpdated.php b/app/Events/Banking/TransactionUpdated.php new file mode 100644 index 00000000..59bafc91 --- /dev/null +++ b/app/Events/Banking/TransactionUpdated.php @@ -0,0 +1,25 @@ +transaction = $transaction; + $this->request = $request; + } +} diff --git a/app/Events/Banking/TransactionUpdating.php b/app/Events/Banking/TransactionUpdating.php new file mode 100644 index 00000000..86850cec --- /dev/null +++ b/app/Events/Banking/TransactionUpdating.php @@ -0,0 +1,25 @@ +transaction = $transaction; + $this->request = $request; + } +} diff --git a/app/Events/Banking/TransferCreated.php b/app/Events/Banking/TransferCreated.php new file mode 100644 index 00000000..ac48b138 --- /dev/null +++ b/app/Events/Banking/TransferCreated.php @@ -0,0 +1,21 @@ +transfer = $transfer; + } +} diff --git a/app/Events/Banking/TransferCreating.php b/app/Events/Banking/TransferCreating.php new file mode 100644 index 00000000..56b7c290 --- /dev/null +++ b/app/Events/Banking/TransferCreating.php @@ -0,0 +1,20 @@ +request = $request; + } +} diff --git a/app/Events/Banking/TransferDeleted.php b/app/Events/Banking/TransferDeleted.php new file mode 100644 index 00000000..dca1a073 --- /dev/null +++ b/app/Events/Banking/TransferDeleted.php @@ -0,0 +1,20 @@ +transfer = $transfer; + } +} diff --git a/app/Events/Banking/TransferDeleting.php b/app/Events/Banking/TransferDeleting.php new file mode 100644 index 00000000..db73749b --- /dev/null +++ b/app/Events/Banking/TransferDeleting.php @@ -0,0 +1,20 @@ +transfer = $transfer; + } +} diff --git a/app/Events/Banking/TransferPrinting.php b/app/Events/Banking/TransferPrinting.php new file mode 100644 index 00000000..e7721975 --- /dev/null +++ b/app/Events/Banking/TransferPrinting.php @@ -0,0 +1,22 @@ +transfer = $transfer; + } +} diff --git a/app/Events/Banking/TransferUpdated.php b/app/Events/Banking/TransferUpdated.php new file mode 100644 index 00000000..39f87341 --- /dev/null +++ b/app/Events/Banking/TransferUpdated.php @@ -0,0 +1,25 @@ +transfer = $transfer; + $this->request = $request; + } +} diff --git a/app/Events/Banking/TransferUpdating.php b/app/Events/Banking/TransferUpdating.php new file mode 100644 index 00000000..131344d3 --- /dev/null +++ b/app/Events/Banking/TransferUpdating.php @@ -0,0 +1,25 @@ +transfer = $transfer; + $this->request = $request; + } +} diff --git a/app/Events/Common/BulkActionsAdding.php b/app/Events/Common/BulkActionsAdding.php new file mode 100644 index 00000000..9cbcbe05 --- /dev/null +++ b/app/Events/Common/BulkActionsAdding.php @@ -0,0 +1,20 @@ +bulk_action = $bulk_action; + } +} diff --git a/app/Events/Common/CompanyCreated.php b/app/Events/Common/CompanyCreated.php new file mode 100644 index 00000000..612cba4e --- /dev/null +++ b/app/Events/Common/CompanyCreated.php @@ -0,0 +1,23 @@ +company = $company; + $this->request = $request; + } +} diff --git a/app/Events/Common/CompanyCreating.php b/app/Events/Common/CompanyCreating.php new file mode 100644 index 00000000..4793fd7f --- /dev/null +++ b/app/Events/Common/CompanyCreating.php @@ -0,0 +1,20 @@ +request = $request; + } +} diff --git a/app/Events/Common/CompanyDeleted.php b/app/Events/Common/CompanyDeleted.php new file mode 100644 index 00000000..817cb9f9 --- /dev/null +++ b/app/Events/Common/CompanyDeleted.php @@ -0,0 +1,24 @@ +company = $company; + $this->current_company_id = $current_company_id; + } +} diff --git a/app/Events/Common/CompanyDeleting.php b/app/Events/Common/CompanyDeleting.php new file mode 100644 index 00000000..2232d2cb --- /dev/null +++ b/app/Events/Common/CompanyDeleting.php @@ -0,0 +1,24 @@ +company = $company; + $this->current_company_id = $current_company_id; + } +} diff --git a/app/Events/Common/CompanyForgettingCurrent.php b/app/Events/Common/CompanyForgettingCurrent.php new file mode 100644 index 00000000..dfd79376 --- /dev/null +++ b/app/Events/Common/CompanyForgettingCurrent.php @@ -0,0 +1,20 @@ +company = $company; + } +} diff --git a/app/Events/Common/CompanyForgotCurrent.php b/app/Events/Common/CompanyForgotCurrent.php new file mode 100644 index 00000000..15beb2b1 --- /dev/null +++ b/app/Events/Common/CompanyForgotCurrent.php @@ -0,0 +1,20 @@ +company = $company; + } +} diff --git a/app/Events/Common/CompanyMadeCurrent.php b/app/Events/Common/CompanyMadeCurrent.php new file mode 100644 index 00000000..828d643c --- /dev/null +++ b/app/Events/Common/CompanyMadeCurrent.php @@ -0,0 +1,20 @@ +company = $company; + } +} diff --git a/app/Events/Common/CompanyMakingCurrent.php b/app/Events/Common/CompanyMakingCurrent.php new file mode 100644 index 00000000..7787922b --- /dev/null +++ b/app/Events/Common/CompanyMakingCurrent.php @@ -0,0 +1,20 @@ +company = $company; + } +} diff --git a/app/Events/Common/CompanySwitched.php b/app/Events/Common/CompanySwitched.php new file mode 100644 index 00000000..db6db4ce --- /dev/null +++ b/app/Events/Common/CompanySwitched.php @@ -0,0 +1,24 @@ +company = $company; + $this->old_company_id = $old_company_id; + } +} diff --git a/app/Events/Common/CompanyUpdated.php b/app/Events/Common/CompanyUpdated.php new file mode 100644 index 00000000..7378bb42 --- /dev/null +++ b/app/Events/Common/CompanyUpdated.php @@ -0,0 +1,24 @@ +company = $company; + $this->request = $request; + } +} diff --git a/app/Events/Common/CompanyUpdating.php b/app/Events/Common/CompanyUpdating.php new file mode 100644 index 00000000..8add2734 --- /dev/null +++ b/app/Events/Common/CompanyUpdating.php @@ -0,0 +1,24 @@ +company = $company; + $this->request = $request; + } +} diff --git a/app/Events/Common/ContactCreated.php b/app/Events/Common/ContactCreated.php new file mode 100644 index 00000000..c9817cd7 --- /dev/null +++ b/app/Events/Common/ContactCreated.php @@ -0,0 +1,21 @@ +contact = $contact; + } +} diff --git a/app/Events/Common/ContactCreating.php b/app/Events/Common/ContactCreating.php new file mode 100644 index 00000000..9db77275 --- /dev/null +++ b/app/Events/Common/ContactCreating.php @@ -0,0 +1,20 @@ +request = $request; + } +} diff --git a/app/Events/Common/ContactDeleted.php b/app/Events/Common/ContactDeleted.php new file mode 100644 index 00000000..fbcef3f2 --- /dev/null +++ b/app/Events/Common/ContactDeleted.php @@ -0,0 +1,20 @@ +contact = $contact; + } +} diff --git a/app/Events/Common/ContactDeleting.php b/app/Events/Common/ContactDeleting.php new file mode 100644 index 00000000..db68ea39 --- /dev/null +++ b/app/Events/Common/ContactDeleting.php @@ -0,0 +1,20 @@ +contact = $contact; + } +} diff --git a/app/Events/Common/ContactUpdated.php b/app/Events/Common/ContactUpdated.php new file mode 100644 index 00000000..ffa72741 --- /dev/null +++ b/app/Events/Common/ContactUpdated.php @@ -0,0 +1,25 @@ +contact = $contact; + $this->request = $request; + } +} diff --git a/app/Events/Common/ContactUpdating.php b/app/Events/Common/ContactUpdating.php new file mode 100644 index 00000000..8107ea7f --- /dev/null +++ b/app/Events/Common/ContactUpdating.php @@ -0,0 +1,25 @@ +contact = $contact; + $this->request = $request; + } +} diff --git a/app/Events/Common/DatesFormating.php b/app/Events/Common/DatesFormating.php new file mode 100644 index 00000000..7108e588 --- /dev/null +++ b/app/Events/Common/DatesFormating.php @@ -0,0 +1,23 @@ +columns = $columns; + $this->request = $request; + } +} diff --git a/app/Events/Common/GlobalSearched.php b/app/Events/Common/GlobalSearched.php new file mode 100644 index 00000000..a471bd83 --- /dev/null +++ b/app/Events/Common/GlobalSearched.php @@ -0,0 +1,20 @@ +search = $search; + } +} diff --git a/app/Events/Common/ImportViewCreated.php b/app/Events/Common/ImportViewCreated.php new file mode 100644 index 00000000..64ab1a94 --- /dev/null +++ b/app/Events/Common/ImportViewCreated.php @@ -0,0 +1,21 @@ +view = $view; + } +} diff --git a/app/Events/Common/ImportViewCreating.php b/app/Events/Common/ImportViewCreating.php new file mode 100644 index 00000000..b184e46d --- /dev/null +++ b/app/Events/Common/ImportViewCreating.php @@ -0,0 +1,21 @@ +view = $view; + } +} diff --git a/app/Events/Common/ItemCreated.php b/app/Events/Common/ItemCreated.php new file mode 100644 index 00000000..5bec41bd --- /dev/null +++ b/app/Events/Common/ItemCreated.php @@ -0,0 +1,25 @@ +item = $item; + $this->request = $request; + } +} diff --git a/app/Events/Common/ItemCreating.php b/app/Events/Common/ItemCreating.php new file mode 100644 index 00000000..182ed381 --- /dev/null +++ b/app/Events/Common/ItemCreating.php @@ -0,0 +1,20 @@ +request = $request; + } +} diff --git a/app/Events/Common/ItemDeleted.php b/app/Events/Common/ItemDeleted.php new file mode 100644 index 00000000..78721f12 --- /dev/null +++ b/app/Events/Common/ItemDeleted.php @@ -0,0 +1,20 @@ +item = $item; + } +} diff --git a/app/Events/Common/ItemDeleting.php b/app/Events/Common/ItemDeleting.php new file mode 100644 index 00000000..484aa555 --- /dev/null +++ b/app/Events/Common/ItemDeleting.php @@ -0,0 +1,20 @@ +item = $item; + } +} diff --git a/app/Events/Common/ItemUpdated.php b/app/Events/Common/ItemUpdated.php new file mode 100644 index 00000000..36d019cf --- /dev/null +++ b/app/Events/Common/ItemUpdated.php @@ -0,0 +1,25 @@ +item = $item; + $this->request = $request; + } +} diff --git a/app/Events/Common/ItemUpdating.php b/app/Events/Common/ItemUpdating.php new file mode 100644 index 00000000..9d0fd9ef --- /dev/null +++ b/app/Events/Common/ItemUpdating.php @@ -0,0 +1,25 @@ +item = $item; + $this->request = $request; + } +} diff --git a/app/Events/Common/RelationshipCounting.php b/app/Events/Common/RelationshipCounting.php new file mode 100644 index 00000000..de6be494 --- /dev/null +++ b/app/Events/Common/RelationshipCounting.php @@ -0,0 +1,20 @@ +record = $record; + } +} diff --git a/app/Events/Common/RelationshipDeleting.php b/app/Events/Common/RelationshipDeleting.php new file mode 100644 index 00000000..1d061791 --- /dev/null +++ b/app/Events/Common/RelationshipDeleting.php @@ -0,0 +1,20 @@ +record = $record; + } +} diff --git a/app/Events/Common/SearchStringApplied.php b/app/Events/Common/SearchStringApplied.php new file mode 100644 index 00000000..0a5f57d1 --- /dev/null +++ b/app/Events/Common/SearchStringApplied.php @@ -0,0 +1,20 @@ +query = $query; + } +} diff --git a/app/Events/Common/SearchStringApplying.php b/app/Events/Common/SearchStringApplying.php new file mode 100644 index 00000000..0c0d71c4 --- /dev/null +++ b/app/Events/Common/SearchStringApplying.php @@ -0,0 +1,20 @@ +query = $query; + } +} diff --git a/app/Events/Document/DocumentCancelled.php b/app/Events/Document/DocumentCancelled.php new file mode 100644 index 00000000..9d71f558 --- /dev/null +++ b/app/Events/Document/DocumentCancelled.php @@ -0,0 +1,22 @@ +document = $document; + } +} diff --git a/app/Events/Document/DocumentCreated.php b/app/Events/Document/DocumentCreated.php new file mode 100644 index 00000000..2f138573 --- /dev/null +++ b/app/Events/Document/DocumentCreated.php @@ -0,0 +1,24 @@ +document = $document; + $this->request = $request; + } +} diff --git a/app/Events/Document/DocumentCreating.php b/app/Events/Document/DocumentCreating.php new file mode 100644 index 00000000..51f1b3d2 --- /dev/null +++ b/app/Events/Document/DocumentCreating.php @@ -0,0 +1,22 @@ +request = $request; + } +} diff --git a/app/Events/Document/DocumentDeleted.php b/app/Events/Document/DocumentDeleted.php new file mode 100644 index 00000000..f29ac492 --- /dev/null +++ b/app/Events/Document/DocumentDeleted.php @@ -0,0 +1,20 @@ +document = $document; + } +} diff --git a/app/Events/Document/DocumentDeleting.php b/app/Events/Document/DocumentDeleting.php new file mode 100644 index 00000000..e2d9b85b --- /dev/null +++ b/app/Events/Document/DocumentDeleting.php @@ -0,0 +1,20 @@ +document = $document; + } +} diff --git a/app/Events/Document/DocumentDuplicating.php b/app/Events/Document/DocumentDuplicating.php new file mode 100644 index 00000000..3f3c7914 --- /dev/null +++ b/app/Events/Document/DocumentDuplicating.php @@ -0,0 +1,20 @@ +document = $document; + } +} diff --git a/app/Events/Document/DocumentMarkedSent.php b/app/Events/Document/DocumentMarkedSent.php new file mode 100644 index 00000000..c79c1fe3 --- /dev/null +++ b/app/Events/Document/DocumentMarkedSent.php @@ -0,0 +1,20 @@ +document = $document; + } +} diff --git a/app/Events/Document/DocumentPrinting.php b/app/Events/Document/DocumentPrinting.php new file mode 100644 index 00000000..33a19c65 --- /dev/null +++ b/app/Events/Document/DocumentPrinting.php @@ -0,0 +1,22 @@ +document = $document; + } +} diff --git a/app/Events/Document/DocumentReceived.php b/app/Events/Document/DocumentReceived.php new file mode 100644 index 00000000..ae24e635 --- /dev/null +++ b/app/Events/Document/DocumentReceived.php @@ -0,0 +1,22 @@ +document = $document; + } +} diff --git a/app/Events/Document/DocumentRecurring.php b/app/Events/Document/DocumentRecurring.php new file mode 100644 index 00000000..72fa68a7 --- /dev/null +++ b/app/Events/Document/DocumentRecurring.php @@ -0,0 +1,21 @@ +document = $document; + } +} diff --git a/app/Events/Document/DocumentReminded.php b/app/Events/Document/DocumentReminded.php new file mode 100644 index 00000000..65a819b2 --- /dev/null +++ b/app/Events/Document/DocumentReminded.php @@ -0,0 +1,24 @@ +document = $document; + $this->notification = $notification; + } +} diff --git a/app/Events/Document/DocumentResourceShowing.php b/app/Events/Document/DocumentResourceShowing.php new file mode 100644 index 00000000..2669cb14 --- /dev/null +++ b/app/Events/Document/DocumentResourceShowing.php @@ -0,0 +1,34 @@ +document = $document; + } + + /** + * Add extra resources to the document API response. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function addResource(string $key, $value): void + { + $this->resources[$key] = $value; + } +} diff --git a/app/Events/Document/DocumentRestored.php b/app/Events/Document/DocumentRestored.php new file mode 100644 index 00000000..de6e5e4b --- /dev/null +++ b/app/Events/Document/DocumentRestored.php @@ -0,0 +1,22 @@ +document = $document; + } +} diff --git a/app/Events/Document/DocumentSending.php b/app/Events/Document/DocumentSending.php new file mode 100644 index 00000000..97db6503 --- /dev/null +++ b/app/Events/Document/DocumentSending.php @@ -0,0 +1,20 @@ +document = $document; + } +} diff --git a/app/Events/Document/DocumentSent.php b/app/Events/Document/DocumentSent.php new file mode 100644 index 00000000..25760023 --- /dev/null +++ b/app/Events/Document/DocumentSent.php @@ -0,0 +1,20 @@ +document = $document; + } +} diff --git a/app/Events/Document/DocumentTemplates.php b/app/Events/Document/DocumentTemplates.php new file mode 100644 index 00000000..7079c215 --- /dev/null +++ b/app/Events/Document/DocumentTemplates.php @@ -0,0 +1,26 @@ +type = $type; + $this->templates = $templates; + } +} diff --git a/app/Events/Document/DocumentUpdated.php b/app/Events/Document/DocumentUpdated.php new file mode 100644 index 00000000..24d21874 --- /dev/null +++ b/app/Events/Document/DocumentUpdated.php @@ -0,0 +1,24 @@ +document = $document; + $this->request = $request; + } +} diff --git a/app/Events/Document/DocumentUpdating.php b/app/Events/Document/DocumentUpdating.php new file mode 100644 index 00000000..bfa699eb --- /dev/null +++ b/app/Events/Document/DocumentUpdating.php @@ -0,0 +1,24 @@ +document = $document; + $this->request = $request; + } +} diff --git a/app/Events/Document/DocumentViewed.php b/app/Events/Document/DocumentViewed.php new file mode 100644 index 00000000..2f8d51b7 --- /dev/null +++ b/app/Events/Document/DocumentViewed.php @@ -0,0 +1,20 @@ +document = $document; + } +} diff --git a/app/Events/Document/PaidAmountCalculated.php b/app/Events/Document/PaidAmountCalculated.php new file mode 100644 index 00000000..015cad99 --- /dev/null +++ b/app/Events/Document/PaidAmountCalculated.php @@ -0,0 +1,20 @@ +model = $model; + } +} diff --git a/app/Events/Document/PaymentReceived.php b/app/Events/Document/PaymentReceived.php new file mode 100644 index 00000000..98854bdc --- /dev/null +++ b/app/Events/Document/PaymentReceived.php @@ -0,0 +1,31 @@ +document = $document; + + if (empty($request['number'])) { + $request['number'] = $this->getNextTransactionNumber(); + } + + $this->request = $request; + } +} diff --git a/app/Events/Document/TransactionsCounted.php b/app/Events/Document/TransactionsCounted.php new file mode 100644 index 00000000..6eea3fcd --- /dev/null +++ b/app/Events/Document/TransactionsCounted.php @@ -0,0 +1,20 @@ +model = $model; + } +} diff --git a/app/Events/Email/InvalidEmailDetected.php b/app/Events/Email/InvalidEmailDetected.php new file mode 100644 index 00000000..fe860efb --- /dev/null +++ b/app/Events/Email/InvalidEmailDetected.php @@ -0,0 +1,50 @@ +email = $email; + + $this->error = $error; + + $this->setContact(); + + $this->setUser(); + } + + public function setContact() + { + $contact = Contact::email($this->email)->enabled()->first(); + + if (empty($contact)) { + return; + } + + $this->contact = $contact; + } + + public function setUser() + { + $user = user_model_class()::email($this->email)->enabled()->first(); + + if (empty($user)) { + return; + } + + $this->user = $user; + } +} diff --git a/app/Events/Email/TooManyEmailsSent.php b/app/Events/Email/TooManyEmailsSent.php new file mode 100644 index 00000000..262916b2 --- /dev/null +++ b/app/Events/Email/TooManyEmailsSent.php @@ -0,0 +1,15 @@ +user_id = $user_id; + } +} diff --git a/app/Events/Export/HeadingsPreparing.php b/app/Events/Export/HeadingsPreparing.php new file mode 100644 index 00000000..64178511 --- /dev/null +++ b/app/Events/Export/HeadingsPreparing.php @@ -0,0 +1,20 @@ +class = $class; + } +} diff --git a/app/Events/Export/RowsPreparing.php b/app/Events/Export/RowsPreparing.php new file mode 100644 index 00000000..6d67e617 --- /dev/null +++ b/app/Events/Export/RowsPreparing.php @@ -0,0 +1,24 @@ +class = $class; + $this->rows = $rows; + } +} diff --git a/app/Events/Import/RowPreparing.php b/app/Events/Import/RowPreparing.php new file mode 100644 index 00000000..d519cbb3 --- /dev/null +++ b/app/Events/Import/RowPreparing.php @@ -0,0 +1,24 @@ +class = $class; + $this->row = $row; + } +} diff --git a/app/Events/Install/UpdateCacheCleared.php b/app/Events/Install/UpdateCacheCleared.php new file mode 100644 index 00000000..b5ed5ae1 --- /dev/null +++ b/app/Events/Install/UpdateCacheCleared.php @@ -0,0 +1,20 @@ +company_id = $company_id; + } +} diff --git a/app/Events/Install/UpdateCopied.php b/app/Events/Install/UpdateCopied.php new file mode 100644 index 00000000..743f113a --- /dev/null +++ b/app/Events/Install/UpdateCopied.php @@ -0,0 +1,28 @@ +alias = $alias; + $this->old = $old; + $this->new = $new; + } +} diff --git a/app/Events/Install/UpdateDownloaded.php b/app/Events/Install/UpdateDownloaded.php new file mode 100644 index 00000000..4b0841a8 --- /dev/null +++ b/app/Events/Install/UpdateDownloaded.php @@ -0,0 +1,28 @@ +alias = $alias; + $this->old = $old; + $this->new = $new; + } +} diff --git a/app/Events/Install/UpdateFailed.php b/app/Events/Install/UpdateFailed.php new file mode 100644 index 00000000..4a8367d9 --- /dev/null +++ b/app/Events/Install/UpdateFailed.php @@ -0,0 +1,35 @@ +alias = $alias; + $this->old = $old; + $this->new = $new; + $this->step = $step; + $this->message = $message; + } +} diff --git a/app/Events/Install/UpdateFinished.php b/app/Events/Install/UpdateFinished.php new file mode 100644 index 00000000..3ace1ec0 --- /dev/null +++ b/app/Events/Install/UpdateFinished.php @@ -0,0 +1,28 @@ +alias = $alias; + $this->new = $new; + $this->old = $old; + } +} diff --git a/app/Events/Install/UpdateUnzipped.php b/app/Events/Install/UpdateUnzipped.php new file mode 100644 index 00000000..7da9ace4 --- /dev/null +++ b/app/Events/Install/UpdateUnzipped.php @@ -0,0 +1,28 @@ +alias = $alias; + $this->old = $old; + $this->new = $new; + } +} diff --git a/app/Events/Menu/AdminCreated.php b/app/Events/Menu/AdminCreated.php new file mode 100644 index 00000000..c9378b2b --- /dev/null +++ b/app/Events/Menu/AdminCreated.php @@ -0,0 +1,20 @@ +menu = $menu; + } +} diff --git a/app/Events/Menu/AdminCreating.php b/app/Events/Menu/AdminCreating.php new file mode 100644 index 00000000..733a896a --- /dev/null +++ b/app/Events/Menu/AdminCreating.php @@ -0,0 +1,20 @@ +menu = $menu; + } +} diff --git a/app/Events/Menu/ItemAuthorizing.php b/app/Events/Menu/ItemAuthorizing.php new file mode 100644 index 00000000..9850b7b3 --- /dev/null +++ b/app/Events/Menu/ItemAuthorizing.php @@ -0,0 +1,20 @@ +item = $item; + } +} diff --git a/app/Events/Menu/NewwCreated.php b/app/Events/Menu/NewwCreated.php new file mode 100644 index 00000000..522005d2 --- /dev/null +++ b/app/Events/Menu/NewwCreated.php @@ -0,0 +1,20 @@ +menu = $menu; + } +} diff --git a/app/Events/Menu/NewwCreating.php b/app/Events/Menu/NewwCreating.php new file mode 100644 index 00000000..e3a2f756 --- /dev/null +++ b/app/Events/Menu/NewwCreating.php @@ -0,0 +1,20 @@ +menu = $menu; + } +} diff --git a/app/Events/Menu/NotificationsCreated.php b/app/Events/Menu/NotificationsCreated.php new file mode 100644 index 00000000..de65397f --- /dev/null +++ b/app/Events/Menu/NotificationsCreated.php @@ -0,0 +1,20 @@ +notifications = $notifications; + } +} diff --git a/app/Events/Menu/NotificationsCreating.php b/app/Events/Menu/NotificationsCreating.php new file mode 100644 index 00000000..9b09aa93 --- /dev/null +++ b/app/Events/Menu/NotificationsCreating.php @@ -0,0 +1,20 @@ +notifications = $notifications; + } +} diff --git a/app/Events/Menu/PortalCreated.php b/app/Events/Menu/PortalCreated.php new file mode 100644 index 00000000..a92061ff --- /dev/null +++ b/app/Events/Menu/PortalCreated.php @@ -0,0 +1,20 @@ +menu = $menu; + } +} diff --git a/app/Events/Menu/PortalCreating.php b/app/Events/Menu/PortalCreating.php new file mode 100644 index 00000000..ccd858e0 --- /dev/null +++ b/app/Events/Menu/PortalCreating.php @@ -0,0 +1,20 @@ +menu = $menu; + } +} diff --git a/app/Events/Menu/ProfileCreated.php b/app/Events/Menu/ProfileCreated.php new file mode 100644 index 00000000..b020cf31 --- /dev/null +++ b/app/Events/Menu/ProfileCreated.php @@ -0,0 +1,20 @@ +menu = $menu; + } +} diff --git a/app/Events/Menu/ProfileCreating.php b/app/Events/Menu/ProfileCreating.php new file mode 100644 index 00000000..e730a863 --- /dev/null +++ b/app/Events/Menu/ProfileCreating.php @@ -0,0 +1,20 @@ +menu = $menu; + } +} diff --git a/app/Events/Menu/SettingsCreated.php b/app/Events/Menu/SettingsCreated.php new file mode 100644 index 00000000..62027386 --- /dev/null +++ b/app/Events/Menu/SettingsCreated.php @@ -0,0 +1,20 @@ +menu = $menu; + } +} diff --git a/app/Events/Menu/SettingsCreating.php b/app/Events/Menu/SettingsCreating.php new file mode 100644 index 00000000..2c3d154f --- /dev/null +++ b/app/Events/Menu/SettingsCreating.php @@ -0,0 +1,20 @@ +menu = $menu; + } +} diff --git a/app/Events/Menu/SettingsFinished.php b/app/Events/Menu/SettingsFinished.php new file mode 100644 index 00000000..ae84b598 --- /dev/null +++ b/app/Events/Menu/SettingsFinished.php @@ -0,0 +1,20 @@ +menu = $menu; + } +} diff --git a/app/Events/Module/Copied.php b/app/Events/Module/Copied.php new file mode 100644 index 00000000..7a286b48 --- /dev/null +++ b/app/Events/Module/Copied.php @@ -0,0 +1,24 @@ +alias = $alias; + $this->company_id = $company_id; + } +} diff --git a/app/Events/Module/Disabled.php b/app/Events/Module/Disabled.php new file mode 100644 index 00000000..01798a8f --- /dev/null +++ b/app/Events/Module/Disabled.php @@ -0,0 +1,24 @@ +alias = $alias; + $this->company_id = $company_id; + } +} diff --git a/app/Events/Module/Disabling.php b/app/Events/Module/Disabling.php new file mode 100644 index 00000000..b8abb005 --- /dev/null +++ b/app/Events/Module/Disabling.php @@ -0,0 +1,24 @@ +alias = $alias; + $this->company_id = $company_id; + } +} diff --git a/app/Events/Module/Enabled.php b/app/Events/Module/Enabled.php new file mode 100644 index 00000000..8e8dc90e --- /dev/null +++ b/app/Events/Module/Enabled.php @@ -0,0 +1,24 @@ +alias = $alias; + $this->company_id = $company_id; + } +} diff --git a/app/Events/Module/Installed.php b/app/Events/Module/Installed.php new file mode 100644 index 00000000..9f9baa59 --- /dev/null +++ b/app/Events/Module/Installed.php @@ -0,0 +1,28 @@ +alias = $alias; + $this->company_id = $company_id; + $this->locale = $locale; + } +} diff --git a/app/Events/Module/Installing.php b/app/Events/Module/Installing.php new file mode 100644 index 00000000..0a97f294 --- /dev/null +++ b/app/Events/Module/Installing.php @@ -0,0 +1,24 @@ +alias = $alias; + $this->company_id = $company_id; + } +} diff --git a/app/Events/Module/PaymentMethodShowing.php b/app/Events/Module/PaymentMethodShowing.php new file mode 100644 index 00000000..9ee9d5e0 --- /dev/null +++ b/app/Events/Module/PaymentMethodShowing.php @@ -0,0 +1,20 @@ +modules = $modules; + } +} diff --git a/app/Events/Module/Uninstalled.php b/app/Events/Module/Uninstalled.php new file mode 100644 index 00000000..87ffb3e0 --- /dev/null +++ b/app/Events/Module/Uninstalled.php @@ -0,0 +1,24 @@ +alias = $alias; + $this->company_id = $company_id; + } +} diff --git a/app/Events/Module/Uninstalling.php b/app/Events/Module/Uninstalling.php new file mode 100644 index 00000000..39b5b971 --- /dev/null +++ b/app/Events/Module/Uninstalling.php @@ -0,0 +1,24 @@ +alias = $alias; + $this->company_id = $company_id; + } +} diff --git a/app/Events/Report/ClassesCreated.php b/app/Events/Report/ClassesCreated.php new file mode 100644 index 00000000..fb064306 --- /dev/null +++ b/app/Events/Report/ClassesCreated.php @@ -0,0 +1,20 @@ +list = $list; + } +} diff --git a/app/Events/Report/DataLoaded.php b/app/Events/Report/DataLoaded.php new file mode 100644 index 00000000..1dd95e79 --- /dev/null +++ b/app/Events/Report/DataLoaded.php @@ -0,0 +1,20 @@ +class = $class; + } +} diff --git a/app/Events/Report/DataLoading.php b/app/Events/Report/DataLoading.php new file mode 100644 index 00000000..6a8c6a03 --- /dev/null +++ b/app/Events/Report/DataLoading.php @@ -0,0 +1,20 @@ +class = $class; + } +} diff --git a/app/Events/Report/FilterApplying.php b/app/Events/Report/FilterApplying.php new file mode 100644 index 00000000..4531d208 --- /dev/null +++ b/app/Events/Report/FilterApplying.php @@ -0,0 +1,28 @@ +class = $class; + $this->model = $model; + $this->args = $args; + } +} diff --git a/app/Events/Report/FilterShowing.php b/app/Events/Report/FilterShowing.php new file mode 100644 index 00000000..c50cb105 --- /dev/null +++ b/app/Events/Report/FilterShowing.php @@ -0,0 +1,20 @@ +class = $class; + } +} diff --git a/app/Events/Report/GroupApplying.php b/app/Events/Report/GroupApplying.php new file mode 100644 index 00000000..44c42a84 --- /dev/null +++ b/app/Events/Report/GroupApplying.php @@ -0,0 +1,28 @@ +class = $class; + $this->model = $model; + $this->args = $args; + } +} diff --git a/app/Events/Report/GroupShowing.php b/app/Events/Report/GroupShowing.php new file mode 100644 index 00000000..7924a841 --- /dev/null +++ b/app/Events/Report/GroupShowing.php @@ -0,0 +1,20 @@ +class = $class; + } +} diff --git a/app/Events/Report/RowsShowing.php b/app/Events/Report/RowsShowing.php new file mode 100644 index 00000000..da16cf10 --- /dev/null +++ b/app/Events/Report/RowsShowing.php @@ -0,0 +1,20 @@ +class = $class; + } +} diff --git a/app/Events/Report/SkipClass.php b/app/Events/Report/SkipClass.php new file mode 100644 index 00000000..43b88545 --- /dev/null +++ b/app/Events/Report/SkipClass.php @@ -0,0 +1,20 @@ +classes = $classes; + } +} diff --git a/app/Events/Report/SkipRowsShowing.php b/app/Events/Report/SkipRowsShowing.php new file mode 100644 index 00000000..3f8b47f0 --- /dev/null +++ b/app/Events/Report/SkipRowsShowing.php @@ -0,0 +1,20 @@ +classes = $classes; + } +} diff --git a/app/Events/Report/TotalCalculated.php b/app/Events/Report/TotalCalculated.php new file mode 100644 index 00000000..b60085ac --- /dev/null +++ b/app/Events/Report/TotalCalculated.php @@ -0,0 +1,40 @@ +class = $class; + $this->model = $model; + $this->date_field = $date_field; + $this->check_type = $check_type; + $this->table = $table; + $this->with_tax = $with_tax; + } +} diff --git a/app/Events/Report/TotalCalculating.php b/app/Events/Report/TotalCalculating.php new file mode 100644 index 00000000..77fb3bba --- /dev/null +++ b/app/Events/Report/TotalCalculating.php @@ -0,0 +1,40 @@ +class = $class; + $this->model = $model; + $this->date_field = $date_field; + $this->check_type = $check_type; + $this->table = $table; + $this->with_tax = $with_tax; + } +} diff --git a/app/Events/Setting/CategoryCreated.php b/app/Events/Setting/CategoryCreated.php new file mode 100644 index 00000000..17fb4b9f --- /dev/null +++ b/app/Events/Setting/CategoryCreated.php @@ -0,0 +1,24 @@ +category = $category; + $this->request = $request; + } +} diff --git a/app/Events/Setting/CategoryCreating.php b/app/Events/Setting/CategoryCreating.php new file mode 100644 index 00000000..a71fe321 --- /dev/null +++ b/app/Events/Setting/CategoryCreating.php @@ -0,0 +1,22 @@ +request = $request; + } +} diff --git a/app/Events/Setting/CategoryDeleted.php b/app/Events/Setting/CategoryDeleted.php new file mode 100644 index 00000000..ff25aeea --- /dev/null +++ b/app/Events/Setting/CategoryDeleted.php @@ -0,0 +1,20 @@ +category = $category; + } +} diff --git a/app/Events/Setting/CategoryDeleting.php b/app/Events/Setting/CategoryDeleting.php new file mode 100644 index 00000000..479f23a4 --- /dev/null +++ b/app/Events/Setting/CategoryDeleting.php @@ -0,0 +1,20 @@ +category = $category; + } +} diff --git a/app/Events/Setting/CategoryUpdated.php b/app/Events/Setting/CategoryUpdated.php new file mode 100644 index 00000000..7262f3ec --- /dev/null +++ b/app/Events/Setting/CategoryUpdated.php @@ -0,0 +1,24 @@ +category = $category; + $this->request = $request; + } +} diff --git a/app/Events/Setting/CategoryUpdating.php b/app/Events/Setting/CategoryUpdating.php new file mode 100644 index 00000000..20a64424 --- /dev/null +++ b/app/Events/Setting/CategoryUpdating.php @@ -0,0 +1,24 @@ +category = $category; + $this->request = $request; + } +} diff --git a/app/Events/Setting/CurrencyCreated.php b/app/Events/Setting/CurrencyCreated.php new file mode 100644 index 00000000..ef286e3d --- /dev/null +++ b/app/Events/Setting/CurrencyCreated.php @@ -0,0 +1,24 @@ +currency = $currency; + $this->request = $request; + } +} diff --git a/app/Events/Setting/CurrencyCreating.php b/app/Events/Setting/CurrencyCreating.php new file mode 100644 index 00000000..554d47e3 --- /dev/null +++ b/app/Events/Setting/CurrencyCreating.php @@ -0,0 +1,20 @@ +request = $request; + } +} diff --git a/app/Events/Setting/CurrencyDeleted.php b/app/Events/Setting/CurrencyDeleted.php new file mode 100644 index 00000000..5e7290b5 --- /dev/null +++ b/app/Events/Setting/CurrencyDeleted.php @@ -0,0 +1,20 @@ +currency = $currency; + } +} diff --git a/app/Events/Setting/CurrencyDeleting.php b/app/Events/Setting/CurrencyDeleting.php new file mode 100644 index 00000000..7804091f --- /dev/null +++ b/app/Events/Setting/CurrencyDeleting.php @@ -0,0 +1,20 @@ +currency = $currency; + } +} diff --git a/app/Events/Setting/CurrencyUpdated.php b/app/Events/Setting/CurrencyUpdated.php new file mode 100644 index 00000000..124dd069 --- /dev/null +++ b/app/Events/Setting/CurrencyUpdated.php @@ -0,0 +1,24 @@ +currency = $currency; + $this->request = $request; + } +} diff --git a/app/Events/Setting/CurrencyUpdating.php b/app/Events/Setting/CurrencyUpdating.php new file mode 100644 index 00000000..0d405e70 --- /dev/null +++ b/app/Events/Setting/CurrencyUpdating.php @@ -0,0 +1,24 @@ +currency = $currency; + $this->request = $request; + } +} diff --git a/app/Events/Setting/SettingUpdated.php b/app/Events/Setting/SettingUpdated.php new file mode 100644 index 00000000..6bed2361 --- /dev/null +++ b/app/Events/Setting/SettingUpdated.php @@ -0,0 +1,20 @@ +request = $request; + } +} diff --git a/app/Events/Setting/SettingUpdating.php b/app/Events/Setting/SettingUpdating.php new file mode 100644 index 00000000..3bf677fe --- /dev/null +++ b/app/Events/Setting/SettingUpdating.php @@ -0,0 +1,20 @@ +request = $request; + } +} diff --git a/app/Events/Setting/TaxCreated.php b/app/Events/Setting/TaxCreated.php new file mode 100644 index 00000000..689127be --- /dev/null +++ b/app/Events/Setting/TaxCreated.php @@ -0,0 +1,24 @@ +tax = $tax; + $this->request = $request; + } +} diff --git a/app/Events/Setting/TaxCreating.php b/app/Events/Setting/TaxCreating.php new file mode 100644 index 00000000..b0df2e9a --- /dev/null +++ b/app/Events/Setting/TaxCreating.php @@ -0,0 +1,20 @@ +request = $request; + } +} diff --git a/app/Events/Setting/TaxDeleted.php b/app/Events/Setting/TaxDeleted.php new file mode 100644 index 00000000..9feb91a4 --- /dev/null +++ b/app/Events/Setting/TaxDeleted.php @@ -0,0 +1,20 @@ +tax = $tax; + } +} diff --git a/app/Events/Setting/TaxDeleting.php b/app/Events/Setting/TaxDeleting.php new file mode 100644 index 00000000..fc69ecdc --- /dev/null +++ b/app/Events/Setting/TaxDeleting.php @@ -0,0 +1,20 @@ +tax = $tax; + } +} diff --git a/app/Events/Setting/TaxUpdated.php b/app/Events/Setting/TaxUpdated.php new file mode 100644 index 00000000..6745d962 --- /dev/null +++ b/app/Events/Setting/TaxUpdated.php @@ -0,0 +1,24 @@ +tax = $tax; + $this->request = $request; + } +} diff --git a/app/Events/Setting/TaxUpdating.php b/app/Events/Setting/TaxUpdating.php new file mode 100644 index 00000000..fe743320 --- /dev/null +++ b/app/Events/Setting/TaxUpdating.php @@ -0,0 +1,24 @@ +tax = $tax; + $this->request = $request; + } +} diff --git a/app/Events/Widget/ClassesCreated.php b/app/Events/Widget/ClassesCreated.php new file mode 100644 index 00000000..e6c90996 --- /dev/null +++ b/app/Events/Widget/ClassesCreated.php @@ -0,0 +1,20 @@ +list = $list; + } +} diff --git a/app/Exceptions/Common/LastDashboard.php b/app/Exceptions/Common/LastDashboard.php new file mode 100644 index 00000000..550fe05e --- /dev/null +++ b/app/Exceptions/Common/LastDashboard.php @@ -0,0 +1,10 @@ +, \Psr\Log\LogLevel::*> + */ + protected $levels = [ + // + ]; + + /** + * A list of the exception types that are not reported. + * + * @var array> + */ + protected $dontReport = [ + // + ]; + + /** + * A list of the inputs that are never flashed to the session on validation exceptions. + * + * @var array + */ + protected $dontFlash = [ + 'current_password', + 'password', + 'password_confirmation', + ]; + + /** + * Register the exception handling callbacks for the application. + * + * @return void + */ + public function register() + { + $this->reportable(function (Throwable $e) { + if (config('logging.default') == 'bugsnag') { + call_user_func(config('bugsnag.before_send'), $e); + } + + if (config('app.error_tracker') === 'sentry') { + \Sentry\Laravel\Integration::captureUnhandledException($e); + } + }); + } + + /** + * Report or log an exception. + * + * @param \Throwable $exception + * @return void + * + * @throws \Exception + */ + public function report(Throwable $exception) + { + if ($exception instanceof MailerHttpTransportException) { + $email = $this->handleMailerExceptions($exception); + + if (! empty($email)) { + return; + } + } + + parent::report($exception); + } + + /** + * Render an exception into an HTTP response. + * + * @param \Illuminate\Http\Request $request + * @param \Throwable $exception + * @return \Symfony\Component\HttpFoundation\Response + * + * @throws \Throwable + */ + public function render($request, Throwable $exception) + { + if (request_is_api($request)) { + return $this->handleApiExceptions($request, $exception); + } + + if (config('app.debug') === false) { + return $this->handleWebExceptions($request, $exception); + } + + return parent::render($request, $exception); + } + + /** + * Convert an authentication exception into a response. + * + * @param \Illuminate\Http\Request $request + * @param \Illuminate\Auth\AuthenticationException $exception + * @return \Symfony\Component\HttpFoundation\Response + */ + protected function unauthenticated($request, AuthenticationException $exception) + { + // Store the current url in the session (fullUrl includes query string) + if ($request->fullUrl() !== config('app.url')) { + session(['url.intended' => $request->fullUrl()]); + } + + return $request->expectsJson() + ? response()->json(['message' => $exception->getMessage()], 401) + : redirect()->to($exception->redirectTo() ?? route('login')); + } + + protected function handleWebExceptions($request, $exception) + { + if ($exception instanceof NotFoundHttpException) { + // ajax 404 json feedback + if ($request->ajax()) { + return response()->json([ + 'error' => trans('errors.header.404'), + ], 404); + } + + flash(trans('errors.body.page_not_found'))->error()->important(); + + // normal 404 view page feedback + return redirect() + ->back() + ->withErrors(['msg', trans('errors.body.page_not_found')]); + } + + if ($exception instanceof ModelNotFoundException) { + // ajax 404 json feedback + if ($request->ajax()) { + return response()->json([ + 'error' => trans('errors.header.404'), + ], 404); + } + + try { + $names = explode('.', $request->route()->getName()); + $names[count($names) - 1] = 'index'; + + $route = route(implode('.', $names)); + + flash(trans('errors.message.record'))->warning()->important(); + + return redirect($route); + } catch (\Exception $e) { + // normal 404 view page feedback + return response()->view('errors.404', [], 404); + } + } + + if ($exception instanceof FatalError) { + // ajax 500 json feedback + if ($request->ajax()) { + return response()->json([ + 'error' => trans('errors.header.500'), + ], 500); + } + + // normal 500 view page feedback + return response()->view('errors.500', [], 500); + } + + if ($exception instanceof ThrottleRequestsException) { + // ajax 500 json feedback + if ($request->ajax()) { + return response()->json([ + 'error' => $exception->getMessage(), + ], 429); + } + } + + if ($exception instanceof ViewException) { + $real_exception = $this->getRealException($exception, ViewException::class); + + if ($real_exception instanceof UnexpectedAmountException) { + if ($request->ajax()) { + return response()->json([ + 'error' => trans('errors.message.amount'), + ], 500); + } + + return response()->view('errors.500', [ + 'message' => trans('errors.message.amount'), + ], 500); + } + } + + if ($exception instanceof MailerHttpTransportException) { + $email = $this->handleMailerExceptions($exception); + + if (! empty($email)) { + $message = trans('notifications.menu.invalid_email.description', ['email' => $email]); + + if ($request->ajax()) { + return response()->json([ + 'error' => $message, + ], $exception->getCode()); + } + + return response()->view('errors.403', [ + 'message' => $message, + ], $exception->getCode()); + } + } + + return parent::render($request, $exception); + } + + protected function handleApiExceptions($request, $exception): Response + { + $replacements = $this->prepareApiReplacements($exception); + + $response = config('api.error_format'); + + array_walk_recursive($response, function (&$value, $key) use ($replacements) { + if (Str::startsWith($value, ':') && isset($replacements[$value])) { + $value = $replacements[$value]; + } + }); + + $response = $this->recursivelyRemoveEmptyApiReplacements($response); + + return new Response($response, $this->getStatusCode($exception), $this->getHeaders($exception)); + } + + protected function handleMailerExceptions(MailerHttpTransportException $exception): string + { + /** + * Couldn't access the SentMessage object to get the email address + * https://symfony.com/doc/current/mailer.html#debugging-emails + * + * https://codespeedy.com/extract-email-addresses-from-a-string-in-php + * https://phpliveregex.com/p/IMG + */ + preg_match("/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}/", $exception->getMessage(), $matches); + + if (empty($matches[0])) { + return ''; + } + + $email = $matches[0]; + + event(new InvalidEmailDetected($email, $exception->getMessage())); + + return $email; + } + + /** + * Prepare the replacements array by gathering the keys and values. + * + * @param Throwable $exception + * + * @return array + */ + protected function prepareApiReplacements(Throwable $exception): array + { + $code = $this->getStatusCode($exception); + + if (! $message = $exception->getMessage()) { + $message = sprintf('%d %s', $code, Response::$statusTexts[$code]); + } + + $replacements = [ + ':message' => $message, + ':status_code' => $code, + ]; + + if ($exception instanceof ResourceException && $exception->hasErrors()) { + $replacements[':errors'] = $exception->getErrors(); + } + + if ($exception instanceof ValidationException) { + $replacements[':errors'] = $exception->errors(); + $replacements[':status_code'] = $exception->status; + } + + if ($code = $exception->getCode()) { + $replacements[':code'] = $code; + } + + if (config('api.debug')) { + $replacements[':debug'] = [ + 'line' => $exception->getLine(), + 'file' => $exception->getFile(), + 'class' => get_class($exception), + 'trace' => explode("\n", $exception->getTraceAsString()), + ]; + + // Attach trace of previous exception, if exists + if (! is_null($exception->getPrevious())) { + $currentTrace = $replacements[':debug']['trace']; + + $replacements[':debug']['trace'] = [ + 'previous' => explode("\n", $exception->getPrevious()->getTraceAsString()), + 'current' => $currentTrace, + ]; + } + } + + return $replacements; + } + + /** + * Recursively remove any empty replacement values in the response array. + * + * @param array $input + * + * @return array + */ + protected function recursivelyRemoveEmptyApiReplacements(array $input) + { + foreach ($input as &$value) { + if (is_array($value)) { + $value = $this->recursivelyRemoveEmptyApiReplacements($value); + } + } + + return array_filter($input, function ($value) { + if (is_string($value)) { + return ! Str::startsWith($value, ':'); + } + + return true; + }); + } + + /** + * Get the status code from the exception. + * + * @param Throwable $exception + * + * @return int + */ + protected function getStatusCode(Throwable $exception): int + { + $code = null; + + if ($exception instanceof ValidationException) { + $code = $exception->status; + } elseif ($exception instanceof HttpExceptionInterface) { + $code = $exception->getStatusCode(); + } else { + // By default throw 500 + $code = 500; + } + + // Be extra defensive + if ($code < 100 || $code > 599) { + $code = 500; + } + + return $code; + } + + /** + * Get the headers from the exception. + */ + protected function getHeaders(Throwable $exception): array + { + return ($exception instanceof HttpExceptionInterface) + ? $exception->getHeaders() + : []; + } + + /** + * Get the real exception. + */ + protected function getRealException(Throwable $exception, string $current): Throwable + { + $previous = $exception->getPrevious() ?? $exception; + + while (($previous instanceof $current) && $previous->getPrevious()) { + $previous = $previous->getPrevious(); + } + + return $previous; + } +} diff --git a/app/Exceptions/Http/Resource.php b/app/Exceptions/Http/Resource.php new file mode 100644 index 00000000..44b0957d --- /dev/null +++ b/app/Exceptions/Http/Resource.php @@ -0,0 +1,59 @@ +errors = new MessageBag; + } else { + $this->errors = is_array($errors) ? new MessageBag($errors) : $errors; + } + + parent::__construct(422, $message, $previous, $headers, $code); + } + + /** + * Get the errors message bag. + * + * @return \Illuminate\Support\MessageBag + */ + public function getErrors() + { + return $this->errors; + } + + /** + * Determine if message bag has any errors. + * + * @return bool + */ + public function hasErrors() + { + return ! $this->errors->isEmpty(); + } +} diff --git a/app/Exceptions/Settings/LastCategoryDelete.php b/app/Exceptions/Settings/LastCategoryDelete.php new file mode 100644 index 00000000..ff8ea9c2 --- /dev/null +++ b/app/Exceptions/Settings/LastCategoryDelete.php @@ -0,0 +1,22 @@ + Akaunting charecters + 800 => Setting category menu number + 40 => CRUD (last item) + 1 => First authorized exception + + Code: 100000841 + */ + public function __construct(string $message = '', int $code = 100000841, Throwable|null $previous = null) + { + parent::__construct($message, $code, $previous); + } +} diff --git a/app/Exceptions/Trackers/Bugsnag.php b/app/Exceptions/Trackers/Bugsnag.php new file mode 100644 index 00000000..7e5e0e49 --- /dev/null +++ b/app/Exceptions/Trackers/Bugsnag.php @@ -0,0 +1,25 @@ +setAppVersion(Version::short()); + + $tags = static::getTrackerTags(); + + app('bugsnag')->registerCallback(function ($report) use($tags) { + $report->setMetaData([ + 'akaunting' => $tags + ]); + }); + } +} diff --git a/app/Exceptions/Trackers/Sentry.php b/app/Exceptions/Trackers/Sentry.php new file mode 100644 index 00000000..5305e935 --- /dev/null +++ b/app/Exceptions/Trackers/Sentry.php @@ -0,0 +1,66 @@ +setRelease(Version::short()); + + $tags = static::getTrackerTags(); + + $event->setTags($tags); + + return $event; + } + + public static function beforeSendLog(Log $log): ?Log + { + $log->setAttribute('release', Version::short()); + + $tags = static::getTrackerTags(); + + foreach ($tags as $key => $value) { + $log->setAttribute($key, $value); + } + + return $log; + } + + public static function tracesSampler(SamplingContext $context): float + { + if (static::shouldFilterAgent()) { + return 0.0; + } + + return config('sentry.traces_sample_rate'); + } + + public static function shouldFilterAgent(): bool + { + $user_agent = request()->userAgent(); + + $filter_agents = explode(',', env('SENTRY_TRACES_FILTER_AGENTS')); + + foreach ($filter_agents as $filter_agent) { + if (! Str::contains($user_agent, $filter_agent)) { + continue; + } + + return true; + } + + return false; + } +} diff --git a/app/Exports/Banking/RecurringTransactions.php b/app/Exports/Banking/RecurringTransactions.php new file mode 100644 index 00000000..87d51745 --- /dev/null +++ b/app/Exports/Banking/RecurringTransactions.php @@ -0,0 +1,28 @@ +ids = $ids; + } + + public function sheets(): array + { + return [ + new Recurring($this->ids), + new Base($this->ids), + ]; + } +} diff --git a/app/Exports/Banking/Sheets/Recurring.php b/app/Exports/Banking/Sheets/Recurring.php new file mode 100644 index 00000000..b57e5128 --- /dev/null +++ b/app/Exports/Banking/Sheets/Recurring.php @@ -0,0 +1,37 @@ +cursor(); + } + + public function map($model): array + { + $model->transaction_number = $model->recurable->number; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'recurable_type', + 'transaction_number', + 'frequency', + 'interval', + 'started_at', + 'limit_date', + 'status', + 'limit_by', + 'limit_count', + 'auto_send', + ]; + } +} diff --git a/app/Exports/Banking/Sheets/RecurringTransactions.php b/app/Exports/Banking/Sheets/RecurringTransactions.php new file mode 100644 index 00000000..43ee656b --- /dev/null +++ b/app/Exports/Banking/Sheets/RecurringTransactions.php @@ -0,0 +1,54 @@ +isRecurring()->cursor(); + } + + public function map($model): array + { + $model->account_name = $model->account->name; + $model->contact_email = $model->contact->email; + $model->category_name = $model->category->name; + $model->invoice_bill_number = $model->document->document_number ?? 0; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'type', + 'number', + 'paid_at', + 'amount', + 'currency_code', + 'currency_rate', + 'account_name', + 'invoice_bill_number', + 'contact_email', + 'category_name', + 'description', + 'payment_method', + 'reference', + 'reconciled', + ]; + } + + public function columnFormats(): array + { + return [ + 'C' => NumberFormat::FORMAT_DATE_YYYYMMDD, + ]; + } +} diff --git a/app/Exports/Banking/Transactions.php b/app/Exports/Banking/Transactions.php new file mode 100644 index 00000000..7fb7668d --- /dev/null +++ b/app/Exports/Banking/Transactions.php @@ -0,0 +1,77 @@ +collectForExport($this->ids, ['paid_at' => 'desc']); + } + + public function map($model): array + { + $model->account_name = $model->account->name; + $model->contact_email = $model->contact->email; + $model->category_name = $model->category->name; + $model->invoice_bill_number = $model->document->document_number ?? 0; + $model->parent_number = Model::isRecurring()->find($model->parent_id)?->number; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'type', + 'number', + 'paid_at', + 'amount', + 'currency_code', + 'currency_rate', + 'account_name', + 'invoice_bill_number', + 'contact_email', + 'category_name', + 'description', + 'payment_method', + 'reference', + 'reconciled', + 'parent_number', + ]; + } + + public function columnValidations(): array + { + return [ + 'type' => [ + 'options' => array_keys(config('type.transaction')) + ], + // 'paid_at' => [ + // 'type' => DataValidation::TYPE_NONE, + // 'prompt_title' => trans('general.validation_warning'), + // 'prompt' => trans('validation.date_format', ['attribute' => 'paid_at', 'format' => 'yyyy-mm-dd']), + // 'hide_error' => true, + // ], + // 'contact_email' => [ + // 'options' => $this->getDropdownOptions(Contact::class, 'email'), + // ] + ]; + } + + public function columnFormats(): array + { + return [ + 'C' => NumberFormat::FORMAT_DATE_YYYYMMDD, + ]; + } +} diff --git a/app/Exports/Banking/Transfers.php b/app/Exports/Banking/Transfers.php new file mode 100644 index 00000000..2bcd522e --- /dev/null +++ b/app/Exports/Banking/Transfers.php @@ -0,0 +1,66 @@ +collectForExport($this->ids); + } + + public function map($model): array + { + $model->transferred_at = Date::parse($model->expense_transaction->paid_at)->format('Y-m-d'); + $model->amount = $model->expense_transaction->amount; + $model->from_account_name = $model->expense_transaction->account->name; + $model->from_currency_code = $model->expense_transaction->currency_code; + $model->from_currency_rate = $model->expense_transaction->currency_rate; + $model->to_account_name = $model->income_transaction->account->name; + $model->to_currency_code = $model->income_transaction->currency_code; + $model->to_currency_rate = $model->income_transaction->currency_rate; + $model->description = $model->income_transaction->description; + $model->payment_method = $model->income_transaction->payment_method; + $model->reference = $model->income_transaction->reference; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'transferred_at', + 'amount', + 'from_currency_code', + 'from_currency_rate', + 'from_account_name', + 'to_currency_code', + 'to_currency_rate', + 'to_account_name', + 'description', + 'payment_method', + 'reference', + ]; + } + + public function columnFormats(): array + { + return [ + 'A' => NumberFormat::FORMAT_DATE_YYYYMMDD, + ]; + } +} diff --git a/app/Exports/Common/Items.php b/app/Exports/Common/Items.php new file mode 100644 index 00000000..7f83a157 --- /dev/null +++ b/app/Exports/Common/Items.php @@ -0,0 +1,28 @@ +ids = $ids; + } + + public function sheets(): array + { + return [ + new Base($this->ids), + new ItemTaxes($this->ids), + ]; + } +} diff --git a/app/Exports/Common/Reports.php b/app/Exports/Common/Reports.php new file mode 100644 index 00000000..482ebc69 --- /dev/null +++ b/app/Exports/Common/Reports.php @@ -0,0 +1,34 @@ +view = $view; + $this->class = $class; + } + + public function view(): View + { + return view($this->view, ['class' => $this->class, 'print' => true]); + } + + public function title(): string + { + return 'reports'; + } +} diff --git a/app/Exports/Common/Sheets/ItemTaxes.php b/app/Exports/Common/Sheets/ItemTaxes.php new file mode 100644 index 00000000..3cfd27f7 --- /dev/null +++ b/app/Exports/Common/Sheets/ItemTaxes.php @@ -0,0 +1,40 @@ +collectForExport($this->ids, null, 'item_id'); + } + + public function map($model): array + { + $item = $model->item; + + if (empty($item)) { + return []; + } + + $model->item_name = $model->item->name; + $model->tax_rate = $model->tax->rate; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'item_name', + 'tax_rate', + ]; + } +} diff --git a/app/Exports/Common/Sheets/Items.php b/app/Exports/Common/Sheets/Items.php new file mode 100644 index 00000000..891232ed --- /dev/null +++ b/app/Exports/Common/Sheets/Items.php @@ -0,0 +1,37 @@ +collectForExport($this->ids); + } + + public function map($model): array + { + $model->category_name = $model->category->name; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'name', + 'type', + 'description', + 'sale_price', + 'purchase_price', + 'category_name', + 'enabled', + ]; + } +} diff --git a/app/Exports/Purchases/Bills/Bills.php b/app/Exports/Purchases/Bills/Bills.php new file mode 100644 index 00000000..dd4213e5 --- /dev/null +++ b/app/Exports/Purchases/Bills/Bills.php @@ -0,0 +1,36 @@ +ids = $ids; + } + + public function sheets(): array + { + return [ + new Base($this->ids), + new BillItems($this->ids), + new BillItemTaxes($this->ids), + new BillHistories($this->ids), + new BillTotals($this->ids), + new BillTransactions($this->ids), + ]; + } +} diff --git a/app/Exports/Purchases/Bills/Sheets/BillHistories.php b/app/Exports/Purchases/Bills/Sheets/BillHistories.php new file mode 100644 index 00000000..8a5c0d97 --- /dev/null +++ b/app/Exports/Purchases/Bills/Sheets/BillHistories.php @@ -0,0 +1,38 @@ +bill()->collectForExport($this->ids, null, 'document_id'); + } + + public function map($model): array + { + $document = $model->document; + + if (empty($document)) { + return []; + } + + $model->bill_number = $document->document_number; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'bill_number', + 'status', + 'notify', + 'description', + ]; + } +} diff --git a/app/Exports/Purchases/Bills/Sheets/BillItemTaxes.php b/app/Exports/Purchases/Bills/Sheets/BillItemTaxes.php new file mode 100644 index 00000000..63a45a89 --- /dev/null +++ b/app/Exports/Purchases/Bills/Sheets/BillItemTaxes.php @@ -0,0 +1,42 @@ +bill()->collectForExport($this->ids, null, 'document_id'); + } + + public function map($model): array + { + $document = $model->document; + + if (empty($document)) { + return []; + } + + $model->bill_number = $document->document_number; + $model->item_name = $model->item->name; + $model->tax_name = $model->tax->name; + $model->tax_rate = $model->tax->rate; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'bill_number', + 'item_name', + 'tax_name', + 'tax_rate', + 'amount', + ]; + } +} diff --git a/app/Exports/Purchases/Bills/Sheets/BillItems.php b/app/Exports/Purchases/Bills/Sheets/BillItems.php new file mode 100644 index 00000000..28dcf4cd --- /dev/null +++ b/app/Exports/Purchases/Bills/Sheets/BillItems.php @@ -0,0 +1,47 @@ +bill()->collectForExport($this->ids, null, 'document_id'); + } + + public function map($model): array + { + $document = $model->document; + + if (empty($document)) { + return []; + } + + $model->bill_number = $document->document_number; + $model->item_name = $model->item->name; + $model->item_description = $model->item->description; + $model->item_type = $model->item->type; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'bill_number', + 'item_name', + 'item_description', + 'item_type', + 'quantity', + 'discount_type', + 'discount_rate', + 'price', + 'total', + 'tax', + ]; + } +} diff --git a/app/Exports/Purchases/Bills/Sheets/BillTotals.php b/app/Exports/Purchases/Bills/Sheets/BillTotals.php new file mode 100644 index 00000000..13c10a2a --- /dev/null +++ b/app/Exports/Purchases/Bills/Sheets/BillTotals.php @@ -0,0 +1,39 @@ +bill()->collectForExport($this->ids, null, 'document_id'); + } + + public function map($model): array + { + $document = $model->document; + + if (empty($document)) { + return []; + } + + $model->bill_number = $document->document_number; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'bill_number', + 'code', + 'name', + 'amount', + 'sort_order', + ]; + } +} diff --git a/app/Exports/Purchases/Bills/Sheets/BillTransactions.php b/app/Exports/Purchases/Bills/Sheets/BillTransactions.php new file mode 100644 index 00000000..f152a60c --- /dev/null +++ b/app/Exports/Purchases/Bills/Sheets/BillTransactions.php @@ -0,0 +1,60 @@ +expense()->isDocument()->collectForExport($this->ids, ['paid_at' => 'desc'], 'document_id'); + } + + public function map($model): array + { + $document = $model->document; + + if (empty($document)) { + return []; + } + + $model->bill_number = $document->document_number; + $model->account_name = $model->account->name; + $model->category_name = $model->category->name; + $model->contact_email = $model->contact->email; + $model->transaction_number = $model->number; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'bill_number', + 'transaction_number', + 'paid_at', + 'amount', + 'currency_code', + 'currency_rate', + 'account_name', + 'contact_email', + 'category_name', + 'description', + 'payment_method', + 'reference', + 'reconciled', + ]; + } + + public function columnFormats(): array + { + return [ + 'C' => NumberFormat::FORMAT_DATE_YYYYMMDD, + ]; + } +} diff --git a/app/Exports/Purchases/Bills/Sheets/Bills.php b/app/Exports/Purchases/Bills/Sheets/Bills.php new file mode 100644 index 00000000..5cfaf362 --- /dev/null +++ b/app/Exports/Purchases/Bills/Sheets/Bills.php @@ -0,0 +1,73 @@ +bill()->collectForExport($this->ids, ['document_number' => 'desc']); + } + + public function map($model): array + { + $country = null; + + if ($model->contact_country && array_key_exists($model->contact_country, trans('countries'))) { + $country = trans('countries.' . $model->contact_country); + } + + $model->category_name = $model->category->name; + $model->bill_number = $model->document_number; + $model->billed_at = $model->issued_at; + $model->contact_country = $country; + $model->parent_number = Model::billRecurring()->find($model->parent_id)?->document_number; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'bill_number', + 'order_number', + 'status', + 'billed_at', + 'due_at', + 'amount', + 'discount_type', + 'discount_rate', + 'currency_code', + 'currency_rate', + 'category_name', + 'contact_name', + 'contact_email', + 'contact_tax_number', + 'contact_phone', + 'contact_address', + 'contact_country', + 'contact_state', + 'contact_zip_code', + 'contact_city', + 'title', + 'subheading', + 'notes', + 'template', + 'color', + 'parent_number' + ]; + } + + public function columnFormats(): array + { + return [ + 'D' => NumberFormat::FORMAT_DATE_YYYYMMDD, + 'E' => NumberFormat::FORMAT_DATE_YYYYMMDD, + ]; + } +} diff --git a/app/Exports/Purchases/RecurringBills/RecurringBills.php b/app/Exports/Purchases/RecurringBills/RecurringBills.php new file mode 100644 index 00000000..ef820156 --- /dev/null +++ b/app/Exports/Purchases/RecurringBills/RecurringBills.php @@ -0,0 +1,36 @@ +ids = $ids; + } + + public function sheets(): array + { + return [ + new Recurring($this->ids), + new Base($this->ids), + new RecurringBillItems($this->ids), + new RecurringBillItemTaxes($this->ids), + new RecurringBillHistories($this->ids), + new RecurringBillTotals($this->ids), + ]; + } +} diff --git a/app/Exports/Purchases/RecurringBills/Sheets/Recurring.php b/app/Exports/Purchases/RecurringBills/Sheets/Recurring.php new file mode 100644 index 00000000..ba15b565 --- /dev/null +++ b/app/Exports/Purchases/RecurringBills/Sheets/Recurring.php @@ -0,0 +1,37 @@ +cursor(); + } + + public function map($model): array + { + $model->bill_number = $model->recurable->document_number; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'recurable_type', + 'bill_number', + 'frequency', + 'interval', + 'started_at', + 'limit_date', + 'status', + 'limit_by', + 'limit_count', + 'auto_send', + ]; + } +} diff --git a/app/Exports/Purchases/RecurringBills/Sheets/RecurringBillHistories.php b/app/Exports/Purchases/RecurringBills/Sheets/RecurringBillHistories.php new file mode 100644 index 00000000..21d9185a --- /dev/null +++ b/app/Exports/Purchases/RecurringBills/Sheets/RecurringBillHistories.php @@ -0,0 +1,38 @@ +billRecurring()->collectForExport($this->ids, null, 'document_id'); + } + + public function map($model): array + { + $document = $model->document; + + if (empty($document)) { + return []; + } + + $model->bill_number = $document->document_number; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'bill_number', + 'status', + 'notify', + 'description', + ]; + } +} diff --git a/app/Exports/Purchases/RecurringBills/Sheets/RecurringBillItemTaxes.php b/app/Exports/Purchases/RecurringBills/Sheets/RecurringBillItemTaxes.php new file mode 100644 index 00000000..8c6528ad --- /dev/null +++ b/app/Exports/Purchases/RecurringBills/Sheets/RecurringBillItemTaxes.php @@ -0,0 +1,42 @@ +billRecurring()->collectForExport($this->ids, null, 'document_id'); + } + + public function map($model): array + { + $document = $model->document; + + if (empty($document)) { + return []; + } + + $model->bill_number = $document->document_number; + $model->item_name = $model->item->name; + $model->tax_name = $model->tax->name; + $model->tax_rate = $model->tax->rate; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'bill_number', + 'item_name', + 'tax_name', + 'tax_rate', + 'amount', + ]; + } +} diff --git a/app/Exports/Purchases/RecurringBills/Sheets/RecurringBillItems.php b/app/Exports/Purchases/RecurringBills/Sheets/RecurringBillItems.php new file mode 100644 index 00000000..33678e79 --- /dev/null +++ b/app/Exports/Purchases/RecurringBills/Sheets/RecurringBillItems.php @@ -0,0 +1,47 @@ +billRecurring()->collectForExport($this->ids, null, 'document_id'); + } + + public function map($model): array + { + $document = $model->document; + + if (empty($document)) { + return []; + } + + $model->bill_number = $document->document_number; + $model->item_name = $model->item->name; + $model->item_description = $model->item->description; + $model->item_type = $model->item->type; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'bill_number', + 'item_name', + 'item_description', + 'item_type', + 'quantity', + 'discount_type', + 'discount_rate', + 'price', + 'total', + 'tax', + ]; + } +} diff --git a/app/Exports/Purchases/RecurringBills/Sheets/RecurringBillTotals.php b/app/Exports/Purchases/RecurringBills/Sheets/RecurringBillTotals.php new file mode 100644 index 00000000..a0f5fb6c --- /dev/null +++ b/app/Exports/Purchases/RecurringBills/Sheets/RecurringBillTotals.php @@ -0,0 +1,39 @@ +billRecurring()->collectForExport($this->ids, null, 'document_id'); + } + + public function map($model): array + { + $document = $model->document; + + if (empty($document)) { + return []; + } + + $model->bill_number = $document->document_number; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'bill_number', + 'code', + 'name', + 'amount', + 'sort_order', + ]; + } +} diff --git a/app/Exports/Purchases/RecurringBills/Sheets/RecurringBills.php b/app/Exports/Purchases/RecurringBills/Sheets/RecurringBills.php new file mode 100644 index 00000000..13b02547 --- /dev/null +++ b/app/Exports/Purchases/RecurringBills/Sheets/RecurringBills.php @@ -0,0 +1,72 @@ +billRecurring()->collectForExport($this->ids, ['document_number' => 'desc']); + } + + public function map($model): array + { + $country = null; + + if ($model->contact_country && array_key_exists($model->contact_country, trans('countries'))) { + $country = trans('countries.' . $model->contact_country); + } + + $model->category_name = $model->category->name; + $model->bill_number = $model->document_number; + $model->billed_at = $model->issued_at; + $model->contact_country = $country; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'bill_number', + 'order_number', + 'status', + 'billed_at', + 'due_at', + 'amount', + 'discount_type', + 'discount_rate', + 'currency_code', + 'currency_rate', + 'category_name', + 'contact_name', + 'contact_email', + 'contact_tax_number', + 'contact_phone', + 'contact_address', + 'contact_country', + 'contact_state', + 'contact_zip_code', + 'contact_city', + 'title', + 'subheading', + 'notes', + 'template', + 'color', + ]; + } + + public function columnFormats(): array + { + return [ + 'D' => NumberFormat::FORMAT_DATE_YYYYMMDD, + 'E' => NumberFormat::FORMAT_DATE_YYYYMMDD, + ]; + } +} diff --git a/app/Exports/Purchases/Vendors.php b/app/Exports/Purchases/Vendors.php new file mode 100644 index 00000000..8822cf18 --- /dev/null +++ b/app/Exports/Purchases/Vendors.php @@ -0,0 +1,48 @@ +vendor()->collectForExport($this->ids); + } + + public function map($model): array + { + $country = null; + + if ($model->country && array_key_exists($model->country, trans('countries'))) { + $country = trans('countries.' . $model->country); + } + + $model->country = $country; + $model->category_name = $model->category->name ?? null; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'name', + 'email', + 'tax_number', + 'phone', + 'address', + 'country', + 'state', + 'zip_code', + 'city', + 'website', + 'currency_code', + 'category_name', + 'reference', + 'enabled', + ]; + } +} diff --git a/app/Exports/Sales/Customers.php b/app/Exports/Sales/Customers.php new file mode 100644 index 00000000..3c8b3a36 --- /dev/null +++ b/app/Exports/Sales/Customers.php @@ -0,0 +1,51 @@ +customer()->collectForExport($this->ids); + } + + public function map($model): array + { + $country = null; + + if ($model->country && array_key_exists($model->country, trans('countries'))) { + $country = trans('countries.' . $model->country); + } + + $model->country = $country; + $model->category_name = $model->category->name ?? null; + + $model->can_login = $model->user_id ? true : false; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'name', + 'email', + 'tax_number', + 'phone', + 'address', + 'country', + 'state', + 'zip_code', + 'city', + 'website', + 'currency_code', + 'category_name', + 'reference', + 'enabled', + 'can_login', + ]; + } +} diff --git a/app/Exports/Sales/Invoices/Invoices.php b/app/Exports/Sales/Invoices/Invoices.php new file mode 100644 index 00000000..d24ba473 --- /dev/null +++ b/app/Exports/Sales/Invoices/Invoices.php @@ -0,0 +1,36 @@ +ids = $ids; + } + + public function sheets(): array + { + return [ + new Base($this->ids), + new InvoiceItems($this->ids), + new InvoiceItemTaxes($this->ids), + new InvoiceHistories($this->ids), + new InvoiceTotals($this->ids), + new InvoiceTransactions($this->ids), + ]; + } +} diff --git a/app/Exports/Sales/Invoices/Sheets/InvoiceHistories.php b/app/Exports/Sales/Invoices/Sheets/InvoiceHistories.php new file mode 100644 index 00000000..4e2a8fb0 --- /dev/null +++ b/app/Exports/Sales/Invoices/Sheets/InvoiceHistories.php @@ -0,0 +1,38 @@ +invoice()->collectForExport($this->ids, null, 'document_id'); + } + + public function map($model): array + { + $document = $model->document; + + if (empty($document)) { + return []; + } + + $model->invoice_number = $document->document_number; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'invoice_number', + 'status', + 'notify', + 'description', + ]; + } +} diff --git a/app/Exports/Sales/Invoices/Sheets/InvoiceItemTaxes.php b/app/Exports/Sales/Invoices/Sheets/InvoiceItemTaxes.php new file mode 100644 index 00000000..2f22d87a --- /dev/null +++ b/app/Exports/Sales/Invoices/Sheets/InvoiceItemTaxes.php @@ -0,0 +1,42 @@ +invoice()->collectForExport($this->ids, null, 'document_id'); + } + + public function map($model): array + { + $document = $model->document; + + if (empty($document)) { + return []; + } + + $model->invoice_number = $document->document_number; + $model->item_name = $model->item->name; + $model->tax_name = $model->tax->name; + $model->tax_rate = $model->tax->rate; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'invoice_number', + 'item_name', + 'tax_name', + 'tax_rate', + 'amount', + ]; + } +} diff --git a/app/Exports/Sales/Invoices/Sheets/InvoiceItems.php b/app/Exports/Sales/Invoices/Sheets/InvoiceItems.php new file mode 100644 index 00000000..66160532 --- /dev/null +++ b/app/Exports/Sales/Invoices/Sheets/InvoiceItems.php @@ -0,0 +1,47 @@ +invoice()->collectForExport($this->ids, null, 'document_id'); + } + + public function map($model): array + { + $document = $model->document; + + if (empty($document)) { + return []; + } + + $model->invoice_number = $document->document_number; + $model->item_name = $model->item->name; + $model->item_description = $model->item->description; + $model->item_type = $model->item->type; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'invoice_number', + 'item_name', + 'item_description', + 'item_type', + 'quantity', + 'discount_type', + 'discount_rate', + 'price', + 'total', + 'tax', + ]; + } +} diff --git a/app/Exports/Sales/Invoices/Sheets/InvoiceTotals.php b/app/Exports/Sales/Invoices/Sheets/InvoiceTotals.php new file mode 100644 index 00000000..a838d2f6 --- /dev/null +++ b/app/Exports/Sales/Invoices/Sheets/InvoiceTotals.php @@ -0,0 +1,40 @@ +invoice()->collectForExport($this->ids, null, 'document_id'); + } + + public function map($model): array + { + $document = $model->document; + + if (empty($document)) { + return []; + } + + $model->invoice_number = $document->document_number; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'invoice_number', + 'code', + 'name', + 'amount', + 'sort_order', + ]; + } +} diff --git a/app/Exports/Sales/Invoices/Sheets/InvoiceTransactions.php b/app/Exports/Sales/Invoices/Sheets/InvoiceTransactions.php new file mode 100644 index 00000000..d3a6cc59 --- /dev/null +++ b/app/Exports/Sales/Invoices/Sheets/InvoiceTransactions.php @@ -0,0 +1,60 @@ +income()->isDocument()->collectForExport($this->ids, ['paid_at' => 'desc'], 'document_id'); + } + + public function map($model): array + { + $document = $model->document; + + if (empty($document)) { + return []; + } + + $model->invoice_number = $document->document_number; + $model->account_name = $model->account->name; + $model->category_name = $model->category->name; + $model->contact_email = $model->contact->email; + $model->transaction_number = $model->number; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'invoice_number', + 'transaction_number', + 'paid_at', + 'amount', + 'currency_code', + 'currency_rate', + 'account_name', + 'contact_email', + 'category_name', + 'description', + 'payment_method', + 'reference', + 'reconciled', + ]; + } + + public function columnFormats(): array + { + return [ + 'C' => NumberFormat::FORMAT_DATE_YYYYMMDD, + ]; + } +} diff --git a/app/Exports/Sales/Invoices/Sheets/Invoices.php b/app/Exports/Sales/Invoices/Sheets/Invoices.php new file mode 100644 index 00000000..33f85cee --- /dev/null +++ b/app/Exports/Sales/Invoices/Sheets/Invoices.php @@ -0,0 +1,74 @@ +invoice()->collectForExport($this->ids, ['document_number' => 'desc']); + } + + public function map($model): array + { + $country = null; + + if ($model->contact_country && array_key_exists($model->contact_country, trans('countries'))) { + $country = trans('countries.' . $model->contact_country); + } + + $model->category_name = $model->category->name; + $model->invoice_number = $model->document_number; + $model->invoiced_at = $model->issued_at; + $model->contact_country = $country; + $model->parent_number = Model::invoiceRecurring()->find($model->parent_id)?->document_number; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'invoice_number', + 'order_number', + 'status', + 'invoiced_at', + 'due_at', + 'amount', + 'discount_type', + 'discount_rate', + 'currency_code', + 'currency_rate', + 'category_name', + 'contact_name', + 'contact_email', + 'contact_tax_number', + 'contact_phone', + 'contact_address', + 'contact_country', + 'contact_state', + 'contact_zip_code', + 'contact_city', + 'title', + 'subheading', + 'notes', + 'footer', + 'template', + 'color', + 'parent_number', + ]; + } + + public function columnFormats(): array + { + return [ + 'D' => NumberFormat::FORMAT_DATE_YYYYMMDD, + 'E' => NumberFormat::FORMAT_DATE_YYYYMMDD, + ]; + } +} diff --git a/app/Exports/Sales/RecurringInvoices/RecurringInvoices.php b/app/Exports/Sales/RecurringInvoices/RecurringInvoices.php new file mode 100644 index 00000000..e57eeaa6 --- /dev/null +++ b/app/Exports/Sales/RecurringInvoices/RecurringInvoices.php @@ -0,0 +1,36 @@ +ids = $ids; + } + + public function sheets(): array + { + return [ + new Recurring($this->ids), + new Base($this->ids), + new RecurringInvoiceItems($this->ids), + new RecurringInvoiceItemTaxes($this->ids), + new RecurringInvoiceHistories($this->ids), + new RecurringInvoiceTotals($this->ids), + ]; + } +} diff --git a/app/Exports/Sales/RecurringInvoices/Sheets/Recurring.php b/app/Exports/Sales/RecurringInvoices/Sheets/Recurring.php new file mode 100644 index 00000000..8eecb817 --- /dev/null +++ b/app/Exports/Sales/RecurringInvoices/Sheets/Recurring.php @@ -0,0 +1,37 @@ +cursor(); + } + + public function map($model): array + { + $model->invoice_number = $model->recurable->document_number; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'recurable_type', + 'invoice_number', + 'frequency', + 'interval', + 'started_at', + 'limit_date', + 'status', + 'limit_by', + 'limit_count', + 'auto_send', + ]; + } +} diff --git a/app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoiceHistories.php b/app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoiceHistories.php new file mode 100644 index 00000000..4e4b8b06 --- /dev/null +++ b/app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoiceHistories.php @@ -0,0 +1,38 @@ +invoiceRecurring()->collectForExport($this->ids, null, 'document_id'); + } + + public function map($model): array + { + $document = $model->document; + + if (empty($document)) { + return []; + } + + $model->invoice_number = $document->document_number; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'invoice_number', + 'status', + 'notify', + 'description', + ]; + } +} diff --git a/app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoiceItemTaxes.php b/app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoiceItemTaxes.php new file mode 100644 index 00000000..d794ad2d --- /dev/null +++ b/app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoiceItemTaxes.php @@ -0,0 +1,42 @@ +invoiceRecurring()->collectForExport($this->ids, null, 'document_id'); + } + + public function map($model): array + { + $document = $model->document; + + if (empty($document)) { + return []; + } + + $model->invoice_number = $document->document_number; + $model->item_name = $model->item->name; + $model->tax_name = $model->tax->name; + $model->tax_rate = $model->tax->rate; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'invoice_number', + 'item_name', + 'tax_name', + 'tax_rate', + 'amount', + ]; + } +} diff --git a/app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoiceItems.php b/app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoiceItems.php new file mode 100644 index 00000000..32d0f004 --- /dev/null +++ b/app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoiceItems.php @@ -0,0 +1,47 @@ +invoiceRecurring()->collectForExport($this->ids, null, 'document_id'); + } + + public function map($model): array + { + $document = $model->document; + + if (empty($document)) { + return []; + } + + $model->invoice_number = $document->document_number; + $model->item_name = $model->item->name; + $model->item_description = $model->item->description; + $model->item_type = $model->item->type; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'invoice_number', + 'item_name', + 'item_description', + 'item_type', + 'quantity', + 'discount_type', + 'discount_rate', + 'price', + 'total', + 'tax', + ]; + } +} diff --git a/app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoiceTotals.php b/app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoiceTotals.php new file mode 100644 index 00000000..85a8e7c4 --- /dev/null +++ b/app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoiceTotals.php @@ -0,0 +1,39 @@ +invoiceRecurring()->collectForExport($this->ids, null, 'document_id'); + } + + public function map($model): array + { + $document = $model->document; + + if (empty($document)) { + return []; + } + + $model->invoice_number = $document->document_number; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'invoice_number', + 'code', + 'name', + 'amount', + 'sort_order', + ]; + } +} diff --git a/app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoices.php b/app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoices.php new file mode 100644 index 00000000..1c30b30d --- /dev/null +++ b/app/Exports/Sales/RecurringInvoices/Sheets/RecurringInvoices.php @@ -0,0 +1,73 @@ +invoiceRecurring()->collectForExport($this->ids, ['document_number' => 'desc']); + } + + public function map($model): array + { + $country = null; + + if ($model->contact_country && array_key_exists($model->contact_country, trans('countries'))) { + $country = trans('countries.' . $model->contact_country); + } + + $model->category_name = $model->category->name; + $model->invoice_number = $model->document_number; + $model->invoiced_at = $model->issued_at; + $model->contact_country = $country; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'invoice_number', + 'order_number', + 'status', + 'invoiced_at', + 'due_at', + 'amount', + 'discount_type', + 'discount_rate', + 'currency_code', + 'currency_rate', + 'category_name', + 'contact_name', + 'contact_email', + 'contact_tax_number', + 'contact_phone', + 'contact_address', + 'contact_country', + 'contact_state', + 'contact_zip_code', + 'contact_city', + 'title', + 'subheading', + 'notes', + 'footer', + 'template', + 'color', + ]; + } + + public function columnFormats(): array + { + return [ + 'D' => NumberFormat::FORMAT_DATE_YYYYMMDD, + 'E' => NumberFormat::FORMAT_DATE_YYYYMMDD, + ]; + } +} diff --git a/app/Exports/Settings/Categories.php b/app/Exports/Settings/Categories.php new file mode 100644 index 00000000..fe9fe8af --- /dev/null +++ b/app/Exports/Settings/Categories.php @@ -0,0 +1,37 @@ +ids); + } + + public function map($model): array + { + $model->parent_name = Model::find($model->parent_id)?->name; + + return parent::map($model); + } + + public function fields(): array + { + return [ + 'code', + 'name', + 'type', + 'color', + 'description', + 'parent_name', + 'enabled', + ]; + } +} diff --git a/app/Exports/Settings/Taxes.php b/app/Exports/Settings/Taxes.php new file mode 100644 index 00000000..42ca69c4 --- /dev/null +++ b/app/Exports/Settings/Taxes.php @@ -0,0 +1,27 @@ +ids); + } + + public function fields(): array + { + return [ + 'name', + 'type', + 'rate', + 'enabled', + ]; + } +} diff --git a/app/Http/Controllers/Api/Auth/Users.php b/app/Http/Controllers/Api/Auth/Users.php new file mode 100644 index 00000000..4aafd9bc --- /dev/null +++ b/app/Http/Controllers/Api/Auth/Users.php @@ -0,0 +1,134 @@ +isNotCustomer()->collect(); + + return Resource::collection($users); + } + + /** + * Display the specified resource. + * + * @param int|string $id + * + * @return \Illuminate\Http\JsonResponse + */ + public function show($id) + { + $model_class = user_model_class(); + + // Check if we're querying by id or email + if (is_numeric($id)) { + $user = $model_class::with('companies', 'permissions', 'roles')->find($id); + } else { + $user = $model_class::with('companies', 'permissions', 'roles')->where('email', $id)->first(); + } + + if (! $user instanceof $model_class) { + return $this->errorInternal('No query results for model [' . $model_class . '] ' . $id); + } + + return new Resource($user); + } + + /** + * Store a newly created resource in storage. + * + * @param $request + * + * @return \Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + $user = $this->dispatch(new CreateUser($request)); + + return $this->created(route('api.users.show', $user->id), new Resource($user)); + } + + /** + * Update the specified resource in storage. + * + * @param $user_id + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function update($user_id, Request $request) + { + $user = user_model_class()::query()->isNotCustomer()->find($user_id); + + $user = $this->dispatch(new UpdateUser($user, $request)); + + return new Resource($user->fresh()); + } + + /** + * Enable the specified resource in storage. + * + * @param $user_id + * + * @return \Illuminate\Http\JsonResponse + */ + public function enable($user_id) + { + $user = user_model_class()::query()->isNotCustomer()->find($user_id); + + $user = $this->dispatch(new UpdateUser($user, request()->merge(['enabled' => 1]))); + + return new Resource($user->fresh()); + } + + /** + * Disable the specified resource in storage. + * + * @param $user_id + * + * @return \Illuminate\Http\JsonResponse + */ + public function disable($user_id) + { + $user = user_model_class()::query()->isNotCustomer()->find($user_id); + + $user = $this->dispatch(new UpdateUser($user, request()->merge(['enabled' => 0]))); + + return new Resource($user->fresh()); + } + + /** + * Remove the specified resource from storage. + * + * @param $user_id + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + * + * @return \Illuminate\Http\Response + */ + public function destroy($user_id) + { + $user = user_model_class()::query()->isNotCustomer()->find($user_id); + + try { + $this->dispatch(new DeleteUser($user)); + + return $this->noContent(); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } +} diff --git a/app/Http/Controllers/Api/Banking/Accounts.php b/app/Http/Controllers/Api/Banking/Accounts.php new file mode 100644 index 00000000..ef65f4d6 --- /dev/null +++ b/app/Http/Controllers/Api/Banking/Accounts.php @@ -0,0 +1,126 @@ +first(); + } + + if (! $account instanceof Account) { + return $this->errorInternal('No query results for model [' . Account::class . '] ' . $id); + } + + return new Resource($account); + } + + /** + * Store a newly created resource in storage. + * + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + $account = $this->dispatch(new CreateAccount($request)); + + return $this->created(route('api.accounts.show', $account->id), new Resource($account)); + } + + /** + * Update the specified resource in storage. + * + * @param $account + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function update(Account $account, Request $request) + { + try { + $account = $this->dispatch(new UpdateAccount($account, $request)); + + return new Resource($account->fresh()); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } + + /** + * Enable the specified resource in storage. + * + * @param Account $account + * @return \Illuminate\Http\JsonResponse + */ + public function enable(Account $account) + { + $account = $this->dispatch(new UpdateAccount($account, request()->merge(['enabled' => 1]))); + + return new Resource($account->fresh()); + } + + /** + * Disable the specified resource in storage. + * + * @param Account $account + * @return \Illuminate\Http\JsonResponse + */ + public function disable(Account $account) + { + try { + $account = $this->dispatch(new UpdateAccount($account, request()->merge(['enabled' => 0]))); + + return new Resource($account->fresh()); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } + + /** + * Remove the specified resource from storage. + * + * @param Account $account + * @return \Illuminate\Http\Response + */ + public function destroy(Account $account) + { + try { + $this->dispatch(new DeleteAccount($account)); + + return $this->noContent(); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } +} diff --git a/app/Http/Controllers/Api/Banking/Reconciliations.php b/app/Http/Controllers/Api/Banking/Reconciliations.php new file mode 100644 index 00000000..2d1ad2c5 --- /dev/null +++ b/app/Http/Controllers/Api/Banking/Reconciliations.php @@ -0,0 +1,81 @@ +collect(); + + return Resource::collection($reconciliations); + } + + /** + * Display the specified resource. + * + * @param $reconciliation + * @return \Illuminate\Http\JsonResponse + */ + public function show(Reconciliation $reconciliation) + { + return new Resource($reconciliation); + } + + /** + * Store a newly created resource in storage. + * + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + $reconciliation = $this->dispatch(new CreateReconciliation($request)); + + return $this->created(route('api.reconciliations.show', $reconciliation->id), new Resource($reconciliation)); + } + + /** + * Update the specified resource in storage. + * + * @param $reconciliation + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function update(Reconciliation $reconciliation, Request $request) + { + $reconciliation = $this->dispatch(new UpdateReconciliation($reconciliation, $request)); + + return new Resource($reconciliation->fresh()); + } + + /** + * Remove the specified resource from storage. + * + * @param Reconciliation $reconciliation + * @return \Illuminate\Http\Response + */ + public function destroy(Reconciliation $reconciliation) + { + try { + $this->dispatch(new DeleteReconciliation($reconciliation)); + + return $this->noContent(); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } +} diff --git a/app/Http/Controllers/Api/Banking/Transactions.php b/app/Http/Controllers/Api/Banking/Transactions.php new file mode 100644 index 00000000..eb3eb1a5 --- /dev/null +++ b/app/Http/Controllers/Api/Banking/Transactions.php @@ -0,0 +1,93 @@ +collect(['paid_at'=> 'desc']); + + return Resource::collection($transactions); + } + + /** + * Display the specified resource. + * + * @param Transaction $transaction + * @return \Illuminate\Http\JsonResponse + */ + public function show(Transaction $transaction) + { + return new Resource($transaction); + } + + /** + * Store a newly created resource in storage. + * + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + if ($request->has('document_id')) { + return $this->errorBadRequest(trans('transactions.messages.create_document_transaction_error')); + } + + $transaction = $this->dispatch(new CreateTransaction($request)); + + return $this->created(route('api.transactions.show', $transaction->id), new Resource($transaction)); + } + + /** + * Update the specified resource in storage. + * + * @param $transaction + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function update(Transaction $transaction, Request $request) + { + if ($request->has('document_id')) { + return $this->errorBadRequest(trans('transactions.messages.update_document_transaction_error')); + } + + $transaction = $this->dispatch(new UpdateTransaction($transaction, $request)); + + return new Resource($transaction->fresh()); + } + + /** + * Remove the specified resource from storage. + * + * @param Transaction $transaction + * @return \Illuminate\Http\Response + */ + public function destroy(Transaction $transaction) + { + if ($transaction->document_id) { + return $this->errorBadRequest(trans('transactions.messages.delete_document_transaction_error')); + } + + try { + $this->dispatch(new DeleteTransaction($transaction)); + + return $this->noContent(); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } +} diff --git a/app/Http/Controllers/Api/Banking/Transfers.php b/app/Http/Controllers/Api/Banking/Transfers.php new file mode 100644 index 00000000..e74022a2 --- /dev/null +++ b/app/Http/Controllers/Api/Banking/Transfers.php @@ -0,0 +1,111 @@ +collect('expense_transaction.paid_at'); + + $special_key = [ + 'expense_transaction.name' => 'from_account', + 'income_transaction.name' => 'to_account', + ]; + + $request = request(); + if (isset($request['sort']) && array_key_exists($request['sort'], $special_key)) { + $items = $transfers->items(); + + $sort_order = []; + + foreach ($items as $key => $value) { + $sort = $request['sort']; + + if (array_key_exists($request['sort'], $special_key)) { + $sort = $special_key[$request['sort']]; + } + + $sort_order[$key] = $value->{$sort}; + } + + $sort_type = (isset($request['order']) && $request['order'] == 'asc') ? SORT_ASC : SORT_DESC; + + array_multisort($sort_order, $sort_type, $items); + + $transfers->setCollection(collect($items)); + } + + return Resource::collection($transfers); + } + + /** + * Display the specified resource. + * + * @param Transfer $transfer + * @return \Illuminate\Http\JsonResponse + */ + public function show(Transfer $transfer) + { + return new Resource($transfer); + } + + /** + * Store a newly created resource in storage. + * + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + $transfer = $this->dispatch(new CreateTransfer($request)); + + return $this->created(route('api.transfers.show', $transfer->id), new Resource($transfer)); + } + + /** + * Update the specified resource in storage. + * + * @param $transfer + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function update(Transfer $transfer, Request $request) + { + $transfer = $this->dispatch(new UpdateTransfer($transfer, $request)); + + return new Resource($transfer->fresh()); + } + + /** + * Remove the specified resource from storage. + * + * @param Transfer $transfer + * @return \Illuminate\Http\Response + */ + public function destroy(Transfer $transfer) + { + try { + $this->dispatch(new DeleteTransfer($transfer)); + + return $this->noContent(); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } +} diff --git a/app/Http/Controllers/Api/Common/Companies.php b/app/Http/Controllers/Api/Common/Companies.php new file mode 100644 index 00000000..98c7cf0c --- /dev/null +++ b/app/Http/Controllers/Api/Common/Companies.php @@ -0,0 +1,148 @@ +companies()->collect(); + + return Resource::collection($companies); + } + + /** + * Display the specified resource. + * + * @param Company $company + * @return \Illuminate\Http\JsonResponse + */ + public function show(Company $company) + { + try { + // Check if user can access company + $this->canAccess($company); + + return new Resource($company); + } catch (\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } + + /** + * Store a newly created resource in storage. + * + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + $company = $this->dispatch(new CreateCompany($request)); + + return $this->created(route('api.companies.show', $company->id), new Resource($company)); + } + + /** + * Update the specified resource in storage. + * + * @param $company + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function update(Company $company, Request $request) + { + try { + $company = $this->dispatch(new UpdateCompany($company, $request)); + + return new Resource($company->fresh()); + } catch (\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } + + /** + * Enable the specified resource in storage. + * + * @param Company $company + * @return \Illuminate\Http\JsonResponse + */ + public function enable(Company $company) + { + try { + $company = $this->dispatch(new UpdateCompany($company, request()->merge(['enabled' => 1]))); + + return new Resource($company->fresh()); + } catch (\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } + + /** + * Disable the specified resource in storage. + * + * @param Company $company + * @return \Illuminate\Http\JsonResponse + */ + public function disable(Company $company) + { + try { + $company = $this->dispatch(new UpdateCompany($company, request()->merge(['enabled' => 0]))); + + return new Resource($company->fresh()); + } catch (\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } + + /** + * Remove the specified resource from storage. + * + * @param Company $company + * @return \Illuminate\Http\Response + */ + public function destroy(Company $company) + { + try { + $this->dispatch(new DeleteCompany($company)); + + return $this->noContent(); + } catch (\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } + + /** + * Check user company assignment + * + * @param Company $company + * + * @return \Illuminate\Http\Response + */ + public function canAccess(Company $company) + { + if (! empty($company) && $this->isUserCompany($company->id)) { + return new Response(''); + } + + $message = trans('companies.error.not_user_company'); + + $this->errorUnauthorized($message); + } +} diff --git a/app/Http/Controllers/Api/Common/Contacts.php b/app/Http/Controllers/Api/Common/Contacts.php new file mode 100644 index 00000000..f492806e --- /dev/null +++ b/app/Http/Controllers/Api/Common/Contacts.php @@ -0,0 +1,126 @@ +first(); + } + + if (! $contact instanceof Contact) { + //return $this->noContent(); + return $this->errorInternal('No query results for model [' . Contact::class . '] ' . $id); + } + + return new Resource($contact); + } + + /** + * Store a newly created resource in storage. + * + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + $contact = $this->dispatch(new CreateContact($request)); + + return $this->created(route('api.contacts.show', $contact->id), new Resource($contact)); + } + + /** + * Update the specified resource in storage. + * + * @param $contact + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function update(Contact $contact, Request $request) + { + $contact = $this->dispatch(new UpdateContact($contact, $request)); + + return new Resource($contact->fresh()); + } + + /** + * Enable the specified resource in storage. + * + * @param Contact $contact + * @return \Illuminate\Http\JsonResponse + */ + public function enable(Contact $contact) + { + $contact = $this->dispatch(new UpdateContact($contact, request()->merge(['enabled' => 1]))); + + return new Resource($contact->fresh()); + } + + /** + * Disable the specified resource in storage. + * + * @param Contact $contact + * @return \Illuminate\Http\JsonResponse + */ + public function disable(Contact $contact) + { + try { + $contact = $this->dispatch(new UpdateContact($contact, request()->merge(['enabled' => 0]))); + + return new Resource($contact->fresh()); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } + + /** + * Remove the specified resource from storage. + * + * @param Contact $contact + * @return \Illuminate\Http\Response + */ + public function destroy(Contact $contact) + { + try { + $this->dispatch(new DeleteContact($contact)); + + return $this->noContent(); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } +} diff --git a/app/Http/Controllers/Api/Common/Dashboards.php b/app/Http/Controllers/Api/Common/Dashboards.php new file mode 100644 index 00000000..20bdf9ef --- /dev/null +++ b/app/Http/Controllers/Api/Common/Dashboards.php @@ -0,0 +1,154 @@ +dashboards()->with('widgets')->collect(); + + return Resource::collection($dashboards); + } + + /** + * Display the specified resource. + * + * @param int|string $id + * @return \Illuminate\Http\JsonResponse + */ + public function show($id) + { + try { + $dashboard = Dashboard::with('widgets')->find($id); + + if (! $dashboard instanceof Dashboard) { + return $this->errorInternal('No query results for model [' . Dashboard::class . '] ' . $id); + } + + // Check if user can access dashboard + $this->canAccess($dashboard); + + return new Resource($dashboard); + } catch (\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } + + /** + * Store a newly created resource in storage. + * + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + $dashboard = $this->dispatch(new CreateDashboard($request)); + + return $this->created(route('api.dashboards.show', $dashboard->id), new Resource($dashboard)); + } + + /** + * Update the specified resource in storage. + * + * @param $dashboard + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function update(Dashboard $dashboard, Request $request) + { + try { + $dashboard = $this->dispatch(new UpdateDashboard($dashboard, $request)); + + return new Resource($dashboard->fresh()); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } + + /** + * Enable the specified resource in storage. + * + * @param Dashboard $dashboard + * @return \Illuminate\Http\JsonResponse + */ + public function enable(Dashboard $dashboard) + { + try { + $dashboard = $this->dispatch(new UpdateDashboard($dashboard, request()->merge(['enabled' => 1]))); + + return new Resource($dashboard->fresh()); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } + + /** + * Disable the specified resource in storage. + * + * @param Dashboard $dashboard + * @return \Illuminate\Http\JsonResponse + */ + public function disable(Dashboard $dashboard) + { + try { + $dashboard = $this->dispatch(new UpdateDashboard($dashboard, request()->merge(['enabled' => 0]))); + + return new Resource($dashboard->fresh()); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } + + /** + * Remove the specified resource from storage. + * + * @param Dashboard $dashboard + * @return \Illuminate\Http\Response + */ + public function destroy(Dashboard $dashboard) + { + try { + $this->dispatch(new DeleteDashboard($dashboard)); + + return $this->noContent(); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } + + /** + * Check user dashboard assignment + * + * @param Dashboard $dashboard + * + * @return \Illuminate\Http\Response + */ + public function canAccess($dashboard) + { + if (!empty($dashboard) && $this->isUserDashboard($dashboard->id)) { + return new Response(''); + } + + $message = trans('dashboards.error.not_user_dashboard'); + + $this->errorUnauthorized($message); + } +} diff --git a/app/Http/Controllers/Api/Common/Items.php b/app/Http/Controllers/Api/Common/Items.php new file mode 100644 index 00000000..09fd8297 --- /dev/null +++ b/app/Http/Controllers/Api/Common/Items.php @@ -0,0 +1,113 @@ +collect(); + + return Resource::collection($items); + } + + /** + * Display the specified resource. + * + * @param int|string $id + * @return \Illuminate\Http\JsonResponse + */ + public function show($id) + { + $item = Item::with('category', 'taxes')->find($id); + + if (! $item instanceof Item) { + return $this->errorInternal('No query results for model [' . Item::class . '] ' . $id); + } + + return new Resource($item); + } + + /** + * Store a newly created resource in storage. + * + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + $item = $this->dispatch(new CreateItem($request)); + + return $this->created(route('api.items.show', $item->id), new Resource($item)); + } + + /** + * Update the specified resource in storage. + * + * @param $item + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function update(Item $item, Request $request) + { + $item = $this->dispatch(new UpdateItem($item, $request)); + + return new Resource($item->fresh()); + } + + /** + * Enable the specified resource in storage. + * + * @param Item $item + * @return \Illuminate\Http\JsonResponse + */ + public function enable(Item $item) + { + $item = $this->dispatch(new UpdateItem($item, request()->merge(['enabled' => 1]))); + + return new Resource($item->fresh()); + } + + /** + * Disable the specified resource in storage. + * + * @param Item $item + * @return \Illuminate\Http\JsonResponse + */ + public function disable(Item $item) + { + $item = $this->dispatch(new UpdateItem($item, request()->merge(['enabled' => 0]))); + + return new Resource($item->fresh()); + } + + /** + * Remove the specified resource from storage. + * + * @param Item $item + * @return \Illuminate\Http\Response + */ + public function destroy(Item $item) + { + try { + $this->dispatch(new DeleteItem($item)); + + return $this->noContent(); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } +} diff --git a/app/Http/Controllers/Api/Common/Ping.php b/app/Http/Controllers/Api/Common/Ping.php new file mode 100644 index 00000000..c0c46ada --- /dev/null +++ b/app/Http/Controllers/Api/Common/Ping.php @@ -0,0 +1,30 @@ +json([ + 'status' => 'ok', + 'timestamp' => Date::now(), + ]); + } +} diff --git a/app/Http/Controllers/Api/Common/Reports.php b/app/Http/Controllers/Api/Common/Reports.php new file mode 100644 index 00000000..d8358f50 --- /dev/null +++ b/app/Http/Controllers/Api/Common/Reports.php @@ -0,0 +1,81 @@ +dispatch(new CreateReport($request)); + + return $this->created(route('api.reports.show', $report->id), new Resource($report)); + } + + /** + * Update the specified resource in storage. + * + * @param $report + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function update(Report $report, Request $request) + { + $report = $this->dispatch(new UpdateReport($report, $request)); + + return new Resource($report->fresh()); + } + + /** + * Remove the specified resource from storage. + * + * @param Report $report + * @return \Illuminate\Http\Response + */ + public function destroy(Report $report) + { + try { + $this->dispatch(new DeleteReport($report)); + + return $this->noContent(); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } +} diff --git a/app/Http/Controllers/Api/Common/Translations.php b/app/Http/Controllers/Api/Common/Translations.php new file mode 100644 index 00000000..9940e722 --- /dev/null +++ b/app/Http/Controllers/Api/Common/Translations.php @@ -0,0 +1,91 @@ +json([ + 'success' => true, + 'error' => false, + 'message' => 'Get file translation', + 'data' => trans($file), + ]); + } + + /** + * Display the specified resource. + * + * @param string $locale + * @return \Illuminate\Http\JsonResponse + */ + public function all($locale) + { + // Security: validate locale format (e.g. en-US, tr-TR) to prevent + // path traversal in the filesystem glob below. + if (! preg_match('/^[a-z]{2}-[A-Z]{2}$/', $locale)) { + abort(404); + } + + App::setLocale($locale); + + $translations = []; + + $filesystem = app(Filesystem::class); + + $path = base_path('resources/lang/' . $locale); + + foreach ($filesystem->glob("{$path}/*") as $file_system) { + $file = str_replace('.php', '', basename($file_system)); + + $translations[$file] = trans($file); + } + + $modules = Module::enabled()->get(); + + foreach ($modules as $module) { + $path = base_path('modules/' . Str::studly($module->alias) . '/Resources/lang/' . $locale); + + foreach ($filesystem->glob("{$path}/*") as $file_system) { + $file = str_replace('.php', '', basename($file_system)); + + $translations[$module->alias . '::' . $file] = trans($file); + } + } + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => 'Get all translation', + 'data' => $translations, + ]); + } +} diff --git a/app/Http/Controllers/Api/Document/DocumentTransactions.php b/app/Http/Controllers/Api/Document/DocumentTransactions.php new file mode 100644 index 00000000..f976b05b --- /dev/null +++ b/app/Http/Controllers/Api/Document/DocumentTransactions.php @@ -0,0 +1,109 @@ +middleware('permission:create-banking-transactions')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-banking-transactions')->only('index', 'show', 'edit', 'export'); + $this->middleware('permission:update-banking-transactions')->only('update', 'enable', 'disable'); + $this->middleware('permission:delete-banking-transactions')->only('destroy'); + } + + /** + * Display a listing of the resource. + * + * @param $document_id + * @return \Illuminate\Http\JsonResponse + */ + public function index($document_id) + { + $transactions = Transaction::with(['document', 'taxes'])->documentId($document_id)->get(); + + return Resource::collection($transactions); + } + + /** + * Display the specified resource. + * + * @param $document_id + * @param $id + * @return \Illuminate\Http\JsonResponse + */ + public function show($document_id, $id) + { + $transaction = Transaction::with(['document', 'taxes'])->documentId($document_id)->find($id); + + if (! $transaction instanceof Transaction) { + return $this->errorInternal('No query results for model [' . Transaction::class . '] ' . $id); + } + + return new Resource($transaction); + } + + /** + * Store a newly created resource in storage. + * + * @param $document_id + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function store($document_id, Request $request) + { + $document = Document::find($document_id); + + $transaction = $this->dispatch(new CreateBankingDocumentTransaction($document, $request)); + + return $this->created(route('api.documents.transactions.show', [$document_id, $transaction->id]), new Resource($transaction)); + } + + /** + * Update the specified resource in storage. + * + * @param $document_id + * @param $id + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function update($document_id, $id, Request $request) + { + $document = Document::find($document_id); + + $transaction = Transaction::documentId($document_id)->find($id); + + $transaction = $this->dispatch(new UpdateBankingDocumentTransaction($document, $transaction, $request)); + + return $this->created(route('api.documents.transactions.show', [$document_id, $transaction->id]), new Resource($transaction)); + } + + /** + * Remove the specified resource from storage. + * + * @param $document_id + * @param $id + * @return \Illuminate\Http\Response + */ + public function destroy($document_id, $id) + { + $transaction = Transaction::documentId($document_id)->find($id); + + $this->dispatch(new DeleteTransaction($transaction)); + + return $this->noContent(); + } +} diff --git a/app/Http/Controllers/Api/Document/Documents.php b/app/Http/Controllers/Api/Document/Documents.php new file mode 100644 index 00000000..205c8ec0 --- /dev/null +++ b/app/Http/Controllers/Api/Document/Documents.php @@ -0,0 +1,119 @@ +collect(['issued_at'=> 'desc']); + + return Resource::collection($documents); + } + + /** + * Display the specified resource. + * + * @param $id + * @return \Illuminate\Http\JsonResponse + */ + public function show($id) + { + // Check if we're querying by id or number + if (is_numeric($id)) { + $document = Document::with([ + 'contact', + 'histories', + 'items', + 'items.taxes', + 'items.taxes.tax', + 'item_taxes', + 'totals', + 'transactions', + 'transactions.currency', + 'transactions.account', + 'transactions.category', + ])->find($id); + } else { + $document = Document::with([ + 'contact', + 'histories', + 'items', + 'items.taxes', + 'items.taxes.tax', + 'item_taxes', + 'totals', + 'transactions', + 'transactions.currency', + 'transactions.account', + 'transactions.category', + ])->where('document_number', $id)->first(); + } + + if (! $document instanceof Document) { + return $this->errorInternal('No query results for model [' . Document::class . '] ' . $id); + } + + return new Resource($document); + } + + /** + * Store a newly created resource in storage. + * + * @param $request + * + * @return \Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + $document = $this->dispatch(new CreateDocument($request)); + + return $this->created(route('api.documents.show', $document->id), new Resource($document)); + } + + /** + * Update the specified resource in storage. + * + * @param $document + * @param $request + * + * @return \Illuminate\Http\JsonResponse + */ + public function update(Document $document, Request $request) + { + $document = $this->dispatch(new UpdateDocument($document, $request)); + + return new Resource($document->fresh()); + } + + /** + * Remove the specified resource from storage. + * + * @param Document $document + * + * @return \Illuminate\Http\Response + */ + public function destroy(Document $document) + { + try { + $this->dispatch(new DeleteDocument($document)); + + return $this->noContent(); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } +} diff --git a/app/Http/Controllers/Api/Settings/Categories.php b/app/Http/Controllers/Api/Settings/Categories.php new file mode 100644 index 00000000..6aea5a64 --- /dev/null +++ b/app/Http/Controllers/Api/Settings/Categories.php @@ -0,0 +1,115 @@ +collect(); + + return Resource::collection($categories); + } + + /** + * Display the specified resource. + * + * @param Category $category + * @return \Illuminate\Http\JsonResponse + */ + public function show(Category $category) + { + return new Resource($category); + } + + /** + * Store a newly created resource in storage. + * + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + $category = $this->dispatch(new CreateCategory($request)); + + return $this->created(route('api.categories.show', $category->id), new Resource($category)); + } + + /** + * Update the specified resource in storage. + * + * @param $category + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function update(Category $category, Request $request) + { + try { + $category = $this->dispatch(new UpdateCategory($category, $request)); + + return new Resource($category->fresh()); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } + + /** + * Enable the specified resource in storage. + * + * @param Category $category + * @return \Illuminate\Http\JsonResponse + */ + public function enable(Category $category) + { + $category = $this->dispatch(new UpdateCategory($category, request()->merge(['enabled' => 1]))); + + return new Resource($category->fresh()); + } + + /** + * Disable the specified resource in storage. + * + * @param Category $category + * @return \Illuminate\Http\JsonResponse + */ + public function disable(Category $category) + { + try { + $category = $this->dispatch(new UpdateCategory($category, request()->merge(['enabled' => 0]))); + + return new Resource($category->fresh()); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } + + /** + * Remove the specified resource from storage. + * + * @param Category $category + * @return \Illuminate\Http\Response + */ + public function destroy(Category $category) + { + try { + $this->dispatch(new DeleteCategory($category)); + + return $this->noContent(); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } +} diff --git a/app/Http/Controllers/Api/Settings/Currencies.php b/app/Http/Controllers/Api/Settings/Currencies.php new file mode 100644 index 00000000..8480eb91 --- /dev/null +++ b/app/Http/Controllers/Api/Settings/Currencies.php @@ -0,0 +1,126 @@ +first(); + } + + if (! $currency instanceof Currency) { + return $this->errorInternal('No query results for model [' . Currency::class . '] ' . $id); + } + + return new Resource($currency); + } + + /** + * Store a newly created resource in storage. + * + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + $currency = $this->dispatch(new CreateCurrency($request)); + + return $this->created(route('api.currencies.show', $currency->id), new Resource($currency)); + } + + /** + * Update the specified resource in storage. + * + * @param $currency + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function update(Currency $currency, Request $request) + { + try { + $currency = $this->dispatch(new UpdateCurrency($currency, $request)); + + return new Resource($currency->fresh()); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } + + /** + * Enable the specified resource in storage. + * + * @param Currency $currency + * @return \Illuminate\Http\JsonResponse + */ + public function enable(Currency $currency) + { + $currency = $this->dispatch(new UpdateCurrency($currency, request()->merge(['enabled' => 1]))); + + return new Resource($currency->fresh()); + } + + /** + * Disable the specified resource in storage. + * + * @param Currency $currency + * @return \Illuminate\Http\JsonResponse + */ + public function disable(Currency $currency) + { + try { + $currency = $this->dispatch(new UpdateCurrency($currency, request()->merge(['enabled' => 0]))); + + return new Resource($currency->fresh()); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } + + /** + * Remove the specified resource from storage. + * + * @param Currency $currency + * @return \Illuminate\Http\Response + */ + public function destroy(Currency $currency) + { + try { + $this->dispatch(new DeleteCurrency($currency)); + + return $this->noContent(); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } +} diff --git a/app/Http/Controllers/Api/Settings/Settings.php b/app/Http/Controllers/Api/Settings/Settings.php new file mode 100644 index 00000000..f491ba3f --- /dev/null +++ b/app/Http/Controllers/Api/Settings/Settings.php @@ -0,0 +1,105 @@ +middleware('permission:create-settings-company|create-settings-defaults|create-settings-localisation')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-settings-company|read-settings-defaults|read-settings-localisation')->only('index', 'show', 'edit', 'export'); + $this->middleware('permission:update-settings-company|update-settings-defaults|update-settings-localisation')->only('update', 'enable', 'disable', 'destroy'); + } + + /** + * Display a listing of the resource. + * + * @return \Illuminate\Http\JsonResponse + */ + public function index() + { + $settings = Setting::all(); + + return Resource::collection($settings); + } + + /** + * Display the specified resource. + * + * @param int|string $id + * @return \Illuminate\Http\JsonResponse + */ + public function show($id) + { + // Check if we're querying by id or key + if (is_numeric($id)) { + $setting = Setting::find($id); + } else { + $setting = Setting::where('key', $id)->first(); + } + + if (! $setting instanceof Setting) { + return $this->errorInternal('No query results for model [' . Setting::class . '] ' . $id); + } + + return new Resource($setting); + } + + /** + * Store a newly created resource in storage. + * + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + $data = $request->all(); + $data['company_id'] = company_id(); + + $setting = Setting::create($data); + + return $this->created(route('api.settings.show', $setting->id), new Resource($setting)); + } + + /** + * Update the specified resource in storage. + * + * @param $setting + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function update(Setting $setting, Request $request) + { + $data = $request->all(); + $data['company_id'] = company_id(); + + $setting->update($data); + + return new Resource($setting->fresh()); + } + + /** + * Remove the specified resource from storage. + * + * @param Setting $setting + * @return \Illuminate\Http\Response + */ + public function destroy(Setting $setting) + { + $setting->delete(); + + return $this->noContent(); + } +} diff --git a/app/Http/Controllers/Api/Settings/Taxes.php b/app/Http/Controllers/Api/Settings/Taxes.php new file mode 100644 index 00000000..f98d47ce --- /dev/null +++ b/app/Http/Controllers/Api/Settings/Taxes.php @@ -0,0 +1,115 @@ +dispatch(new CreateTax($request)); + + return $this->created(route('api.taxes.show', $tax->id), new Resource($tax)); + } + + /** + * Update the specified resource in storage. + * + * @param $tax + * @param $request + * @return \Illuminate\Http\JsonResponse + */ + public function update(Tax $tax, Request $request) + { + try { + $tax = $this->dispatch(new UpdateTax($tax, $request)); + + return new Resource($tax->fresh()); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } + + /** + * Enable the specified resource in storage. + * + * @param Tax $tax + * @return \Illuminate\Http\JsonResponse + */ + public function enable(Tax $tax) + { + $tax = $this->dispatch(new UpdateTax($tax, request()->merge(['enabled' => 1]))); + + return new Resource($tax->fresh()); + } + + /** + * Disable the specified resource in storage. + * + * @param Tax $tax + * @return \Illuminate\Http\JsonResponse + */ + public function disable(Tax $tax) + { + try { + $tax = $this->dispatch(new UpdateTax($tax, request()->merge(['enabled' => 0]))); + + return new Resource($tax->fresh()); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } + + /** + * Remove the specified resource from storage. + * + * @param Tax $tax + * @return \Illuminate\Http\Response + */ + public function destroy(Tax $tax) + { + try { + $this->dispatch(new DeleteTax($tax)); + + return $this->noContent(); + } catch(\Exception $e) { + $this->errorUnauthorized($e->getMessage()); + } + } +} diff --git a/app/Http/Controllers/Auth/Forgot.php b/app/Http/Controllers/Auth/Forgot.php new file mode 100644 index 00000000..fa2c757f --- /dev/null +++ b/app/Http/Controllers/Auth/Forgot.php @@ -0,0 +1,102 @@ +middleware('guest'); + } + + /** + * Display the form to request a password reset link. + * + * @return \Illuminate\Http\Response + */ + public function create() + { + return view('auth.forgot.create'); + } + + /** + * Send a reset link to the given user. + * + * @param \App\Http\Requests\Auth\Forgot $request + * @return \Illuminate\Http\RedirectResponse + */ + public function store(Request $request) + { + // We will send the password reset link to this user. Once we have attempted + // to send the link, we will examine the response then see the message we + // need to show to the user. Finally, we'll send out a proper response. + $response = $this->broker()->sendResetLink( + $request->only('email') + ); + + return $response == Password::RESET_LINK_SENT + ? $this->sendResetLinkResponse($response) + : $this->sendResetLinkFailedResponse($request, $response); + } + + /** + * Get the response for a successful password reset link. + * + * @param string $response + * @return \Illuminate\Http\RedirectResponse + */ + protected function sendResetLinkResponse($response) + { + $response = [ + 'status' => null, + 'success' => true, + 'error' => false, + 'message' => trans('passwords.sent'), + 'data' => null, + 'redirect' => null, + ]; + + return response()->json($response); + } + + /** + * Get the response for a failed password reset link. + * + * @param \Illuminate\Http\Request + * @param string $response + * @return \Illuminate\Http\RedirectResponse + */ + protected function sendResetLinkFailedResponse($response) + { + $response = [ + 'status' => null, + 'success' => false, + 'error' => true, + 'message' => trans('passwords.user'), + 'data' => null, + 'redirect' => route('forgot'), + ]; + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Auth/Login.php b/app/Http/Controllers/Auth/Login.php new file mode 100644 index 00000000..07453c7e --- /dev/null +++ b/app/Http/Controllers/Auth/Login.php @@ -0,0 +1,150 @@ +middleware('guest', ['except' => 'destroy']); + } + + public function create() + { + return view('auth.login.create'); + } + + public function store(Request $request) + { + // Attempt to login + if (! auth()->attempt($request->only('email', 'password'), $request->get('remember', false))) { + return $this->respondLoginFailed(); + } + + // Get user object + $user = user(); + + // Check if user is enabled + if (! $user->enabled) { + $this->logout(); + + // Security (CWE-204): avoid distinct error messages that would + // allow valid email enumeration. Log the real reason server-side + // for administrators/auditing without leaking it to the client. + Log::info('Login denied: account disabled', [ + 'email' => $request->email, + ]); + + return $this->respondLoginFailed(); + } + + $company = $user->withoutEvents(function () use ($user) { + return $user->companies()->enabled()->first(); + }); + + // Logout if no company assigned + if (! $company) { + $this->logout(); + + // Security (CWE-204): do not expose "no company" message to the + // client; log it server-side instead. + Log::info('Login denied: no company assigned', [ + 'email' => $request->email, + ]); + + return $this->respondLoginFailed(); + } + + // Redirect to portal if is customer + if ($user->isCustomer()) { + $path = session('url.intended', ''); + + // Path must start with company id and 'portal' prefix + if (!Str::startsWith($path, $company->id . '/portal')) { + $path = route('portal.dashboard', ['company_id' => $company->id]); + } + + return response()->json([ + 'status' => null, + 'success' => true, + 'error' => false, + 'message' => trans('auth.login_redirect'), + 'data' => null, + 'redirect' => url($path), + ]); + } + + // Redirect to landing page if is user + $url = route($user->landing_page, ['company_id' => $company->id]); + + return response()->json([ + 'status' => null, + 'success' => true, + 'error' => false, + 'message' => trans('auth.login_redirect'), + 'data' => null, + 'redirect' => redirect()->intended($url)->getTargetUrl(), + ]); + } + + /** + * Build the generic failed-login JSON response. + * + * Security (CWE-204): every rejected login returns the same message and + * response shape so an unauthenticated attacker cannot distinguish + * "email does not exist" from "disabled" or "no company" and enumerate + * valid registered email addresses. + */ + protected function respondLoginFailed() + { + return response()->json([ + 'status' => null, + 'success' => false, + 'error' => true, + 'message' => trans('auth.failed'), + 'data' => null, + 'redirect' => null, + ]); + } + + public function destroy() + { + $this->logout(); + + return redirect()->route('login'); + } + + public function logout() + { + auth()->logout(); + + // Session destroy is required if stored in database + if (config('session.driver') == 'database') { + $request = app('Illuminate\Http\Request'); + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + $request->session()->getHandler()->destroy($request->session()->getId()); + } + } +} diff --git a/app/Http/Controllers/Auth/Register.php b/app/Http/Controllers/Auth/Register.php new file mode 100644 index 00000000..4b8c711e --- /dev/null +++ b/app/Http/Controllers/Auth/Register.php @@ -0,0 +1,88 @@ +middleware('guest'); + } + + public function create($token) + { + $invitation = UserInvitation::token($token)->first(); + + if ($invitation) { + return view('auth.register.create', ['token' => $token]); + } + + abort(403); + } + + public function store(Request $request) + { + $invitation = UserInvitation::token($request->get('token'))->first(); + + if (!$invitation) { + abort(403); + } + + $user = $invitation->user; + + $this->dispatch(new DeleteInvitation($invitation)); + + event(new Registered($user)); + + if ($response = $this->registered($request, $user)) { + return $response; + } + } + + /** + * The user has been registered. + * + * @param \Illuminate\Http\Request $request + * @param mixed $user + * @return mixed + */ + protected function registered(Request $request, $user) + { + $user->forceFill([ + 'password' => $request->password, + 'remember_token' => Str::random(60), + ])->save(); + + $this->guard()->login($user); + + $message = trans('messages.success.connected', ['type' => trans_choice('general.users', 1)]); + + flash($message)->success(); + + return response()->json([ + 'redirect' => url($this->redirectPath()), + ]); + } +} diff --git a/app/Http/Controllers/Auth/Reset.php b/app/Http/Controllers/Auth/Reset.php new file mode 100644 index 00000000..ea7dcba6 --- /dev/null +++ b/app/Http/Controllers/Auth/Reset.php @@ -0,0 +1,146 @@ +middleware('guest'); + } + + public function create(BaseRequest $request, $token = null) + { + return view('auth.reset.create')->with( + ['token' => $token, 'email' => $request->email] + ); + } + + /** + * Send a reset link to the given user. + * + * @param \App\Http\Requests\Auth\Reset $request + * + * @return \Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + // Here we will attempt to reset the user's password. If it is successful we + // will update the password on an actual user model and persist it to the + // database. Otherwise we will parse the error and return the response. + $response = $this->broker()->reset( + $this->credentials($request), function ($user, $password) { + $this->resetPassword($user, $password); + } + ); + + // If the password was successfully reset, we will redirect the user back to + // the application's home authenticated view. If there is an error we can + // redirect them back to where they came from with their error message. + return $response == Password::PASSWORD_RESET + ? $this->sendResetResponse($response) + : $this->sendResetFailedResponse($request, $response); + } + + /** + * Reset the given user's password. + * + * @param \Illuminate\Contracts\Auth\CanResetPassword $user + * @param string $password + * @return void + */ + protected function resetPassword($user, $password) + { + $user->forceFill([ + 'password' => $password, + 'remember_token' => Str::random(60), + ])->save(); + + $this->guard()->login($user); + } + + /** + * Get the response for a successful password reset. + * + * @param string $response + * + * @return \Illuminate\Http\JsonResponse + */ + protected function sendResetResponse($response) + { + $user = user(); + + $company = $user::withoutEvents(function () use ($user) { + return $user->companies()->enabled()->first(); + }); + + // Logout if no company assigned + if (! $company) { + $this->guard()->logout(); + + return response()->json([ + 'status' => null, + 'success' => false, + 'error' => true, + 'message' => trans('auth.error.no_company'), + 'data' => null, + 'redirect' => null, + ]); + } + + // Redirect to portal if is customer + if ($user->isCustomer()) { + $this->redirectTo = route('portal.dashboard', ['company_id' => $company->id]); + } + + return response()->json([ + 'status' => null, + 'success' => true, + 'error' => false, + 'message' => null, + 'data' => null, + 'redirect' => url($this->redirectTo), + ]); + } + + /** + * Get the response for a failed password reset. + * + * @param \Illuminate\Http\Request + * @param string $response + * + * @return \Illuminate\Http\JsonResponse + */ + protected function sendResetFailedResponse(Request $request, $response) + { + return response()->json([ + 'status' => null, + 'success' => false, + 'error' => true, + 'message' => trans($response), + 'data' => null, + 'redirect' => null, + ]); + } +} diff --git a/app/Http/Controllers/Auth/Users.php b/app/Http/Controllers/Auth/Users.php new file mode 100644 index 00000000..f5ddea68 --- /dev/null +++ b/app/Http/Controllers/Auth/Users.php @@ -0,0 +1,448 @@ +middleware('permission:create-auth-users')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-auth-users')->only('index', 'show', 'export'); + $this->middleware('permission:update-auth-users')->only('enable', 'disable'); + $this->middleware('permission:delete-auth-users')->only('destroy'); + + $this->middleware('permission:read-auth-users|read-auth-profile')->only('edit'); + $this->middleware('permission:update-auth-users|update-auth-profile')->only('update'); + } + + /** + * Display a listing of the resource. + * + * @return Response + */ + public function index() + { + $users = user_model_class()::with('companies', 'media', 'permissions', 'roles')->isNotCustomer()->collect(); + + return $this->response('auth.users.index', compact('users')); + } + + /** + * Show the form for viewing the specified resource. + * + * @return Response + */ + public function show($user_id) + { + $user = user_model_class()::find($user_id); + + $u = new \stdClass(); + $u->role = $user->roles()->first(); + $u->landing_pages = []; + + event(new LandingPageShowing($u)); + + $landing_pages = $u->landing_pages; + + $companies = $user->companies()->collect(); + + return view('auth.users.show', compact('user', 'landing_pages', 'companies')); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + $u = new \stdClass(); + $u->landing_pages = []; + + event(new LandingPageShowing($u)); + + $landing_pages = $u->landing_pages; + + $roles = role_model_class()::all()->reject(function ($r) { + $status = $r->hasPermission('read-client-portal'); + + if ($r->name == 'employee') { + $status = true; + } + + return $status; + })->pluck('display_name', 'id'); + + $companies = user()->companies()->take(setting('default.select_limit'))->get()->sortBy('name')->pluck('name', 'id'); + + $roles_url = $this->getCloudRolesPageUrl(); + + return view('auth.users.create', compact('roles', 'companies', 'landing_pages', 'roles_url')); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $response = $this->ajaxDispatch(new CreateUser($request)); + + if ($response['success']) { + $response['redirect'] = route('users.show', $response['data']->id); + + $message = trans('messages.success.invited', ['type' => trans_choice('general.users', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('users.create'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param $user_id + * + * @return Response + */ + public function edit($user_id) + { + $user = user_model_class()::query()->isNotCustomer()->find($user_id); + + if ((user()->cannot('read-auth-users') && ($user->id != user()->id)) || empty($user)) { + abort(403); + } + + $u = new \stdClass(); + $u->role = $user->roles()->first(); + $u->landing_pages = []; + + event(new LandingPageShowing($u)); + + $landing_pages = $u->landing_pages; + + if ($user->isCustomer()) { + // Show only roles with customer permission + $roles = role_model_class()::all()->reject(function ($r) { + return ! $r->hasPermission('read-client-portal'); + })->pluck('display_name', 'id'); + } else if ($user->isEmployee()) { + // Show only roles with employee permission + $roles = role_model_class()::where('name', 'employee')->get()->pluck('display_name', 'id'); + } else { + // Don't show roles with customer permission + $roles = role_model_class()::all()->reject(function ($r) { + $status = $r->hasPermission('read-client-portal'); + + if ($r->name == 'employee') { + $status = true; + } + + return $status; + })->pluck('display_name', 'id'); + } + + $companies = user()->companies()->take(setting('default.select_limit'))->get()->sortBy('name')->pluck('name', 'id'); + + if ($user->company_ids) { + foreach ($user->company_ids as $company_id) { + if ($companies->has($company_id)) { + continue; + } + + $company = company($company_id); + + $companies->put($company->id, $company->name); + } + } + + $roles_url = $this->getCloudRolesPageUrl(); + + $route = (request()->route()->getName() == 'profile.edit') ? 'profile.update' : 'users.update'; + + return view('auth.users.edit', compact('user', 'companies', 'roles', 'landing_pages', 'roles_url', 'route')); + } + + /** + * Update the specified resource in storage. + * + * @param $user_id + * @param Request $request + * + * @return Response + */ + public function update($user_id, Request $request) + { + $user = user_model_class()::find($user_id); + + if ((user()->cannot('update-auth-users') && ($user->id != user()->id)) || empty($user)) { + abort(403); + } + + $response = $this->ajaxDispatch(new UpdateUser($user, $request)); + + if ($response['success']) { + $response['redirect'] = user()->can('read-auth-users') ? route('users.show', $user->id) : route('users.edit', $user->id); + + $message = trans('messages.success.updated', ['type' => $user->name]); + + flash($message)->success(); + } else { + $response['redirect'] = route('users.edit', $user->id); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Enable the specified resource. + * + * @param $user_id + * + * @return Response + */ + public function enable($user_id) + { + $user = user_model_class()::query()->isNotCustomer()->find($user_id); + + if (user()->cannot('update-auth-users') || empty($user)) { + abort(403); + } + + $response = $this->ajaxDispatch(new UpdateUser($user, request()->merge(['enabled' => 1]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.enabled', ['type' => $user->name]); + } + + return response()->json($response); + } + + /** + * Disable the specified resource. + * + * @param $user_id + * + * @return Response + */ + public function disable($user_id) + { + $user = user_model_class()::query()->isNotCustomer()->find($user_id); + + if (user()->cannot('update-auth-users') || empty($user)) { + abort(403); + } + + $response = $this->ajaxDispatch(new UpdateUser($user, request()->merge(['enabled' => 0]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.disabled', ['type' => $user->name]); + } + + return response()->json($response); + } + + /** + * Remove the specified resource from storage. + * + * @param $user_id + * + * @return Response + */ + public function destroy($user_id) + { + $user = user_model_class()::query()->isNotCustomer()->find($user_id); + + if (user()->cannot('delete-auth-users') || empty($user)) { + abort(403); + } + + $response = $this->ajaxDispatch(new DeleteUser($user)); + + $response['redirect'] = route('users.index'); + + if ($response['success']) { + $message = trans('messages.success.deleted', ['type' => $user->name]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Mark upcoming bills notifications are read and redirect to bills page. + * + * @param $user_id + * + * @return Response + */ + public function readUpcomingBills($user_id) + { + $user = user_model_class()::query()->isNotCustomer()->find($user_id); + + if (user()->cannot('read-auth-users') || empty($user)) { + abort(403); + } + + // Mark bill notifications as read + foreach ($user->unreadNotifications as $notification) { + // Not a bill notification + if ($notification->getAttribute('type') != 'App\Notifications\Purchase\Bill') { + continue; + } + + $notification->markAsRead(); + } + + return redirect()->route('bills.index'); + } + + /** + * Mark overdue invoices notifications are read and redirect to invoices page. + * + * @param $user_id + * + * @return Response + */ + public function readOverdueInvoices($user_id) + { + $user = user_model_class()::query()->isNotCustomer()->find($user_id); + + if (user()->cannot('read-auth-users') || empty($user)) { + abort(403); + } + + // Mark invoice notifications as read + foreach ($user->unreadNotifications as $notification) { + // Not an invoice notification + if ($notification->getAttribute('type') != 'App\Notifications\Sale\Invoice') { + continue; + } + + $notification->markAsRead(); + } + + return redirect()->route('invoices.index'); + } + + public function autocomplete(BaseRequest $request) + { + $user = false; + $data = false; + + $column = $request['column']; + $value = $request['value']; + + if (! empty($column) && ! empty($value)) { + switch ($column) { + case 'id': + $user = user_model_class()::find((int) $value); + break; + case 'email': + $user = user_model_class()::where('email', $value)->first(); + break; + default: + $user = user_model_class()::where($column, $value)->first(); + } + + $data = $user; + } elseif (! empty($column) && empty($value)) { + $data = trans('validation.required', ['attribute' => $column]); + } + + return response()->json([ + 'errors' => ($user) ? false : true, + 'success' => ($user) ? true : false, + 'data' => $data, + ]); + } + + /** + * Process request for reinviting the specified resource. + * + * @param $user_id + * + * @return Response + */ + public function invite($user_id) + { + $user = user_model_class()::find($user_id); + + $response = $this->ajaxDispatch(new CreateInvitation($user, company())); + + if ($response['success']) { + $message = trans('messages.success.invited', ['type' => trans_choice('general.users', 1)]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return redirect()->route('users.index'); + } + + /** + * Process request for reinviting the specified resource. + * + * @param role_model_class() $role + * + * @return Response + */ + public function landingPages(BaseRequest $request) + { + $role = false; + + if ($request->has('role_id')) { + $role = role_model_class()::find($request->get('role_id')); + } + + $u = new \stdClass(); + $u->role = $role; + $u->landing_pages = []; + + event(new LandingPageShowing($u)); + + $landing_pages = $u->landing_pages; + + return response()->json([ + 'success' => true, + 'error' => false, + 'data' => $landing_pages, + 'message' => 'Get role by landing pages..', + ]); + } +} diff --git a/app/Http/Controllers/Banking/Accounts.php b/app/Http/Controllers/Banking/Accounts.php new file mode 100644 index 00000000..d5cc0889 --- /dev/null +++ b/app/Http/Controllers/Banking/Accounts.php @@ -0,0 +1,307 @@ +collect(); + + return $this->response('banking.accounts.index', compact('accounts')); + } + + /** + * Show the form for viewing the specified resource. + * + * @return Response + */ + public function show(Account $account) + { + $transactions = Transaction::with('category', 'contact', 'contact.media', 'document', 'document.totals', 'document.media', 'recurring', 'media')->where('account_id', $account->id)->collect(['paid_at'=> 'desc']); + + $transfers = Transfer::with('expense_transaction', 'expense_transaction.account', 'income_transaction', 'income_transaction.account') + ->whereHas('expense_transaction', fn ($query) => $query->where('account_id', $account->id)) + ->orWhereHas('income_transaction', fn ($query) => $query->where('account_id', $account->id)) + ->collect(['expense_transaction.paid_at' => 'desc']); + + $incoming_amount = money($account->income_balance, $account->currency_code); + $outgoing_amount = money($account->expense_balance, $account->currency_code); + $current_amount = money($account->balance, $account->currency_code); + + $summary_amounts = [ + 'incoming_exact' => $incoming_amount->format(), + 'incoming_for_humans' => $incoming_amount->formatForHumans(), + 'outgoing_exact' => $outgoing_amount->format(), + 'outgoing_for_humans' => $outgoing_amount->formatForHumans(), + 'current_exact' => $current_amount->format(), + 'current_for_humans' => $current_amount->formatForHumans(), + ]; + + return view('banking.accounts.show', compact('account', 'transactions', 'transfers', 'summary_amounts')); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + $currency = Currency::where('code', '=', default_currency())->first(); + + return view('banking.accounts.create', compact('currency')); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $response = $this->ajaxDispatch(new CreateAccount($request)); + + if ($response['success']) { + $response['redirect'] = route('accounts.show', $response['data']->id); + + $message = trans('messages.success.created', ['type' => trans_choice('general.accounts', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('accounts.create'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Duplicate the specified resource. + * + * @param Account $account + * + * @return Response + */ + public function duplicate(Account $account) + { + $clone = $account->duplicate(); + + $message = trans('messages.success.duplicated', ['type' => trans_choice('general.accounts', 1)]); + + flash($message)->success(); + + return redirect()->route('accounts.edit', $clone->id); + } + + /** + * Show the form for editing the specified resource. + * + * @param Account $account + * + * @return Response + */ + public function edit(Account $account) + { + $account->default_account = ($account->id == setting('default.account')) ? 1 : 0; + + $currency = Currency::where('code', '=', $account->currency_code)->first(); + + return view('banking.accounts.edit', compact('account', 'currency')); + } + + /** + * Update the specified resource in storage. + * + * @param Account $account + * @param Request $request + * + * @return Response + */ + public function update(Account $account, Request $request) + { + $response = $this->ajaxDispatch(new UpdateAccount($account, $request)); + + if ($response['success']) { + $response['redirect'] = route('accounts.show', $account->id); + + $message = trans('messages.success.updated', ['type' => $account->name]); + + flash($message)->success(); + } else { + $response['redirect'] = route('accounts.edit', $account->id); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Enable the specified resource. + * + * @param Account $account + * + * @return Response + */ + public function enable(Account $account) + { + $response = $this->ajaxDispatch(new UpdateAccount($account, request()->merge(['enabled' => 1]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.enabled', ['type' => $account->name]); + } + + return response()->json($response); + } + + /** + * Disable the specified resource. + * + * @param Account $account + * + * @return Response + */ + public function disable(Account $account) + { + $response = $this->ajaxDispatch(new UpdateAccount($account, request()->merge(['enabled' => 0]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.disabled', ['type' => $account->name]); + } + + return response()->json($response); + } + + /** + * Remove the specified resource from storage. + * + * @param Account $account + * + * @return Response + */ + public function destroy(Account $account) + { + $response = $this->ajaxDispatch(new DeleteAccount($account)); + + $response['redirect'] = route('accounts.index'); + + if ($response['success']) { + $message = trans('messages.success.deleted', ['type' => $account->name]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + public function createIncome(Account $account) + { + $data['account_id'] = $account->id; + + return redirect()->route('transactions.create', ['type' => 'income'])->withInput($data); + } + + public function createExpense(Account $account) + { + $data['account_id'] = $account->id; + + return redirect()->route('transactions.create', ['type' => 'expense'])->withInput($data); + } + + public function createTransfer(Account $account) + { + $data['from_account_id'] = $account->id; + + return redirect()->route('transfers.create')->withInput($data); + } + + public function seePerformance(Account $account) + { + $data = [ + 'year' => Date::now()->year, + 'basis' => 'accrual', + 'account_id' => $account->id, + ]; + + $report = Reports::getClassInstance('App\Reports\IncomeExpenseSummary'); + + if (empty($report) || empty($report->model)) { + $message = trans('accounts.create_report'); + + flash($message)->warning()->important(); + + return redirect()->route('reports.create'); + } + + return redirect()->route('reports.show', $report->model->id)->withInput($data); + } + + public function currency() + { + $account_id = (int) request('account_id'); + + if (empty($account_id)) { + return response()->json([]); + } + + $account = Account::find($account_id); + + if (empty($account)) { + return response()->json([]); + } + + $currency_code = default_currency(); + + if (isset($account->currency_code)) { + $currencies = Currency::enabled()->pluck('name', 'code')->toArray(); + + if (array_key_exists($account->currency_code, $currencies)) { + $currency_code = $account->currency_code; + } + } + + // Get currency object + $currency = Currency::where('code', $currency_code)->first(); + + return response()->json([ + 'id' => $account->id, + 'currency_name' => $currency->name, + 'currency_code' => $currency_code, + 'currency_rate' => $currency->rate, + 'thousands_separator' => $currency->thousands_separator, + 'decimal_mark' => $currency->decimal_mark, + 'precision' => (int) $currency->precision, + 'symbol_first' => $currency->symbol_first, + 'symbol' => $currency->symbol, + ]); + } +} diff --git a/app/Http/Controllers/Banking/FundAccounts.php b/app/Http/Controllers/Banking/FundAccounts.php new file mode 100644 index 00000000..f574071f --- /dev/null +++ b/app/Http/Controllers/Banking/FundAccounts.php @@ -0,0 +1,182 @@ + 'bank_accounts', + 'alipay' => 'alipay_accounts', + 'web_wallet' => 'web_wallet_accounts', + 'pdd' => 'pdd_accounts', + 'dewu' => 'dewu_accounts', + ]; + + public function __construct() + { + $this->middleware('permission:read-banking-accounts')->only('bankIndex', 'storeIndex', 'platformIndex'); + $this->middleware('permission:create-banking-accounts')->only('create', 'store', 'import', 'template'); + $this->middleware('permission:update-banking-accounts')->only('edit', 'update', 'export'); + $this->middleware('permission:delete-banking-accounts')->only('destroy'); + } + + public function bankIndex() + { + return $this->index('bank'); + } + + public function storeIndex() + { + return view('banking.fund_accounts.store-index', [ + 'platforms' => array_filter(self::PLATFORMS, fn ($platform) => $platform !== 'bank', ARRAY_FILTER_USE_KEY), + ]); + } + + public function platformIndex(string $platform) + { + return $this->index($platform); + } + + public function create(string $platform) + { + return view('banking.fund_accounts.form', [ + 'platform' => $this->platform($platform), + 'account' => new FundAccount([ + 'company_name' => company()?->name ?? '', + ]), + ]); + } + + public function template(string $platform, FundAccountSpreadsheet $spreadsheet): StreamedResponse + { + return $spreadsheet->downloadTemplate($this->platform($platform)); + } + + public function export(HttpRequest $request, string $platform, FundAccountSpreadsheet $spreadsheet): StreamedResponse + { + $platform = $this->platform($platform); + $ids = $request->validate([ + 'ids' => ['nullable', 'array', 'max:5000'], + 'ids.*' => ['integer'], + ])['ids'] ?? []; + + $query = FundAccount::where('platform', $platform)->oldest('id'); + + if (! empty($ids)) { + $query->whereKey($ids); + } + + return $spreadsheet->downloadAccounts($platform, $query->get()); + } + + public function import(HttpRequest $request, string $platform, FundAccountSpreadsheet $spreadsheet): RedirectResponse + { + $platform = $this->platform($platform); + $validated = $request->validate([ + 'import' => ['required', 'file', 'mimes:xlsx,xls,csv', 'max:5120'], + ], [ + 'import.required' => trans('fund_accounts.import_required'), + 'import.mimes' => trans('fund_accounts.import_file_type'), + 'import.max' => trans('fund_accounts.import_file_size'), + ]); + + $count = $spreadsheet->import($platform, $validated['import']); + + flash(trans('fund_accounts.import_success', ['count' => $count]))->success(); + + return redirect()->route('fund-accounts.platform.index', $platform); + } + + public function store(Request $request, string $platform): RedirectResponse + { + $platform = $this->platform($platform); + $data = $request->validated(); + + if ($platform === 'bank') { + $data['account_name'] = $data['bank_name']; + } + + $account = FundAccount::create(array_merge($data, [ + 'company_id' => company_id(), + 'platform' => $platform, + 'created_from' => 'core::ui', + 'created_by' => user()->id, + ])); + + flash(trans('messages.success.created', ['type' => $this->title($account->platform)]))->success(); + + return redirect()->route('fund-accounts.platform.index', $account->platform); + } + + public function edit(string $platform, FundAccount $fundAccount) + { + abort_unless($fundAccount->platform === $this->platform($platform), 404); + + return view('banking.fund_accounts.form', [ + 'platform' => $fundAccount->platform, + 'account' => $fundAccount, + ]); + } + + public function update(Request $request, string $platform, FundAccount $fundAccount): RedirectResponse + { + abort_unless($fundAccount->platform === $this->platform($platform), 404); + + $data = $request->validated(); + + if (empty($data['login_password'])) { + unset($data['login_password']); + } + + if ($fundAccount->platform === 'bank') { + $data['account_name'] = $data['bank_name']; + } + + $fundAccount->update($data); + + flash(trans('messages.success.updated', ['type' => $this->title($fundAccount->platform)]))->success(); + + return redirect()->route('fund-accounts.platform.index', $fundAccount->platform); + } + + public function destroy(string $platform, FundAccount $fundAccount): RedirectResponse + { + abort_unless($fundAccount->platform === $this->platform($platform), 404); + + $fundAccount->delete(); + + flash(trans('messages.success.deleted', ['type' => $this->title($platform)]))->success(); + + return redirect()->route('fund-accounts.platform.index', $platform); + } + + private function index(string $platform) + { + $platform = $this->platform($platform); + + return view('banking.fund_accounts.index', [ + 'platform' => $platform, + 'accounts' => FundAccount::where('platform', $platform)->latest()->paginate(setting('default.list_limit', 25)), + ]); + } + + private function platform(string $platform): string + { + abort_unless(array_key_exists($platform, self::PLATFORMS), 404); + + return $platform; + } + + private function title(string $platform): string + { + return trans('fund_accounts.' . self::PLATFORMS[$platform]); + } +} diff --git a/app/Http/Controllers/Banking/Reconciliations.php b/app/Http/Controllers/Banking/Reconciliations.php new file mode 100644 index 00000000..22defce1 --- /dev/null +++ b/app/Http/Controllers/Banking/Reconciliations.php @@ -0,0 +1,256 @@ +collect(); + + $reconciled_amount = money($reconciliations->where('reconciled', 1)->sum('closing_balance')); + $in_progress_amount = money($reconciliations->where('reconciled', 0)->sum('closing_balance')); + + $summary_amounts = [ + 'amount_exact' => $reconciled_amount->format(), + 'amount_for_humans' => $reconciled_amount->formatForHumans(), + 'in_progress_exact' => $in_progress_amount->format(), + 'in_progress_for_humans' => $in_progress_amount->formatForHumans(), + ]; + + return $this->response('banking.reconciliations.index', compact('reconciliations', 'summary_amounts')); + } + + /** + * Show the form for viewing the specified resource. + * + * @return Response + */ + public function show() + { + return redirect()->route('reconciliations.index'); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + $account_id = request('account_id', setting('default.account')); + $started_at = request('started_at', Date::now()->firstOfMonth()->toDateString()); + $ended_at = request('ended_at', Date::now()->endOfMonth()->toDateString()); + + $account = Account::find($account_id); + + $currency = $account->currency; + + $transactions = $this->getTransactions($account, $started_at, $ended_at); + + $opening_balance = $this->getOpeningBalance($account, $started_at); + + return view('banking.reconciliations.create', compact('account', 'currency', 'opening_balance', 'transactions')); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $response = $this->ajaxDispatch(new CreateReconciliation($request)); + + if ($response['success']) { + $response['redirect'] = route('reconciliations.index'); + + $message = trans('messages.success.created', ['type' => trans_choice('general.reconciliations', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('reconciliations.create'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param Reconciliation $reconciliation + * + * @return Response + */ + public function edit(Reconciliation $reconciliation) + { + $account = $reconciliation->account; + + $currency = $account->currency; + + $transactions = $this->getTransactions($account, $reconciliation->started_at, $reconciliation->ended_at); + + $opening_balance = $this->getOpeningBalance($account, $reconciliation->started_at, $reconciliation->ended_at); + + return view('banking.reconciliations.edit', compact('reconciliation', 'account', 'currency', 'opening_balance', 'transactions')); + } + + /** + * Update the specified resource in storage. + * + * @param Reconciliation $reconciliation + * @param Request $request + * + * @return Response + */ + public function update(Reconciliation $reconciliation, Request $request) + { + $response = $this->ajaxDispatch(new UpdateReconciliation($reconciliation, $request)); + + if ($response['success']) { + $response['redirect'] = route('reconciliations.index'); + + $message = trans('messages.success.updated', ['type' => trans_choice('general.reconciliations', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('reconciliations.edit', $reconciliation->id); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Remove the specified resource from storage. + * + * @param Reconciliation $reconciliation + * + * @return Response + */ + public function destroy(Reconciliation $reconciliation) + { + $response = $this->ajaxDispatch(new DeleteReconciliation($reconciliation)); + + $response['redirect'] = route('reconciliations.index'); + + if ($response['success']) { + $message = trans('messages.success.deleted', ['type' => trans_choice('general.reconciliations', 1)]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Add transactions array. + * + * @param $account + * @param $started_at + * @param $ended_at + * + * @return mixed + */ + protected function getTransactions($account, $started_at, $ended_at) + { + $started = explode(' ', $started_at)[0] . ' 00:00:00'; + $ended = explode(' ', $ended_at)[0] . ' 23:59:59'; + + $transactions = Transaction::with('account', 'contact')->where('account_id', $account->id)->whereBetween('paid_at', [$started, $ended])->get(); + + return collect($transactions)->sortByDesc('paid_at'); + } + + /** + * Get the opening balance + * + * @param $account + * @param $started_at + * + * @return string + */ + public function getOpeningBalance($account, $started_at) + { + // Opening Balance + $total = $account->opening_balance; + + // Sum income transactions + $transactions = $account->income_transactions()->whereDate('paid_at', '<', $started_at)->get(); + foreach ($transactions as $item) { + $total += $item->amount; + } + + // Subtract expense transactions + $transactions = $account->expense_transactions()->whereDate('paid_at', '<', $started_at)->get(); + foreach ($transactions as $item) { + $total -= $item->amount; + } + + return $total; + } + + public function calculate(CalculateRequest $request) + { + $currency_code = $request['currency_code']; + $closing_balance = $request['closing_balance']; + + $json = new \stdClass(); + + $cleared_amount = $difference = $income_total = $expense_total = 0; + + if ($transactions = $request['transactions']) { + $opening_balance = $request['opening_balance']; + + foreach ($transactions as $key => $value) { + $k = explode('_', $key); + + if ($k[1] == 'income') { + $income_total += $value; + } else { + $expense_total += $value; + } + } + + $cleared_amount = $opening_balance + ($income_total - $expense_total); + } + + $difference = $closing_balance - $cleared_amount; + + $json->closing_balance = money($closing_balance, $currency_code)->format(); + $json->cleared_amount = money($cleared_amount, $currency_code)->format(); + $json->difference = money($difference, $currency_code)->format(); + $json->difference_raw = (int) $difference; + + return response()->json($json); + } +} diff --git a/app/Http/Controllers/Banking/RecurringTransactions.php b/app/Http/Controllers/Banking/RecurringTransactions.php new file mode 100644 index 00000000..e2c731de --- /dev/null +++ b/app/Http/Controllers/Banking/RecurringTransactions.php @@ -0,0 +1,271 @@ +middleware('permission:create-banking-transactions')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-banking-transactions')->only('index', 'show', 'edit', 'export'); + $this->middleware('permission:update-banking-transactions')->only('update', 'enable', 'disable', 'end'); + $this->middleware('permission:delete-banking-transactions')->only('destroy'); + } + + /** + * Display a listing of the resource. + * + * @return Response + */ + public function index() + { + $transactions = Transaction::with('category', 'recurring')->isRecurring()->collect(['paid_at'=> 'desc']); + + return $this->response('banking.recurring_transactions.index', compact('transactions')); + } + + /** + * Show the form for viewing the specified resource. + * + * @return Response + */ + public function show(Transaction $recurring_transaction) + { + $recurring_transaction->load(['category', 'recurring', 'children']); + + $title = ($recurring_transaction->type == 'income-recurring') ? trans_choice('general.recurring_incomes', 1) : trans_choice('general.recurring_expenses', 1); + + return view('banking.recurring_transactions.show', compact('recurring_transaction', 'title')); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + $type = $this->getTypeRecurringTransaction(request()->get('type', 'income-recurring')); + $real_type = $this->getTypeTransaction(request()->get('real_type', $this->getRealTypeOfRecurringTransaction($type))); + $contact_type = config('type.transaction.' . $real_type . '.contact_type', 'customer'); + + $number = $this->getNextTransactionNumber($type, '-recurring'); + + $account_currency_code = Account::where('id', setting('default.account'))->pluck('currency_code')->first(); + + $currency = Currency::where('code', $account_currency_code)->first(); + + $taxes = Tax::enabled()->orderBy('name')->get(); + + return view('banking.recurring_transactions.create', compact( + 'type', + 'real_type', + 'number', + 'contact_type', + 'account_currency_code', + 'currency', + 'taxes' + )); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $request->merge(['paid_at' => $request->get('recurring_started_at')]); + + $response = $this->ajaxDispatch(new CreateTransaction($request)); + + if ($response['success']) { + $response['redirect'] = route('recurring-transactions.show', $response['data']->id); + + $message = trans('messages.success.created', ['type' => trans_choice('general.recurring_transactions', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('recurring-transactions.create'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Duplicate the specified resource. + * + * @param Transaction $recurring_transaction + * + * @return Response + */ + public function duplicate(Transaction $recurring_transaction) + { + $clone = $recurring_transaction->duplicate(); + + $message = trans('messages.success.duplicated', ['type' => trans_choice('general.recurring_transactions', 1)]); + + flash($message)->success(); + + return redirect()->route('recurring-transactions.edit', $clone->id); + } + + /** + * Import the specified resource. + * + * @param ImportRequest $request + * + * @return Response + */ + public function import(ImportRequest $request) + { + $response = $this->importExcel(new Import, $request, trans_choice('general.recurring_transactions', 2)); + + if ($response['success']) { + $response['redirect'] = route('recurring-transactions.index'); + + flash($response['message'])->success(); + } else { + $response['redirect'] = route('import.create', ['banking', 'recurring-transactions']); + + flash($response['message'])->error()->important(); + } + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param Transaction $recurring_transaction + * + * @return Response + */ + public function edit(Transaction $recurring_transaction) + { + $type = $recurring_transaction->type; + $real_type = $this->getTypeTransaction(request()->get('real_type', $this->getRealTypeOfRecurringTransaction($type))); + $contact_type = config('type.transaction.' . $real_type . '.contact_type', 'customer'); + + $number = $this->getNextTransactionNumber($type, '-recurring'); + + $currency = Currency::where('code', $recurring_transaction->currency_code)->first(); + + $taxes = Tax::enabled()->orderBy('name')->get(); + + $date_format = $this->getCompanyDateFormat(); + + return view('banking.recurring_transactions.edit', compact( + 'type', + 'real_type', + 'number', + 'contact_type', + 'recurring_transaction', + 'currency', + 'taxes', + 'date_format' + )); + } + + /** + * Update the specified resource in storage. + * + * @param Transaction $recurring_transaction + * @param Request $request + * + * @return Response + */ + public function update(Transaction $recurring_transaction, Request $request) + { + $request->merge(['paid_at' => $request->get('recurring_started_at')]); + + $response = $this->ajaxDispatch(new UpdateTransaction($recurring_transaction, $request)); + + if ($response['success']) { + $response['redirect'] = route('recurring-transactions.show', $recurring_transaction->id); + + $message = trans('messages.success.updated', ['type' => trans_choice('general.recurring_transactions', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('recurring-transactions.edit', $recurring_transaction->id); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Export the specified resource. + * + * @return Response + */ + public function export() + { + return $this->exportExcel(new Export, trans_choice('general.recurring_transactions', 2)); + } + + /** + * End recurring template. + * + * @return Response + */ + public function end(Transaction $recurring_transaction) + { + $response = $this->ajaxDispatch(new UpdateTransaction($recurring_transaction, [ + 'recurring_frequency' => $recurring_transaction->recurring->frequency, + 'recurring_interval' => $recurring_transaction->recurring->interval, + 'recurring_started_at' => $recurring_transaction->recurring->started_at, + 'recurring_limit' => $recurring_transaction->recurring->limit, + 'recurring_limit_count' => $recurring_transaction->recurring->limit_count, + 'recurring_limit_date' => $recurring_transaction->recurring->limit_date, + 'created_from' => $recurring_transaction->created_from, + 'created_by' => $recurring_transaction->created_by, + 'recurring_status' => Recurring::END_STATUS, + ])); + + if ($response['success']) { + $message = trans('messages.success.ended', ['type' => trans_choice('general.recurring_transactions', 1)]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return redirect()->route('recurring-transactions.index'); + } +} diff --git a/app/Http/Controllers/Banking/Transactions.php b/app/Http/Controllers/Banking/Transactions.php new file mode 100644 index 00000000..2730679a --- /dev/null +++ b/app/Http/Controllers/Banking/Transactions.php @@ -0,0 +1,462 @@ +setActiveTabForTransactions(); + + $transactions = Transaction::with( + 'account', + 'category', + 'contact', + 'taxes', + 'document', + 'document.items', + 'document.last_history' + )->collect(['paid_at'=> 'desc']); + + $total_transactions = Transaction::count(); + + $totals = [ + 'income' => 0, + 'expense' => 0, + 'profit' => 0, + ]; + + $transactions->each(function ($transaction) use (&$totals) { + if (($transaction->isNotIncome() && $transaction->isNotExpense()) || $transaction->isTransferTransaction()) { + return; + } + + $type = $transaction->isIncome() ? 'income' : 'expense'; + + $totals[$type] += $transaction->getAmountConvertedToDefault(); + }); + + $totals['profit'] = $totals['income'] - $totals['expense']; + + $incoming_amount = money($totals['income']); + $expense_amount = money($totals['expense']); + $profit_amount = money($totals['profit']); + + $summary_amounts = [ + 'incoming_exact' => $incoming_amount->format(), + 'incoming_for_humans' => $incoming_amount->formatForHumans(), + 'expense_exact' => $expense_amount->format(), + 'expense_for_humans' => $expense_amount->formatForHumans(), + 'profit_exact' => $profit_amount->format(), + 'profit_for_humans' => $profit_amount->formatForHumans(), + ]; + + $search_type = search_string_value('type'); + $type = empty($search_type) ? 'transactions' : (($search_type == 'income') ? 'income' : 'expense'); + + return $this->response('banking.transactions.index', compact( + 'type', + 'transactions', + 'total_transactions', + 'summary_amounts' + )); + } + + /** + * Show the form for viewing the specified resource. + * + * @return Response + */ + public function show(Transaction $transaction) + { + $transaction->load('taxes'); + + $title = $transaction->isIncome() ? trans_choice('general.receipts', 1) : trans('transactions.payment_made'); + $real_type = $this->getRealTypeTransaction($transaction->type); + + return view('banking.transactions.show', compact('transaction', 'title', 'real_type')); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + $type = $this->getTypeTransaction(request()->get('type', 'income')); + $real_type = $this->getRealTypeTransaction($type); + + $number = $this->getNextTransactionNumber($type); + + $contact_type = config('type.transaction.' . $type . '.contact_type'); + + $account_currency_code = Account::where('id', setting('default.account'))->pluck('currency_code')->first(); + + $currency = Currency::where('code', $account_currency_code)->first(); + + $taxes = Tax::enabled()->orderBy('name')->get(); + + return view('banking.transactions.create', compact( + 'type', + 'real_type', + 'number', + 'contact_type', + 'account_currency_code', + 'currency', + 'taxes' + )); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $response = $this->ajaxDispatch(new CreateTransaction($request)); + + if ($response['success']) { + $response['redirect'] = route('transactions.show', $response['data']->id); + + $message = trans('messages.success.created', ['type' => trans_choice('general.transactions', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('transactions.create'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Duplicate the specified resource. + * + * @param Transaction $transaction + * + * @return Response + */ + public function duplicate(Transaction $transaction) + { + $clone = $this->dispatch(new DuplicateTransaction($transaction)); + + $message = trans('messages.success.duplicated', ['type' => trans_choice('general.transactions', 1)]); + + flash($message)->success(); + + return redirect()->route('transactions.edit', $clone->id); + } + + /** + * Import the specified resource. + * + * @param ImportRequest $request + * + * @return Response + */ + public function import(ImportRequest $request) + { + $response = $this->importExcel(new Import, $request, trans_choice('general.transactions', 2)); + + if ($response['success']) { + $response['redirect'] = route('transactions.index'); + + flash($response['message'])->success(); + } else { + $response['redirect'] = route('import.create', ['banking', 'transactions']); + + flash($response['message'])->error()->important(); + } + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param Transaction $transaction + * + * @return Response + */ + public function edit(Transaction $transaction) + { + $type = $transaction->type; + $contact_type = config('type.transaction.' . $type . '.contact_type'); + + $currency = Currency::where('code', $transaction->currency_code)->first(); + + $taxes = Tax::enabled()->orderBy('name')->get(); + + $date_format = $this->getCompanyDateFormat(); + + return view('banking.transactions.edit', compact( + 'type', + 'contact_type', + 'transaction', + 'currency', + 'taxes', + 'date_format' + )); + } + + /** + * Update the specified resource in storage. + * + * @param Transaction $transaction + * @param Request $request + * + * @return Response + */ + public function update(Transaction $transaction, Request $request) + { + if ($transaction->document_id) { + $message = trans('transactions.messages.update_document_transaction'); + + flash($message)->error()->important(); + + return response()->json([ + 'success' => false, + 'error' => true, + 'message' => $message, + 'redirect' => route('transactions.edit', $transaction->id), + ]); + } + + $response = $this->ajaxDispatch(new UpdateTransaction($transaction, $request)); + + if ($response['success']) { + $response['redirect'] = route('transactions.show', $transaction->id); + + $message = trans('messages.success.updated', ['type' => trans_choice('general.transactions', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('transactions.edit', $transaction->id); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Remove the specified resource from storage. + * + * @param Transaction $transaction + * + * @return Response + */ + public function destroy(Transaction $transaction) + { + $response = $this->ajaxDispatch(new DeleteTransaction($transaction)); + + $response['redirect'] = route('transactions.index'); + + if ($response['success']) { + $message = trans('messages.success.deleted', ['type' => trans_choice('general.transactions', 1)]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Export the specified resource. + * + * @return Response + */ + public function export() + { + return $this->exportExcel(new Export, trans_choice('general.transactions', 2)); + } + + /** + * Download the PDF file of transaction. + * + * @param Transaction $transaction + * + * @return Response + */ + public function emailTransaction(Transaction $transaction) + { + if (empty($transaction->contact->email)) { + return redirect()->back(); + } + + $response = $this->ajaxDispatch(new SendTransaction($transaction)); + + if ($response['success']) { + $message = trans('documents.messages.email_sent', ['type' => trans_choice('general.transactions', 1)]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return redirect()->back(); + } + + /** + * Print the transaction. + * + * @param Transaction $transaction + * + * @return Response + */ + public function printTransaction(Transaction $transaction) + { + event(new TransactionPrinting($transaction)); + + $real_type = $this->getRealTypeTransaction($transaction->type); + + $view = view('banking.transactions.print_default', compact('transaction', 'real_type')); + + return mb_convert_encoding($view, 'HTML-ENTITIES', 'UTF-8'); + } + + /** + * Download the PDF file of transaction. + * + * @param Transaction $transaction + * + * @return Response + */ + public function pdfTransaction(Transaction $transaction) + { + event(new TransactionPrinting($transaction)); + + $currency_style = true; + + $real_type = $this->getRealTypeTransaction($transaction->type); + + $view = view('banking.transactions.print_default', compact('transaction', 'currency_style', 'real_type'))->render(); + $html = mb_convert_encoding($view, 'HTML-ENTITIES', 'UTF-8'); + + $pdf = app('dompdf.wrapper'); + $pdf->loadHTML($html); + + //$pdf->setPaper('A4', 'portrait'); + + $file_name = $this->getTransactionFileName($transaction); + + return $pdf->download($file_name); + } + + public function dial(Transaction $transaction) + { + $documents = collect([]); + + if ($transaction->isIncome() && $transaction->contact->exists) { + $builder = $transaction->contact->invoices(); + } + + if ($transaction->isIncome() && ! $transaction->contact->exists) { + $builder = Document::invoice(); + } + + if ($transaction->isExpense() && $transaction->contact->exists) { + $builder = $transaction->contact->bills(); + } + + if ($transaction->isExpense() && ! $transaction->contact->exists) { + $builder = Document::bill(); + } + + if (isset($builder)) { + $documents = $builder->notPaid() + ->where('currency_code', $transaction->currency_code) + ->with(['media', 'totals', 'transactions']) + ->get() + ->toJson(); + } + + $translations = collect($this->getTranslationsForConnect($transaction->type)); + + $data = [ + 'transaction' => $transaction->load(['account', 'category', 'taxes'])->toJson(), + 'currency' => $transaction->currency->toJson(), + 'documents' => $documents, + 'translations' => $translations->toJson(), + ]; + + return response()->json($data); + } + + public function connect(Transaction $transaction, TransactionConnect $request) + { + $total_items = count($request->data['items']); + + if ($total_items == 1) { + $document = Document::find($request->data['items'][0]['document_id']); + + if (!is_null($document)) { + $response = $this->ajaxDispatch(new MatchBankingDocumentTransaction($document, $transaction)); + } + } + + if ($total_items > 1) { + $response = $this->ajaxDispatch(new SplitTransaction($transaction, $request->data)); + } + + $response['redirect'] = route('transactions.index'); + + if ($response['success']) { + $message = trans('messages.success.connected', ['type' => trans_choice('general.transactions', 1)]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Banking/Transfers.php b/app/Http/Controllers/Banking/Transfers.php new file mode 100644 index 00000000..defd4d4e --- /dev/null +++ b/app/Http/Controllers/Banking/Transfers.php @@ -0,0 +1,252 @@ +collect(['expense_transaction.paid_at' => 'desc']); + + return $this->response('banking.transfers.index', compact('transfers')); + } + + /** + * Show the form for viewing the specified resource. + * + * @return Response + */ + public function show(Transfer $transfer) + { + return view('banking.transfers.show', compact('transfer')); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + $accounts = Account::enabled()->orderBy('name')->get()->pluck('title', 'id'); + + $currency = Currency::where('code', default_currency())->first(); + + return view('banking.transfers.create', compact('accounts', 'currency')); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $response = $this->ajaxDispatch(new CreateTransfer($request)); + + if ($response['success']) { + $response['redirect'] = route('transfers.show', $response['data']->id); + + $message = trans('messages.success.created', ['type' => trans_choice('general.transfers', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('transfers.create'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Duplicate the specified resource. + * + * @param Transfer $transfer + * + * @return Response + */ + public function duplicate(Transfer $transfer) + { + $clone = $transfer->duplicate(); + + $message = trans('messages.success.duplicated', ['type' => trans_choice('general.transfers', 1)]); + + flash($message)->success(); + + return redirect()->route('transfers.show', $clone->id); + } + + /** + * Import the specified resource. + * + * @param ImportRequest $request + * + * @return Response + */ + public function import(ImportRequest $request) + { + $response = $this->importExcel(new Import, $request, trans_choice('general.transfers', 2)); + + if ($response['success']) { + $response['redirect'] = route('transfers.index'); + + flash($response['message'])->success(); + } else { + $response['redirect'] = route('import.create', ['banking', 'transfers']); + + flash($response['message'])->error()->important(); + } + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param Transfer $transfer + * + * @return Response + */ + public function edit(Transfer $transfer) + { + $accounts = Account::enabled()->orderBy('name')->get()->pluck('title', 'id'); + + $currency_code = ($transfer->expense_transaction->account) ? $transfer->expense_transaction->account->currency_code : default_currency(); + + $currency = Currency::where('code', $currency_code)->first(); + + return view('banking.transfers.edit', compact('transfer', 'accounts', 'currency')); + } + + /** + * Update the specified resource in storage. + * + * @param $id + * @param Request $request + * + * @return Response + */ + public function update(Transfer $transfer, Request $request) + { + $response = $this->ajaxDispatch(new UpdateTransfer($transfer, $request)); + + if ($response['success']) { + $response['redirect'] = route('transfers.show', $transfer->id); + + $message = trans('messages.success.updated', ['type' => trans_choice('general.transfers', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('transfers.edit', $transfer->id); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Remove the specified resource from storage. + * + * @param $id + * + * @return Response + */ + public function destroy(Transfer $transfer) + { + $response = $this->ajaxDispatch(new DeleteTransfer($transfer)); + + $response['redirect'] = route('transfers.index'); + + if ($response['success']) { + $message = trans('messages.success.deleted', ['type' => trans_choice('general.transfers', 1)]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Export the specified resource. + * + * @return Response + */ + public function export() + { + return $this->exportExcel(new Export, trans_choice('general.transfers', 2)); + } + + /** + * Print the transfer. + * + * @param Transfer $transfer + * + * @return Response + */ + public function printTransfer(Transfer $transfer) + { + event(new \App\Events\Banking\TransferPrinting($transfer)); + + $view = view($transfer->template_path, compact('transfer')); + + return mb_convert_encoding($view, 'HTML-ENTITIES', 'UTF-8'); + } + + /** + * Download the PDF file of transfer. + * + * @param Transfer $transfer + * + * @return Response + */ + public function pdfTransfer(Transfer $transfer) + { + event(new \App\Events\Banking\TransferPrinting($transfer)); + + $currency_style = true; + + $view = view($transfer->template_path, compact('transfer', 'currency_style'))->render(); + $html = mb_convert_encoding($view, 'HTML-ENTITIES', 'UTF-8'); + + $pdf = app('dompdf.wrapper'); + $pdf->loadHTML($html); + + //$pdf->setPaper('A4', 'portrait'); + + $file_name = trans_choice('general.transfers', 1) . '-' . Str::slug($transfer->id, '-', language()->getShortCode()) . '-' . time() . '.pdf'; + + return $pdf->download($file_name); + } +} diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php new file mode 100644 index 00000000..0e16b4b7 --- /dev/null +++ b/app/Http/Controllers/Business/Pages.php @@ -0,0 +1,99 @@ +first(); + + abort_if(empty($section), 403, '当前账号没有已授权的侧边栏功能。'); + + $page = collect($section['pages'] ?? [])->first(); + $parameters = ['section' => $section['slug']]; + + if ($page) { + $parameters['page'] = $page['slug']; + + if (! empty($page['tabs'])) { + $parameters['tab'] = $page['tabs'][0]['slug']; + } + } + + return redirect()->route('business.pages.show', $parameters); + } + + public function show(string $section, ?string $page = null, ?string $tab = null) + { + $sectionConfig = collect(SidebarPermissions::navigationFor(user()))->firstWhere('slug', $section); + + abort_if(empty($sectionConfig), 403, '当前账号没有该页面权限。'); + + $pages = collect($sectionConfig['pages'] ?? []); + + if ($pages->isEmpty()) { + abort_if($page !== null || $tab !== null, 404); + + return view('business.pages.show', [ + 'section' => $sectionConfig, + 'page' => null, + 'tabs' => collect(), + 'activeTab' => null, + ]); + } + + if ($page === null) { + return $this->redirectToPage($sectionConfig, $pages->first()); + } + + $pageConfig = $pages->firstWhere('slug', $page); + + abort_if(empty($pageConfig), 404); + + $tabs = collect($pageConfig['tabs'] ?? []); + + if ($tabs->isEmpty()) { + abort_if($tab !== null, 404); + + return view('business.pages.show', [ + 'section' => $sectionConfig, + 'page' => $pageConfig, + 'tabs' => $tabs, + 'activeTab' => null, + ]); + } + + if ($tab === null) { + return $this->redirectToPage($sectionConfig, $pageConfig); + } + + $activeTab = $tabs->firstWhere('slug', $tab); + + abort_if(empty($activeTab), 404); + + return view('business.pages.show', [ + 'section' => $sectionConfig, + 'page' => $pageConfig, + 'tabs' => $tabs, + 'activeTab' => $activeTab, + ]); + } + + protected function redirectToPage(array $section, array $page) + { + $parameters = [ + 'section' => $section['slug'], + 'page' => $page['slug'], + ]; + + if (! empty($page['tabs'])) { + $parameters['tab'] = $page['tabs'][0]['slug']; + } + + return redirect()->route('business.pages.show', $parameters); + } +} diff --git a/app/Http/Controllers/Common/BulkActions.php b/app/Http/Controllers/Common/BulkActions.php new file mode 100644 index 00000000..16ad79bb --- /dev/null +++ b/app/Http/Controllers/Common/BulkActions.php @@ -0,0 +1,167 @@ +get('handle', '*'); + + if ($handle == '*') { + return response()->json([ + 'success' => false, + 'redirect' => true, + 'error' => true, + 'data' => [], + 'message' => '' + ]); + } + + // Check is module + $module = module($group); + $page = ucfirst($type); + + if ($module instanceof \Akaunting\Module\Module) { + $tmp = explode('.', $type); + $file_name = !empty($tmp[1]) ? Str::studly($tmp[0]) . '\\' . Str::studly($tmp[1]) : Str::studly($tmp[0]); + + $bulk_actions = app('Modules\\' . $module->getStudlyName() . '\BulkActions\\' . $file_name); + + $page = ucfirst($file_name); + } else { + $bulk_actions = app('App\BulkActions\\' . ucfirst($group) . '\\' . ucfirst($type)); + } + + // Security: resolve the handle to a declared action and enforce its permission. + // + // Handles come in two flavors: + // 1. Direct action keys in $actions (e.g. "delete", "enable", "export"). + // 2. Modal submit handles declared via the 'handle' field of a modal-type + // action (e.g. "update" declared by 'edit' => ['handle' => 'update']). + // + // Without validating against this allowlist, an attacker can bypass the + // permission check by calling inherited methods directly — e.g. handle=destroy + // instead of handle=delete, since destroy() exists on the abstract base but is + // not keyed in $actions. We also inherit the permission from the parent action + // for modal submit handles so that handle=update is gated by update-* just like + // handle=edit. + + $action_key = $handle; + + if (! array_key_exists($action_key, $bulk_actions->actions)) { + // The handle is not a direct action; check if it's a modal submit handle. + $found = false; + + foreach ($bulk_actions->actions as $key => $action) { + if (isset($action['handle']) && $action['handle'] === $handle) { + $action_key = $key; + $found = true; + break; + } + } + + if (! $found) { + flash(trans('errors.message.403'))->error()->important(); + + return response()->json([ + 'success' => false, + 'redirect' => true, + 'error' => true, + 'data' => [], + 'message' => trans('errors.message.403') + ]); + } + } + + if ( + isset($bulk_actions->actions[$action_key]['permission']) + && ! user()->can($bulk_actions->actions[$action_key]['permission']) + ) { + flash(trans('errors.message.403'))->error()->important(); + + return response()->json([ + 'success' => false, + 'redirect' => true, + 'error' => true, + 'data' => [], + 'message' => trans('errors.message.403') + ]); + } + + $result = $bulk_actions->{$handle}($request); + + $count = count($request->get('selected')); + $not_passed = 0; + + flash()->messages->each(function ($message) use (&$not_passed) { + if (in_array($message->level, ['danger', 'warning'])) { + $not_passed++; + } + }); + + $message = trans($bulk_actions->messages['general'], ['type' => $handle, 'count' => $count - $not_passed]); + + if (array_key_exists($handle, $bulk_actions->messages) && $not_passed === 0) { + $message = trans($bulk_actions->messages[$handle], ['type' => $page]); + } + + $level = $not_passed > 0 ? 'info' : 'success'; + + if ( + ( + isset($bulk_actions->actions[$action_key]['type']) + && $bulk_actions->actions[$action_key]['type'] != 'modal' + ) + || ! isset($bulk_actions->actions[$action_key]['type']) + || $not_passed > 0 + ) { + flash($message)->{$level}(); + } + + if ( + ! empty($result) + && ($result instanceof \Symfony\Component\HttpFoundation\BinaryFileResponse) + ) { + return $result; + } else if ( + ! empty($result) + && ($result instanceof RedirectResponse) + ) { + return response()->json([ + 'success' => true, + 'redirect' => $result->getTargetUrl(), + 'error' => false, + 'data' => [], + 'message' => $message, + ]); + } else if ( + ! empty($result) + && ($result instanceof JsonResponse) + ) { + return $result; + } else { + return response()->json([ + 'success' => true, + 'redirect' => true, + 'error' => false, + 'data' => [], + 'message' => $message, + ]); + } + } +} diff --git a/app/Http/Controllers/Common/Companies.php b/app/Http/Controllers/Common/Companies.php new file mode 100644 index 00000000..5136b3c7 --- /dev/null +++ b/app/Http/Controllers/Common/Companies.php @@ -0,0 +1,264 @@ +middleware('permission:create-common-companies')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-common-companies')->only('show', 'edit', 'export'); + $this->middleware('permission:update-common-companies')->only('update', 'enable', 'disable'); + $this->middleware('permission:delete-common-companies')->only('destroy'); + } + + /** + * Display a listing of the resource. + * + * @return Response + */ + public function index() + { + $companies = user()->companies()->collect(); + + return $this->response('common.companies.index', compact('companies')); + } + + /** + * Show the form for viewing the specified resource. + * + * @return Response + */ + public function show() + { + return redirect()->route('companies.index'); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + $money_currencies = MoneyCurrency::getCurrencies(); + + $currencies = []; + + foreach ($money_currencies as $key => $item) { + $currencies[$key] = $key . ' - ' . $item['name']; + } + + return view('common.companies.create', compact('currencies')); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $current_company_id = company_id(); + + $response = $this->ajaxDispatch(new CreateCompany($request)); + + if ($response['success']) { + $response['redirect'] = route('companies.switch', $response['data']->id); + + $message = trans('messages.success.created', ['type' => trans_choice('general.companies', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('companies.create'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + company($current_company_id)->makeCurrent(); + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param Company $company + * + * @return Response + */ + public function edit(Company $company) + { + if ($this->isNotUserCompany($company->id)) { + return redirect()->route('companies.index'); + } + + $money_currencies = MoneyCurrency::getCurrencies(); + + $currencies = []; + + foreach ($money_currencies as $key => $item) { + $currencies[$key] = $key . ' - ' . $item['name']; + } + + return view('common.companies.edit', compact('company', 'currencies')); + } + + /** + * Update the specified resource in storage. + * + * @param Company $company + * @param Request $request + * + * @return Response + */ + public function update(Company $company, Request $request) + { + $current_company_id = company_id(); + + $response = $this->ajaxDispatch(new UpdateCompany($company, $request, company_id())); + + if ($response['success']) { + $response['redirect'] = route('companies.index'); + + $message = trans('messages.success.updated', ['type' => trans_choice('general.companies', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('companies.edit', $company->id); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + company($current_company_id)->makeCurrent(); + + return response()->json($response); + } + + /** + * Enable the specified resource. + * + * @param Company $company + * + * @return Response + */ + public function enable(Company $company) + { + $response = $this->ajaxDispatch(new UpdateCompany($company, request()->merge(['enabled' => 1]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.enabled', ['type' => trans_choice('general.companies', 1)]); + } + + return response()->json($response); + } + + /** + * Disable the specified resource. + * + * @param Company $company + * + * @return Response + */ + public function disable(Company $company) + { + $response = $this->ajaxDispatch(new UpdateCompany($company, request()->merge(['enabled' => 0]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.disabled', ['type' => trans_choice('general.companies', 1)]); + } + + return response()->json($response); + } + + /** + * Remove the specified resource from storage. + * + * @param Company $company + * + * @return Response + */ + public function destroy(Company $company) + { + $response = $this->ajaxDispatch(new DeleteCompany($company)); + + $response['redirect'] = route('companies.index'); + + if ($response['success']) { + $message = trans('messages.success.deleted', ['type' => trans_choice('general.companies', 1)]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Change the active company. + * + * @param Company $company + * + * @return Response + */ + public function switch(Company $company) + { + if ($this->isUserCompany($company->id)) { + $old_company_id = company_id(); + + $company->makeCurrent(); + + session(['dashboard_id' => user()->dashboards()->enabled()->pluck('id')->first()]); + + event(new \App\Events\Common\CompanySwitched($company, $old_company_id)); + + // Check wizard + if (! setting('wizard.completed', false)) { + return redirect()->route('wizard.edit', ['company_id' => $company->id]); + } + } + + return redirect()->route('dashboard', ['company_id' => $company->id]); + } + + public function autocomplete() + { + $query = request('query'); + + $autocomplete = Company::autocomplete([ + 'name' => $query + ]); + + $companies = $autocomplete->get()->sortBy('name')->pluck('name', 'id'); + + return response()->json([ + 'success' => true, + 'message' => 'Get all companies.', + 'errors' => [], + 'count' => $companies->count(), + 'data' => ($companies->count()) ? $companies : null, + ]); + } +} diff --git a/app/Http/Controllers/Common/Contacts.php b/app/Http/Controllers/Common/Contacts.php new file mode 100644 index 00000000..5534193f --- /dev/null +++ b/app/Http/Controllers/Common/Contacts.php @@ -0,0 +1,33 @@ +middleware('permission:read-sales-customers|read-purchases-vendors')->only('index'); + } + + /** + * Display a listing of the resource. + * + * @return \Illuminate\Http\Response + */ + public function index() + { + return response()->json([ + 'success' => true, + 'error' => false, + 'data' => Contact::collect(), + 'message' => '', + ]); + } +} diff --git a/app/Http/Controllers/Common/Dashboards.php b/app/Http/Controllers/Common/Dashboards.php new file mode 100644 index 00000000..25f90e68 --- /dev/null +++ b/app/Http/Controllers/Common/Dashboards.php @@ -0,0 +1,261 @@ +middleware('permission:create-common-dashboards')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-common-dashboards')->only('show'); + $this->middleware('permission:update-common-dashboards')->only('index', 'edit', 'export', 'update', 'enable', 'disable', 'share'); + $this->middleware('permission:delete-common-dashboards')->only('destroy'); + } + + /** + * Display a listing of the resource. + * + * @return Response + */ + public function index() + { + // Eager load users relationship to prevent N+1 queries in dashboard index view + $dashboards = user()->dashboards()->with('users')->collect(); + + return $this->response('common.dashboards.index', compact('dashboards')); + } + + /** + * Show the form for viewing the specified resource. + * + * @return Response + */ + public function show($dashboard_id = null) + { + $dashboard_id = $dashboard_id ?? session('dashboard_id'); + + try { + $dashboard = Dashboard::findOrFail($dashboard_id); + } catch (ModelNotFoundException $e) { + $dashboard = user()->dashboards()->enabled()->first(); + } + + if (empty($dashboard)) { + $dashboard = $this->dispatch(new CreateDashboard([ + 'company_id' => company_id(), + 'name' => trans_choice('general.dashboards', 1), + 'default_widgets' => 'core', + ])); + } + + session(['dashboard_id' => $dashboard->id]); + + $widgets = Widget::where('dashboard_id', $dashboard->id)->orderBy('sort', 'asc')->get()->filter(function ($widget) { + return Widgets::canShow($widget->class); + }); + + $user_dashboards = user()->dashboards()->enabled()->get(); + + $date_picker_shortcuts = $this->getDatePickerShortcuts(); + + if (! request()->has('start_date')) { + request()->merge(['start_date' => $date_picker_shortcuts[trans('general.date_range.this_year')]['start']]); + request()->merge(['end_date' => $date_picker_shortcuts[trans('general.date_range.this_year')]['end']]); + } + + return view('common.dashboards.show', compact('dashboard', 'widgets', 'user_dashboards', 'date_picker_shortcuts')); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + $users = company()->users()->get()->reject(function ($user) { + if ($user->cannot('read-admin-panel')) { + return true; + } + })->sortBy('name'); + + return view('common.dashboards.create', compact('users')); + } + + /** + * Store a newly created resource in storage. + * + * @param $request + * @return Response + */ + public function store(Request $request) + { + $response = $this->ajaxDispatch(new CreateDashboard($request)); + + if ($response['success']) { + $response['redirect'] = route('dashboards.index'); + + $message = trans('messages.success.created', ['type' => trans_choice('general.dashboards', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('dashboards.create'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param Dashboard $dashboard + * + * @return Response + */ + public function edit(Dashboard $dashboard) + { + if ($this->isNotUserDashboard($dashboard->id)) { + return redirect()->route('dashboards.index'); + } + + $dashboard->load('users'); + + $users = company()->users()->get()->reject(function ($user) { + if ($user->cannot('read-admin-panel')) { + return true; + } + })->sortBy('name'); + + return view('common.dashboards.edit', compact('dashboard', 'users')); + } + + /** + * Update the specified resource in storage. + * + * @param Dashboard $dashboard + * @param $request + * @return Response + */ + public function update(Dashboard $dashboard, Request $request) + { + $response = $this->ajaxDispatch(new UpdateDashboard($dashboard, $request)); + + if ($response['success']) { + $response['redirect'] = route('dashboards.index'); + + $message = trans('messages.success.updated', ['type' => trans_choice('general.dashboards', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('dashboards.edit', $dashboard->id); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Enable the specified resource. + * + * @param Dashboard $dashboard + * + * @return Response + */ + public function enable(Dashboard $dashboard) + { + $response = $this->ajaxDispatch(new UpdateDashboard($dashboard, request()->merge(['enabled' => 1]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.enabled', ['type' => trans_choice('general.dashboards', 1)]); + } + + return response()->json($response); + } + + /** + * Disable the specified resource. + * + * @param Dashboard $dashboard + * + * @return Response + */ + public function disable(Dashboard $dashboard) + { + $response = $this->ajaxDispatch(new UpdateDashboard($dashboard, request()->merge(['enabled' => 0]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.disabled', ['type' => trans_choice('general.dashboards', 1)]); + } + + return response()->json($response); + } + + /** + * Remove the specified resource from storage. + * + * @param Dashboard $dashboard + * + * @return Response + */ + public function destroy(Dashboard $dashboard) + { + $response = $this->ajaxDispatch(new DeleteDashboard($dashboard)); + + $response['redirect'] = route('dashboards.index'); + + if ($response['success']) { + $message = trans('messages.success.deleted', ['type' => $dashboard->name]); + + flash($message)->success(); + + session(['dashboard_id' => user()->dashboards()->enabled()->pluck('id')->first()]); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Change the active dashboard. + * + * @param Dashboard $dashboard + * + * @return Response + */ + public function switch(Dashboard $dashboard) + { + if ($this->isUserDashboard($dashboard->id)) { + session(['dashboard_id' => $dashboard->id]); + } + + return redirect()->route('dashboard'); + } +} diff --git a/app/Http/Controllers/Common/Import.php b/app/Http/Controllers/Common/Import.php new file mode 100644 index 00000000..6dc4c2b3 --- /dev/null +++ b/app/Http/Controllers/Common/Import.php @@ -0,0 +1,27 @@ +getImportView($group, $type, $route); + + return view($view, $data); + } +} diff --git a/app/Http/Controllers/Common/Items.php b/app/Http/Controllers/Common/Items.php new file mode 100644 index 00000000..5f5b43c0 --- /dev/null +++ b/app/Http/Controllers/Common/Items.php @@ -0,0 +1,324 @@ +collect(); + + return $this->response('common.items.index', compact('items')); + } + + /** + * Show the form for viewing the specified resource. + * + * @return Response + */ + public function show() + { + return redirect()->route('items.index'); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + $taxes = Tax::enabled()->orderBy('name')->get()->pluck('title', 'id'); + + return view('common.items.create', compact('taxes')); + } + + /** + * Store a newly created resource in storage. + * + * @param $request + * @return Response + */ + public function store(Request $request) + { + $response = $this->ajaxDispatch(new CreateItem($request)); + + if ($response['success']) { + $response['redirect'] = route('items.index'); + + $message = trans('messages.success.created', ['type' => trans_choice('general.items', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('items.create'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Duplicate the specified resource. + * + * @param Item $item + * + * @return Response + */ + public function duplicate(Item $item) + { + $clone = $item->duplicate(); + + $message = trans('messages.success.duplicated', ['type' => trans_choice('general.items', 1)]); + + flash($message)->success(); + + return redirect()->route('items.edit', $clone->id); + } + + /** + * Import the specified resource. + * + * @param ImportRequest $request + * + * @return Response + */ + public function import(ImportRequest $request) + { + $response = $this->importExcel(new Import, $request, trans_choice('general.items', 2)); + + if ($response['success']) { + $response['redirect'] = route('items.index'); + + flash($response['message'])->success(); + } else { + $response['redirect'] = route('import.create', ['common', 'items']); + + flash($response['message'])->error()->important(); + } + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param Item $item + * + * @return Response + */ + public function edit(Item $item) + { + $taxes = Tax::enabled()->orderBy('name')->get()->pluck('title', 'id'); + + return view('common.items.edit', compact('item', 'taxes')); + } + + /** + * Update the specified resource in storage. + * + * @param $item + * @param $request + * @return Response + */ + public function update(Item $item, Request $request) + { + $response = $this->ajaxDispatch(new UpdateItem($item, $request)); + + if ($response['success']) { + $response['redirect'] = route('items.index'); + + $message = trans('messages.success.updated', ['type' => $item->name]); + + flash($message)->success(); + } else { + $response['redirect'] = route('items.edit', $item->id); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Enable the specified resource. + * + * @param Item $item + * + * @return Response + */ + public function enable(Item $item) + { + $response = $this->ajaxDispatch(new UpdateItem($item, request()->merge(['enabled' => 1]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.enabled', ['type' => $item->name]); + } + + return response()->json($response); + } + + /** + * Disable the specified resource. + * + * @param Item $item + * + * @return Response + */ + public function disable(Item $item) + { + $response = $this->ajaxDispatch(new UpdateItem($item, request()->merge(['enabled' => 0]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.disabled', ['type' => $item->name]); + } + + return response()->json($response); + } + + /** + * Remove the specified resource from storage. + * + * @param Item $item + * + * @return Response + */ + public function destroy(Item $item) + { + $response = $this->ajaxDispatch(new DeleteItem($item)); + + $response['redirect'] = route('items.index'); + + if ($response['success']) { + $message = trans('messages.success.deleted', ['type' => $item->name]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Export the specified resource. + * + * @return Response + */ + public function export() + { + return $this->exportExcel(new Export, trans_choice('general.items', 2)); + } + + public function autocomplete() + { + $type = request('type'); + $query = request('query'); + $currency_code = request('currency_code'); + + if (empty($currency_code) || (strtolower($currency_code) == 'null')) { + $currency_code = default_currency(); + } + + $autocomplete = Item::with('taxes')->autocomplete([ + 'name' => $query + ]); + + // Eager load taxes and tax relationships to prevent N+1 queries + $items = $autocomplete->with(['taxes.tax'])->get(); + + if ($items) { + foreach ($items as $item) { + $item_price = ($type == 'bill') ? $item->purchase_price : $item->sale_price; + $item_tax_price = 0; + + if ($item->taxes->count()) { + $inclusives = $compounds = []; + + foreach($item->taxes as $item_tax) { + // Tax relationship is now eager loaded, preventing N+1 query + $tax = $item_tax->tax; + + switch ($tax->type) { + case 'inclusive': + $inclusives[] = $tax; + break; + case 'compound': + $compounds[] = $tax; + break; + case 'fixed': + $item_tax_price += $tax->rate; + break; + case 'withholding': + $item_tax_amount = 0 - $item_price * ($tax->rate / 100); + + $item_tax_price += $item_tax_amount; + break; + default: + $item_tax_amount = ($item_price / 100) * $tax->rate; + + $item_tax_price += $item_tax_amount; + break; + } + } + + if ($inclusives) { + $item_base_rate = $item_price / (1 + collect($inclusives)->sum('rate') / 100); + $item_tax_price = $item_price - $item_base_rate; + } + + if ($compounds) { + foreach ($compounds as $compound) { + $item_tax_price += ($item_tax_price / 100) * $compound->rate; + } + } + } + + switch ($type) { + case 'bill': + $total = $item->purchase_price + $item_tax_price; + break; + case 'invoice': + default: + $total = $item->sale_price + $item_tax_price; + break; + } + + $item->total = $total; + } + } + + return response()->json([ + 'success' => true, + 'message' => 'Get all items.', + 'errors' => [], + 'count' => $items->count(), + 'data' => ($items->count()) ? $items : null, + ]); + } +} diff --git a/app/Http/Controllers/Common/Plans.php b/app/Http/Controllers/Common/Plans.php new file mode 100644 index 00000000..b139f740 --- /dev/null +++ b/app/Http/Controllers/Common/Plans.php @@ -0,0 +1,23 @@ +clearPlansCache(); + + return redirect()->back(); + } +} diff --git a/app/Http/Controllers/Common/Reports.php b/app/Http/Controllers/Common/Reports.php new file mode 100644 index 00000000..0a209ffa --- /dev/null +++ b/app/Http/Controllers/Common/Reports.php @@ -0,0 +1,316 @@ +middleware('permission:create-common-reports')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-common-reports')->only('index', 'show', 'export'); + $this->middleware('permission:update-common-reports')->only('edit', 'update', 'enable', 'disable'); + $this->middleware('permission:delete-common-reports')->only('destroy'); + } + + /** + * Display a listing of the resource. + * + * @return Response + */ + public function index() + { + $icons = $categories = []; + + $reports = Report::orderBy('name')->get(); + + // Pre-compute which module aliases are enabled once, so the loop + // never re-reads module JSON files from disk (150+ modules were causing 256s+ timeouts). + $moduleStatusCache = []; + + foreach ($reports as $report) { + $alias = $report->alias ?? 'core'; + + // Check module enabled status from cache + if ($alias !== 'core') { + if (! array_key_exists($alias, $moduleStatusCache)) { + $moduleStatusCache[$alias] = Utility::isModuleEnabled($report->class); + } + + if (! $moduleStatusCache[$alias]) { + continue; + } + } + + // Permission check (no disk I/O, just DB/memory) + if (Utility::cannotRead($report->class)) { + continue; + } + + if (! class_exists($report->class)) { + continue; + } + + // Read icon & category from class properties via reflection + // instead of instantiating (which triggers setGroups() event dispatch for every single report). + $ref = new \ReflectionClass($report->class); + + $iconProp = $ref->getProperty('icon'); + $icons[$report->id] = $iconProp->getDefaultValue() ?? 'donut_small'; + + $categoryProp = $ref->getProperty('category'); + $categoryKey = $categoryProp->getDefaultValue() ?? 'reports.income_expense'; + $category = trans($categoryKey); + + if (empty($categories[$category])) { + $descKey = null; + + if ($ref->hasProperty('category_description')) { + $descKey = $ref->getProperty('category_description')->getDefaultValue(); + } + + $categories[$category] = [ + 'name' => $category, + 'description' => $descKey ? trans($descKey) : '', + 'reports' => [$report], + ]; + } else { + $categories[$category]['reports'][] = $report; + } + } + + return $this->response('common.reports.index', compact('categories', 'icons')); + } + + /** + * Show the form for viewing the specified resource. + * + * @param Report $report + * @param ShowRequest $request + * @return Response + */ + public function show(Report $report, ShowRequest $request) + { + if (Utility::cannotShow($report->class)) { + abort(403); + } + + $class = Utility::getClassInstance($report); + + return $class->show(); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + $classes = Utility::getClasses(); + + return view('common.reports.create', compact('classes')); + } + + /** + * Store a newly created resource in storage. + * + * @param $request + * @return Response + */ + public function store(Request $request) + { + $response = $this->ajaxDispatch(new CreateReport($request)); + + if ($response['success']) { + $response['redirect'] = route('reports.index'); + + $message = trans('messages.success.created', ['type' => trans_choice('general.reports', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('reports.create'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Duplicate the specified resource. + * + * @param Report $report + * + * @return Response + */ + public function duplicate(Report $report) + { + $clone = $report->duplicate(); + + $message = trans('messages.success.duplicated', ['type' => trans_choice('general.reports', 1)]); + + flash($message)->success(); + + return redirect()->route('reports.edit', $clone->id); + } + + /** + * Show the form for editing the specified resource. + * + * @param Report $report + * + * @return Response + */ + public function edit(Report $report) + { + $classes = Utility::getClasses(); + + $class = Utility::getClassInstance($report, false); + + return view('common.reports.edit', compact('report', 'classes', 'class')); + } + + /** + * Update the specified resource in storage. + * + * @param Report $report + * @param $request + * @return Response + */ + public function update(Report $report, Request $request) + { + $response = $this->ajaxDispatch(new UpdateReport($report, $request)); + + if ($response['success']) { + $response['redirect'] = route('reports.index'); + + $message = trans('messages.success.updated', ['type' => $report->name]); + + flash($message)->success(); + } else { + $response['redirect'] = route('reports.edit', $report->id); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Remove the specified resource from storage. + * + * @param Report $report + * + * @return Response + */ + public function destroy(Report $report) + { + $response = $this->ajaxDispatch(new DeleteReport($report)); + + $response['redirect'] = route('reports.index'); + + if ($response['success']) { + $message = trans('messages.success.deleted', ['type' => $report->name]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Print the report. + * + * @param Report $report + * @return Response + */ + public function print(Report $report) + { + if (Utility::cannotShow($report->class)) { + abort(403); + } + + return Utility::getClassInstance($report)->print(); + } + + /** + * Download PDF file of the report. + * + * @param Report $report + * @return Response + */ + public function pdf(Report $report) + { + if (Utility::cannotShow($report->class)) { + abort(403); + } + + return Utility::getClassInstance($report)->pdf(); + } + + /** + * Export the report. + * + * @param Report $report + * @return Response + */ + public function export(Report $report) + { + if (Utility::cannotShow($report->class)) { + abort(403); + } + + return Utility::getClassInstance($report)->export(); + } + + /** + * Get fields of the specified resource. + * + * @return Response + */ + public function fields() + { + $class = request('class'); + + if (!class_exists($class)) { + return response()->json([ + 'success' => false, + 'error' => true, + 'message' => 'Class does not exist', + 'html' => '', + ]); + } + + $fields = (new $class())->getFields(); + + $html = view('components.reports.fields', compact('fields'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => '', + 'html' => $html, + ]); + } +} diff --git a/app/Http/Controllers/Common/Uploads.php b/app/Http/Controllers/Common/Uploads.php new file mode 100644 index 00000000..3940f326 --- /dev/null +++ b/app/Http/Controllers/Common/Uploads.php @@ -0,0 +1,317 @@ +authorizeMedia($media)) { + return response(null, 204); + } + + // Get file path + if (!$this->getMediaPathOnStorage($media)) { + return response(null, 204); + } + + return $this->streamMedia($media); + } + + public function inline($id) + { + try { + $media = Media::find($id); + } catch (\Exception $e) { + return response(null, 204); + } + + // Authorization: prevent same-company IDOR (CWE-639/862). + if (! $this->authorizeMedia($media)) { + return response(null, 204); + } + + // Get file path + if (!$this->getMediaPathOnStorage($media)) { + return response(null, 204); + } + + return $this->streamMedia($media, 'inline'); + } + + /** + * Get the specified resource. + * + * @param $id + * @return mixed + */ + public function show($id, Request $request) + { + $file = false; + $options = false; + $column_name = 'attachment'; + + if ($request->has('column_name')) { + $column_name = $request->get('column_name'); + } + + if ($request->has('page')) { + $options = [ + 'page' => $request->get('page'), + 'key' => $request->get('key'), + ]; + } + + try { + $media = Media::find($id); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'error' => true, + 'data' => [], + 'message' => 'null', + 'html' => '', + ]); + } + + // Authorization: prevent same-company IDOR (CWE-639/862). + if (! $this->authorizeMedia($media)) { + return response()->json([ + 'success' => false, + 'error' => true, + 'data' => [], + 'message' => 'null', + 'html' => '', + ]); + } + + // Get file path + if (!$this->getMediaPathOnStorage($media)) { + return response()->json([ + 'success' => false, + 'error' => true, + 'data' => [], + 'message' => 'null', + 'html' => '', + ]); + } + + $file = $media; + + $html = view('components.media.file', compact('file', 'column_name', 'options'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'data' => [], + 'message' => 'null', + 'html' => $html, + ]); + } + + /** + * Download the specified resource. + * + * @param $id + * @return mixed + */ + public function download($id) + { + try { + $media = Media::find($id); + } catch (\Exception $e) { + return false; + } + + // Authorization: prevent same-company IDOR (CWE-639/862). + if (! $this->authorizeMedia($media)) { + return false; + } + + // Get file path + if (!$this->getMediaPathOnStorage($media)) { + return false; + } + + return $this->streamMedia($media); + } + + /** + * Destroy the specified resource. + * + * @param $id + * @return callable + */ + public function destroy($id, Request $request) + { + $return = back(); + + if ($request->has('ajax') && $request->get('ajax')) { + $return = [ + 'success' => true, + 'errors' => false, + 'message' => '', + 'redirect' => $request->get('redirect') + ]; + } + + try { + $media = Media::find($id); + } catch (\Exception $e) { + return $return; + } + + // Get file path + if (!$path = $this->getMediaPathOnStorage($media)) { + $message = trans('messages.warning.deleted', ['name' => $media->basename, 'text' => $media->basename]); + + flash($message)->warning()->important(); + + return $return; + } + + $media->delete(); //will not delete files + + Storage::delete($path); + + if (!empty($request->input('page'))) { + switch ($request->input('page')) { + case 'setting': + setting()->set($request->input('key'), ''); + + setting()->save(); + break; + } + } + + return $return; + } + + /** + * Authorize media access to prevent IDOR (CWE-639/862). + * + * The common uploads routes sit behind plain `auth` middleware with no + * permission check. The Media `Tenants` trait scopes by company_id so + * cross-company access is blocked, but any same-company low-privilege user + * (including a portal customer) could enumerate media IDs to read other + * users' files (invoice attachments, bills, receipts, etc.). + * + * Policy: + * - Admin users (read-admin-panel): allowed — they have legitimate + * company-wide file access. + * - Portal users (read-client-portal): allowed only if the media is + * attached to a mediable parent whose `contact_id` matches the portal + * user's own contact. This mirrors the existing ownership checks in + * Portal\InvoiceShow / Portal\PaymentShow FormRequests. + * - Otherwise: denied (403 / 204). + * + * @param Media $media + * @return bool + */ + private function authorizeMedia($media): bool + { + if (! is_object($media)) { + return false; + } + + $user = user(); + + // Defensive: auth middleware guarantees a user, but guard against + // any edge-case invocation without one (default-deny). + if (! $user) { + return false; + } + + // Admin users have company-wide file access. + if ($user->can('read-admin-panel')) { + return true; + } + + // Portal users: check ownership via the mediable parent's contact_id. + if ($user->isCustomer()) { + return $this->mediaBelongsToContact($media, $user); + } + + // Any other authenticated user without admin/portal access: deny. + return false; + } + + /** + * Determine whether the given media is attached to a mediable record + * owned by the portal user's contact. + * + * Resolves the polymorphic mediables pivot for the media and, for each + * parent model that has a `contact_id` column, checks whether it matches + * the user's contact_id. If no mediable parent exists or none has a + * contact_id, access is denied (default-deny). + * + * @param Media $media + * @param \App\Models\Auth\User $user + * @return bool + */ + private function mediaBelongsToContact($media, $user): bool + { + $contact = $user->contact; + + if (! $contact) { + return false; + } + + $rows = DB::table(config('mediable.mediables_table', 'mediables')) + ->where('media_id', $media->id) + ->get(['mediable_type', 'mediable_id']); + + if ($rows->isEmpty()) { + // Orphaned media (no mediable parent) — deny for portal users. + return false; + } + + foreach ($rows as $row) { + if (! class_exists($row->mediable_type)) { + continue; + } + + try { + $parent = app($row->mediable_type)::find($row->mediable_id); + } catch (\Exception $e) { + continue; + } + + if (! $parent) { + continue; + } + + // If the parent model has a contact_id column, enforce ownership. + if (in_array('contact_id', $parent->getFillable(), true) || isset($parent->contact_id)) { + if (isset($parent->contact_id) && (int) $parent->contact_id === (int) $contact->id) { + return true; + } + } + } + + // No matching owned parent found — default deny. + return false; + } +} diff --git a/app/Http/Controllers/Common/Widgets.php b/app/Http/Controllers/Common/Widgets.php new file mode 100644 index 00000000..7da5eb5f --- /dev/null +++ b/app/Http/Controllers/Common/Widgets.php @@ -0,0 +1,200 @@ +json($widgets); + } + + /** + * Show the form for viewing the specified resource. + * + * @return Response + */ + public function show() + { + return redirect()->route('dashboard'); + } + + /** + * Store a newly created resource in storage. + * + * @param $request + * @return Response + */ + public function store(Request $request) + { + $request['settings'] = [ + 'width' => $request->get('width'), + ]; + + $response = $this->ajaxDispatch(new CreateWidget($request)); + + if ($response['success']) { + $response['redirect'] = route('dashboard'); + + $widget = $response['data']; + + $settings = $widget->settings; + + $response['data'] = [ + 'class' => $widget->class, + 'name' => $widget->name, + 'settings' => $settings, + 'sort' => $widget->sort, + ]; + + $message = trans('messages.success.added', ['type' => $widget->name]); + + flash($message)->success(); + } else { + $response['redirect'] = route('dashboard'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param Widget $widget + * + * @return Response + */ + public function edit(Widget $widget) + { + $settings = $widget->settings; + + return response()->json([ + 'class' => $widget->class, + 'name' => $widget->name, + 'settings' => $settings, + 'sort' => $widget->sort, + ]); + } + + /** + * Update the specified resource in storage. + * + * @param Widget $widget + * @param $request + * @return Response + */ + public function update(Widget $widget, Request $request) + { + $request['settings'] = [ + 'width' => $request->get('width'), + ]; + + $response = $this->ajaxDispatch(new UpdateWidget($widget, $request)); + + $response['redirect'] = route('dashboard'); + + if ($response['success']) { + $settings = $response['data']->settings; + + $response['data'] = [ + 'class' => $widget->class, + 'name' => $widget->name, + 'settings' => $settings, + 'sort' => $widget->sort, + ]; + + $message = trans('messages.success.updated', ['type' => $widget->name]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Remove the specified resource from storage. + * + * @param Widget $widget + * + * @return Response + */ + public function destroy(Widget $widget) + { + $response = $this->ajaxDispatch(new DeleteWidget($widget)); + + $response['redirect'] = route('dashboard'); + + if ($response['success']) { + $message = trans('messages.success.deleted', ['type' => $widget->name]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + public function getData(Request $request) + { + $widget_name = $request->get('widget'); + $method = $request->get('method', 'show'); + + // Security: validate the widget name contains only alphanumeric + // characters to prevent namespace traversal. + if (empty($widget_name) || ! preg_match('/^[a-zA-Z0-9]+$/', $widget_name)) { + abort(404); + } + + // Check is module + $module = module($widget_name); + + if ($module instanceof \Akaunting\Module\Module) { + $widget = app('Modules\\' . $module->getStudlyName() . '\\Widgets\\' . ucfirst($widget_name)); + } else { + $widget = app('App\Widgets\\' . ucfirst($widget_name)); + } + + // Security: ensure the resolved instance is a Widget subclass. + if (! ($widget instanceof \App\Abstracts\Widget)) { + abort(404); + } + + // Security: only allow methods declared in the widget's $allowed_methods + // array. This prevents calling arbitrary internal methods (applyFilters, + // calculateDocumentTotals, setData, etc.) that could be abused. + // Subclasses can extend $allowed_methods for their own safe, read-only methods. + if (! in_array($method, $widget->allowed_methods)) { + abort(403); + } + + $response = $widget->{$method}($request); + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Install/Database.php b/app/Http/Controllers/Install/Database.php new file mode 100644 index 00000000..fc7a2aa2 --- /dev/null +++ b/app/Http/Controllers/Install/Database.php @@ -0,0 +1,62 @@ + env('DB_HOST' , 'localhost'), + 'username' => env('DB_USERNAME', ''), + 'password' => env('DB_PASSWORD', ''), + 'database' => env('DB_DATABASE', ''), + ]); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $connection = config('database.default','mysql'); + + $host = $request['hostname']; + $port = config("database.connections.$connection.port", '3306'); + $database = $request['database']; + $username = $request['username']; + $password = $request['password']; + $prefix = config("database.connections.$connection.prefix", null); + + // Check database connection + if (!Installer::createDbTables($host, $port, $database, $username, $password, $prefix)) { + $response = [ + 'status' => null, + 'success' => false, + 'error' => true, + 'message' => trans('install.error.connection'), + 'data' => null, + 'redirect' => null, + ]; + } + + if (empty($response)) { + $response['redirect'] = route('install.settings'); + } + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Install/Language.php b/app/Http/Controllers/Install/Language.php new file mode 100644 index 00000000..2b03b743 --- /dev/null +++ b/app/Http/Controllers/Install/Language.php @@ -0,0 +1,59 @@ +allowed(); + + if (! $locale || ! array_key_exists($locale, $lang_allowed)) { + $locale = 'en-GB'; + } + + return view('install.language.create', compact('locale', 'lang_allowed')); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + // Set locale + session(['locale' => $request->get('lang')]); + app()->setLocale($request->get('lang')); + + $response['redirect'] = route('install.database'); + + return response()->json($response); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function getLanguages() + { + $response = [ + 'languages' => language()->allowed(), + ]; + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Install/Requirements.php b/app/Http/Controllers/Install/Requirements.php new file mode 100644 index 00000000..35778732 --- /dev/null +++ b/app/Http/Controllers/Install/Requirements.php @@ -0,0 +1,36 @@ +send(); + } else { + foreach ($requirements as $requirement) { + flash($requirement)->error()->important(); + } + + return view('install.requirements.show', compact('requirements')); + } + } +} diff --git a/app/Http/Controllers/Install/Settings.php b/app/Http/Controllers/Install/Settings.php new file mode 100644 index 00000000..326dc043 --- /dev/null +++ b/app/Http/Controllers/Install/Settings.php @@ -0,0 +1,49 @@ +get('company_name'), $request->get('company_email'), $locale); + + // Create user + Installer::createUser($request->get('user_email'), $request->get('user_password'), $locale); + }); + + // Make the final touches + Installer::finalTouches(); + + // Redirect to dashboard + $response['redirect'] = route('login'); + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Install/Updates.php b/app/Http/Controllers/Install/Updates.php new file mode 100644 index 00000000..92181690 --- /dev/null +++ b/app/Http/Controllers/Install/Updates.php @@ -0,0 +1,323 @@ +all(); + + if ($rows) { + foreach ($rows as $row) { + $alias = $row->get('alias'); + + if (!isset($updates[$alias])) { + continue; + } + + $m = new \stdClass(); + $m->name = $row->getName(); + $m->alias = $row->get('alias'); + $m->installed = $row->get('version'); + $m->latest = $updates[$alias]->latest; + $m->errors = $updates[$alias]->errors; + $m->message = $updates[$alias]->message; + + $modules[] = $m; + } + } + + return view('install.updates.index', compact('core', 'modules')); + } + + public function changelog() + { + return Versions::changelog(); + } + + /** + * Check for updates. + * + * @return Response + */ + public function check() + { + // Clear cache in order to check for updates + Cache::forget('updates'); + Cache::forget('versions'); + + event(new UpdateCacheCleared(company_id())); + + return redirect()->back(); + } + + /** + * Run the update. + * + * @param $alias + * @param $version + * @return Response + */ + public function run($alias, $version) + { + if ($alias == 'core') { + $name = 'Akaunting ' . $version; + + $installed = version('short'); + } else { + // Get module instance + $module = module($alias); + + $name = $module->getName(); + + $installed = $module->get('version'); + + if (version_compare($installed, $version, '>=')) { + flash(trans('modules.warning.latest_version', ['module' => $name]))->warning()->important(); + + return $this->check(); + } + } + + return view('install.updates.edit', compact('alias', 'name', 'installed', 'version')); + } + + /** + * Show the form for viewing the specified resource. + * + * @param $request + * + * @return Response + */ + public function steps(InstallRequest $request) + { + $steps = []; + + $name = $request['name']; + + // Download + $steps[] = [ + 'text' => trans('modules.installation.download', ['module' => $name]), + 'url' => route('updates.download'), + ]; + + // Unzip + $steps[] = [ + 'text' => trans('modules.installation.unzip', ['module' => $name]), + 'url' => route('updates.unzip'), + ]; + + // Copy files + $steps[] = [ + 'text' => trans('modules.installation.file_copy', ['module' => $name]), + 'url' => route('updates.copy'), + ]; + + // Finish/Apply + $steps[] = [ + 'text' => trans('modules.installation.finish', ['module' => $name]), + 'url' => route('updates.finish'), + ]; + + // Redirect + $steps[] = [ + 'text' => trans('modules.installation.redirect', ['module' => $name]), + 'url' => route('updates.redirect'), + ]; + + return response()->json([ + 'success' => true, + 'error' => false, + 'data' => $steps, + 'message' => null + ]); + } + + /** + * Download the file + * + * @param $request + * + * @return Response + */ + public function download(InstallRequest $request) + { + set_time_limit(900); // 15 minutes + + try { + $path = $this->dispatch(new DownloadFile($request['alias'], $request['version'])); + + event(new UpdateDownloaded($request['alias'], $request['version'], $request['installed'])); + + $json = [ + 'success' => true, + 'error' => false, + 'message' => null, + 'data' => [ + 'path' => $path, + ], + ]; + } catch (\Exception $e) { + $json = [ + 'success' => false, + 'error' => true, + 'message' => $e->getMessage(), + 'data' => [], + ]; + } + + return response()->json($json); + } + + /** + * Unzip the downloaded file + * + * @param $request + * + * @return Response + */ + public function unzip(InstallRequest $request) + { + set_time_limit(900); // 15 minutes + + try { + $path = $this->dispatch(new UnzipFile($request['alias'], $request['path'])); + + event(new UpdateUnzipped($request['alias'], $request['version'], $request['installed'])); + + $json = [ + 'success' => true, + 'error' => false, + 'message' => null, + 'data' => [ + 'path' => $path, + ], + ]; + } catch (\Exception $e) { + $json = [ + 'success' => false, + 'error' => true, + 'message' => $e->getMessage(), + 'data' => [], + ]; + } + + return response()->json($json); + } + + /** + * Copy files + * + * @param $request + * + * @return Response + */ + public function copyFiles(InstallRequest $request) + { + set_time_limit(900); // 15 minutes + + try { + $path = $this->dispatch(new CopyFiles($request['alias'], $request['path'])); + + event(new UpdateCopied($request['alias'], $request['version'], $request['installed'])); + + $json = [ + 'success' => true, + 'error' => false, + 'message' => null, + 'data' => [ + 'path' => $path, + ], + ]; + } catch (\Exception $e) { + $json = [ + 'success' => false, + 'error' => true, + 'message' => $e->getMessage(), + 'data' => [], + ]; + } + + return response()->json($json); + } + + /** + * Finish the update + * + * @param $request + * + * @return Response + */ + public function finish(InstallRequest $request) + { + set_time_limit(900); // 15 minutes + + try { + $this->dispatch(new FinishUpdate($request['alias'], $request['version'], $request['installed'], company_id())); + + $json = [ + 'success' => true, + 'error' => false, + 'message' => null, + 'data' => [], + ]; + } catch (\Exception $e) { + $json = [ + 'success' => false, + 'error' => true, + 'message' => $e->getMessage(), + 'data' => [], + ]; + } + + return response()->json($json); + } + + /** + * Redirect back + * + * @param $request + * + * @return Response + */ + public function redirect() + { + $json = [ + 'success' => true, + 'errors' => false, + 'redirect' => route('updates.index'), + 'data' => [], + ]; + + return response()->json($json); + } +} diff --git a/app/Http/Controllers/Modals/Accounts.php b/app/Http/Controllers/Modals/Accounts.php new file mode 100644 index 00000000..e26cc019 --- /dev/null +++ b/app/Http/Controllers/Modals/Accounts.php @@ -0,0 +1,62 @@ +middleware('permission:create-banking-accounts')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-banking-accounts')->only('index', 'show', 'edit', 'export'); + $this->middleware('permission:update-banking-accounts')->only('update', 'enable', 'disable'); + $this->middleware('permission:delete-banking-accounts')->only('destroy'); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + $currency = Currency::where('code', '=', default_currency())->first(); + + $html = view('modals.accounts.create', compact('currency'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => 'null', + 'html' => $html, + ]); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $request['enabled'] = 1; + + $response = $this->ajaxDispatch(new CreateAccount($request)); + + if ($response['success']) { + $response['message'] = trans('messages.success.created', ['type' => trans_choice('general.accounts', 1)]); + } + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Modals/Categories.php b/app/Http/Controllers/Modals/Categories.php new file mode 100644 index 00000000..2eaff0d7 --- /dev/null +++ b/app/Http/Controllers/Modals/Categories.php @@ -0,0 +1,89 @@ +middleware('permission:create-settings-categories')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-settings-categories')->only('index', 'show', 'edit', 'export'); + $this->middleware('permission:update-settings-categories')->only('update', 'enable', 'disable'); + $this->middleware('permission:delete-settings-categories')->only('destroy'); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create(IRequest $request) + { + $type = $request->get('type', Category::ITEM_TYPE); + + $category_types = $this->getTypeCategoryTypes($type); + $hide_code_types = $this->hideCodeCategoryTypes($category_types); + + $categories = collect(); + + Category::type($category_types) + ->enabled() + ->orderBy('name') + ->get() + ->each(function ($category) use (&$categories) { + $categories->push([ + 'id' => $category->id, + 'title' => $category->name, + 'level' => $category->level, + ]); + }); + + $type_group = count($category_types) > 1 ? true : false; + $types = $this->getCategoryTypes(group: true, types: $category_types); + + $html = view('modals.categories.create', compact('type', 'types', 'categories', 'type_group', 'hide_code_types'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => 'null', + 'html' => $html, + ]); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $request['enabled'] = 1; + $request['type'] = $request->get('type', Category::ITEM_TYPE); + $request['color'] = $request->get('color', '#' . dechex(rand(0x000000, 0xFFFFFF))); + + $response = $this->ajaxDispatch(new CreateCategory($request)); + + if ($response['success']) { + $response['message'] = trans('messages.success.created', ['type' => trans_choice('general.categories', 1)]); + } + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Modals/Companies.php b/app/Http/Controllers/Modals/Companies.php new file mode 100644 index 00000000..4e9a7a7e --- /dev/null +++ b/app/Http/Controllers/Modals/Companies.php @@ -0,0 +1,38 @@ +middleware('permission:read-settings-company')->only('index', 'show', 'edit', 'export'); + $this->middleware('permission:update-settings-company')->only('update', 'enable', 'disable'); + } + + /** + * Show the form for editing the specified resource. + * + * @param Company $company + * + * @return Response + */ + public function edit(Company $company) + { + $html = view('modals.companies.edit', compact('company'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => 'null', + 'html' => $html, + ]); + } +} diff --git a/app/Http/Controllers/Modals/Currencies.php b/app/Http/Controllers/Modals/Currencies.php new file mode 100644 index 00000000..c3010a0c --- /dev/null +++ b/app/Http/Controllers/Modals/Currencies.php @@ -0,0 +1,86 @@ +middleware('permission:create-settings-currencies')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-settings-currencies')->only('index', 'show', 'edit', 'export'); + $this->middleware('permission:update-settings-currencies')->only('update', 'enable', 'disable'); + $this->middleware('permission:delete-settings-currencies')->only('destroy'); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + // Get current currencies + $current = Currency::pluck('code')->toArray(); + + // Prepare codes + $codes = []; + $currencies = MoneyCurrency::getCurrencies(); + + foreach ($currencies as $key => $item) { + // Don't show if already available + if (in_array($key, $current)) { + continue; + } + + $codes[$key] = $key; + } + + $html = view('modals.currencies.create', compact('codes'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => 'null', + 'html' => $html, + ]); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $currency = currency($request->get('code')); + + $request['precision'] = (int) $currency->getPrecision(); + $request['symbol'] = $currency->getSymbol(); + $request['symbol_first'] = $currency->isSymbolFirst() ? 1 : 0; + $request['decimal_mark'] = $currency->getDecimalMark(); + $request['thousands_separator'] = $currency->getThousandsSeparator(); + + $request['enabled'] = 1; + $request['default_currency'] = false; + + $response = $this->ajaxDispatch(new CreateCurrency($request->all())); + + if ($response['success']) { + $response['message'] = trans('messages.success.created', ['type' => trans_choice('general.currencies', 1)]); + } + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Modals/Customers.php b/app/Http/Controllers/Modals/Customers.php new file mode 100644 index 00000000..475724e3 --- /dev/null +++ b/app/Http/Controllers/Modals/Customers.php @@ -0,0 +1,113 @@ +middleware('permission:create-sales-customers')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-sales-customers')->only('index', 'show', 'edit', 'export'); + $this->middleware('permission:update-sales-customers')->only('update', 'enable', 'disable'); + $this->middleware('permission:delete-sales-customers')->only('destroy'); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + $contact_selector = false; + + if (request()->has('contact_selector')) { + $contact_selector = request()->get('contact_selector'); + } + + $html = view('modals.customers.create', compact('contact_selector'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => 'null', + 'html' => $html, + ]); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $request['enabled'] = 1; + + $response = $this->ajaxDispatch(new CreateContact($request)); + + if ($response['success']) { + $response['message'] = trans('messages.success.created', ['type' => trans_choice('general.customers', 1)]); + } + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param Contact $customer + * + * @return Response + */ + public function edit(Contact $customer) + { + $contact_selector = false; + + if (request()->has('contact_selector')) { + $contact_selector = request()->get('contact_selector'); + } + + $html = view('modals.customers.edit', compact('customer', 'contact_selector'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => 'null', + 'html' => $html, + ]); + } + + /** + * Update the specified resource in storage. + * + * @param Contact $customer + * @param Request $request + * + * @return Response + */ + public function update(Contact $customer, Request $request) + { + $request['enabled'] = 1; + + $response = $this->ajaxDispatch(new UpdateContact($customer, $request)); + + if ($response['success']) { + $response['message'] = trans('messages.success.updated', ['type' => trans_choice('general.customers', 1)]); + } + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Modals/DocumentItemColumns.php b/app/Http/Controllers/Modals/DocumentItemColumns.php new file mode 100644 index 00000000..db5be4a9 --- /dev/null +++ b/app/Http/Controllers/Modals/DocumentItemColumns.php @@ -0,0 +1,138 @@ +middleware('permission:read-settings-invoice')->only('index', 'edit'); + $this->middleware('permission:update-settings-invoice')->only('update', 'enable', 'disable'); + } + + /** + * Show the form for editing the specified resource. + * + * @param Contact $customer + * + * @return Response + */ + public function edit() + { + $type = request()->get('type', 'invoice'); + + $item_names = [ + 'hide' => trans('settings.invoice.hide.item_name'), + 'settings.invoice.item' => trans('settings.invoice.item'), + 'settings.invoice.product' => trans('settings.invoice.product'), + 'settings.invoice.service' => trans('settings.invoice.service'), + 'custom' => trans('settings.invoice.custom'), + ]; + + $price_names = [ + 'hide' => trans('settings.invoice.hide.price'), + 'settings.invoice.price' => trans('settings.invoice.price'), + 'settings.invoice.rate' => trans('settings.invoice.rate'), + 'custom' => trans('settings.invoice.custom'), + ]; + + $quantity_names = [ + 'hide' => trans('settings.invoice.hide.quantity'), + 'settings.invoice.quantity' => trans('settings.invoice.quantity'), + 'custom' => trans('settings.invoice.custom'), + ]; + + $item_name = setting($this->getDocumentSettingKey($type, 'item_name')); + $item_name_input = setting($this->getDocumentSettingKey($type, 'item_name_input')); + $price_name = setting($this->getDocumentSettingKey($type, 'price_name')); + $price_name_input = setting($this->getDocumentSettingKey($type, 'price_name_input')); + $quantity_name = setting($this->getDocumentSettingKey($type, 'quantity_name')); + $quantity_name_input = setting($this->getDocumentSettingKey($type, 'quantity_name_input')); + $hide_item_name = setting($this->getDocumentSettingKey($type, 'hide_item_name')); + $hide_item_description = setting($this->getDocumentSettingKey($type, 'hide_item_description')); + $hide_quantity = setting($this->getDocumentSettingKey($type, 'hide_quantity')); + $hide_price = setting($this->getDocumentSettingKey($type, 'hide_price')); + $hide_amount = setting($this->getDocumentSettingKey($type, 'hide_amount')); + + $html = view('modals.documents.item_columns', compact( + 'type', + 'item_names', + 'price_names', + 'quantity_names', + 'item_name', + 'item_name_input', + 'price_name', + 'price_name_input', + 'quantity_name', + 'quantity_name_input', + 'hide_item_name', + 'hide_item_description', + 'hide_quantity', + 'hide_price', + 'hide_amount', + ))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => 'null', + 'html' => $html, + ]); + } + + /** + * Update the specified resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function update(Request $request) + { + $fields = $request->all(); + $type = $request->get('type', 'invoice'); + $company_id = $request->get('company_id'); + + if (empty($company_id)) { + $company_id = company_id(); + } + + foreach ($fields as $key => $value) { + $real_key = $this->getDocumentSettingKey($type, $key); + + // Don't process unwanted keys + if (in_array($key, $this->skip_keys)) { + continue; + } + + setting()->set($real_key, $value); + } + + // Save all settings + setting()->save(); + + $message = trans('messages.success.updated', ['type' => trans_choice('general.settings', 2)]); + + $response = [ + 'status' => null, + 'success' => true, + 'error' => false, + 'message' => $message, + 'data' => null, + 'redirect' => route('settings.invoice.edit'), + ]; + + flash($message)->success(); + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Modals/DocumentTransactions.php b/app/Http/Controllers/Modals/DocumentTransactions.php new file mode 100644 index 00000000..e6c9303b --- /dev/null +++ b/app/Http/Controllers/Modals/DocumentTransactions.php @@ -0,0 +1,332 @@ +middleware('permission:create-sales-invoices')->only('create', 'store', 'duplicate', 'import'); + //$this->middleware('permission:read-sales-invoices')->only('index', 'show', 'edit', 'export'); + //$this->middleware('permission:update-sales-invoices')->only('update', 'enable', 'disable'); + //$this->middleware('permission:delete-sales-invoices')->only('destroy'); + } + + /** + * Show the form for creating a new resource. + * + * @param Document $document + * + * @return Response + */ + public function create(Document $document) + { + $document->load(['totals', 'transactions']); + + $currency = Currency::where('code', $document->currency_code)->first(); + + $paid = $document->paid; + + $document->paid_amount = $paid; + + $number = $this->getNextTransactionNumber(); + + // Get document Totals + foreach ($document->totals as $document_total) { + $document->{$document_total->code} = $document_total->amount; + } + + $d_total = $document?->total ?? 0; + + $total = money($d_total, $currency->code)->format(); + + $document->grand_total = money($total, $currency->code, false)->getAmount(); + + if (! empty($paid)) { + $document->grand_total = round($d_total - $paid, $currency->precision); + } + + $amount = $document->grand_total; + + $document->paid_at = Date::now()->toDateString(); + + $buttons = [ + 'cancel' => [ + 'text' => trans('general.cancel'), + 'class' => 'btn-outline-secondary' + ], + 'payment' => [ + 'text' => trans('invoices.accept_payments'), + 'class' => 'long-texts', + 'before_text' => trans('general.get_paid_faster'), + 'class' => 'px-6 py-1.5 text-xs bg-gray-200 hover:bg-purple-200 font-medium rounded-lg leading-6 long-texts', + 'url' => route('apps.categories.show', [ + 'alias' => 'payment-method', + 'utm_source' => $document->type . '_payment', + 'utm_medium' => 'app', + 'utm_campaign' => 'payment_method', + ]) + ], + 'send' => [ + 'text' => trans('general.save_and_send'), + 'class' => 'disabled:bg-green-100', + 'disabled' => empty($document->contact->has_email) ? true : false, + ], + 'confirm' => [ + 'text' => trans('general.save'), + 'class' => 'disabled:bg-green-100' + ], + ]; + + $method = 'POST'; + + $route = ['modals.documents.document.transactions.store', $document->id]; + + $html = view('modals.documents.payment', compact('document', 'method', 'route', 'currency', 'number', 'amount'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => 'null', + 'html' => $html, + 'data' => [ + 'title' => trans('general.title.new', ['type' => trans_choice('general.payments', 1)]), + 'buttons' => $buttons, + ] + ]); + } + + /** + * Store a newly created resource in storage. + * + * @param Document $document + * @param Request $request + * + * @return Response + */ + public function store(Document $document, Request $request) + { + $response = $this->ajaxDispatch(new CreateBankingDocumentTransaction($document, $request)); + + if ($response['success']) { + $response['redirect'] = $this->getRedirectUrl($document, $request); + + $message = trans('messages.success.added', ['type' => trans_choice('general.payments', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = null; + } + + return response()->json($response); + } + + /** + * Show the form for creating a new resource. + * + * @param Document $document + * + * @return Response + */ + public function edit(Document $document, Transaction $transaction) + { + $document->load(['totals', 'transactions']); + + $currency = Currency::where('code', $document->currency_code)->first(); + + // if you edit transaction before remove transaction amount + $paid = $document->paid - $transaction->amount_for_document; + + $document->paid_amount = $paid; + + $number = $transaction->number; + + $amount = money($transaction->amount_for_document, $currency->code, false)->getAmount(); + + // Get document Totals + foreach ($document->totals as $document_total) { + $document->{$document_total->code} = $document_total->amount; + } + + $d_total = $document?->total ?? 0; + + $total = money($d_total, $currency->code)->format(); + + $document->grand_total = money($total, $currency->code, false)->getAmount(); + + if (! empty($paid)) { + $document->grand_total = round($d_total - $paid, $currency->precision); + } + + $document->paid_at = $transaction->paid_at; + + $buttons = [ + 'cancel' => [ + 'text' => trans('general.cancel'), + 'class' => 'btn-outline-secondary' + ], + 'payment' => [ + 'text' => trans('invoices.accept_payments'), + 'before_text' => trans('general.get_paid_faster'), + 'class' => 'px-6 py-1.5 text-xs bg-gray-200 hover:bg-purple-200 font-medium rounded-lg leading-6 long-texts', + 'url' => route('apps.categories.show', [ + 'alias' => 'payment-method', + 'utm_source' => $document->type . '_payment', + 'utm_medium' => 'app', + 'utm_campaign' => 'payment_method', + ]) + ], + 'confirm' => [ + 'text' => trans('general.save'), + 'class' => 'disabled:bg-green-100' + ], + ]; + + $method = 'PATCH'; + + $route = ['modals.documents.document.transactions.update', $document->id, $transaction->id]; + + $html = view('modals.documents.payment', compact('document', 'transaction', 'method', 'route', 'currency', 'number', 'amount'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => 'null', + 'html' => $html, + 'data' => [ + 'title' => trans('general.title.edit', ['type' => trans_choice('general.payments', 1)]), + 'buttons' => $buttons, + ] + ]); + } + + /** + * Update the specified resource in storage. + * + * @param $item + * @param $request + * @return Response + */ + public function update(Document $document, Transaction $transaction, Request $request) + { + $response = $this->ajaxDispatch(new UpdateBankingDocumentTransaction($document, $transaction, $request)); + + if ($response['success']) { + $route = config('type.document.' . $document->type . '.route.prefix'); + + if ($alias = config('type.document.' . $document->type . '.alias')) { + $route = $alias . '.' . $route; + } + + $redirect = route($route . '.show', $document->id); + + $response['redirect'] = $this->getRedirectUrl($document, $request, $redirect); + + $message = trans('messages.success.updated', ['type' => trans_choice('general.payments', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = null; + } + + return response()->json($response); + } + + /** + * Remove the specified resource from storage. + * + * @param Transaction $transaction + * + * @return Response + */ + public function destroy(Document $document, Transaction $transaction) + { + $response = $this->ajaxDispatch(new DeleteTransaction($transaction)); + + $route = config('type.document.' . $document->type . '.route.prefix'); + + if ($alias = config('type.document.' . $document->type . '.alias')) { + $route = $alias . '.' . $route; + } + + $response['redirect'] = route($route . '.show', $document->id); + + if ($response['success']) { + $message = trans('messages.success.deleted', ['type' => trans_choice('general.payments', 1)]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + protected function getRedirectUrl($document, $request, $url = null) + { + $redirect = $url ?? url()->previous(); + + if ($request->has('sendtransaction')) { + $paramaters = [ + $document->type => $document->id, + 'sendtransaction' => true, + ]; + + $quin = '?'; + + if (Str::contains($redirect, '?')) { + $quin = '&'; + } + + $redirect .= $quin . http_build_query($paramaters); + } + + return $redirect; + } + + protected function getTransactionConvertAmount($document, $transaction) + { + if (empty($document->amount)) { + return 0; + } + + $paid = 0; + + $code = $document->currency_code; + $rate = $document->currency_rate; + $precision = currency($code)->getPrecision(); + + $amount = $transaction->amount; + + if ($code != $transaction->currency_code) { + $amount = $this->convertBetween($amount, $transaction->currency_code, $transaction->currency_rate, $code, $rate); + } + + $paid += $amount; + + return round($paid, $precision); + } +} diff --git a/app/Http/Controllers/Modals/InvoiceEmails.php b/app/Http/Controllers/Modals/InvoiceEmails.php new file mode 100644 index 00000000..14d17461 --- /dev/null +++ b/app/Http/Controllers/Modals/InvoiceEmails.php @@ -0,0 +1,84 @@ +middleware('permission:create-sales-invoices')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-sales-invoices')->only('index', 'show', 'edit', 'export'); + $this->middleware('permission:update-sales-invoices')->only('update', 'enable', 'disable'); + $this->middleware('permission:delete-sales-invoices')->only('destroy'); + } + + public function create(Document $invoice): JsonResponse + { + $contacts = $invoice->contact->withPersons(); + + $notification = new Notification($invoice, 'invoice_new_customer', true); + + $store_route = 'modals.invoices.emails.store'; + + $html = view('modals.invoices.email', compact('invoice', 'contacts', 'notification', 'store_route'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => 'null', + 'html' => $html, + 'data' => [ + 'title' => trans('general.title.new', ['type' => trans_choice('general.email', 1)]), + 'buttons' => [ + 'cancel' => [ + 'text' => trans('general.cancel'), + 'class' => 'btn-outline-secondary', + ], + 'confirm' => [ + 'text' => trans('general.send'), + 'class' => 'disabled:bg-green-100', + ] + ] + ] + ]); + } + + public function store(Request $request): JsonResponse + { + $response = $this->sendEmail(new SendDocumentAsCustomMail($request, 'invoice_new_customer')); + + if ($response['success']) { + $invoice = Document::find($request->get('document_id')); + + $route = config('type.document.' . $invoice->type . '.route.prefix'); + + if ($alias = config('type.document.' . $invoice->type . '.alias')) { + $route = $alias . '.' . $route; + } + + $response['redirect'] = route($route . '.show', $invoice->id); + + $message = trans('documents.messages.email_sent', ['type' => trans_choice('general.invoices', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = null; + } + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Modals/InvoiceShare.php b/app/Http/Controllers/Modals/InvoiceShare.php new file mode 100644 index 00000000..28e08593 --- /dev/null +++ b/app/Http/Controllers/Modals/InvoiceShare.php @@ -0,0 +1,81 @@ +middleware('permission:create-sales-invoices')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-sales-invoices')->only('index', 'show', 'edit', 'export'); + $this->middleware('permission:update-sales-invoices')->only('update', 'enable', 'disable'); + $this->middleware('permission:delete-sales-invoices')->only('destroy'); + } + + /** + * Show the form for creating a new resource. + * + * @param Document $invoice + * + * @return Response + */ + public function create(Document $invoice) + { + $page = config('type.document.' . $invoice->type . '.route.prefix'); + $alias = config('type.document.' . $invoice->type . '.alias'); + + $route = ''; + + if (! empty($alias)) { + $route .= $alias . '.'; + } + + $preview_route = $route . 'preview.' . $page . '.show'; + $signed_route = $route . 'signed.' . $page . '.show'; + + try { + $previewUrl = route($preview_route, $invoice->id); + } catch (\Exception $e) { + $previewUrl = ''; + } + + try { + route($signed_route, [$this->document->id, 'company_id' => company_id()]); + + $signedUrl = URL::signedRoute($signed_route, [$invoice->id]); + } catch (\Exception $e) { + $signedUrl = URL::signedRoute('signed.invoices.show', [$invoice->id]); + } + + $html = view('modals.invoices.share', compact('invoice', 'previewUrl', 'signedUrl'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => 'null', + 'html' => $html, + 'data' => [ + 'title' => trans('general.title.new', ['type' => trans('general.share_link')]), + 'success_message' => trans('invoices.share.success_message'), + 'buttons' => [ + 'cancel' => [ + 'text' => trans('general.cancel'), + 'class' => 'btn-outline-secondary', + ], + 'confirm' => [ + 'text' => trans('general.copy_link'), + 'class' => 'disabled:bg-green-100', + ], + ] + ] + ]); + } +} diff --git a/app/Http/Controllers/Modals/InvoiceTemplates.php b/app/Http/Controllers/Modals/InvoiceTemplates.php new file mode 100644 index 00000000..3f70fc51 --- /dev/null +++ b/app/Http/Controllers/Modals/InvoiceTemplates.php @@ -0,0 +1,67 @@ +middleware('permission:create-settings-invoice')->only('create', 'store'); + $this->middleware('permission:read-settings-invoice')->only('index', 'edit'); + $this->middleware('permission:update-settings-invoice')->only('update', 'enable', 'disable'); + $this->middleware('permission:delete-settings-invoice')->only('destroy'); + } + + /** + * Update the specified resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function update(Request $request) + { + $fields = $request->all(); + $prefix = $request->get('_prefix', 'invoice'); + $company_id = $request->get('company_id'); + + if (empty($company_id)) { + $company_id = company_id(); + } + + foreach ($fields as $key => $value) { + $real_key = $prefix . '.' . $key; + + // Don't process unwanted keys + if (in_array($key, $this->skip_keys)) { + continue; + } + + setting()->set($real_key, $value); + } + + // Save all settings + setting()->save(); + + $message = trans('messages.success.updated', ['type' => trans_choice('general.settings', 2)]); + + $response = [ + 'status' => null, + 'success' => true, + 'error' => false, + 'message' => $message, + 'data' => null, + 'redirect' => route('settings.invoice.edit'), + ]; + + flash($message)->success(); + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Modals/Items.php b/app/Http/Controllers/Modals/Items.php new file mode 100644 index 00000000..eb0034af --- /dev/null +++ b/app/Http/Controllers/Modals/Items.php @@ -0,0 +1,76 @@ +middleware('permission:create-common-items')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-common-items')->only('index', 'show', 'edit', 'export'); + $this->middleware('permission:update-common-items')->only('update', 'enable', 'disable'); + $this->middleware('permission:delete-common-items')->only('destroy'); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create(IRequest $request) + { + $taxes = Tax::enabled()->orderBy('name')->get()->pluck('title', 'id'); + + $currency = Currency::where('code', default_currency())->first(); + + $html = view('modals.items.create', compact('taxes', 'currency'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => 'null', + 'html' => $html, + ]); + } + + /** + * Store a newly created resource in storage. + * + * @param $request + * @return Response + */ + public function store(IRequest $request) + { + if ($request->get('type', false) == 'inline') { + $data = [ + 'company_id' => company_id(), + 'name' => '', + 'sale_price' => 0, + 'purchase_price' => 0, + 'enabled' => 1, + ]; + + $data[$request->get('field')] = $request->get('value'); + + $request = $data; + } + + $response = $this->ajaxDispatch(new CreateItem($request)); + + if ($response['success']) { + $response['message'] = trans('messages.success.created', ['type' => trans_choice('general.items', 1)]); + } + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Modals/Taxes.php b/app/Http/Controllers/Modals/Taxes.php new file mode 100644 index 00000000..0d843d23 --- /dev/null +++ b/app/Http/Controllers/Modals/Taxes.php @@ -0,0 +1,82 @@ +middleware('permission:create-settings-taxes')->only('create', 'store'); + $this->middleware('permission:read-settings-taxes')->only('index', 'edit'); + $this->middleware('permission:update-settings-taxes')->only('update', 'enable', 'disable'); + $this->middleware('permission:delete-settings-taxes')->only('destroy'); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + $types = [ + 'fixed' => trans('taxes.fixed'), + 'normal' => trans('taxes.normal'), + 'inclusive' => trans('taxes.inclusive'), + 'withholding' => trans('taxes.withholding'), + 'compound' => trans('taxes.compound'), + ]; + + $tax_selector = false; + + if (request()->has('tax_selector')) { + $tax_selector = request()->get('tax_selector'); + } + + $rand = rand(); + + $disable_options = []; + + if ($compound = Tax::compound()->first()) { + $disable_options = ['compound']; + } + + $html = view('modals.taxes.create', compact('types', 'tax_selector', 'disable_options', 'rand'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => 'null', + 'html' => $html, + ]); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $request['enabled'] = 1; + + $response = $this->ajaxDispatch(new CreateTax($request)); + + if ($response['success']) { + $response['message'] = trans('messages.success.created', ['type' => trans_choice('general.taxes', 1)]); + } + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Modals/TransactionEmails.php b/app/Http/Controllers/Modals/TransactionEmails.php new file mode 100644 index 00000000..f69f054a --- /dev/null +++ b/app/Http/Controllers/Modals/TransactionEmails.php @@ -0,0 +1,101 @@ +middleware('permission:create-banking-transactions')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-banking-transactions')->only('index', 'show', 'edit', 'export'); + $this->middleware('permission:update-banking-transactions')->only('update', 'enable', 'disable'); + $this->middleware('permission:delete-banking-transactions')->only('destroy'); + } + + public function create(Transaction $transaction): JsonResponse + { + $contacts = $transaction->contact->withPersons(); + + $email_template = config('type.transaction.' . $transaction->type . '.email_template'); + + if (request()->get('email_template')) { + $email_template = request()->get('email_template'); + } + + switch ($email_template) { + case 'invoice_payment_customer': + $notification = new PaymentReceivedNotification($transaction->document, $transaction, $email_template, true); + break; + + default: + $notification = new TransactionNotification($transaction, $email_template, true); + break; + } + + $store_route = 'modals.transactions.emails.store'; + + $html = view('modals.transactions.email', compact('transaction', 'contacts', 'notification', 'store_route'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => 'null', + 'html' => $html, + 'data' => [ + 'title' => trans('general.title.new', ['type' => trans_choice('general.email', 1)]), + 'buttons' => [ + 'cancel' => [ + 'text' => trans('general.cancel'), + 'class' => 'btn-outline-secondary', + ], + 'confirm' => [ + 'text' => trans('general.send'), + 'class' => 'disabled:bg-green-100', + ] + ] + ] + ]); + } + + public function store(Request $request): JsonResponse + { + $transaction = Transaction::find($request->get('transaction_id')); + + $email_template = config('type.transaction.' . $transaction->type . '.email_template'); + + $response = $this->sendEmail(new SendTransactionAsCustomMail($request, $email_template)); + + if ($response['success']) { + $route = config('type.transaction.' . $transaction->type . '.route.prefix'); + + if ($alias = config('type.transaction.' . $transaction->type . '.alias')) { + $route = $alias . '.' . $route; + } + + $response['redirect'] = route($route . '.show', $transaction->id); + + $message = trans('documents.messages.email_sent', ['type' => trans_choice('general.transactions', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = null; + } + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Modals/TransactionShare.php b/app/Http/Controllers/Modals/TransactionShare.php new file mode 100644 index 00000000..36a0fcb7 --- /dev/null +++ b/app/Http/Controllers/Modals/TransactionShare.php @@ -0,0 +1,80 @@ +middleware('permission:create-banking-transactions')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-banking-transactions')->only('index', 'show', 'edit', 'export'); + $this->middleware('permission:update-banking-transactions')->only('update', 'enable', 'disable'); + $this->middleware('permission:delete-banking-transactions')->only('destroy'); + } + + /** + * Show the form for creating a new resource. + * + * @param Transaction $transaction + * + * @return Response + */ + public function create(Transaction $transaction) + { + $alias = config('type.transaction.' . $transaction->type . '.alias'); + + $route = ''; + + if (! empty($alias)) { + $route .= $alias . '.'; + } + + $preview_route = $route . 'preview.payments.show'; + $signed_route = $route . 'signed.payments.show'; + + try { + $previewUrl = route($preview_route, $transaction->id); + } catch (\Exception $e) { + $previewUrl = ''; + } + + try { + route($signed_route, [$this->document->id, 'company_id' => company_id()]); + + $signedUrl = URL::signedRoute($signed_route, [$transaction->id]); + } catch (\Exception $e) { + $signedUrl = URL::signedRoute('signed.payments.show', [$transaction->id]); + } + + $html = view('modals.transactions.share', compact('transaction', 'previewUrl', 'signedUrl'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => 'null', + 'html' => $html, + 'data' => [ + 'title' => trans('general.title.new', ['type' => trans('general.share_link')]), + 'success_message' => trans('invoices.share.success_message'), + 'buttons' => [ + 'cancel' => [ + 'text' => trans('general.cancel'), + 'class' => 'btn-outline-secondary', + ], + 'confirm' => [ + 'text' => trans('general.copy_link'), + 'class' => 'disabled:bg-green-100', + ], + ] + ] + ]); + } +} diff --git a/app/Http/Controllers/Modals/TransferTemplates.php b/app/Http/Controllers/Modals/TransferTemplates.php new file mode 100644 index 00000000..a52e859f --- /dev/null +++ b/app/Http/Controllers/Modals/TransferTemplates.php @@ -0,0 +1,67 @@ +middleware('permission:create-banking-transfers')->only('create', 'store'); + $this->middleware('permission:read-banking-transfers')->only('index', 'edit'); + $this->middleware('permission:update-banking-transfers')->only('update', 'enable', 'disable'); + $this->middleware('permission:delete-banking-transfers')->only('destroy'); + } + + /** + * Update the specified resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function update(Request $request) + { + $fields = $request->all(); + $prefix = $request->get('_prefix', 'transfer'); + $company_id = $request->get('company_id'); + + if (empty($company_id)) { + $company_id = company_id(); + } + + foreach ($fields as $key => $value) { + $real_key = $prefix . '.' . $key; + + // Don't process unwanted keys + if (in_array($key, $this->skip_keys)) { + continue; + } + + setting()->set($real_key, $value); + } + + // Save all settings + setting()->save(); + + $message = trans('messages.success.updated', ['type' => trans_choice('general.settings', 2)]); + + $response = [ + 'status' => null, + 'success' => true, + 'error' => false, + 'message' => $message, + 'data' => null, + 'redirect' => url()->previous(), + ]; + + flash($message)->success(); + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Modals/Vendors.php b/app/Http/Controllers/Modals/Vendors.php new file mode 100644 index 00000000..f47374b4 --- /dev/null +++ b/app/Http/Controllers/Modals/Vendors.php @@ -0,0 +1,113 @@ +middleware('permission:create-purchases-vendors')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-purchases-vendors')->only('index', 'show', 'edit', 'export'); + $this->middleware('permission:update-purchases-vendors')->only('update', 'enable', 'disable'); + $this->middleware('permission:delete-purchases-vendors')->only('destroy'); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + $contact_selector = false; + + if (request()->has('contact_selector')) { + $contact_selector = request()->get('contact_selector'); + } + + $html = view('modals.vendors.create', compact('contact_selector'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => 'null', + 'html' => $html, + ]); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $request['enabled'] = 1; + + $response = $this->ajaxDispatch(new CreateContact($request)); + + if ($response['success']) { + $response['message'] = trans('messages.success.created', ['type' => trans_choice('general.vendors', 1)]); + } + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param Contact $vendor + * + * @return Response + */ + public function edit(Contact $vendor) + { + $contact_selector = false; + + if (request()->has('contact_selector')) { + $contact_selector = request()->get('contact_selector'); + } + + $html = view('modals.vendors.edit', compact('vendor','contact_selector'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => 'null', + 'html' => $html, + ]); + } + + /** + * Update the specified resource in storage. + * + * @param Contact $vendor + * @param Request $request + * + * @return Response + */ + public function update(Contact $vendor, Request $request) + { + $request['enabled'] = 1; + + $response = $this->ajaxDispatch(new UpdateContact($vendor, $request)); + + if ($response['success']) { + $response['message'] = trans('messages.success.updated', ['type' => trans_choice('general.vendors', 1)]); + } + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Modules/ApiKey.php b/app/Http/Controllers/Modules/ApiKey.php new file mode 100644 index 00000000..063f5d47 --- /dev/null +++ b/app/Http/Controllers/Modules/ApiKey.php @@ -0,0 +1,46 @@ +set('apps.api_key', $request['api_key']); + + setting()->save(); + + $this->clearPlansCache(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'redirect' => route('apps.home.index'), + 'message' => '', + ]); + } +} diff --git a/app/Http/Controllers/Modules/Home.php b/app/Http/Controllers/Modules/Home.php new file mode 100644 index 00000000..ca97ed77 --- /dev/null +++ b/app/Http/Controllers/Modules/Home.php @@ -0,0 +1,28 @@ +response('modules.home.index'); + } + + /** + * Show the form for viewing the specified resource. + * + * @return Response + */ + public function show() + { + return redirect()->route('apps.home.index'); + } +} diff --git a/app/Http/Controllers/Modules/Item.php b/app/Http/Controllers/Modules/Item.php new file mode 100644 index 00000000..0b08038f --- /dev/null +++ b/app/Http/Controllers/Modules/Item.php @@ -0,0 +1,401 @@ +middleware('permission:create-modules-item')->only('install'); + $this->middleware('permission:update-modules-item')->only('update', 'enable', 'disable'); + $this->middleware('permission:delete-modules-item')->only('uninstall'); + } + + /** + * Show the form for viewing the specified resource. + * + * @param $alias + * + * @return Response + */ + public function show($alias) + { + $enable = false; + $installed = false; + + $module = $this->getModule($alias); + + if (empty($module)) { + return redirect()->route('apps.home.index')->send(); + } + + if ($this->moduleExists($alias) && ($model = Module::alias($alias)->first())) { + $installed = true; + + if ($model->enabled) { + $enable = true; + } + } + + if (request()->get('utm_source')) { + $parameters = request()->all(); + + $character = '?'; + + if (strpos($module->action_url, '?') !== false) { + $character = '&'; + } + + $module->action_url .= $character . http_build_query($parameters); + } + + if ($module->status_type == 'pre_sale') { + return view('modules.item.pre_sale', compact('module', 'installed', 'enable')); + } + + return view('modules.item.show', compact('module', 'installed', 'enable')); + } + + /** + * Show the form for viewing the specified resource. + * + * @param $request + * + * @return Response + */ + public function steps(InstallRequest $request) + { + $steps = []; + + $name = $request['name']; + $alias = $request['alias']; + + if ($this->moduleExists($alias)) { + // Install + $steps[] = [ + 'text' => trans('modules.installation.install', ['module' => $name]), + 'url' => route('apps.install') + ]; + } else { + // Download + $steps[] = [ + 'text' => trans('modules.installation.download', ['module' => $name]), + 'url' => route('apps.download') + ]; + + // Unzip + $steps[] = [ + 'text' => trans('modules.installation.unzip', ['module' => $name]), + 'url' => route('apps.unzip') + ]; + + // Copy + $steps[] = [ + 'text' => trans('modules.installation.file_copy', ['module' => $name]), + 'url' => route('apps.copy') + ]; + + // Install + $steps[] = [ + 'text' => trans('modules.installation.install', ['module' => $name]), + 'url' => route('apps.install') + ]; + } + + return response()->json([ + 'success' => true, + 'error' => false, + 'data' => $steps, + 'message' => null + ]); + } + + /** + * Show the form for viewing the specified resource. + * + * @param $request + * + * @return Response + */ + public function download(InstallRequest $request) + { + try { + $path = $this->dispatch(new DownloadFile($request['alias'], $request['version'])); + + $json = [ + 'success' => true, + 'error' => false, + 'message' => null, + 'data' => [ + 'path' => $path, + ], + ]; + } catch (\Exception $e) { + $json = [ + 'success' => false, + 'error' => true, + 'message' => $e->getMessage(), + 'data' => [], + ]; + } + + return response()->json($json); + } + + /** + * Show the form for viewing the specified resource. + * + * @param $request + * + * @return Response + */ + public function unzip(InstallRequest $request) + { + try { + $path = $this->dispatch(new UnzipFile($request['alias'], $request['path'])); + + $json = [ + 'success' => true, + 'error' => false, + 'message' => null, + 'data' => [ + 'path' => $path, + ], + ]; + } catch (\Exception $e) { + $json = [ + 'success' => false, + 'error' => true, + 'message' => $e->getMessage(), + 'data' => [], + ]; + } + + return response()->json($json); + } + + /** + * Show the form for viewing the specified resource. + * + * @param $request + * + * @return Response + */ + public function copy(InstallRequest $request) + { + try { + $this->dispatch(new CopyFiles($request['alias'], $request['path'])); + + event(new \App\Events\Module\Copied($request['alias'], company_id())); + + $json = [ + 'success' => true, + 'error' => false, + 'message' => null, + 'data' => [ + 'alias' => $request['alias'], + ], + ]; + } catch (\Exception $e) { + $json = [ + 'success' => false, + 'error' => true, + 'message' => $e->getMessage(), + 'data' => [], + ]; + } + + return response()->json($json); + } + + /** + * Show the form for viewing the specified resource. + * + * @param $request + * + * @return Response + */ + public function install(InstallRequest $request) + { + try { + event(new \App\Events\Module\Installing($request['alias'], company_id())); + + $this->dispatch(new InstallModule($request['alias'], company_id(), setting('default.locale'))); + + $name = module($request['alias'])->getName(); + $module_routes = module_attribute($request['alias'], 'routes', []); + + $message = trans('modules.installed', ['module' => $name]); + + flash($message)->success(); + + $redirect = route('apps.app.show', $request['alias']); + + // Get module.json redirect route + if (! empty($module_routes['redirect_after_install'])) { + if (is_array($module_routes['redirect_after_install'])) { + $route = array_shift($module_routes['redirect_after_install']); + + $redirect = route($route, $module_routes['redirect_after_install']); + } else { + $redirect = route($module_routes['redirect_after_install']); + } + } + + $json = [ + 'success' => true, + 'error' => false, + 'message' => null, + 'redirect' => $redirect, + 'data' => [ + 'name' => $name, + 'alias' => $request['alias'], + ], + ]; + } catch (\Exception $e) { + $message = $e->getMessage(); + + flash($message)->error()->important(); + + $json = [ + 'success' => false, + 'error' => true, + 'message' => $message, + 'data' => [], + ]; + } + + return response()->json($json); + } + + public function uninstall($alias) + { + try { + $name = module($alias)->getName(); + + $this->dispatch(new UninstallModule($alias, company_id(), setting('default.locale'))); + + $message = trans('modules.uninstalled', ['module' => $name]); + + flash($message)->success(); + } catch (\Exception $e) { + $message = $e->getMessage(); + + flash($message)->error()->important(); + } + + return redirect()->route('apps.app.show', $alias)->send(); + } + + public function enable($alias) + { + try { + $name = module($alias)->getName(); + + $this->dispatch(new EnableModule($alias, company_id(), setting('default.locale'))); + + $message = trans('modules.enabled', ['module' => $name]); + + flash($message)->success(); + } catch (\Exception $e) { + $message = $e->getMessage(); + + flash($message)->error()->important(); + } + + return redirect()->route('apps.app.show', $alias)->send(); + } + + public function disable($alias) + { + try { + $name = module($alias)->getName(); + + $this->dispatch(new DisableModule($alias, company_id(), setting('default.locale'))); + + $message = trans('modules.disabled', ['module' => $name]); + + flash($message)->success(); + } catch (\Exception $e) { + $message = $e->getMessage(); + + flash($message)->error()->important(); + } + + return redirect()->route('apps.app.show', $alias)->send(); + } + + public function releases($alias, Request $request) + { + $data = [ + 'query' => [ + 'page' => $request->get('page', 1), + ] + ]; + + $releases = $this->getModuleReleases($alias, $data); + + $html = view('components.layouts.modules.releases', compact('releases'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'data' => $releases, + 'message' => null, + 'html' => $html, + ]); + } + + public function reviews($alias, Request $request) + { + $data = [ + 'query' => [ + 'page' => $request->get('page', 1), + ] + ]; + + $reviews = $this->getModuleReviews($alias, $data); + + $html = view('components.layouts.modules.reviews', compact('reviews'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'data' => $reviews, + 'message' => null, + 'html' => $html, + ]); + } + + public function documentation($alias) + { + $documentation = $this->getModuleDocumentation($alias); + + $back = route('apps.app.show', $alias); + + if (empty($documentation)) { + return redirect()->route($back)->send(); + } + + return view('modules.item.documentation', compact('documentation', 'back')); + } +} diff --git a/app/Http/Controllers/Modules/My.php b/app/Http/Controllers/Modules/My.php new file mode 100644 index 00000000..880ecdc0 --- /dev/null +++ b/app/Http/Controllers/Modules/My.php @@ -0,0 +1,24 @@ +getMyModules(['query' => ['limit' => 16]]); + $installed = $this->getInstalledModules(); + + return $this->response('modules.my.index', compact('purchase', 'installed')); + } +} diff --git a/app/Http/Controllers/Modules/Tiles.php b/app/Http/Controllers/Modules/Tiles.php new file mode 100644 index 00000000..e4332520 --- /dev/null +++ b/app/Http/Controllers/Modules/Tiles.php @@ -0,0 +1,242 @@ + [ + 'page' => $page, + ] + ]; + + $data = $this->getModulesByCategory($alias, $data); + + if (empty($data)) { + return redirect()->route('apps.home.index')->send(); + } + + $title = !empty($data->category) ? $data->category->name : Str::studly($alias); + $modules = !empty($data->modules) ? $data->modules : []; + $installed = Module::all()->pluck('enabled', 'alias')->toArray(); + + return $this->response('modules.tiles.index', compact('modules', 'title', 'installed')); + } + + /** + * Show the form for viewing the specified resource. + * + * @param $alias + * + * @return Response + */ + public function vendorModules($alias) + { + $page = request('page', 1); + + $data = [ + 'query' => [ + 'page' => $page, + ] + ]; + + $data = $this->getModulesByVendor($alias, $data); + + $title = !empty($data->vendor) ? $data->vendor->name : Str::studly($alias); + $modules = !empty($data->modules) ? $data->modules : []; + $installed = Module::all()->pluck('enabled', 'alias')->toArray(); + + return $this->response('modules.tiles.index', compact('modules', 'title', 'installed')); + } + + /** + * Show the form for viewing the specified resource. + * + * @return Response + */ + public function paidModules() + { + $page = request('page', 1); + + $data = [ + 'query' => [ + 'page' => $page, + ] + ]; + + $title = trans('modules.top_paid'); + $modules = $this->getPaidModules($data); + $installed = Module::all()->pluck('enabled', 'alias')->toArray(); + + return $this->response('modules.tiles.index', compact('modules', 'title', 'installed')); + } + + /** + * Show the form for viewing the specified resource. + * + * @return Response + */ + public function newModules() + { + $page = request('page', 1); + + $data = [ + 'query' => [ + 'page' => $page, + ] + ]; + + $title = trans('modules.new'); + $modules = $this->getNewModules($data); + $installed = Module::all()->pluck('enabled', 'alias')->toArray(); + + return $this->response('modules.tiles.index', compact('modules', 'title', 'installed')); + } + + /** + * Show the form for viewing the specified resource. + * + * @return Response + */ + public function freeModules() + { + $page = request('page', 1); + + $data = [ + 'query' => [ + 'page' => $page, + ] + ]; + + $title = trans('modules.top_free'); + $modules = $this->getFreeModules($data); + $installed = Module::all()->pluck('enabled', 'alias')->toArray(); + + return $this->response('modules.tiles.index', compact('modules', 'title','installed')); + } + + /** + * Show the form for viewing the specified resource. + * + * @return Response + */ + public function searchModules(Request $request) + { + $keyword = $request->get('keyword'); + $page = $request->get('page', 1); + + $data = [ + 'query' => [ + 'keyword' => $keyword, + 'page' => $page, + ] + ]; + + $title = trans('general.search'); + $modules = $this->getSearchModules($data); + $installed = Module::all()->pluck('enabled', 'alias')->toArray(); + + return $this->response('modules.tiles.index', compact('modules', 'title', 'keyword', 'installed')); + } + + public function loadMore($type, Request $request) + { + $page = $request->get('page', 1); + + $modules = []; + + $data = [ + 'query' => [ + 'page' => $page, + ] + ]; + + $last_page = 1; + + switch ($type) { + case 'categories': + $alias = $request->get('alias'); + $response = $this->getModulesByCategory($alias, $data); + + $response = !empty($response->modules) ? $response->modules : []; + $last_page = ! empty($response) ? $response->last_page : 1; + + $modules = $this->prepareModules($response); + break; + case 'vendors': + $alias = $request->get('alias'); + $response = $this->getModulesByVendor($alias, $data); + + $response = !empty($response->modules) ? $response->modules : []; + $last_page = ! empty($response) ? $response->last_page : 1; + + $modules = $this->prepareModules($response); + break; + case 'paid': + $response = $this->getPaidModules($data); + + $last_page = ! empty($response) ? $response->last_page : 1; + $modules = $this->prepareModules($response); + break; + case 'new': + $response = $this->getNewModules($data); + + $last_page = ! empty($response) ? $response->last_page : 1; + $modules = $this->prepareModules($response); + break; + case 'free': + $response = $this->getFreeModules($data); + + $last_page = ! empty($response) ? $response->last_page : 1; + $modules = $this->prepareModules($response); + break; + case 'search': + $data['query']['keyword'] = $request->get('keyword'); + + $response = $this->getSearchModules($data); + + $last_page = ! empty($response) ? $response->last_page : 1; + $modules = $this->prepareModules($response); + break; + } + + $html = view('components.modules.raw_items', compact('modules'))->render(); + + return response()->json([ + 'success' => true, + 'error' => false, + 'message' => 'null', + 'modules' => $modules, + 'last_page' => $last_page, + 'html' => $html, + ]); + } + + protected function prepareModules($response) + { + if (! empty($response->data)) { + return $response->data; + } + + return $response; + } +} diff --git a/app/Http/Controllers/Portal/Dashboard.php b/app/Http/Controllers/Portal/Dashboard.php new file mode 100644 index 00000000..45c460db --- /dev/null +++ b/app/Http/Controllers/Portal/Dashboard.php @@ -0,0 +1,101 @@ +contact; + + // Redirect user redirect landing page.. + if (!$contact) { + return redirect(user()->getLandingPageOfUser()); + } + + $financial_year = $this->getFinancialYear(); + + $start = Date::parse(request('start_date', $financial_year->copy()->getStartDate()->toDateString())); + $end = Date::parse(request('end_date', $financial_year->copy()->getEndDate()->toDateString())); + + //$invoices = Document::invoice()->accrued()->where('contact_id', $contact->id)->get(); + $invoices = Document::invoice()->accrued()->whereBetween('due_at', [$start, $end])->where('contact_id', $contact->id)->get(); + + $amounts = $this->calculateAmounts($invoices, $start, $end); + + return view('portal.dashboard.index', compact('contact', 'invoices')); + } + + private function calculateAmounts($invoices, $start, $end) + { + $amounts = ['paid', 'unpaid', 'overdue']; + + $date_format = 'Y-m'; + + $n = 1; + $start_date = $start->format($date_format); + $end_date = $end->format($date_format); + $next_date = $start_date; + + $s = clone $start; + + while ($next_date <= $end_date) { + $amounts['paid'][$next_date] = $amounts['unpaid'][$next_date] = $amounts['overdue'][$next_date] = 0; + + $next_date = $s->addMonths($n)->format($date_format); + } + + $this->setAmounts($amounts, $invoices, $date_format); + + return $amounts; + } + + private function setAmounts(&$amounts, $invoices, $date_format) + { + $today = Date::today()->format('Y-m-d'); + + foreach ($invoices as $invoice) { + $date = Date::parse($invoice->due_at)->format($date_format); + + $amount = $invoice->getAmountConvertedToDefault(); + + $is_overdue = $today > $invoice->due_at->format('Y-m-d'); + + switch ($invoice->status) { + case 'paid': + $amounts['paid'][$date] += $amount; + break; + case 'partial': + $paid = $invoice->paid; + $remainder = $amount - $paid; + + $amounts['paid'][$date] += $paid; + + if ($is_overdue) { + $amounts['overdue'][$date] += $remainder; + } else { + $amounts['unpaid'][$date] += $remainder; + } + break; + default: + if ($is_overdue) { + $amounts['overdue'][$date] += $amount; + } else { + $amounts['unpaid'][$date] += $amount; + } + } + } + } +} diff --git a/app/Http/Controllers/Portal/Invoices.php b/app/Http/Controllers/Portal/Invoices.php new file mode 100644 index 00000000..732d9472 --- /dev/null +++ b/app/Http/Controllers/Portal/Invoices.php @@ -0,0 +1,203 @@ +with('contact', 'histories', 'items', 'payments') + ->accrued()->where('contact_id', user()->contact->id) + ->collect(['document_number'=> 'desc']); + + return $this->response('portal.invoices.index', compact('invoices')); + } + + /** + * Show the form for viewing the specified resource. + * + * @param Document $invoice + * + * @return Response + */ + public function show(Document $invoice, Request $request) + { + $invoice->load([ + 'items.taxes.tax', + 'items.item', + 'totals', + 'contact', + 'currency', + 'category', + 'histories', + 'media', + 'transactions', + 'recurring', + 'children', + ]); + + $payment_methods = Modules::getPaymentMethods(); + + event(new \App\Events\Document\DocumentViewed($invoice)); + + $text_document_transaction = 'documents.transaction'; + + if ( + request()->isSigned($invoice->company_id) + || request()->isPreview($invoice->company_id) + || request()->isPortal($invoice->company_id) + ) { + $text_document_transaction = 'documents.portal_transaction'; + } + + return view('portal.invoices.show', compact('invoice', 'payment_methods', 'text_document_transaction')); + } + + /** + * Show the form for viewing the specified resource. + * + * @param Document $invoice + * + * @return Response + */ + public function finish(Document $invoice, Request $request) + { + $layout = $request->isPortal($invoice->company_id) ? 'portal' : 'signed'; + + return view('portal.invoices.finish', compact('invoice', 'layout')); + } + + /** + * Show the form for viewing the specified resource. + * + * @param Document $invoice + * + * @return Response + */ + public function printInvoice(Document $invoice, Request $request) + { + event(new \App\Events\Document\DocumentPrinting($invoice)); + + $view = view($invoice->template_path, compact('invoice')); + + return mb_convert_encoding($view, 'HTML-ENTITIES', 'UTF-8'); + } + + /** + * Show the form for viewing the specified resource. + * + * @param Document $invoice + * + * @return Response + */ + public function pdfInvoice(Document $invoice, Request $request) + { + event(new \App\Events\Document\DocumentPrinting($invoice)); + + $currency_style = true; + + $view = view($invoice->template_path, compact('invoice', 'currency_style'))->render(); + $html = mb_convert_encoding($view, 'HTML-ENTITIES', 'UTF-8'); + + $pdf = app('dompdf.wrapper'); + $pdf->loadHTML($html); + + //$pdf->setPaper('A4', 'portrait'); + + $file_name = $this->getDocumentFileName($invoice); + + return $pdf->download($file_name); + } + + public function preview(Document $invoice) + { + if (empty($invoice)) { + return redirect()->route('login'); + } + + $payment_actions = []; + + $payment_methods = Modules::getPaymentMethods(); + + foreach ($payment_methods as $payment_method_key => $payment_method_value) { + $codes = explode('.', $payment_method_key); + + if (!isset($payment_actions[$codes[0]])) { + $payment_actions[$codes[0]] = URL::signedRoute('signed.' . $codes[0] . '.invoices.show', [$invoice->id]); + } + } + + $text_document_transaction = 'documents.transaction'; + + if ( + request()->isSigned($invoice->company_id) + || request()->isPreview($invoice->company_id) + || request()->isPortal($invoice->company_id) + ) { + $text_document_transaction = 'documents.portal_transaction'; + } + + return view('portal.invoices.preview', compact('invoice', 'payment_methods', 'payment_actions', 'text_document_transaction')); + } + + public function signed(Document $invoice) + { + if (empty($invoice)) { + return redirect()->route('login'); + } + + $payment_actions = []; + + $payment_methods = Modules::getPaymentMethods(); + + foreach ($payment_methods as $payment_method_key => $payment_method_value) { + $codes = explode('.', $payment_method_key); + + if (!isset($payment_actions[$codes[0]])) { + $payment_actions[$codes[0]] = URL::signedRoute('signed.' . $codes[0] . '.invoices.show', [$invoice->id]); + } + } + + $print_action = URL::signedRoute('signed.invoices.print', [$invoice->id]); + $pdf_action = URL::signedRoute('signed.invoices.pdf', [$invoice->id]); + + // Guest or Invoice contact user track the invoice viewed. + if (empty(user()) || user()->id == $invoice->contact->user_id) { + event(new \App\Events\Document\DocumentViewed($invoice)); + } + + $text_document_transaction = 'documents.transaction'; + + if ( + request()->isSigned($invoice->company_id) + || request()->isPreview($invoice->company_id) + || request()->isPortal($invoice->company_id) + ) { + $text_document_transaction = 'documents.portal_transaction'; + } + + return view('portal.invoices.signed', compact('invoice', 'payment_methods', 'payment_actions', 'print_action', 'pdf_action', 'text_document_transaction')); + } +} diff --git a/app/Http/Controllers/Portal/Payments.php b/app/Http/Controllers/Portal/Payments.php new file mode 100644 index 00000000..dc04102b --- /dev/null +++ b/app/Http/Controllers/Portal/Payments.php @@ -0,0 +1,132 @@ +get('search'); + + $payments = Transaction::income()->where('contact_id', '=', user()->contact->id)->usingSearchString($search)->sortable('paid_at')->paginate(); + + $payment_methods = Modules::getPaymentMethods('all'); + + return $this->response('portal.payments.index', compact('payments', 'payment_methods')); + } + + /** + * Show the form for viewing the specified resource. + * + * @param Transaction $payment + * + * @return Response + */ + public function show(Transaction $payment, Request $request) + { + $payment_methods = Modules::getPaymentMethods('all'); + + return view('portal.payments.show', compact('payment', 'payment_methods')); + } + + /** + * Display a listing of the resource. + * + * @return Response + */ + public function currencies() + { + $currencies = Currency::collect(); + + return $this->response('portal.currencies.index', compact('currencies')); + } + + /** + * Show the form for viewing the specified resource. + * + * @param Transaction $payment + * + * @return Response + */ + public function printPayment(Transaction $payment, Request $request) + { + event(new TransactionPrinting($payment)); + + $transaction = $payment; + + $real_type = $this->getRealTypeTransaction($transaction->type); + + $view = view('banking.transactions.print_default', compact('transaction', 'real_type')); + + return mb_convert_encoding($view, 'HTML-ENTITIES', 'UTF-8'); + } + + /** + * Show the form for viewing the specified resource. + * + * @param Transaction $payment + * + * @return Response + */ + public function pdfPayment(Transaction $payment, Request $request) + { + event(new TransactionPrinting($payment)); + + $currency_style = true; + + $transaction = $payment; + + $real_type = $this->getRealTypeTransaction($transaction->type); + + $view = view('banking.transactions.print_default', compact('transaction', 'currency_style', 'real_type'))->render(); + $html = mb_convert_encoding($view, 'HTML-ENTITIES', 'UTF-8'); + + $pdf = app('dompdf.wrapper'); + $pdf->loadHTML($html); + + //$pdf->setPaper('A4', 'portrait'); + + $file_name = $this->getTransactionFileName($payment); + + return $pdf->download($file_name); + } + + public function preview(Transaction $payment) + { + if (empty($payment)) { + return redirect()->route('login'); + } + + return view('portal.payments.preview', compact('payment')); + } + + public function signed(Transaction $payment) + { + if (empty($payment)) { + return redirect()->route('login'); + } + + $payment_methods = Modules::getPaymentMethods(); + + $print_action = URL::signedRoute('signed.payments.print', [$payment->id]); + $pdf_action = URL::signedRoute('signed.payments.pdf', [$payment->id]); + + return view('portal.payments.signed', compact('payment', 'payment_methods', 'print_action', 'pdf_action')); + } +} diff --git a/app/Http/Controllers/Portal/Profile.php b/app/Http/Controllers/Portal/Profile.php new file mode 100644 index 00000000..40ea19d4 --- /dev/null +++ b/app/Http/Controllers/Portal/Profile.php @@ -0,0 +1,107 @@ +edit(); + } + + public function show() + { + return $this->edit(); + } + + /** + * Show the form for editing the specified resource. + * + * @return Response + */ + public function edit() + { + $user = user(); + + return view('portal.profile.edit', compact('user')); + } + + /** + * Update the specified resource in storage. + * + * @param User $user + * @param Request $request + * + * @return Response + */ + public function update($user_id, Request $request) + { + $user = user(); + + // Do not reset password if not entered/changed + if (empty($request['password'])) { + unset($request['current_password']); + unset($request['password']); + unset($request['password_confirmation']); + } + + // Update user + $user->update($request->input()); + + // Upload picture + if ($request->file('picture')) { + $media = $this->getMedia($request->file('picture'), 'users'); + + $user->attachMedia($media, 'picture'); + } + + // Update customer + $user->contact->update($request->only([ + 'name', 'email', 'tax_number', 'phone', + 'address', 'city', 'state', 'zip_code', 'country' + ])); + + $message = trans('messages.success.updated', ['type' => trans('auth.profile')]); + + flash($message)->success(); + + $response = [ + 'success' => true, + 'error' => false, + 'data' => null, + 'message' => '', + 'redirect' => route('portal.profile.edit', $user->id), + ]; + + return response()->json($response); + } + + /** + * Mark overdue invoices notifications are read and redirect to invoices page. + * + * @return Response + */ + public function readOverdueInvoices() + { + $user = user(); + + // Mark invoice notifications as read + foreach ($user->unreadNotifications as $notification) { + // Not an invoice notification + if ($notification->getAttribute('type') != 'App\Notifications\Sale\Invoice') { + continue; + } + + $notification->markAsRead(); + } + + // Redirect to invoices + return redirect()->route('portal.invoices.index'); + } +} diff --git a/app/Http/Controllers/Purchases/Bills.php b/app/Http/Controllers/Purchases/Bills.php new file mode 100644 index 00000000..cd5ecbc3 --- /dev/null +++ b/app/Http/Controllers/Purchases/Bills.php @@ -0,0 +1,353 @@ +middleware('permission:update-purchases-bills')->only('markReceived', 'markCancelled', 'restoreBill'); + } + + /** + * Display a listing of the resource. + * + * @return Response + */ + public function index() + { + $this->setActiveTabForDocuments(); + + $bills = Document::bill()->with('contact', 'items', 'items.taxes', 'item_taxes', 'last_history', 'transactions', 'totals', 'histories', 'media')->collect(['issued_at' => 'desc']); + + $total_bills = Document::bill()->count(); + + return $this->response('purchases.bills.index', compact('bills', 'total_bills')); + } + + /** + * Show the form for viewing the specified resource. + * + * @param Document $bill + * + * @return Response + */ + public function show(Document $bill) + { + $bill->load([ + 'items.taxes.tax', + 'items.item', + 'totals', + 'contact', + 'currency', + 'category', + 'histories', + 'media', + 'transactions', + 'recurring', + 'children', + ]); + + return view('purchases.bills.show', compact('bill')); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + return view('purchases.bills.create'); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $response = $this->ajaxDispatch(new CreateDocument($request)); + + if ($response['success']) { + $response['redirect'] = route('bills.show', $response['data']->id); + + $message = trans('messages.success.created', ['type' => trans_choice('general.bills', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('bills.create'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Duplicate the specified resource. + * + * @param Document $bill + * + * @return Response + */ + public function duplicate(Document $bill) + { + $clone = $this->dispatch(new DuplicateDocument($bill)); + + $message = trans('messages.success.duplicated', ['type' => trans_choice('general.bills', 1)]); + + flash($message)->success(); + + return redirect()->route('bills.edit', $clone->id); + } + + /** + * Import the specified resource. + * + * @param ImportRequest $request + * + * @return Response + */ + public function import(ImportRequest $request) + { + $response = $this->importExcel(new Import, $request, trans_choice('general.bills', 2)); + + if ($response['success']) { + $response['redirect'] = route('bills.index'); + + flash($response['message'])->success(); + } else { + $response['redirect'] = route('import.create', ['purchases', 'bills']); + + flash($response['message'])->error()->important(); + } + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param Document $bill + * + * @return Response + */ + public function edit(Document $bill) + { + return view('purchases.bills.edit', compact('bill')); + } + + /** + * Update the specified resource in storage. + * + * @param Document $bill + * @param Request $request + * + * @return Response + */ + public function update(Document $bill, Request $request) + { + $response = $this->ajaxDispatch(new UpdateDocument($bill, $request)); + + if ($response['success']) { + $response['redirect'] = route('bills.show', $response['data']->id); + + $message = trans('messages.success.updated', ['type' => trans_choice('general.bills', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('bills.edit', $bill->id); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Remove the specified resource from storage. + * + * @param $bill + * + * @return Response + */ + public function destroy(Document $bill) + { + $response = $this->ajaxDispatch(new DeleteDocument($bill)); + + $response['redirect'] = route('bills.index'); + + if ($response['success']) { + $message = trans('messages.success.deleted', ['type' => trans_choice('general.bills', 1)]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Export the specified resource. + * + * @return Response + */ + public function export() + { + return $this->exportExcel(new Export, trans_choice('general.bills', 2)); + } + + /** + * Mark the bill as received. + * + * @param Document $bill + * + * @return Response + */ + public function markReceived(Document $bill) + { + event(new \App\Events\Document\DocumentReceived($bill)); + + $message = trans('documents.messages.marked_received', ['type' => trans_choice('general.bills', 1)]); + + flash($message)->success(); + + return redirect()->back(); + } + + /** + * Mark the bill as cancelled. + * + * @param Document $bill + * + * @return Response + */ + public function markCancelled(Document $bill) + { + event(new \App\Events\Document\DocumentCancelled($bill)); + + $message = trans('documents.messages.marked_cancelled', ['type' => trans_choice('general.bills', 1)]); + + flash($message)->success(); + + return redirect()->back(); + } + + /** + * Restore the bill. + * + * @param Document $bill + * + * @return Response + */ + public function restoreBill(Document $bill) + { + event(new \App\Events\Document\DocumentRestored($bill)); + + $message = trans('documents.messages.restored', ['type' => trans_choice('general.bills', 1)]); + + flash($message)->success(); + + return redirect()->back(); + } + + /** + * Print the bill. + * + * @param Document $bill + * + * @return Response + */ + public function printBill(Document $bill) + { + $bill = $this->prepareBill($bill); + + $view = view($bill->template_path, compact('bill')); + + return mb_convert_encoding($view, 'HTML-ENTITIES', 'UTF-8'); + } + + /** + * Download the PDF file of bill. + * + * @param Document $bill + * + * @return Response + */ + public function pdfBill(Document $bill) + { + $bill = $this->prepareBill($bill); + + $currency_style = true; + + $view = view($bill->template_path, compact('bill', 'currency_style'))->render(); + $html = mb_convert_encoding($view, 'HTML-ENTITIES', 'UTF-8'); + + $pdf = app('dompdf.wrapper'); + $pdf->loadHTML($html); + + $file_name = $this->getDocumentFileName($bill); + + return $pdf->download($file_name); + } + + protected function prepareBill(Document $bill) + { + $paid = 0; + + foreach ($bill->transactions as $item) { + $amount = $item->amount; + + if ($bill->currency_code != $item->currency_code) { + $item->default_currency_code = $bill->currency_code; + + $amount = $item->getAmountConvertedFromDefault(); + } + + $paid += $amount; + } + + $bill->paid = $paid; + + $bill->template_path = 'purchases.bills.print'; + + return $bill; + } +} diff --git a/app/Http/Controllers/Purchases/RecurringBills.php b/app/Http/Controllers/Purchases/RecurringBills.php new file mode 100644 index 00000000..a392eae6 --- /dev/null +++ b/app/Http/Controllers/Purchases/RecurringBills.php @@ -0,0 +1,235 @@ +middleware('permission:create-purchases-bills')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-purchases-bills')->only('index', 'show', 'edit', 'export'); + $this->middleware('permission:update-purchases-bills')->only('update', 'enable', 'disable', 'end'); + $this->middleware('permission:delete-purchases-bills')->only('destroy'); + } + + /** + * Display a listing of the resource. + * + * @return Response + */ + public function index() + { + $bills = Document::with('contact', 'transactions', 'recurring')->billRecurring()->collect(['issued_at' => 'desc']); + + return $this->response('purchases.recurring_bills.index', compact('bills')); + } + + /** + * Show the form for viewing the specified resource. + * + * @param Document $recurring_bill + * + * @return Response + */ + public function show(Document $recurring_bill) + { + $recurring_bill->load(['category', 'recurring', 'children']); + + return view('purchases.recurring_bills.show', compact('recurring_bill')); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + return view('purchases.recurring_bills.create'); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $issue_at = Date::parse($request->get('recurring_started_at'))->format('Y-m-d'); + + $request->merge(['issued_at' => $issue_at]); + + $response = $this->ajaxDispatch(new CreateDocument($request)); + + if ($response['success']) { + $response['redirect'] = route('recurring-bills.show', $response['data']->id); + + $message = trans('messages.success.created', ['type' => trans_choice('general.recurring_bills', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('recurring-bills.create'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Duplicate the specified resource. + * + * @param Document $recurring_bill + * + * @return Response + */ + public function duplicate(Document $recurring_bill) + { + $clone = $this->dispatch(new DuplicateDocument($recurring_bill)); + + $message = trans('messages.success.duplicated', ['type' => trans_choice('general.recurring_bills', 1)]); + + flash($message)->success(); + + return redirect()->route('recurring-bills.edit', $clone->id); + } + + /** + * Import the specified resource. + * + * @param ImportRequest $request + * + * @return Response + */ + public function import(ImportRequest $request) + { + $response = $this->importExcel(new Import, $request, trans_choice('general.recurring_bills', 2)); + + if ($response['success']) { + $response['redirect'] = route('recurring-bills.index'); + + flash($response['message'])->success(); + } else { + $response['redirect'] = route('import.create', ['purchases', 'recurring-bills']); + + flash($response['message'])->error()->important(); + } + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param Document $recurring_bill + * + * @return Response + */ + public function edit(Document $recurring_bill) + { + return view('purchases.recurring_bills.edit', compact('recurring_bill')); + } + + /** + * Update the specified resource in storage. + * + * @param Document $recurring_bill + * @param Request $request + * + * @return Response + */ + public function update(Document $recurring_bill, Request $request) + { + $issue_at = Date::parse($request->get('recurring_started_at'))->format('Y-m-d'); + + $request->merge(['issued_at' => $issue_at]); + + $response = $this->ajaxDispatch(new UpdateDocument($recurring_bill, $request)); + + if ($response['success']) { + $response['redirect'] = route('recurring-bills.show', $response['data']->id); + + $message = trans('messages.success.updated', ['type' => trans_choice('general.recurring_bills', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('recurring-bills.edit', $recurring_bill->id); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Export the specified resource. + * + * @return Response + */ + public function export() + { + return $this->exportExcel(new Export, trans_choice('general.recurring_bills', 2)); + } + + /** + * End recurring template. + * + * @return Response + */ + public function end(Document $recurring_bill) + { + $response = $this->ajaxDispatch(new UpdateDocument($recurring_bill, [ + 'recurring_frequency' => $recurring_bill->recurring->frequency, + 'recurring_interval' => $recurring_bill->recurring->interval, + 'recurring_started_at' => $recurring_bill->recurring->started_at, + 'recurring_limit' => $recurring_bill->recurring->limit, + 'recurring_limit_count' => $recurring_bill->recurring->limit_count, + 'recurring_limit_date' => $recurring_bill->recurring->limit_date, + 'created_from' => $recurring_bill->created_from, + 'created_by' => $recurring_bill->created_by, + 'recurring_status' => Recurring::END_STATUS, + ])); + + if ($response['success']) { + $message = trans('messages.success.ended', ['type' => trans_choice('general.recurring_bills', 1)]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return redirect()->route('recurring-bills.index'); + } +} diff --git a/app/Http/Controllers/Purchases/Vendors.php b/app/Http/Controllers/Purchases/Vendors.php new file mode 100644 index 00000000..7b2f1359 --- /dev/null +++ b/app/Http/Controllers/Purchases/Vendors.php @@ -0,0 +1,269 @@ +withCount([ + 'contact_persons as contact_persons_with_email_count' => function ($query) { + $query->whereNotNull('email'); + } + ]) + ->vendor() + ->collect(); + + return $this->response('purchases.vendors.index', compact('vendors')); + } + + /** + * Show the form for viewing the specified resource. + * + * @param Contact $vendor + * + * @return Response + */ + public function show(Contact $vendor) + { + return view('purchases.vendors.show', compact('vendor')); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + return view('purchases.vendors.create'); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $response = $this->ajaxDispatch(new CreateContact($request)); + + if ($response['success']) { + $response['redirect'] = route('vendors.show', $response['data']->id); + + $message = trans('messages.success.created', ['type' => trans_choice('general.vendors', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('vendors.create'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Duplicate the specified resource. + * + * @param Contact $vendor + * + * @return Response + */ + public function duplicate(Contact $vendor) + { + $clone = $this->dispatch(new DuplicateContact($vendor)); + + $message = trans('messages.success.duplicated', ['type' => trans_choice('general.vendors', 1)]); + + flash($message)->success(); + + return redirect()->route('vendors.edit', $clone->id); + } + + /** + * Import the specified resource. + * + * @param ImportRequest $request + * + * @return Response + */ + public function import(ImportRequest $request) + { + $response = $this->importExcel(new Import, $request, trans_choice('general.vendors', 2)); + + if ($response['success']) { + $response['redirect'] = route('vendors.index'); + + flash($response['message'])->success(); + } else { + $response['redirect'] = route('import.create', ['purchases', 'vendors']); + + flash($response['message'])->error()->important(); + } + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param Contact $vendor + * + * @return Response + */ + public function edit(Contact $vendor) + { + return view('purchases.vendors.edit', compact('vendor')); + } + + /** + * Update the specified resource in storage. + * + * @param Contact $vendor + * @param Request $request + * + * @return Response + */ + public function update(Contact $vendor, Request $request) + { + $response = $this->ajaxDispatch(new UpdateContact($vendor, $request)); + + if ($response['success']) { + $response['redirect'] = route('vendors.show', $response['data']->id); + + $message = trans('messages.success.updated', ['type' => $vendor->name]); + + flash($message)->success(); + } else { + $response['redirect'] = route('vendors.edit', $vendor->id); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Enable the specified resource. + * + * @param Contact $vendor + * + * @return Response + */ + public function enable(Contact $vendor) + { + $response = $this->ajaxDispatch(new UpdateContact($vendor, request()->merge(['enabled' => 1]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.enabled', ['type' => $vendor->name]); + } + + return response()->json($response); + } + + /** + * Disable the specified resource. + * + * @param Contact $vendor + * + * @return Response + */ + public function disable(Contact $vendor) + { + $response = $this->ajaxDispatch(new UpdateContact($vendor, request()->merge(['enabled' => 0]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.disabled', ['type' => $vendor->name]); + } + + return response()->json($response); + } + + /** + * Remove the specified resource from storage. + * + * @param Contact $vendor + * + * @return Response + */ + public function destroy(Contact $vendor) + { + $response = $this->ajaxDispatch(new DeleteContact($vendor)); + + $response['redirect'] = route('vendors.index'); + + if ($response['success']) { + $message = trans('messages.success.deleted', ['type' => $vendor->name]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Export the specified resource. + * + * @return Response + */ + public function export() + { + return $this->exportExcel(new Export, trans_choice('general.vendors', 2)); + } + + public function createBill(Contact $vendor) + { + $data['contact'] = $vendor; + + return redirect()->route('bills.create')->withInput($data); + } + + public function createExpense(Contact $vendor) + { + $data['contact'] = $vendor; + + return redirect()->route('transactions.create', ['type' => 'expense'])->withInput($data); + } +} diff --git a/app/Http/Controllers/Sales/Customers.php b/app/Http/Controllers/Sales/Customers.php new file mode 100644 index 00000000..cffe307a --- /dev/null +++ b/app/Http/Controllers/Sales/Customers.php @@ -0,0 +1,269 @@ +with([ + 'media', + 'invoices.histories', + 'invoices.totals', + 'invoices.transactions', + 'invoices.media' + ]) + ->withCount([ + 'contact_persons as contact_persons_with_email_count' => function ($query) { + $query->whereNotNull('email'); + } + ]) + ->collect(); + + return $this->response('sales.customers.index', compact('customers')); + } + + /** + * Show the form for viewing the specified resource. + * + * @param Contact $customer + * + * @return Response + */ + public function show(Contact $customer) + { + return view('sales.customers.show', compact('customer')); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + return view('sales.customers.create'); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $response = $this->ajaxDispatch(new CreateContact($request)); + + if ($response['success']) { + $response['redirect'] = route('customers.show', $response['data']->id); + + $message = trans('messages.success.created', ['type' => trans_choice('general.customers', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('customers.create'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Duplicate the specified resource. + * + * @param Contact $customer + * + * @return Response + */ + public function duplicate(Contact $customer) + { + $clone = $this->dispatch(new DuplicateContact($customer)); + + $message = trans('messages.success.duplicated', ['type' => trans_choice('general.customers', 1)]); + + flash($message)->success(); + + return redirect()->route('customers.edit', $clone->id); + } + + /** + * Import the specified resource. + * + * @param ImportRequest $request + * + * @return Response + */ + public function import(ImportRequest $request) + { + $response = $this->importExcel(new Import, $request, trans_choice('general.customers', 2)); + + if ($response['success']) { + $response['redirect'] = route('customers.index'); + + flash($response['message'])->success(); + } else { + $response['redirect'] = route('import.create', ['sales', 'customers']); + + flash($response['message'])->error()->important(); + } + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param Contact $customer + * + * @return Response + */ + public function edit(Contact $customer) + { + return view('sales.customers.edit', compact('customer')); + } + + /** + * Update the specified resource in storage. + * + * @param Contact $customer + * @param Request $request + * + * @return Response + */ + public function update(Contact $customer, Request $request) + { + $response = $this->ajaxDispatch(new UpdateContact($customer, $request)); + + if ($response['success']) { + $response['redirect'] = route('customers.show', $response['data']->id); + + $message = trans('messages.success.updated', ['type' => $customer->name]); + + flash($message)->success(); + } else { + $response['redirect'] = route('customers.edit', $customer->id); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Enable the specified resource. + * + * @param Contact $customer + * + * @return Response + */ + public function enable(Contact $customer) + { + $response = $this->ajaxDispatch(new UpdateContact($customer, request()->merge(['enabled' => 1]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.enabled', ['type' => $customer->name]); + } + + return response()->json($response); + } + + /** + * Disable the specified resource. + * + * @param Contact $customer + * + * @return Response + */ + public function disable(Contact $customer) + { + $response = $this->ajaxDispatch(new UpdateContact($customer, request()->merge(['enabled' => 0]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.disabled', ['type' => $customer->name]); + } + + return response()->json($response); + } + + /** + * Remove the specified resource from storage. + * + * @param Contact $customer + * + * @return Response + */ + public function destroy(Contact $customer) + { + $response = $this->ajaxDispatch(new DeleteContact($customer)); + + $response['redirect'] = route('customers.index'); + + if ($response['success']) { + $message = trans('messages.success.deleted', ['type' => $customer->name]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Export the specified resource. + * + * @return Response + */ + public function export() + { + return $this->exportExcel(new Export, trans_choice('general.customers', 2)); + } + + public function createInvoice(Contact $customer) + { + $data['contact'] = $customer; + + return redirect()->route('invoices.create')->withInput($data); + } + + public function createIncome(Contact $customer) + { + $data['contact'] = $customer; + + return redirect()->route('transactions.create', ['type' => 'income'])->withInput($data); + } +} diff --git a/app/Http/Controllers/Sales/Invoices.php b/app/Http/Controllers/Sales/Invoices.php new file mode 100644 index 00000000..860225fe --- /dev/null +++ b/app/Http/Controllers/Sales/Invoices.php @@ -0,0 +1,378 @@ +middleware('permission:update-sales-invoices')->only('markSent', 'markCancelled', 'restoreInvoice'); + } + + /** + * Display a listing of the resource. + * + * @return Response + */ + public function index() + { + $this->setActiveTabForDocuments(); + + $invoices = Document::invoice()->with([ + 'contact' => function ($query) { + $query->withCount([ + 'contact_persons as contact_persons_with_email_count' => function ($query) { + $query->whereNotNull('email'); + }, + ]); + }, + 'items', + 'items.taxes', + 'item_taxes', + 'last_history', + 'transactions', + 'totals', + 'histories', + 'media', + ])->collect(['document_number'=> 'desc']); + + $total_invoices = Document::invoice()->count(); + + return $this->response('sales.invoices.index', compact('invoices', 'total_invoices')); + } + + /** + * Show the form for viewing the specified resource. + * + * @param Document $invoice + * + * @return Response + */ + public function show(Document $invoice) + { + $invoice->load([ + 'items.taxes.tax', + 'items.item', + 'totals', + 'contact', + 'currency', + 'category', + 'histories', + 'media', + 'transactions', + 'recurring', + 'children', + ]); + + return view('sales.invoices.show', compact('invoice')); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + return view('sales.invoices.create'); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $response = $this->ajaxDispatch(new CreateDocument($request)); + + if ($response['success']) { + $paramaters = ['invoice' => $response['data']->id]; + + if ($request->has('senddocument')) { + $paramaters['senddocument'] = true; + } + + $response['redirect'] = route('invoices.show', $paramaters); + + $message = trans('messages.success.created', ['type' => trans_choice('general.invoices', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('invoices.create'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Duplicate the specified resource. + * + * @param Document $invoice + * + * @return Response + */ + public function duplicate(Document $invoice) + { + $clone = $this->dispatch(new DuplicateDocument($invoice)); + + $message = trans('messages.success.duplicated', ['type' => trans_choice('general.invoices', 1)]); + + flash($message)->success(); + + return redirect()->route('invoices.edit', $clone->id); + } + + /** + * Import the specified resource. + * + * @param ImportRequest $request + * + * @return Response + */ + public function import(ImportRequest $request) + { + $response = $this->importExcel(new Import, $request, trans_choice('general.invoices', 2)); + + if ($response['success']) { + $response['redirect'] = route('invoices.index'); + + flash($response['message'])->success(); + } else { + $response['redirect'] = route('import.create', ['sales', 'invoices']); + + flash($response['message'])->error()->important(); + } + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param Document $invoice + * + * @return Response + */ + public function edit(Document $invoice) + { + return view('sales.invoices.edit', compact('invoice')); + } + + /** + * Update the specified resource in storage. + * + * @param Document $invoice + * @param Request $request + * + * @return Response + */ + public function update(Document $invoice, Request $request) + { + $response = $this->ajaxDispatch(new UpdateDocument($invoice, $request)); + + if ($response['success']) { + $paramaters = ['invoice' => $response['data']->id]; + + if ($request->has('senddocument')) { + $paramaters['senddocument'] = true; + } + + $response['redirect'] = route('invoices.show', $paramaters); + + $message = trans('messages.success.updated', ['type' => trans_choice('general.invoices', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('invoices.edit', $invoice->id); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Remove the specified resource from storage. + * + * @param Document $invoice + * + * @return Response + */ + public function destroy(Document $invoice) + { + $response = $this->ajaxDispatch(new DeleteDocument($invoice)); + + $response['redirect'] = route('invoices.index'); + + if ($response['success']) { + $message = trans('messages.success.deleted', ['type' => trans_choice('general.invoices', 1)]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Export the specified resource. + * + * @return Response + */ + public function export() + { + return $this->exportExcel(new Export, trans_choice('general.invoices', 2)); + } + + /** + * Mark the invoice as sent. + * + * @param Document $invoice + * + * @return Response + */ + public function markSent(Document $invoice) + { + event(new \App\Events\Document\DocumentMarkedSent($invoice)); + + $message = trans('documents.messages.marked_sent', ['type' => trans_choice('general.invoices', 1)]); + + flash($message)->success(); + + return redirect()->back(); + } + + /** + * Mark the invoice as cancelled. + * + * @param Document $invoice + * + * @return Response + */ + public function markCancelled(Document $invoice) + { + event(new \App\Events\Document\DocumentCancelled($invoice)); + + $message = trans('documents.messages.marked_cancelled', ['type' => trans_choice('general.invoices', 1)]); + + flash($message)->success(); + + return redirect()->back(); + } + + /** + * Restore the invoice. + * + * @param Document $invoice + * + * @return Response + */ + public function restoreInvoice(Document $invoice) + { + event(new \App\Events\Document\DocumentRestored($invoice)); + + $message = trans('documents.messages.restored', ['type' => trans_choice('general.invoices', 1)]); + + flash($message)->success(); + + return redirect()->back(); + } + + /** + * Download the PDF file of invoice. + * + * @param Document $invoice + * + * @return Response + */ + public function emailInvoice(Document $invoice) + { + if (empty($invoice->contact_email)) { + return redirect()->back(); + } + + $response = $this->ajaxDispatch(new SendDocument($invoice)); + + if ($response['success']) { + $message = trans('documents.messages.email_sent', ['type' => trans_choice('general.invoices', 1)]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return redirect()->back(); + } + + /** + * Print the invoice. + * + * @param Document $invoice + * + * @return Response + */ + public function printInvoice(Document $invoice) + { + event(new \App\Events\Document\DocumentPrinting($invoice)); + + $view = view($invoice->template_path, compact('invoice')); + + return mb_convert_encoding($view, 'HTML-ENTITIES', 'UTF-8'); + } + + /** + * Download the PDF file of invoice. + * + * @param Document $invoice + * + * @return Response + */ + public function pdfInvoice(Document $invoice) + { + event(new \App\Events\Document\DocumentPrinting($invoice)); + + return $this->dispatch(new DownloadDocument($invoice, null, null, false, 'download')); + } +} diff --git a/app/Http/Controllers/Sales/RecurringInvoices.php b/app/Http/Controllers/Sales/RecurringInvoices.php new file mode 100644 index 00000000..04fe8087 --- /dev/null +++ b/app/Http/Controllers/Sales/RecurringInvoices.php @@ -0,0 +1,235 @@ +middleware('permission:create-sales-invoices')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-sales-invoices')->only('index', 'show', 'edit', 'export'); + $this->middleware('permission:update-sales-invoices')->only('update', 'enable', 'disable', 'end'); + $this->middleware('permission:delete-sales-invoices')->only('destroy'); + } + + /** + * Display a listing of the resource. + * + * @return Response + */ + public function index() + { + $invoices = Document::with('contact', 'transactions', 'recurring')->invoiceRecurring()->collect(['issued_at' => 'desc']); + + return $this->response('sales.recurring_invoices.index', compact('invoices')); + } + + /** + * Show the form for viewing the specified resource. + * + * @param Document $recurring_invoice + * + * @return Response + */ + public function show(Document $recurring_invoice) + { + $recurring_invoice->load(['category', 'recurring', 'children']); + + return view('sales.recurring_invoices.show', compact('recurring_invoice')); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + return view('sales.recurring_invoices.create'); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $issue_at = Date::parse($request->get('recurring_started_at'))->format('Y-m-d'); + + $request->merge(['issued_at' => $issue_at]); + + $response = $this->ajaxDispatch(new CreateDocument($request)); + + if ($response['success']) { + $response['redirect'] = route('recurring-invoices.show', $response['data']->id); + + $message = trans('messages.success.created', ['type' => trans_choice('general.recurring_invoices', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('recurring-invoices.create'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Duplicate the specified resource. + * + * @param Document $recurring_invoice + * + * @return Response + */ + public function duplicate(Document $recurring_invoice) + { + $clone = $this->dispatch(new DuplicateDocument($recurring_invoice)); + + $message = trans('messages.success.duplicated', ['type' => trans_choice('general.recurring_invoices', 1)]); + + flash($message)->success(); + + return redirect()->route('recurring-invoices.edit', $clone->id); + } + + /** + * Import the specified resource. + * + * @param ImportRequest $request + * + * @return Response + */ + public function import(ImportRequest $request) + { + $response = $this->importExcel(new Import, $request, trans_choice('general.recurring_invoices', 2)); + + if ($response['success']) { + $response['redirect'] = route('recurring-invoices.index'); + + flash($response['message'])->success(); + } else { + $response['redirect'] = route('import.create', ['sales', 'recurring-invoices']); + + flash($response['message'])->error()->important(); + } + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param Document $recurring_invoice + * + * @return Response + */ + public function edit(Document $recurring_invoice) + { + return view('sales.recurring_invoices.edit', compact('recurring_invoice')); + } + + /** + * Update the specified resource in storage. + * + * @param Document $recurring_invoice + * @param Request $request + * + * @return Response + */ + public function update(Document $recurring_invoice, Request $request) + { + $issue_at = Date::parse($request->get('recurring_started_at'))->format('Y-m-d'); + + $request->merge(['issued_at' => $issue_at]); + + $response = $this->ajaxDispatch(new UpdateDocument($recurring_invoice, $request)); + + if ($response['success']) { + $response['redirect'] = route('recurring-invoices.show', $response['data']->id); + + $message = trans('messages.success.updated', ['type' => trans_choice('general.recurring_invoices', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('recurring-invoices.edit', $recurring_invoice->id); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Export the specified resource. + * + * @return Response + */ + public function export() + { + return $this->exportExcel(new Export, trans_choice('general.recurring_invoices', 2)); + } + + /** + * End recurring template. + * + * @return Response + */ + public function end(Document $recurring_invoice) + { + $response = $this->ajaxDispatch(new UpdateDocument($recurring_invoice, [ + 'recurring_frequency' => $recurring_invoice->recurring->frequency, + 'recurring_interval' => $recurring_invoice->recurring->interval, + 'recurring_started_at' => $recurring_invoice->recurring->started_at, + 'recurring_limit' => $recurring_invoice->recurring->limit, + 'recurring_limit_count' => $recurring_invoice->recurring->limit_count, + 'recurring_limit_date' => $recurring_invoice->recurring->limit_date, + 'created_from' => $recurring_invoice->created_from, + 'created_by' => $recurring_invoice->created_by, + 'recurring_status' => Recurring::END_STATUS, + ])); + + if ($response['success']) { + $message = trans('messages.success.ended', ['type' => trans_choice('general.recurring_invoices', 1)]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return redirect()->route('recurring-invoices.index'); + } +} diff --git a/app/Http/Controllers/Settings/Categories.php b/app/Http/Controllers/Settings/Categories.php new file mode 100644 index 00000000..7cb6d051 --- /dev/null +++ b/app/Http/Controllers/Settings/Categories.php @@ -0,0 +1,311 @@ +withSubCategory(); + } + + $types = $this->getCategoryTypes(); + + if (request()->get('list_records') == 'all') { + $query->type(array_keys($types)); + } + + $categories = $query->collect(); + + $hide_code_column = true; + + $search_string_type = search_string_value('type'); + $selected_types = ! empty($search_string_type) ? explode(',', $search_string_type) : array_keys($types); + + foreach (config('type.category', []) as $type => $config) { + if (! is_array($config)) { + continue; + } + + if (! in_array($type, $selected_types)) { + continue; + } + + if (empty($config['hide']) || ! in_array('code', $config['hide'])) { + $hide_code_column = false; + break; + } + } + + return $this->response('settings.categories.index', compact('categories', 'types', 'hide_code_column')); + } + + /** + * Show the form for viewing the specified resource. + * + * @return Response + */ + public function show() + { + return redirect()->route('categories.index'); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + $categories = []; + + foreach (config('type.category') as $type => $config) { + $categories[$type] = []; + } + + $type_group = $this->isGroupCategoryType(); + $hide_code_types = $this->hideCodeCategoryTypes(array_keys($categories)); + + $types = $this->getCategoryTypes(group: $type_group); + + Category::enabled()->orderBy('name')->get()->each(function ($category) use (&$categories) { + $categories[$category->type][] = [ + 'id' => $category->id, + 'title' => $category->name, + 'level' => $category->level, + ]; + }); + + return view('settings.categories.create', compact('types', 'categories', 'type_group', 'hide_code_types')); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $response = $this->ajaxDispatch(new CreateCategory($request)); + + if ($response['success']) { + $response['redirect'] = route('categories.index'); + + $message = trans('messages.success.created', ['type' => trans_choice('general.categories', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('categories.create'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Import the specified resource. + * + * @param ImportRequest $request + * + * @return Response + */ + public function import(ImportRequest $request) + { + $response = $this->importExcel(new Import, $request, trans_choice('general.categories', 2)); + + if ($response['success']) { + $response['redirect'] = route('categories.index'); + + flash($response['message'])->success(); + } else { + $response['redirect'] = route('import.create', ['settings', 'categories']); + + flash($response['message'])->error()->important(); + } + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param Category $category + * + * @return Response + */ + public function edit(Category $category) + { + $type_disabled = (Category::where('type', $category->type)->count() == 1) ?: false; + + $edited_category_id = $category->id; + + $categories = []; + + foreach (config('type.category') as $type => $config) { + $categories[$type] = []; + } + + $type_group = $this->isGroupCategoryType(); + $hide_code_types = $this->hideCodeCategoryTypes(array_keys($categories)); + + $types = $this->getCategoryTypes(group: $type_group); + + $skip_categories = []; + $skip_categories[] = $edited_category_id; + + foreach ($category->sub_categories as $sub_category) { + $skip_categories[] = $sub_category->id; + + if ($sub_category->sub_categories) { + $skips = $this->getChildrenCategoryIds($sub_category); + + foreach ($skips as $skip) { + $skip_categories[] = $skip; + } + } + } + + Category::enabled()->orderBy('name')->get()->each(function ($category) use (&$categories, &$skip_categories) { + if (in_array($category->id, $skip_categories)) { + return; + } + + $categories[$category->type][] = [ + 'id' => $category->id, + 'title' => $category->name, + 'level' => $category->level, + ]; + }); + + $parent_categories = $categories[$category->type] ?? []; + + return view('settings.categories.edit', compact('category', 'types', 'type_disabled', 'categories', 'parent_categories', 'type_group', 'hide_code_types')); + } + + /** + * Update the specified resource in storage. + * + * @param Category $category + * @param Request $request + * + * @return Response + */ + public function update(Category $category, Request $request) + { + $response = $this->ajaxDispatch(new UpdateCategory($category, $request)); + + if ($response['success']) { + $response['redirect'] = route('categories.index'); + + $message = trans('messages.success.updated', ['type' => $category->name]); + + flash($message)->success(); + } else { + $response['redirect'] = route('categories.edit', $category->id); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Enable the specified resource. + * + * @param Category $category + * + * @return Response + */ + public function enable(Category $category) + { + $response = $this->ajaxDispatch(new UpdateCategory($category, request()->merge(['enabled' => 1]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.enabled', ['type' => $category->name]); + } + + return response()->json($response); + } + + /** + * Disable the specified resource. + * + * @param Category $category + * + * @return Response + */ + public function disable(Category $category) + { + $response = $this->ajaxDispatch(new UpdateCategory($category, request()->merge(['enabled' => 0]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.disabled', ['type' => $category->name]); + } + + return response()->json($response); + } + + /** + * Remove the specified resource from storage. + * + * @param Category $category + * + * @return Response + */ + public function destroy(Category $category) + { + $response = $this->ajaxDispatch(new DeleteCategory($category)); + + $response['redirect'] = route('categories.index'); + + if ($response['success']) { + $message = trans('messages.success.deleted', ['type' => $category->name]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Export the specified resource. + * + * @return Response + */ + public function export() + { + return $this->exportExcel(new Export, trans_choice('general.categories', 2)); + } +} diff --git a/app/Http/Controllers/Settings/Company.php b/app/Http/Controllers/Settings/Company.php new file mode 100644 index 00000000..b31bdb2e --- /dev/null +++ b/app/Http/Controllers/Settings/Company.php @@ -0,0 +1,13 @@ +response('settings.currencies.index', compact('currencies')); + } + + /** + * Show the form for viewing the specified resource. + * + * @return Response + */ + public function show() + { + return redirect()->route('currencies.index'); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + // Get current currencies + $current = Currency::pluck('code')->toArray(); + + // Prepare codes + $codes = []; + $currencies = MoneyCurrency::getCurrencies(); + + foreach ($currencies as $key => $item) { + // Don't show if already available + if (in_array($key, $current)) { + continue; + } + + $codes[$key] = $key; + } + + $precisions = (object) [ + '0' => '0', + '1' => '1', + '2' => '2', + '3' => '3', + '4' => '4', + ]; + + return view('settings.currencies.create', compact('codes', 'precisions')); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $response = $this->ajaxDispatch(new CreateCurrency($request)); + + if ($response['success']) { + $response['redirect'] = route('currencies.index'); + + $message = trans('messages.success.created', ['type' => trans_choice('general.currencies', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('currencies.create'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param Currency $currency + * + * @return Response + */ + public function edit(Currency $currency) + { + // Get current currencies + $current = Currency::pluck('code')->toArray(); + + // Prepare codes + $codes = []; + $currencies = MoneyCurrency::getCurrencies(); + + foreach ($currencies as $key => $item) { + // Don't show if already available + if (($key != $currency->code) && in_array($key, $current)) { + continue; + } + + $codes[$key] = $key; + } + + // Set default currency + $currency->default_currency = ($currency->code == default_currency()) ? 1 : 0; + + $precisions = (object) [ + '0' => '0', + '1' => '1', + '2' => '2', + '3' => '3', + '4' => '4', + ]; + + return view('settings.currencies.edit', compact('currency', 'codes', 'precisions')); + } + + /** + * Update the specified resource in storage. + * + * @param Currency $currency + * @param Request $request + * + * @return Response + */ + public function update(Currency $currency, Request $request) + { + $response = $this->ajaxDispatch(new UpdateCurrency($currency, $request)); + + if ($response['success']) { + $response['redirect'] = route('currencies.index'); + + $message = trans('messages.success.updated', ['type' => $currency->name]); + + flash($message)->success(); + } else { + $response['redirect'] = route('currencies.edit', $currency->id); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Enable the specified resource. + * + * @param Currency $currency + * + * @return Response + */ + public function enable(Currency $currency) + { + $response = $this->ajaxDispatch(new UpdateCurrency($currency, request()->merge(['enabled' => 1]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.enabled', ['type' => $currency->name]); + } + + return response()->json($response); + } + + /** + * Disable the specified resource. + * + * @param Currency $currency + * + * @return Response + */ + public function disable(Currency $currency) + { + $response = $this->ajaxDispatch(new UpdateCurrency($currency, request()->merge(['enabled' => 0]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.disabled', ['type' => $currency->name]); + } + + return response()->json($response); + } + + /** + * Remove the specified resource from storage. + * + * @param Currency $currency + * + * @return Response + */ + public function destroy(Currency $currency) + { + $response = $this->ajaxDispatch(new DeleteCurrency($currency)); + + $response['redirect'] = route('currencies.index'); + + if ($response['success']) { + $message = trans('messages.success.deleted', ['type' => $currency->name]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + public function config() + { + $json = new \stdClass(); + + $code = request('code'); + + if ($code) { + $currencies = Currency::all()->pluck('rate', 'code'); + + $currency = (object) currency($code)->toArray()[$code]; + + $currency->rate = isset($currencies[$code]) ? $currencies[$code] : null; + $currency->symbol_first = ! empty($currency->symbol_first) ? 1 : 0; + + $json = $currency; + } + + return response()->json($json); + } +} diff --git a/app/Http/Controllers/Settings/Defaults.php b/app/Http/Controllers/Settings/Defaults.php new file mode 100644 index 00000000..0b116353 --- /dev/null +++ b/app/Http/Controllers/Settings/Defaults.php @@ -0,0 +1,57 @@ +orderBy('name')->get()->pluck('title', 'id'); + + $sales_categories = Category::income()->enabled()->orderBy('name')->take(setting('default.select_limit'))->get(); + + $sale_category_id = setting('default.income_category'); + + if ($sale_category_id && !$sales_categories->pluck('id')->flip()->has($sale_category_id)) { + $category = Category::find($sale_category_id); + + if ($category) { + $sales_categories->put($category->id, $category->name); + } + } + + $purchases_categories = Category::expense()->enabled()->orderBy('name')->take(setting('default.select_limit'))->get(); + + $expense_category_id = setting('default.expense_category'); + + if ($expense_category_id && !$purchases_categories->pluck('id')->flip()->has($expense_category_id)) { + $category = Category::find($expense_category_id); + + if ($category) { + $purchases_categories->put($category->id, $category->name); + } + } + + $taxes = Tax::enabled()->orderBy('name')->get()->pluck('title', 'id'); + + $income_category_types = $this->getIncomeCategoryTypes('string'); + $expense_category_types = $this->getExpenseCategoryTypes('string'); + + return view('settings.default.edit', compact( + 'accounts', + 'sales_categories', + 'purchases_categories', + 'taxes', + 'income_category_types', + 'expense_category_types', + )); + } +} diff --git a/app/Http/Controllers/Settings/Email.php b/app/Http/Controllers/Settings/Email.php new file mode 100644 index 00000000..1ea69b9b --- /dev/null +++ b/app/Http/Controllers/Settings/Email.php @@ -0,0 +1,23 @@ + trans('settings.email.php'), + 'smtp' => trans('settings.email.smtp.name'), + 'sendmail' => trans('settings.email.sendmail'), + 'log' => trans('settings.email.log'), + ]; + + return view('settings.email.edit', compact('email_protocols')); + } +} diff --git a/app/Http/Controllers/Settings/EmailTemplates.php b/app/Http/Controllers/Settings/EmailTemplates.php new file mode 100644 index 00000000..8a83a9db --- /dev/null +++ b/app/Http/Controllers/Settings/EmailTemplates.php @@ -0,0 +1,84 @@ +reject(function($template) { + if (Str::startsWith($template->class, 'App')) { + return false; + } + + $class = explode('\\', $template->class); + + return $this->moduleIsDisabled(Str::kebab($class[1])); + })->each(function ($template) use (&$templates) { + $templates[$template->group][$template->id] = $template; + }); + + ksort($templates); + + return view('settings.email-templates.edit', compact('templates')); + } + + /** + * Update the specified resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function update(Request $request) + { + $template = EmailTemplate::find($request->id); + + $response = $this->ajaxDispatch(new UpdateEmailTemplate($template, $request)); + + if ($response['success']) { + $response['redirect'] = url()->previous();; + + $message = trans('messages.success.updated', ['type' => trans($template->name)]); + + flash($message)->success(); + } else { + $response['redirect'] = url()->previous();; + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + public function get() + { + $template = EmailTemplate::find(request()->id); + + $template->tags = trans('settings.email.templates.tags', ['tag_list'=> implode(', ', app($template->class)->getTags())]); + + return response()->json([ + 'errors' => false, + 'success' => true, + 'data' => $template, + ]); + } +} diff --git a/app/Http/Controllers/Settings/Invoice.php b/app/Http/Controllers/Settings/Invoice.php new file mode 100644 index 00000000..effbbc85 --- /dev/null +++ b/app/Http/Controllers/Settings/Invoice.php @@ -0,0 +1,38 @@ + trans('settings.invoice.hide.item_name'), + 'settings.invoice.item' => trans('settings.invoice.item'), + 'settings.invoice.product' => trans('settings.invoice.product'), + 'settings.invoice.service' => trans('settings.invoice.service'), + 'custom' => trans('settings.invoice.custom'), + ]; + + $price_names = [ + 'hide' => trans('settings.invoice.hide.price'), + 'settings.invoice.price' => trans('settings.invoice.price'), + 'settings.invoice.rate' => trans('settings.invoice.rate'), + 'custom' => trans('settings.invoice.custom'), + ]; + + $quantity_names = [ + 'hide' => trans('settings.invoice.hide.quantity'), + 'settings.invoice.quantity' => trans('settings.invoice.quantity'), + 'custom' => trans('settings.invoice.custom'), + ]; + + return view('settings.invoice.edit', compact( + 'item_names', + 'price_names', + 'quantity_names' + )); + } +} diff --git a/app/Http/Controllers/Settings/Localisation.php b/app/Http/Controllers/Settings/Localisation.php new file mode 100644 index 00000000..8211dd6e --- /dev/null +++ b/app/Http/Controllers/Settings/Localisation.php @@ -0,0 +1,58 @@ +getTimezones(); + + $date_formats = [ + 'd M Y' => '31 Dec 2017', + 'd F Y' => '31 December 2017', + 'd m Y' => '31 12 2017', + 'm d Y' => '12 31 2017', + 'Y m d' => '2017 12 31', + ]; + + $date_separators = [ + 'dash' => trans('settings.localisation.date.dash'), + 'slash' => trans('settings.localisation.date.slash'), + 'dot' => trans('settings.localisation.date.dot'), + 'comma' => trans('settings.localisation.date.comma'), + 'space' => trans('settings.localisation.date.space'), + ]; + + $percent_positions = [ + 'before' => trans('settings.localisation.percent.before'), + 'after' => trans('settings.localisation.percent.after'), + ]; + + $discount_locations = [ + 'no' => trans('general.disabled'), + 'item' => trans('settings.localisation.discount_location.item'), + 'total' => trans('settings.localisation.discount_location.total'), + 'both' => trans('settings.localisation.discount_location.both'), + ]; + + $financial_denote_options = [ + 'begins' => trans('settings.localisation.financial_denote.begins'), + 'ends' => trans('settings.localisation.financial_denote.ends'), + ]; + + return view('settings.localisation.edit', compact( + 'timezones', + 'date_formats', + 'date_separators', + 'percent_positions', + 'discount_locations', + 'financial_denote_options' + )); + } +} diff --git a/app/Http/Controllers/Settings/Modules.php b/app/Http/Controllers/Settings/Modules.php new file mode 100644 index 00000000..94f75b99 --- /dev/null +++ b/app/Http/Controllers/Settings/Modules.php @@ -0,0 +1,82 @@ +segment(2); + + // Add CRUD permission check + $this->middleware('permission:create-' . $alias . '-settings')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-' . $alias . '-settings')->only('index', 'show', 'edit', 'export'); + $this->middleware('permission:update-' . $alias . '-settings')->only('update', 'enable', 'disable'); + $this->middleware('permission:delete-' . $alias . '-settings')->only('destroy'); + } + + /** + * Show the form for editing the specified resource. + * + * @return Response + */ + public function edit($alias) + { + $accounts = Account::enabled()->orderBy('name')->pluck('name', 'id'); + $categories = Category::income()->enabled()->orderBy('name')->pluck('name', 'id'); + + $setting = Setting::prefix($alias)->get()->transform(function ($s) use ($alias) { + $s->key = str_replace($alias . '.', '', $s->key); + return $s; + })->pluck('value', 'key'); + + $module = module($alias); + + return view('settings.modules.edit', compact('setting', 'module', 'accounts', 'categories')); + } + + /** + * Update the specified resource in storage. + * + * @param $alias + * + * @return Response + */ + public function update($alias, Request $request) + { + $fields = $request->except(['company_id', '_method', '_token', 'module_alias']); + + foreach ($fields as $key => $value) { + setting()->set($alias . '.' . $key, $value); + } + + setting()->save(); + + Utility::clearPaymentMethodsCache(); + + $message = trans('messages.success.updated', ['type' => trans_choice('general.settings', 2)]); + + $response = [ + 'status' => null, + 'success' => true, + 'error' => false, + 'message' => $message, + 'data' => null, + 'redirect' => route('settings.module.edit', $alias)//('settings/apps/' . $alias), + ]; + + flash($message)->success(); + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Settings/Navigation.php b/app/Http/Controllers/Settings/Navigation.php new file mode 100644 index 00000000..28c1f269 --- /dev/null +++ b/app/Http/Controllers/Settings/Navigation.php @@ -0,0 +1,77 @@ +middleware(function ($request, $next) { + abort_unless(user()?->hasRole('admin'), 403); + + return $next($request); + }); + } + + public function manage() + { + return view('settings.navigation.edit', [ + 'sections' => BusinessNavigation::all(), + ]); + } + + public function save(Request $request) + { + $request->validate([ + 'section_titles' => ['nullable', 'array'], + 'section_titles.*' => ['nullable', 'string', 'max:50'], + 'page_titles' => ['nullable', 'array'], + 'page_titles.*' => ['nullable', 'array'], + 'page_titles.*.*' => ['nullable', 'string', 'max:50'], + ]); + + $sectionTitles = (array) $request->input('section_titles', []); + $pageTitles = (array) $request->input('page_titles', []); + $titles = ['sections' => [], 'pages' => []]; + + // Only persist labels for items that exist in the configured navigation. + foreach (config('business_navigation', []) as $section) { + $sectionSlug = $section['slug']; + $sectionTitle = trim((string) ($sectionTitles[$sectionSlug] ?? '')); + + if ($sectionTitle !== '') { + $titles['sections'][$sectionSlug] = $sectionTitle; + } + + foreach ($section['pages'] ?? [] as $page) { + $pageTitle = trim((string) ($pageTitles[$sectionSlug][$page['slug']] ?? '')); + + if ($pageTitle !== '') { + $titles['pages'][$sectionSlug][$page['slug']] = $pageTitle; + } + } + } + + setting()->set('navigation.titles', $titles); + setting()->save(); + + $message = '保存成功,侧边栏功能名称已更新。'; + + flash($message)->success(); + + return response()->json([ + 'status' => null, + 'success' => true, + 'error' => false, + 'message' => $message, + 'data' => null, + 'redirect' => route('dashboard'), + ]); + } +} diff --git a/app/Http/Controllers/Settings/Schedule.php b/app/Http/Controllers/Settings/Schedule.php new file mode 100644 index 00000000..3ff79aca --- /dev/null +++ b/app/Http/Controllers/Settings/Schedule.php @@ -0,0 +1,13 @@ + trans('taxes.fixed'), + 'normal' => trans('taxes.normal'), + 'inclusive' => trans('taxes.inclusive'), + 'withholding' => trans('taxes.withholding'), + 'compound' => trans('taxes.compound'), + ]; + + return $this->response('settings.taxes.index', compact('taxes', 'types')); + } + + /** + * Show the form for viewing the specified resource. + * + * @return Response + */ + public function show() + { + return redirect()->route('taxes.index'); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + $types = [ + 'fixed' => trans('taxes.fixed'), + 'normal' => trans('taxes.normal'), + 'inclusive' => trans('taxes.inclusive'), + 'withholding' => trans('taxes.withholding'), + 'compound' => trans('taxes.compound'), + ]; + + $disable_options = []; + + if ($compound = Tax::compound()->first()) { + $disable_options = ['compound']; + } + + return view('settings.taxes.create', compact('types', 'disable_options')); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $response = $this->ajaxDispatch(new CreateTax($request)); + + if ($response['success']) { + $response['redirect'] = route('taxes.index'); + + $message = trans('messages.success.created', ['type' => trans_choice('general.taxes', 1)]); + + flash($message)->success(); + } else { + $response['redirect'] = route('taxes.create'); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Import the specified resource. + * + * @param ImportRequest $request + * + * @return Response + */ + public function import(ImportRequest $request) + { + $response = $this->importExcel(new Import, $request, trans_choice('general.taxes', 2)); + + if ($response['success']) { + $response['redirect'] = route('taxes.index'); + + flash($response['message'])->success(); + } else { + $response['redirect'] = route('import.create', ['settings', 'taxes']); + + flash($response['message'])->error()->important(); + } + + return response()->json($response); + } + + /** + * Show the form for editing the specified resource. + * + * @param Tax $tax + * + * @return Response + */ + public function edit(Tax $tax) + { + $types = [ + 'fixed' => trans('taxes.fixed'), + 'normal' => trans('taxes.normal'), + 'inclusive' => trans('taxes.inclusive'), + 'withholding' => trans('taxes.withholding'), + 'compound' => trans('taxes.compound'), + ]; + + $disable_options = []; + + if ($tax->type != 'compound' && $compound = Tax::compound()->first()) { + $disable_options = ['compound']; + } + + return view('settings.taxes.edit', compact('tax', 'types', 'disable_options')); + } + + /** + * Update the specified resource in storage. + * + * @param Tax $tax + * @param Request $request + * + * @return Response + */ + public function update(Tax $tax, Request $request) + { + $response = $this->ajaxDispatch(new UpdateTax($tax, $request)); + + if ($response['success']) { + $response['redirect'] = route('taxes.index'); + + $message = trans('messages.success.updated', ['type' => $tax->name]); + + flash($message)->success(); + } else { + $response['redirect'] = route('taxes.edit', $tax->id); + + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Enable the specified resource. + * + * @param Tax $tax + * + * @return Response + */ + public function enable(Tax $tax) + { + $response = $this->ajaxDispatch(new UpdateTax($tax, request()->merge(['enabled' => 1]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.enabled', ['type' => $tax->name]); + } + + return response()->json($response); + } + + /** + * Disable the specified resource. + * + * @param Tax $tax + * + * @return Response + */ + public function disable(Tax $tax) + { + $response = $this->ajaxDispatch(new UpdateTax($tax, request()->merge(['enabled' => 0]))); + + if ($response['success']) { + $response['message'] = trans('messages.success.disabled', ['type' => $tax->name]); + } + + return response()->json($response); + } + + /** + * Remove the specified resource from storage. + * + * @param Tax $tax + * + * @return Response + */ + public function destroy(Tax $tax) + { + $response = $this->ajaxDispatch(new DeleteTax($tax)); + + $response['redirect'] = route('taxes.index'); + + if ($response['success']) { + $message = trans('messages.success.deleted', ['type' => $tax->name]); + + flash($message)->success(); + } else { + $message = $response['message']; + + flash($message)->error()->important(); + } + + return response()->json($response); + } + + /** + * Export the specified resource. + * + * @return Response + */ + public function export() + { + return $this->exportExcel(new Export, trans_choice('general.taxes', 2)); + } +} diff --git a/app/Http/Controllers/SystemSettings/Databases.php b/app/Http/Controllers/SystemSettings/Databases.php new file mode 100644 index 00000000..27ebc337 --- /dev/null +++ b/app/Http/Controllers/SystemSettings/Databases.php @@ -0,0 +1,195 @@ +middleware(function ($request, $next) { + abort_unless(SystemAccess::canOpenSettings(), 403); + + return $next($request); + }); + } + + public function index() + { + return view('system_settings.databases.index', [ + 'connections' => DatabaseConnection::where('company_id', company_id())->orderBy('id')->get(), + ]); + } + + public function create() + { + return view('system_settings.databases.form', ['connection' => null]); + } + + public function store(Request $request) + { + $data = $this->validateConnection($request); + $data['company_id'] = company_id(); + $data['enabled'] = (bool) ($data['enabled'] ?? false); + + DatabaseConnection::create($data); + + flash('数据库配置已新增。')->success(); + + return redirect()->route('system-settings.databases.index'); + } + + public function edit(int $id) + { + return view('system_settings.databases.form', [ + 'connection' => $this->findConnection($id), + ]); + } + + public function update(int $id, Request $request) + { + $connection = $this->findConnection($id); + $data = $this->validateConnection($request, $connection); + $data['enabled'] = (bool) ($data['enabled'] ?? false); + + if (empty($data['password'])) { + unset($data['password']); + } + + $connection->update($data); + + flash('数据库配置已修改。')->success(); + + return redirect()->route('system-settings.databases.index'); + } + + public function destroy(int $id) + { + $this->findConnection($id)->delete(); + + flash('数据库配置已删除。')->success(); + + return redirect()->route('system-settings.databases.index'); + } + + public function export() + { + $connections = DatabaseConnection::where('company_id', company_id())->orderBy('id')->get(); + + return response()->streamDownload(function () use ($connections) { + $stream = fopen('php://output', 'w'); + fwrite($stream, "\xEF\xBB\xBF"); + fputcsv($stream, ['name', 'driver', 'host', 'port', 'database_name', 'username', 'password', 'charset', 'enabled']); + + foreach ($connections as $connection) { + fputcsv($stream, [ + $connection->name, + $connection->driver, + $connection->host, + $connection->port, + $connection->database_name, + $connection->username, + '', + $connection->charset, + $connection->enabled ? 1 : 0, + ]); + } + + fclose($stream); + }, 'database-connections.csv', ['Content-Type' => 'text/csv; charset=UTF-8']); + } + + public function import(Request $request) + { + $request->validate(['file' => ['required', 'file', 'mimes:csv,txt', 'max:5120']]); + + $handle = fopen($request->file('file')->getRealPath(), 'r'); + $header = fgetcsv($handle); + $header = array_map(fn ($value) => trim((string) $value, "\xEF\xBB\xBF \t\n\r\0\x0B"), $header ?: []); + $required = ['name', 'driver', 'database_name']; + + if (array_diff($required, $header)) { + fclose($handle); + + return back()->withErrors(['file' => '文件表头不正确,请先导出模板后填写。']); + } + + $count = 0; + + DB::transaction(function () use ($handle, $header, &$count) { + while (($row = fgetcsv($handle)) !== false) { + if (count(array_filter($row, fn ($value) => trim((string) $value) !== '')) === 0) { + continue; + } + + $row = array_pad($row, count($header), ''); + $data = array_combine($header, array_slice($row, 0, count($header))); + $driver = in_array($data['driver'] ?? '', ['mysql', 'pgsql', 'sqlite', 'sqlsrv'], true) + ? $data['driver'] + : 'mysql'; + + $attributes = [ + 'driver' => $driver, + 'host' => $data['host'] ?: null, + 'port' => ($data['port'] ?? '') !== '' ? (int) $data['port'] : null, + 'database_name' => trim((string) ($data['database_name'] ?? '')), + 'username' => $data['username'] ?: null, + 'charset' => $data['charset'] ?: 'utf8mb4', + 'enabled' => filter_var($data['enabled'] ?? true, FILTER_VALIDATE_BOOL), + ]; + + if (($data['password'] ?? '') !== '') { + $attributes['password'] = $data['password']; + } + + if (trim((string) ($data['name'] ?? '')) === '' || $attributes['database_name'] === '') { + continue; + } + + DatabaseConnection::updateOrCreate( + ['company_id' => company_id(), 'name' => trim($data['name'])], + $attributes + ); + $count++; + } + }); + + fclose($handle); + flash("已导入 {$count} 条数据库配置。")->success(); + + return redirect()->route('system-settings.databases.index'); + } + + private function findConnection(int $id): DatabaseConnection + { + return DatabaseConnection::where('company_id', company_id())->findOrFail($id); + } + + private function validateConnection(Request $request, ?DatabaseConnection $connection = null): array + { + return $request->validate([ + 'name' => [ + 'required', + 'string', + 'max:191', + Rule::unique('system_database_connections', 'name') + ->where(fn ($query) => $query->where('company_id', company_id())) + ->ignore($connection?->id), + ], + 'driver' => ['required', Rule::in(['mysql', 'pgsql', 'sqlite', 'sqlsrv'])], + 'host' => ['nullable', 'string', 'max:191'], + 'port' => ['nullable', 'integer', 'between:1,65535'], + 'database_name' => ['required', 'string', 'max:191'], + 'username' => ['nullable', 'string', 'max:191'], + 'password' => ['nullable', 'string', 'max:1000'], + 'charset' => ['required', 'string', 'max:30'], + 'enabled' => ['nullable', 'boolean'], + ]); + } +} diff --git a/app/Http/Controllers/SystemSettings/Sidebar.php b/app/Http/Controllers/SystemSettings/Sidebar.php new file mode 100644 index 00000000..897f351a --- /dev/null +++ b/app/Http/Controllers/SystemSettings/Sidebar.php @@ -0,0 +1,102 @@ +middleware(function ($request, $next) { + abort_unless(SystemAccess::canOpenSettings(), 403); + + return $next($request); + }); + } + + public function edit(Request $request) + { + $subjects = $this->subjects(); + $subject = $request->string('subject')->toString() ?: array_key_first($subjects); + + abort_unless(isset($subjects[$subject]), 404); + + $settings = SidebarPermissions::settings(); + $configured = in_array($subject, $settings['configured'] ?? [], true); + + return view('system_settings.sidebar.edit', [ + 'navigation' => BusinessNavigation::all(), + 'subjects' => $subjects, + 'subject' => $subject, + 'configured' => $configured, + 'selected' => $configured ? (array) ($settings['subjects'][$subject] ?? []) : SidebarPermissions::allKeys(), + ]); + } + + public function update(Request $request) + { + $subjects = $this->subjects(); + $subject = $request->input('subject'); + + $request->validate([ + 'subject' => ['required', Rule::in(array_keys($subjects))], + 'permissions' => ['nullable', 'array'], + 'permissions.*' => ['string', Rule::in(SidebarPermissions::allKeys())], + ]); + + $settings = SidebarPermissions::settings(); + $settings['configured'] = array_values(array_unique(array_merge( + (array) ($settings['configured'] ?? []), + [$subject] + ))); + $settings['subjects'][$subject] = array_values(array_unique((array) $request->input('permissions', []))); + + setting()->set('system.sidebar_permissions', json_encode($settings, JSON_UNESCAPED_UNICODE)); + setting()->save(); + + flash('保存成功,侧边栏权限已更新。')->success(); + + return redirect()->route('dashboard'); + } + + private function subjects(): array + { + $labels = SystemAccess::roleLabels(); + + if (SystemAccess::isSuperAdmin()) { + $subjects = [ + 'role_super_admin' => '用户类型:' . $labels[SystemAccess::SUPER_ADMIN], + 'role_admin' => '用户类型:' . $labels[SystemAccess::ADMIN], + 'role_user' => '用户类型:' . $labels[SystemAccess::USER], + ]; + + $users = User::query() + ->whereHas('companies', fn ($company) => $company->where('companies.id', company_id())) + ->whereHas('roles', fn ($roles) => $roles->whereIn('roles.name', [SystemAccess::SUPER_ADMIN, SystemAccess::ADMIN])) + ->with('roles') + ->orderBy('name') + ->get(); + } else { + $subjects = ['role_user' => '用户类型:' . $labels[SystemAccess::USER]]; + $users = User::query() + ->whereHas('companies', fn ($company) => $company->where('companies.id', company_id())) + ->whereHas('roles', fn ($roles) => $roles->where('roles.name', SystemAccess::USER)) + ->orderBy('name') + ->get(); + } + + foreach ($users as $managedUser) { + $role = SystemAccess::roleName($managedUser); + $subjects['user_' . $managedUser->id] = '指定用户:' . $managedUser->name . ' - ' . ($labels[$role] ?? $role); + } + + return $subjects; + } +} diff --git a/app/Http/Controllers/SystemSettings/Users.php b/app/Http/Controllers/SystemSettings/Users.php new file mode 100644 index 00000000..e6a78333 --- /dev/null +++ b/app/Http/Controllers/SystemSettings/Users.php @@ -0,0 +1,182 @@ +middleware(function ($request, $next) { + abort_unless(SystemAccess::canOpenSettings(), 403); + + return $next($request); + }); + } + + public function index() + { + $users = $this->queryForActor()->with('roles')->orderBy('id')->get(); + + return view('system_settings.users.index', [ + 'users' => $users, + 'roleLabels' => SystemAccess::roleLabels(), + ]); + } + + public function create() + { + return view('system_settings.users.form', [ + 'managedUser' => null, + 'roles' => SystemAccess::assignableRoles(), + ]); + } + + public function store(Request $request) + { + $data = $this->validateUser($request); + $role = Role::where('name', $data['role'])->firstOrFail(); + + DB::transaction(function () use ($data, $role) { + $created = User::create([ + 'name' => $data['name'], + 'email' => $data['email'], + 'password' => $data['password'], + 'locale' => 'zh-CN', + 'enabled' => (bool) ($data['enabled'] ?? false), + 'landing_page' => 'dashboard', + 'created_by' => user()->id, + 'created_from' => 'system_settings', + ]); + + $created->roles()->attach($role->id); + $created->companies()->attach(company_id()); + }); + + flash('用户已新增。')->success(); + + return redirect()->route('system-settings.users.index'); + } + + public function edit(int $id) + { + $managedUser = $this->findManagedUser($id); + + abort_unless(SystemAccess::canManageUser(user(), $managedUser), 403); + + return view('system_settings.users.form', [ + 'managedUser' => $managedUser, + 'roles' => SystemAccess::assignableRoles(), + ]); + } + + public function update(int $id, Request $request) + { + $managedUser = $this->findManagedUser($id); + + abort_unless(SystemAccess::canManageUser(user(), $managedUser), 403); + + $data = $this->validateUser($request, $managedUser); + $role = Role::where('name', $data['role'])->firstOrFail(); + + if ( + $managedUser->id === user()->id + && SystemAccess::isSuperAdmin($managedUser) + && $data['role'] !== SystemAccess::SUPER_ADMIN + && $this->superAdminCount() <= 1 + ) { + return back()->withErrors(['role' => '系统至少需要保留一名超级管理员。'])->withInput(); + } + + if ($managedUser->id === user()->id && ! ($data['enabled'] ?? false)) { + return back()->withErrors(['enabled' => '不能停用当前登录账号。'])->withInput(); + } + + DB::transaction(function () use ($managedUser, $data, $role) { + $attributes = [ + 'name' => $data['name'], + 'email' => $data['email'], + 'enabled' => (bool) ($data['enabled'] ?? false), + ]; + + if (! empty($data['password'])) { + $attributes['password'] = $data['password']; + } + + $managedUser->update($attributes); + $managedUser->roles()->sync([$role->id]); + }); + + flash('用户已修改。')->success(); + + return redirect()->route('system-settings.users.index'); + } + + public function destroy(int $id) + { + $managedUser = $this->findManagedUser($id); + + abort_if($managedUser->id === user()->id, 422, '不能删除当前登录账号。'); + abort_unless(SystemAccess::canManageUser(user(), $managedUser), 403); + + if (SystemAccess::isSuperAdmin($managedUser) && $this->superAdminCount() <= 1) { + return back()->withErrors(['user' => '系统至少需要保留一名超级管理员。']); + } + + $managedUser->delete(); + + flash('用户已删除。')->success(); + + return redirect()->route('system-settings.users.index'); + } + + private function queryForActor() + { + $query = User::query()->whereHas('companies', function ($company) { + $company->where('companies.id', company_id()); + }); + + if (! SystemAccess::isSuperAdmin()) { + $query->whereHas('roles', fn ($roles) => $roles->where('roles.name', SystemAccess::USER)); + } + + return $query; + } + + private function findManagedUser(int $id): User + { + return $this->queryForActor()->with('roles')->findOrFail($id); + } + + private function validateUser(Request $request, ?User $managedUser = null): array + { + $assignable = array_keys(SystemAccess::assignableRoles()); + + return $request->validate([ + 'name' => ['required', 'string', 'max:191'], + 'email' => [ + 'required', + 'email', + 'max:191', + Rule::unique('users', 'email')->ignore($managedUser?->id)->whereNull('deleted_at'), + ], + 'role' => ['required', Rule::in($assignable)], + 'password' => [$managedUser ? 'nullable' : 'required', 'string', 'min:8', 'confirmed'], + 'enabled' => ['nullable', 'boolean'], + ]); + } + + private function superAdminCount(): int + { + return User::query() + ->whereHas('roles', fn ($roles) => $roles->where('roles.name', SystemAccess::SUPER_ADMIN)) + ->count(); + } +} diff --git a/app/Http/Controllers/Wizard/Companies.php b/app/Http/Controllers/Wizard/Companies.php new file mode 100644 index 00000000..acff6470 --- /dev/null +++ b/app/Http/Controllers/Wizard/Companies.php @@ -0,0 +1,130 @@ +middleware('permission:create-common-companies')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-common-companies')->only('index', 'show', 'edit', 'export'); + $this->middleware('permission:update-common-companies')->only('update', 'enable', 'disable'); + $this->middleware('permission:delete-common-companies')->only('destroy'); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function edit() + { + $company = Company::find(company_id()); + + return $this->response('wizard.companies.edit', compact('company')); + } + + /** + * Update the specified resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function update(Request $request) + { + // Company + $company = Company::find(company_id()); + + $fields = $request->all(); + + $skip_keys = ['company_id', '_method', '_token', '_prefix']; + $file_keys = ['company.logo']; + $uploaded_file_keys = ['company.uploaded_logo']; + + // Clear setting media + foreach ($file_keys as $file_key) { + $keys = explode('.', $file_key); + + if (! setting($file_key, false)) { + continue; + } + + $file_old_key = 'uploaded_' . $keys[1]; + if (array_key_exists($file_old_key, $fields)) { + continue; + } + + setting()->forget($file_key); + } + + foreach ($fields as $key => $value) { + // Don't process unwanted keys + if (in_array($key, $skip_keys)) { + continue; + } + + $real_key = match($key) { + 'api_key' => 'apps.api_key', + 'financial_start' => 'localisation.financial_start', + 'locale' => 'default.locale', + default => 'company.' . $key, + }; + + // change dropzone middleware already uploaded file + if (in_array($real_key, $uploaded_file_keys)) { + continue; + } + + // Process file uploads + if (in_array($real_key, $file_keys)) { + // Upload attachment + if ($request->file($key)) { + $media = $this->getMedia($request->file($key), 'settings'); + + $company->attachMedia($media, Str::snake($real_key)); + + $value = $media->id; + } + + // Prevent reset + if (empty($value)) { + continue; + } + } + + if ($real_key == 'default.locale') { + if (! in_array($value, config('language.allowed'))) { + continue; + } + + user()->setAttribute('locale', $value)->save(); + } + + setting()->set($real_key, $value); + } + + // Save all settings + setting()->save(); + + return response()->json([ + 'status' => null, + 'success' => true, + 'error' => false, + 'message' => trans('messages.success.updated', ['type' => trans_choice('general.companies', 2)]), + 'data' => null, + ]); + } +} diff --git a/app/Http/Controllers/Wizard/Currencies.php b/app/Http/Controllers/Wizard/Currencies.php new file mode 100644 index 00000000..4c39a5e9 --- /dev/null +++ b/app/Http/Controllers/Wizard/Currencies.php @@ -0,0 +1,131 @@ +middleware('permission:create-settings-currencies')->only('create', 'store', 'duplicate', 'import'); + $this->middleware('permission:read-settings-currencies')->only('index', 'show', 'edit', 'export'); + $this->middleware('permission:update-settings-currencies')->only('update', 'enable', 'disable'); + $this->middleware('permission:delete-settings-currencies')->only('destroy'); + } + + /** + * Show the form for editing the specified resource. + * + * @return Response + */ + public function index() + { + $currencies = Currency::collect(); + + // Get current currencies + $current = Currency::orderBy('code')->pluck('code')->toArray(); + + // Prepare codes + $codes = []; + $money_currencies = MoneyCurrency::getCurrencies(); + + foreach ($money_currencies as $key => $item) { + $codes[$key] = $key; + } + + return $this->response('wizard.currencies.index', compact('currencies', 'codes')); + } + + /** + * Show the form for viewing the specified resource. + * + * @return Response + */ + public function show() + { + return redirect()->route('wizard.currencies.index'); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * + * @return Response + */ + public function store(Request $request) + { + $response = $this->ajaxDispatch(new CreateCurrency($request)); + + if ($response['success']) { + $message = trans('messages.success.created', ['type' => trans_choice('general.currencies', 1)]); + } else { + $message = $response['message']; + } + + $response['message'] = $message; + + return response()->json($response); + } + + /** + * Update the specified resource in storage. + * + * @param Currency $currency + * @param Request $request + * + * @return Response + */ + public function update(Currency $currency, Request $request) + { + $response = $this->ajaxDispatch(new UpdateCurrency($currency, $request)); + + $currency->default = default_currency() == $currency->code; + + if ($response['success']) { + $message = trans('messages.success.updated', ['type' => $currency->name]); + } else { + $message = $response['message']; + } + + $response['message'] = $message; + + return response()->json($response); + } + + /** + * Remove the specified resource from storage. + * + * @param Currency $currency + * + * @return Response + */ + public function destroy(Currency $currency) + { + $currency_id = $currency->id; + + $response = $this->ajaxDispatch(new DeleteCurrency($currency)); + + if ($response['success']) { + $message = trans('messages.success.deleted', ['type' => $currency->name]); + } else { + $message = $response['message']; + } + + $response['currency_id'] = $currency_id; + $response['message'] = $message; + + return response()->json($response); + } +} diff --git a/app/Http/Controllers/Wizard/Finish.php b/app/Http/Controllers/Wizard/Finish.php new file mode 100644 index 00000000..7ae264ed --- /dev/null +++ b/app/Http/Controllers/Wizard/Finish.php @@ -0,0 +1,58 @@ +middleware('permission:read-admin-panel')->only('index', 'show', 'edit', 'export'); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function index() + { + setting()->set('wizard.completed', 1); + + // Save all settings + setting()->save(); + + $data = [ + 'query' => [ + 'limit' => 6 + ] + ]; + + $modules = $this->getFeaturedModules($data); + + return $this->response('wizard.finish.index', compact('modules')); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function update() + { + setting()->set('wizard.completed', 1); + + // Save all settings + setting()->save(); + + return response()->json([]); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php new file mode 100644 index 00000000..be763dbc --- /dev/null +++ b/app/Http/Kernel.php @@ -0,0 +1,217 @@ + [ + 'cookies.encrypt', + 'cookies.response', + 'session.start', + // 'session.auth', + 'session.errors', + 'csrf', + 'install.redirect', + 'header.x', + 'language', + 'firewall.all', + ], + + 'install' => [ + 'web', + 'install.can', + ], + + 'api' => [ + 'header.www.authenticate', // Centralize WWW-Authenticate handling for API 401 responses + 'auth.dynamic.once', // Unified: Bearer (OAuth) OR Basic Auth — auto-detected per request + 'auth.disabled', + 'throttle:api', + 'permission:read-api', + 'company.identify', + 'bindings', + 'read.only', + 'language', + 'firewall.all', + ], + + 'common' => [ + 'web', + 'company.identify', + 'bindings', + 'read.only', + 'wizard.redirect', + ], + + 'guest' => [ + 'web', + 'auth.redirect', + 'read.only', + ], + + 'admin' => [ + 'web', + 'auth', + 'auth.disabled', + 'company.identify', + 'bindings', + 'read.only', + 'wizard.redirect', + 'menu.admin', + 'permission:read-admin-panel', + 'plan.limits', + 'module.subscription', + ], + + 'wizard' => [ + 'web', + 'auth', + 'auth.disabled', + 'company.identify', + 'bindings', + 'read.only', + 'permission:read-admin-panel', + ], + + 'portal' => [ + 'web', + 'auth', + 'auth.disabled', + 'company.identify', + 'bindings', + 'read.only', + 'menu.portal', + 'permission:read-client-portal', + ], + + 'preview' => [ + 'web', + 'auth', + 'auth.disabled', + 'company.identify', + 'bindings', + 'read.only', + 'permission:read-admin-panel', + ], + + 'signed' => [ + 'cookies.encrypt', + 'cookies.response', + 'session.start', + 'session.errors', + 'csrf', + 'signature', + 'company.identify', + 'bindings', + 'read.only', + 'header.x', + 'language', + 'firewall.all', + ], + + 'import' => [ + 'throttle:import', + ], + + 'email' => [ + 'throttle:email', + ], + ]; + + /** + * The application's middleware aliases. + * + * Aliases may be used to conveniently assign middleware to routes and groups. + * + * @var array + */ + protected $middlewareAliases = [ + // Laravel + 'auth' => \App\Http\Middleware\Authenticate::class, + 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, + 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'cookies.encrypt' => \App\Http\Middleware\EncryptCookies::class, + 'cookies.response' => \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + 'csrf' => \App\Http\Middleware\VerifyCsrfToken::class, + 'session.auth' => \Illuminate\Session\Middleware\AuthenticateSession::class, + 'session.errors' => \Illuminate\View\Middleware\ShareErrorsFromSession::class, + 'session.start' => \Illuminate\Session\Middleware\StartSession::class, + //'signature' => \Illuminate\Routing\Middleware\ValidateSignature::class, + 'signature' => \App\Http\Middleware\ValidateSignature::class, + 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + + // Akaunting + 'api.key' => \App\Http\Middleware\RedirectIfNoApiKey::class, + 'auth.basic.once' => \App\Http\Middleware\AuthenticateOnceWithBasicAuth::class, + 'auth.oauth.once' => \App\Http\Middleware\AuthenticateOnceWithOAuth::class, + 'auth.dynamic.once' => \App\Http\Middleware\AuthenticateOnceWithDynamicApi::class, // Unified Bearer+Basic, per-request detection (active: api group) + 'auth.disabled' => \App\Http\Middleware\LogoutIfUserDisabled::class, + 'auth.redirect' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'company.identify' => \App\Http\Middleware\IdentifyCompany::class, + 'dropzone' => \App\Http\Middleware\Dropzone::class, + 'header.x' => \App\Http\Middleware\AddXHeader::class, + 'header.www.authenticate' => \App\Http\Middleware\AddWWWAuthenticateHeader::class, + 'plan.limits' => \App\Http\Middleware\RedirectIfHitPlanLimits::class, + 'module.subscription' => \App\Http\Middleware\RedirectIfHitModuleSubscription::class, + 'menu.admin' => \App\Http\Middleware\AdminMenu::class, + 'menu.portal' => \App\Http\Middleware\PortalMenu::class, + 'date.format' => \App\Http\Middleware\DateFormat::class, + 'install.can' => \App\Http\Middleware\CanInstall::class, + 'install.redirect' => \App\Http\Middleware\RedirectIfNotInstalled::class, + 'money' => \App\Http\Middleware\Money::class, + 'read.only' => \App\Http\Middleware\CheckForReadOnlyMode::class, + 'wizard.redirect' => \App\Http\Middleware\RedirectIfWizardNotCompleted::class, + + // Vendor + 'ability' => \Laratrust\Middleware\LaratrustAbility::class, + 'role' => \Laratrust\Middleware\LaratrustRole::class, + 'permission' => \Laratrust\Middleware\LaratrustPermission::class, + ]; + + /** + * The priority-sorted list of middleware. + * + * This forces non-global middleware to always be in the given order. + * + * @var array + */ + protected $middlewarePriority = [ + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\Authenticate::class, + \Illuminate\Routing\Middleware\ThrottleRequests::class, + \Illuminate\Session\Middleware\AuthenticateSession::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + \Illuminate\Auth\Middleware\Authorize::class, + ]; +} diff --git a/app/Http/Livewire/Common/Search.php b/app/Http/Livewire/Common/Search.php new file mode 100644 index 00000000..49947993 --- /dev/null +++ b/app/Http/Livewire/Common/Search.php @@ -0,0 +1,273 @@ +user = user(); + + $this->search(); + + return view('livewire.common.search'); + } + + /** + * Searching operation. + * If the keyword is empty searching will be cancelled. + * + * @return void + */ + public function search() + { + $this->results = []; + + if (empty($this->keyword)) { + return; + } + + $this->searchOnAccounts(); + $this->searchOnItems(); + $this->searchOnInvoices(); + $this->searchOnCustomers(); + $this->searchOnBills(); + $this->searchOnVendors(); + + $this->dispatchGlobalSearched(); + } + + /** + * Resets keyword. + * + * @return void + */ + public function resetKeyword() + { + $this->keyword = ''; + } + + /** + * Fires GlobalSearched event with the keyword and results. + * + * @return void + */ + public function dispatchGlobalSearched() + { + $search = new \stdClass(); + $search->results = $this->results; + $search->keyword = $this->keyword; + + event(new GlobalSearched($search)); + + $this->results = $search->results; + } + + /** + * Searching on Banking Accounts with given keyword. + * + * @return void + */ + public function searchOnAccounts() + { + if (!$this->user->can('read-banking-accounts')) { + return; + } + + $accounts = Account::enabled() + ->usingSearchString($this->keyword) + ->take(setting('default.select_limit')) + ->get(); + + if ($accounts->isEmpty()) { + return; + } + + foreach ($accounts as $account) { + $this->results[] = (object) [ + 'id' => $account->id, + 'name' => $account->name, + 'type' => trans_choice('general.accounts', 1), + 'color' => '#55588b', + 'href' => route('accounts.show', $account->id), + ]; + } + } + + /** + * Searching on Items with given keyword. + * + * @return void + */ + public function searchOnItems() + { + if (!$this->user->can('read-common-items')) { + return; + } + + $items = Item::enabled() + ->usingSearchString($this->keyword) + ->take(setting('default.select_limit')) + ->get(); + + if ($items->isEmpty()) { + return; + } + + foreach ($items as $item) { + $this->results[] = (object) [ + 'id' => $item->id, + 'name' => $item->name, + 'type' => trans_choice('general.items', 1), + 'color' => '#efad32', + 'href' => route('items.edit', $item->id), + ]; + } + } + + /** + * Searching on Invoices with given keyword. + * + * @return void + */ + public function searchOnInvoices() + { + if (!$this->user->can('read-sales-invoices')) { + return; + } + + $invoices = Document::invoice() + ->usingSearchString($this->keyword) + ->take(setting('default.select_limit')) + ->get(); + + if ($invoices->isEmpty()) { + return; + } + + foreach ($invoices as $invoice) { + $this->results[] = (object) [ + 'id' => $invoice->id, + 'name' => $invoice->document_number . ' - ' . $invoice->contact_name, + 'type' => trans_choice('general.invoices', 1), + 'color' => '#6da252', + 'href' => route('invoices.show', $invoice->id), + ]; + } + } + + /** + * Searching on Customers with given keyword. + * + * @return void + */ + public function searchOnCustomers() + { + if (!$this->user->can('read-sales-customers')) { + return; + } + + $customers = Contact::customer() + ->enabled() + ->usingSearchString($this->keyword) + ->take(setting('default.select_limit')) + ->get(); + + if ($customers->isEmpty()) { + return; + } + + foreach ($customers as $customer) { + $this->results[] = (object) [ + 'id' => $customer->id, + 'name' => $customer->name, + 'type' => trans_choice('general.customers', 1), + 'color' => '#328aef', + 'href' => route('customers.show', $customer->id), + ]; + } + } + + /** + * Searching on Bills with given keyword. + * + * @return void + */ + public function searchOnBills() + { + if (!$this->user->can('read-purchases-bills')) { + return; + } + + $bills = Document::bill() + ->usingSearchString($this->keyword) + ->take(setting('default.select_limit')) + ->get(); + + if ($bills->isEmpty()) { + return; + } + + foreach ($bills as $bill) { + $this->results[] = (object) [ + 'id' => $bill->id, + 'name' => $bill->document_number . ' - ' . $bill->contact_name, + 'type' => trans_choice('general.bills', 1), + 'color' => '#ef3232', + 'href' => route('bills.show', $bill->id), + ]; + } + } + + /** + * Searching on Vendors with given keyword. + * + * @return void + */ + public function searchOnVendors() + { + if (!$this->user->can('read-purchases-vendors')) { + return; + } + + $vendors = Contact::vendor() + ->enabled() + ->usingSearchString($this->keyword) + ->take(setting('default.select_limit')) + ->get(); + + if ($vendors->isEmpty()) { + return; + } + + foreach ($vendors as $vendor) { + $this->results[] = (object) [ + 'id' => $vendor->id, + 'name' => $vendor->name, + 'type' => trans_choice('general.vendors', 1), + 'color' => '#efef32', + 'href' => route('vendors.show', $vendor->id), + ]; + } + } +} diff --git a/app/Http/Livewire/Menu/Favorite.php b/app/Http/Livewire/Menu/Favorite.php new file mode 100644 index 00000000..e8f760fe --- /dev/null +++ b/app/Http/Livewire/Menu/Favorite.php @@ -0,0 +1,99 @@ +id, []); + + if (!empty($favorites)) { + $favorites = json_decode($favorites, true); + + foreach ($favorites as $favorite) { + if ($this->title == $favorite['title']) { + $this->favorited = true; + + break; + } + } + } + + return view('livewire.menu.favorite'); + } + + public function changeStatus() + { + if ($this->favorited) { + $this->removeFavorite(); + } else { + $this->addFavorite(); + } + } + + public function addFavorite() + { + $favorites = setting('favorites.menu.' . user()->id, []); + + if (!empty($favorites)) { + $favorites = json_decode($favorites, true); + } + + /* + if (in_array($this->title, $favorites)) { + return; + } + */ + + $favorites[] = [ + 'title' => $this->title, + 'icon' => $this->icon, + 'route' => $this->route, + 'url' => $this->url, + ]; + + $this->favorited = true; + + setting(['favorites.menu.' . user()->id => json_encode($favorites)])->save(); + + $this->dispatch('addedFavorite'); + } + + public function removeFavorite() + { + $favorites = setting('favorites.menu.' . user()->id, []); + + if (!empty($favorites)) { + $favorites = json_decode($favorites, true); + } + + foreach ($favorites as $key => $favorited) { + if ($favorited['title'] != $this->title) { + continue; + } + + unset($favorites[$key]); + $this->favorited = false; + + break; + } + + setting(['favorites.menu.' . user()->id => json_encode($favorites)])->save(); + + $this->dispatch('removedFavorite'); + } +} diff --git a/app/Http/Livewire/Menu/Favorites.php b/app/Http/Livewire/Menu/Favorites.php new file mode 100644 index 00000000..c609880f --- /dev/null +++ b/app/Http/Livewire/Menu/Favorites.php @@ -0,0 +1,101 @@ + 'render', + 'removedFavorite' => 'render', + ]; + + public function render(): View + { + $this->favorites = collect(); + + $favorites = setting('favorites.menu.' . user()->id, []); + + if (!empty($favorites)) { + $favorites = json_decode($favorites, true); + + foreach ($favorites as $favorite) { + $favorite['active'] = false; + + try { + $favorite['url'] = $this->getUrl($favorite); + } catch (\Exception $e) { + continue; + } + + $favorite['id'] = $this->getFavoriteId($favorite); + + if ($this->isActive($favorite['url'])) { + $favorite['active'] = true; + } + + $this->favorites->push($favorite); + } + } + + return view('livewire.menu.favorites'); + } + + /** + * Get url. + * + * @return string + */ + public function getUrl($favorite) + { + if (! empty($favorite['route'])) { + $route = $favorite['route']; + + if (is_array($route)) { + $url = route($route[0], $route[1]); + } else { + $url = route($route); + } + + return str_replace(url('/') . '/', '', $url); + } + + if (empty($favorite['url'])) { + return '/#'; + } + + return str_replace(url('/') . '/', '', url($favorite['url'])); + } + + /** + * Get active state for current item. + * + * @return mixed + */ + public function isActive($url) + { + if (empty($url) || in_array($url, ['/'])) { + return Request::is($url); + } else { + return Request::is($url, $url . '/*'); + } + } + + public function getFavoriteId($favorite) + { + $id = Str::of($favorite['url']) + ->replace(url('/'), '-') + ->replace(company_id(), '') + ->replace(['/', '?', '='], '-') + ->trim('-') + ->squish(); + + return 'menu-favorites-' . $id; + } +} diff --git a/app/Http/Livewire/Menu/Neww.php b/app/Http/Livewire/Menu/Neww.php new file mode 100644 index 00000000..3f4a9984 --- /dev/null +++ b/app/Http/Livewire/Menu/Neww.php @@ -0,0 +1,83 @@ +user = user(); + + menu()->create('neww', function ($menu) { + $menu->style('tailwind'); + + event(new NewwCreating($menu)); + + event(new NewwCreated($menu)); + + foreach($menu->getItems() as $item) { + if ($this->availableInSearch($item)) { + continue; + } + + $menu->removeByTitle($item->title); + } + }); + + return view('livewire.menu.neww'); + } + + public function availableInSearch($item): bool + { + if (empty($this->keyword)) { + return true; + } + + return $this->search($item); + } + + public function search($item): bool + { + $status = false; + + $keywords = explode(' ', $this->keyword); + + foreach ($keywords as $keyword) { + if (Str::contains(Str::lower($item->title), Str::lower($keyword))) { + $status = true; + + break; + } + + if ( + !empty($item->attributes['search_keywords']) + && Str::contains(Str::lower($item->attributes['search_keywords']), Str::lower($keyword)) + ) { + $status = true; + + break; + } + } + + return $status; + } + + public function resetKeyword(): void + { + $this->keyword = ''; + } +} diff --git a/app/Http/Livewire/Menu/Notifications.php b/app/Http/Livewire/Menu/Notifications.php new file mode 100644 index 00000000..810364c8 --- /dev/null +++ b/app/Http/Livewire/Menu/Notifications.php @@ -0,0 +1,163 @@ +user = user(); + + $this->notifications = $this->getNotifications(); + + return view('livewire.menu.notifications'); + } + + public function markRead($type, $notification_id, $message = true) + { + switch ($type) { + case 'updates': + $this->markUpdateRead($notification_id); + break; + case 'new-apps': + $this->markNewAppRead($notification_id); + break; + default: + $notification = DatabaseNotification::find($notification_id); + $data = $notification->getAttribute('data'); + + $notification->markAsRead(); + } + + if (! $message) { + return; + } + + $type = isset($data['file_name']) ?: trans('general.export'); + + $this->dispatch('mark-read', type: 'notification', message: trans('notifications.messages.mark_read', ['type' => $type])); + } + + public function markReadAll() + { + // Mark all DB notifications as read using the loaded collection. + // Matches Akaunting's own pattern (Auth\Users, Portal\Profile) — + // iterates unreadNotifications so each markAsRead() fires properly. + foreach (user()->unreadNotifications as $notification) { + $notification->markAsRead(); + } + + // Handle non-DB notification types (new-apps, updates). + foreach ($this->getNotifications() as $notification) { + if ($notification->type !== 'new-apps' || empty($notification->data['alias'])) { + continue; + } + + setting()->set( + 'notifications.' . $notification->notifiable_id . '.' . $notification->data['alias'], + '1' + ); + } + + setting()->save(); + + $this->dispatch('mark-read-all', type: 'notification', message: trans('notifications.messages.mark_read_all', ['type' => trans('general.export')])); + } + + public function markUpdateRead($notification_id) + { + // + } + + public function markNewAppRead($notification_id) + { + $notifications = $this->getNotifications(); + + foreach ($notifications as $notification) { + if ($notification->id == $notification_id) { + setting()->set('notifications.' . $notification->notifiable_id . '.' . $notification->data['alias'], '1'); + + setting()->save(); + break; + } + } + } + + public function getNotifications(): array + { + $notifications = new \stdClass(); + $notifications->notifications = collect(); + $notifications->keyword = $this->keyword; + + event(new NotificationsCreating($notifications)); + + event(new NotificationsCreated($notifications)); + + $rows = []; + + foreach ($notifications->notifications as $notification) { + if (! $this->availableInSearch($notification)) { + continue; + } + + $rows[] = $notification; + } + + return $rows; + } + + public function availableInSearch($notification): bool + { + if (empty($this->keyword)) { + return true; + } + + return $this->search($notification); + } + + public function search($notification): bool + { + $status = false; + + $keywords = explode(' ', $this->keyword); + + foreach ($keywords as $keyword) { + if (Str::contains(Str::lower($notification->data['title']), Str::lower($keyword))) { + $status = true; + + break; + } + + if ( + !empty($notification->data['description']) + && Str::contains(Str::lower($notification->data['description']), Str::lower($keyword)) + ) { + $status = true; + + break; + } + } + + return $status; + } + + public function resetKeyword(): void + { + $this->keyword = ''; + } +} diff --git a/app/Http/Livewire/Menu/Profile.php b/app/Http/Livewire/Menu/Profile.php new file mode 100644 index 00000000..503de4d8 --- /dev/null +++ b/app/Http/Livewire/Menu/Profile.php @@ -0,0 +1,32 @@ +create('profile', function ($menu) { + $menu->style('tailwind'); + + event(new ProfileCreating($menu)); + + event(new ProfileCreated($menu)); + + foreach($menu->getItems() as $item) { + if ($item->isActive()) { + $this->active_menu = 1; + } + } + }); + + return view('livewire.menu.profile'); + } +} diff --git a/app/Http/Livewire/Menu/Settings.php b/app/Http/Livewire/Menu/Settings.php new file mode 100644 index 00000000..f3697ae5 --- /dev/null +++ b/app/Http/Livewire/Menu/Settings.php @@ -0,0 +1,55 @@ +create('settings', function ($menu) { + $menu->style('tailwind'); + + if (! SystemAccess::canOpenSettings()) { + return; + } + + $items = [ + ['system-settings.users.index', '用户管理', 'people', '用户 类型 权限'], + ['system-settings.sidebar.edit', '侧边栏管理', 'menu', '侧边栏 用户类型 指定管理员'], + ]; + + foreach ($items as $index => [$route, $title, $icon, $keywords]) { + if ($this->keyword && ! Str::contains(Str::lower($title . ' ' . $keywords), Str::lower($this->keyword))) { + continue; + } + + $item = $menu->route($route, $title, [], ($index + 1) * 10, [ + 'icon' => $icon, + 'search_keywords' => $keywords, + ]); + + if ($item->isActive()) { + $this->active_menu = 1; + } + } + }); + + return view('livewire.menu.settings'); + } + + public function resetKeyword(): void + { + $this->keyword = ''; + } +} diff --git a/app/Http/Livewire/Notification/Browser.php b/app/Http/Livewire/Notification/Browser.php new file mode 100644 index 00000000..5e276088 --- /dev/null +++ b/app/Http/Livewire/Notification/Browser.php @@ -0,0 +1,49 @@ +header('User-Agent'); + + $view = 'livewire.notification.browser.index'; + + if (Str::contains($user_agent, 'Firefox')) { + $view = 'livewire.notification.browser.firefox'; + + if ($this->status || ! empty($_COOKIE['firefox-icon-notification-confirm'])) { + $this->status = true; + } else { + $this->status = false; + } + } elseif (Str::contains($user_agent, 'Edg')) { + // $view = 'livewire.notification.browser.edge'; + } elseif (Str::contains($user_agent, 'Safari')) { + // $view = 'livewire.notification.browser.safari'; + } elseif (Str::contains($user_agent, 'Chrome')) { + // $view = 'livewire.notification.browser.chrome'; + } elseif (Str::contains($user_agent, 'Opera')) { + // $view = 'livewire.notification.browser.opera'; + } + + return view($view); + } + + public function firefoxConfirm() + { + $expire = time() + (86400 * 30 * 30 * 30); + + setcookie('firefox-icon-notification-confirm', true, $expire, '/'); + + $this->status = true; + } +} diff --git a/app/Http/Livewire/Report/Pin.php b/app/Http/Livewire/Report/Pin.php new file mode 100644 index 00000000..b193448b --- /dev/null +++ b/app/Http/Livewire/Report/Pin.php @@ -0,0 +1,99 @@ +report) + || ! class_exists($this->report->class) + || Utility::cannotShow($this->report->class) + ) { + $this->pinned = false; + + return view('livewire.report.pin'); + } + + $pins = setting('favorites.report.' . user()->id, []); + + if (!empty($pins)) { + $pins = json_decode($pins, true); + + $this->pinned = in_array($this->report->id, $pins); + } + + return view('livewire.report.pin'); + } + + public function changeStatus($report_id) + { + if ($this->pinned) { + $this->removePin($report_id); + } else { + $this->addPin($report_id); + } + } + + public function addPin($report_id) + { + $pins = setting('favorites.report.' . user()->id, []); + + if (!empty($pins)) { + $pins = json_decode($pins, true); + } + + if (in_array($report_id, $pins)) { + return; + } + + if (count($pins) >= 6) { + return; + } + + $pins[] = $report_id; + + $this->pinned = true; + + setting(['favorites.report.' . user()->id => json_encode($pins)])->save(); + + $this->dispatch('addedPin'); + } + + public function removePin($report_id) + { + $pins = setting('favorites.report.' . user()->id, []); + + if (!empty($pins)) { + $pins = json_decode($pins, true); + } + + foreach ($pins as $key => $pinned_id) { + if ($pinned_id != $report_id) { + continue; + } + + unset($pins[$key]); + $this->pinned = false; + + break; + } + + setting(['favorites.report.' . user()->id => json_encode($pins)])->save(); + + $this->dispatch('removedPin'); + } +} diff --git a/app/Http/Livewire/Report/Pins.php b/app/Http/Livewire/Report/Pins.php new file mode 100644 index 00000000..670a943d --- /dev/null +++ b/app/Http/Livewire/Report/Pins.php @@ -0,0 +1,69 @@ + 'render', + 'removedPin' => 'render', + ]; + + public function render(): View + { + $this->reports = collect(); + + $pins = setting('favorites.report.' . user()->id, []); + + if (!empty($pins)) { + $pins = json_decode($pins, true); + + foreach ($this->categories as $category) { + foreach($category['reports'] as $report) { + if (is_array($report)) { + $report = Report::find($report['id']); + } + + // Only pinned reports are displayed, so skip everything else before the + // expensive canShow()/getClassInstance() work — instantiating all 150+ + // reports on render was causing 256s timeouts. Pins are capped at 6. + if (empty($report) || ! in_array($report->id, $pins)) { + continue; + } + + if (! Utility::canShow($report->class)) { + continue; + } + + $class = Utility::getClassInstance($report, false); + + if (empty($class)) { + continue; + } + + $this->reports->push($report); + + $this->icons[$report->id] = $class->getIcon(); + } + } + } + + return view('livewire.report.pins'); + } +} diff --git a/app/Http/Livewire/Tab/Pin.php b/app/Http/Livewire/Tab/Pin.php new file mode 100644 index 00000000..f89228b6 --- /dev/null +++ b/app/Http/Livewire/Tab/Pin.php @@ -0,0 +1,96 @@ + 'render', + 'removedPin' => 'render', + ]; + + public function render(): View + { + $this->pinned = false; + + $pins = setting('favorites.tab.' . user()->id, []); + + if (! empty($pins) && ! $this->pinned) { + $pins = json_decode($pins, true); + $type_pinned = $pins[$this->type] ?? null; + + if (isset($pins[$this->type]) && $this->tab == $type_pinned) { + $this->pinned = true; + } + } + + return view('livewire.tab.pin'); + } + + public function changeStatus($tab) + { + if ($this->pinned) { + $this->removePin($tab); + } else { + $this->addPin($tab); + } + } + + public function addPin($tab) + { + $pins = setting('favorites.tab.' . user()->id, []); + + if (!empty($pins)) { + $pins = json_decode($pins, true); + } + + $type_pinned = $pins[$this->type] ?? null; + + if ($this->tab == $type_pinned) { + return; + } + + $pins[$this->type] = $tab; + + $this->pinned = true; + + setting(['favorites.tab.' . user()->id => json_encode($pins)])->save(); + + $this->dispatch('addedPin'); + } + + public function removePin($tab) + { + $pins = setting('favorites.tab.' . user()->id, []); + + if (!empty($pins)) { + $pins = json_decode($pins, true); + } + + foreach ($pins as $key => $pinned_id) { + if ($pinned_id != $tab) { + continue; + } + + unset($pins[$key]); + $this->pinned = false; + + break; + } + + setting(['favorites.tab.' . user()->id => json_encode($pins)])->save(); + + $this->dispatch('removedPin'); + } +} diff --git a/app/Http/Middleware/AddWWWAuthenticateHeader.php b/app/Http/Middleware/AddWWWAuthenticateHeader.php new file mode 100644 index 00000000..6c89437b --- /dev/null +++ b/app/Http/Middleware/AddWWWAuthenticateHeader.php @@ -0,0 +1,118 @@ +getStatusCode() === 401) { + $this->addWWWAuthenticateHeader($response, $request); + } + + return $response; + } + + /** + * Add WWW-Authenticate header to response. + * + * @param \Symfony\Component\HttpFoundation\Response $response + * @param \Illuminate\Http\Request $request + * @return void + */ + protected function addWWWAuthenticateHeader(Response $response, Request $request): void + { + // Use the named route if available (registered in MCP ai.php routes). + // The MCP package's AddWwwAuthenticateHeader middleware also checks for this + // same named route, so using it here keeps both middlewares consistent. + if (app('router')->has('mcp.oauth.protected-resource')) { + $resourceMetadataUrl = route('mcp.oauth.protected-resource', ['path' => $request->path()]); + } else { + $resourceMetadataUrl = route('oauth.well-known.protected-resource', ['path' => $request->path()]); + } + + $realm = config('app.name', 'Akaunting'); + + // If another middleware already set a challenge (e.g. Bearer+Basic), + // preserve it and only enrich with resource_metadata when Bearer exists. + $existingHeader = $response->headers->get('WWW-Authenticate'); + + if (! empty($existingHeader)) { + if (str_contains($existingHeader, 'resource_metadata="')) { + return; + } + + if (preg_match('/\bBearer\b/i', $existingHeader) === 1) { + $response->headers->set( + 'WWW-Authenticate', + $existingHeader . ', resource_metadata="' . $resourceMetadataUrl . '"' + ); + } + + return; + } + + // RFC 6750: 'error' should only be present when a token was provided + // but failed authentication. For unauthenticated probes (no token), + // only realm and resource_metadata are needed so the client can + // initiate the OAuth flow. + if (! empty($request->bearerToken())) { + // Token was present but invalid/expired – include error details + $error = 'invalid_token'; + $errorDescription = 'OAuth: The access token is invalid or has expired'; + + // Try to extract error from JSON response body + $contentType = $response->headers->get('Content-Type', ''); + + if (str_contains($contentType, 'application/json')) { + $content = json_decode($response->getContent(), true); + + if (isset($content['error'])) { + $error = $content['error']; + } + + if (isset($content['error_description'])) { + $errorDescription = $content['error_description']; + } + } + + $headerValue = 'Bearer' + . ' realm="' . $realm . '"' + . ', resource_metadata="' . $resourceMetadataUrl . '"' + . ', error="' . $error . '"' + . ', error_description="' . addslashes($errorDescription) . '"'; + } else { + // No token provided – minimal challenge so client can start OAuth + $headerValue = 'Bearer' + . ' realm="' . $realm . '"' + . ', resource_metadata="' . $resourceMetadataUrl . '"'; + } + + // Set the WWW-Authenticate header + $response->headers->set('WWW-Authenticate', $headerValue); + } +} diff --git a/app/Http/Middleware/AddXHeader.php b/app/Http/Middleware/AddXHeader.php new file mode 100644 index 00000000..b37f8ed7 --- /dev/null +++ b/app/Http/Middleware/AddXHeader.php @@ -0,0 +1,32 @@ +header('X-Akaunting', 'Online Accounting Software'); + $response->header('X-Content-Type-Options', 'nosniff'); + $response->header('X-Frame-Options', 'SAMEORIGIN'); + $response->header('Referrer-Policy', 'strict-origin-when-cross-origin'); + $response->header('X-XSS-Protection', '0'); + $response->header('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); + } + + return $response; + } +} diff --git a/app/Http/Middleware/AdminMenu.php b/app/Http/Middleware/AdminMenu.php new file mode 100644 index 00000000..d644f24a --- /dev/null +++ b/app/Http/Middleware/AdminMenu.php @@ -0,0 +1,35 @@ +check()) { + return $next($request); + } + + menu()->create('admin', function ($menu) { + $menu->style('tailwind'); + + event(new AdminCreating($menu)); + + event(new AdminCreated($menu)); + }); + + return $next($request); + } +} diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php new file mode 100644 index 00000000..881e8059 --- /dev/null +++ b/app/Http/Middleware/Authenticate.php @@ -0,0 +1,48 @@ +bearerToken()) { + return response()->json([ + 'error' => 'invalid_token', + 'error_description' => 'The access token provided is expired, revoked, malformed, or invalid.', + ], 401, [ + 'WWW-Authenticate' => 'Bearer error="invalid_token", error_description="Invalid or expired token"', + ]); + } + + throw $e; + } + } + + /** + * Get the path the user should be redirected to when they are not authenticated. + * + * @param \Illuminate\Http\Request $request + * @return string|null + */ + protected function redirectTo($request) + { + if (!$request->expectsJson()) { + return route('login'); + } + } +} diff --git a/app/Http/Middleware/AuthenticateOnceWithBasicAuth.php b/app/Http/Middleware/AuthenticateOnceWithBasicAuth.php new file mode 100644 index 00000000..70181bbf --- /dev/null +++ b/app/Http/Middleware/AuthenticateOnceWithBasicAuth.php @@ -0,0 +1,20 @@ + → AuthenticateOnceWithOAuth::handle() + * Authorization: Basic → AuthenticateOnceWithBasicAuth::handle() + * (no header) → 401 + * + * Auth logic lives in the dedicated classes — extend/fix them independently. + */ +class AuthenticateOnceWithDynamicApi +{ + public function __construct( + private readonly AuthenticateOnceWithOAuth $oauthMiddleware, + private readonly AuthenticateOnceWithBasicAuth $basicMiddleware, + ) {} + + public function handle(Request $request, Closure $next): mixed + { + $shouldLog = (bool) config('oauth.verbose_log', false) || ! app()->environment('production'); + + // Skip if OAuth module is disabled to avoid unnecessary processing; delegate to Basic Auth directly since it's simpler and more likely to be used without OAuth. + if (! config('oauth.enabled', false)) { + if ($shouldLog) { + Log::debug('OAuth Dispatcher: OAuth disabled, using basic auth', [ + 'path' => $request->path(), + 'method' => $request->method(), + ]); + } + + return $this->basicMiddleware->handle($request, function (Request $req) use ($next) { + $req->attributes->set('auth_method', 'basic'); + + return $next($req); + }); + } + + $authorization = (string) $request->header('Authorization', ''); + + if ($shouldLog) { + Log::debug('OAuth Dispatcher: Authorization header detected', [ + 'path' => $request->path(), + 'method' => $request->method(), + 'authorization_type' => preg_match('/^\s*(\w+)\s+/i', $authorization, $m) === 1 ? strtolower($m[1]) : 'none', + 'has_header' => $authorization !== '' ? 'yes' : 'no', + ]); + } + + // ── Bearer token → delegate to OAuth middleware ─────────────────────── + if (preg_match('/^\s*Bearer\s+/i', $authorization) === 1) { + return $this->oauthMiddleware->handle($request, function (Request $req) use ($next) { + $req->attributes->set('auth_method', 'oauth'); + + return $next($req); + }); + } + + // ── Basic credentials → delegate to Basic Auth middleware ───────────── + if (preg_match('/^\s*Basic\s+/i', $authorization) === 1) { + return $this->basicMiddleware->handle($request, function (Request $req) use ($next) { + $req->attributes->set('auth_method', 'basic'); + + return $next($req); + }); + } + + // ── No Authorization header → 401 ───────────────────────────────────── + $realm = config('app.name', 'Akaunting'); + + return response()->json([ + 'message' => 'Unauthenticated.', + 'error' => 'invalid_client', + 'error_description' => 'Authentication required. Use an OAuth Bearer token or HTTP Basic credentials.', + ], 401, [ + 'WWW-Authenticate' => sprintf('Bearer realm="%s", Basic realm="%s"', $realm, $realm), + ]); + } +} diff --git a/app/Http/Middleware/AuthenticateOnceWithOAuth.php b/app/Http/Middleware/AuthenticateOnceWithOAuth.php new file mode 100644 index 00000000..af06c2e4 --- /dev/null +++ b/app/Http/Middleware/AuthenticateOnceWithOAuth.php @@ -0,0 +1,107 @@ +restoreAuthorizationHeaderIfMissing($request); + + $guard = config('oauth.guards.api', 'passport'); + $shouldLog = (bool) config('oauth.verbose_log', false) || ! app()->environment('production'); + + if ($shouldLog) { + // Keep verbose diagnostics outside production only. + Log::debug('OAuth: Attempting authentication', [ + 'method' => $request->method(), + 'path' => $request->path(), + 'has_bearer' => $request->bearerToken() ? 'yes' : 'no', + 'has_authorization_header' => $request->headers->has('Authorization') ? 'yes' : 'no', + 'guard' => $guard, + ]); + } + + // Check if user is authenticated via Passport + if (! Auth::guard($guard)->check()) { + if ($shouldLog) { + Log::warning('OAuth: Authentication failed', [ + 'guard' => $guard, + 'ip' => $request->ip(), + 'user_agent' => $request->userAgent(), + ]); + } + + return response()->json([ + 'message' => 'Unauthenticated.', + 'error' => 'invalid_token', + 'error_description' => 'The access token provided is expired, revoked, malformed, or invalid.', + ], 401); + } + + // Fire authenticated event with passport protocol + if ($user = Auth::guard($guard)->user()) { + if ($shouldLog) { + Log::debug('OAuth: Authentication successful', [ + 'user_id' => $user->id, + ]); + } + + // Set the user on the default guard so that the user() helper + // and middleware like IdentifyCompany (which calls auth()->user()) + // can resolve the authenticated user without starting a session. + Auth::setUser($user); + } + + return $next($request); + } + + /** + * Restore Authorization header from common proxy server variables. + * + * Some reverse proxies do not pass the Authorization header directly, + * but expose it via alternate server variables. + * + * @param \Illuminate\Http\Request $request + * @return void + */ + protected function restoreAuthorizationHeaderIfMissing($request): void + { + if (! empty($request->header('Authorization'))) { + return; + } + + $candidates = [ + $request->server('HTTP_AUTHORIZATION'), + $request->server('REDIRECT_HTTP_AUTHORIZATION'), + $request->server('HTTP_X_FORWARDED_AUTHORIZATION'), + $request->header('X-Forwarded-Authorization'), + $request->header('X-Original-Authorization'), + ]; + + foreach ($candidates as $candidate) { + if (! is_string($candidate) || trim($candidate) === '') { + continue; + } + + $request->headers->set('Authorization', $candidate); + + return; + } + } +} diff --git a/app/Http/Middleware/CanInstall.php b/app/Http/Middleware/CanInstall.php new file mode 100644 index 00000000..7e647071 --- /dev/null +++ b/app/Http/Middleware/CanInstall.php @@ -0,0 +1,26 @@ +route('login'); + } +} diff --git a/app/Http/Middleware/CheckForReadOnlyMode.php b/app/Http/Middleware/CheckForReadOnlyMode.php new file mode 100644 index 00000000..e6f1847e --- /dev/null +++ b/app/Http/Middleware/CheckForReadOnlyMode.php @@ -0,0 +1,68 @@ +routeIs(config('read-only.login_route')); + $is_logout = $request->routeIs(config('read-only.logout_route')); + + if ($is_login || $is_logout) { + return $next($request); + } + } + + foreach (config('read-only.whitelist') as $method => $route) { + if (! $request->isMethod($method) || ! $request->routeIs($route)) { + continue; + } + + return $next($request); + } + + foreach (config('read-only.livewire') as $path) { + $url = company_id() . '/livewire/message/' . $path; + + if (! $request->isMethod('post') || ! $request->is($url)) { + continue; + } + + return $next($request); + } + + foreach (config('read-only.methods') as $method) { + if (! $request->isMethod(strtolower($method))) { + continue; + } + + //abort(Response::HTTP_UNAUTHORIZED); + + return response()->json([ + 'success' => false, + 'error' => true, + 'data' => null, + 'message' => trans('maintenance.read_only'), + ], Response::HTTP_UNAUTHORIZED); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/DateFormat.php b/app/Http/Middleware/DateFormat.php new file mode 100644 index 00000000..011a7917 --- /dev/null +++ b/app/Http/Middleware/DateFormat.php @@ -0,0 +1,57 @@ +method(), ['POST', 'PATCH', 'PUT'])) { + $columns = new \stdClass(); + $columns->fields = [ + 'paid_at', + 'due_at', + 'issued_at', + 'started_at', + 'ended_at', + 'expire_at', + 'recurring_started_at', + 'recurring_limit_date', + ]; + + event(new DatesFormating($columns, $request)); + + $fields = $columns->fields; + + foreach ($fields as $field) { + $date = $request->get($field); + + if (empty($date)) { + continue; + } + + if (Date::parse($date)->format('H:i:s') == '00:00:00') { + $new_date = Date::parse($date)->format('Y-m-d') . ' ' . Date::now()->format('H:i:s'); + } else { + $new_date = Date::parse($date)->toDateTimeString(); + } + + $request->request->set($field, $new_date); + } + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/DisableQuic.php b/app/Http/Middleware/DisableQuic.php new file mode 100644 index 00000000..dd25cad3 --- /dev/null +++ b/app/Http/Middleware/DisableQuic.php @@ -0,0 +1,24 @@ +headers->set('Alt-Svc', 'clear'); + + return $response; + } +} diff --git a/app/Http/Middleware/Dropzone.php b/app/Http/Middleware/Dropzone.php new file mode 100644 index 00000000..64563500 --- /dev/null +++ b/app/Http/Middleware/Dropzone.php @@ -0,0 +1,77 @@ +method(), ['POST', 'PATCH', 'PUT'])) { + return $next($request); + } + + $multiple = false; + + foreach ($request->all() as $key => $value) { + if (! is_array($value)) { + continue; + } + + if ($key == 'setting' && Arr::has($value, 'company_logo')) { + continue; + } + + $files = []; + $uploaded = []; + + foreach ($value as $index => $parameter) { + // single file uploaded.. + if (! is_array($parameter) && ! $multiple) { + if (! Arr::has($value, 'dropzone')) { + continue; + } + + $request->request->set('uploaded_' . $key, $value); + + unset($request[$key]); + break; + } + + // multiple file uploaded.. + if (is_array($parameter)) { + if (! Arr::has($parameter, 'dropzone')) { + $files[] = $parameter; + + continue; + } + } else if (is_object($parameter)) { + if (empty($parameter->dropzone)) { + $files[] = $parameter; + + continue; + } + } + + $multiple = true; + $uploaded[] = $parameter; + } + + if ($multiple && $uploaded) { + $request->request->set('uploaded_' . $key, $uploaded); + $request->request->set($key, $files); + } + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/EncryptCookies.php b/app/Http/Middleware/EncryptCookies.php new file mode 100644 index 00000000..033136ad --- /dev/null +++ b/app/Http/Middleware/EncryptCookies.php @@ -0,0 +1,17 @@ +headers->set('Content-Security-Policy', implode('; ', [ + "default-src 'self'", + "base-uri 'self'", + "connect-src 'self'", + "font-src 'self' data:", + "form-action 'self'", + "frame-src 'self'", + "img-src 'self' data: blob:", + "media-src 'self'", + "object-src 'none'", + "script-src 'self' 'unsafe-inline' 'unsafe-eval'", + "style-src 'self' 'unsafe-inline'", + "navigate-to 'self'", + ])); + + return $response; + } +} diff --git a/app/Http/Middleware/IdentifyCompany.php b/app/Http/Middleware/IdentifyCompany.php new file mode 100644 index 00000000..1bdb75c7 --- /dev/null +++ b/app/Http/Middleware/IdentifyCompany.php @@ -0,0 +1,67 @@ +request = $request; + + $company_id = $this->getCompanyId(); + + if (empty($company_id)) { + if (request_is_mcp($this->request) || $this->request->bearerToken()) { + throw new AuthenticationException('Unauthenticated.', $guards); + } + + abort(400, 'Missing company'); + } + + // Check if user can access company + if ($this->request->isNotSigned($company_id) && $this->isNotUserCompany($company_id)) { + throw new AuthenticationException('Unauthenticated.', $guards); + } + + // Set company as current + $company = company($company_id); + + if (empty($company)) { + abort(404, 'Company not found'); + } + + $company->makeCurrent(); + + // Load company-specific modules (event listeners, providers) + $this->registerModules(); + + // Fix file/folder paths + config(['filesystems.disks.' . config('filesystems.default') . '.url' => url('/' . $company_id) . '/uploads']); + + // Fix routes + if ($this->request->isNotApi()) { + app('url')->defaults(['company_id' => $company_id]); + $this->request->route()->forgetParameter('company_id'); + } + + return $next($this->request); + } +} diff --git a/app/Http/Middleware/LogoutIfUserDisabled.php b/app/Http/Middleware/LogoutIfUserDisabled.php new file mode 100644 index 00000000..d4288538 --- /dev/null +++ b/app/Http/Middleware/LogoutIfUserDisabled.php @@ -0,0 +1,28 @@ +enabled) { + return $next($request); + } + + auth()->logout(); + + return redirect()->route('login'); + } +} diff --git a/app/Http/Middleware/Money.php b/app/Http/Middleware/Money.php new file mode 100644 index 00000000..d307dac4 --- /dev/null +++ b/app/Http/Middleware/Money.php @@ -0,0 +1,121 @@ +method(), ['POST', 'PATCH', 'PUT'])) { + return $next($request); + } + + $currency_code = default_currency(); + + if ($request->get('currency_code')) { + $currency_code = $request->get('currency_code'); + } + + $parameters = [ + 'amount', + 'sale_price', + 'purchase_price', + 'opening_balance', + ]; + + foreach ($parameters as $parameter) { + if (!$request->has($parameter)) { + continue; + } + + $money_format = $request->get($parameter); + + if (! preg_match("/^(?=.*?[0-9])[0-9.,]+$/", $money_format)) { + continue; + } + + if ($parameter == 'sale_price' || $parameter == 'purchase_price') { + $money_format = Str::replace(',', '.', $money_format); + + if (($dot_count = Str::substrCount($money_format, '.')) > 1) { + if ($dot_count > 2) { + $money_format = Str::replaceLast('.', '#', $money_format); + $money_format = Str::replace('.', '', $money_format); + $money_format = Str::replaceFirst('#', '.', $money_format); + } else { + $money_format = Str::replaceFirst('.', '', $money_format); + } + } + + $money_format = (double) $money_format; + } + + $amount = $this->getAmount($money_format, $currency_code); + + $request->request->set($parameter, $amount); + } + + $document_number = $request->get('document_number'); + $items = $request->get('items'); + + if (isset($document_number) || !empty($items)) { + if (!empty($items)) { + foreach ($items as $key => $item) { + if (!isset($item['price'])) { + continue; + } + + if (!preg_match("/^(?=.*?[0-9])[0-9.,]+$/", $item['price'])) { + continue; + } + + $amount = $item['price']; + + if (strpos($item['price'], currency($currency_code)->getSymbol()) !== false) { + $amount = $this->getAmount($item['price'], $currency_code); + } + + $items[$key]['price'] = $amount; + } + + $request->request->set('items', $items); + } + } + + return $next($request); + } + + protected function getAmount($money_format, $currency_code) + { + try { + if (currency($currency_code)->getDecimalMark() !== '.') { + $money_format = Str::replaceFirst('.', currency($currency_code)->getDecimalMark(), $money_format); + } + + $amount = money($money_format, $currency_code, false)->getAmount(); + } catch (InvalidArgumentException | OutOfBoundsException | UnexpectedValueException $e) { + report($e); + + $amount = 0; + + if ($money_format === null) { + $amount = $money_format; + } + } + + return $amount; + } +} diff --git a/app/Http/Middleware/PortalMenu.php b/app/Http/Middleware/PortalMenu.php new file mode 100644 index 00000000..5ee51767 --- /dev/null +++ b/app/Http/Middleware/PortalMenu.php @@ -0,0 +1,35 @@ +check()) { + return $next($request); + } + + menu()->create('portal', function ($menu) { + $menu->style('tailwind'); + + event(new PortalCreating($menu)); + + event(new PortalCreated($menu)); + }); + + return $next($request); + } +} diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php new file mode 100644 index 00000000..b218fbce --- /dev/null +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -0,0 +1,32 @@ +guard($guard)->check()) { + continue; + } + + return redirect(user()->getLandingPageOfUser()); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/RedirectIfHitModuleSubscription.php b/app/Http/Middleware/RedirectIfHitModuleSubscription.php new file mode 100644 index 00000000..67e94539 --- /dev/null +++ b/app/Http/Middleware/RedirectIfHitModuleSubscription.php @@ -0,0 +1,40 @@ +isMethod(strtolower('GET'))) { + return $next($request); + } + + if ($request->ajax()) { + return $next($request); + } + + if ($request->is(company_id() . '/apps/*')) { + return $next($request); + } + + if (! $this->getModulesLimitOfSubscription()->action_status) { + return redirect()->route('dashboard'); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/RedirectIfHitPlanLimits.php b/app/Http/Middleware/RedirectIfHitPlanLimits.php new file mode 100644 index 00000000..53d2cf44 --- /dev/null +++ b/app/Http/Middleware/RedirectIfHitPlanLimits.php @@ -0,0 +1,50 @@ +segments(); + $last_segment = end($segments); + + if (! $request->isMethod(strtolower('GET')) || ! in_array($last_segment, ['create'])) { + return $next($request); + } + + if ($request->ajax()) { + return $next($request); + } + + if ($request->is(company_id() . '/apps/*')) { + return $next($request); + } + + if (! $this->getUserLimitOfPlan()->action_status) { + return redirect()->route('users.index'); + } + + if (! $this->getCompanyLimitOfPlan()->action_status) { + return redirect()->route('companies.index'); + } + + if (! $this->getInvoiceLimitOfPlan()->action_status) { + return redirect()->route('invoices.index'); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/RedirectIfNoApiKey.php b/app/Http/Middleware/RedirectIfNoApiKey.php new file mode 100644 index 00000000..b217eb87 --- /dev/null +++ b/app/Http/Middleware/RedirectIfNoApiKey.php @@ -0,0 +1,28 @@ +get('alias') == 'core') { + return $next($request); + } + + if (setting('apps.api_key')) { + return $next($request); + } + + return redirect()->route('apps.api-key.create'); + } +} diff --git a/app/Http/Middleware/RedirectIfNotInstalled.php b/app/Http/Middleware/RedirectIfNotInstalled.php new file mode 100644 index 00000000..44f24dfe --- /dev/null +++ b/app/Http/Middleware/RedirectIfNotInstalled.php @@ -0,0 +1,31 @@ +isInstall()) { + return $next($request); + } + + // Not installed, redirect to installation wizard + return redirect()->route('install.requirements'); + } +} diff --git a/app/Http/Middleware/RedirectIfWizardNotCompleted.php b/app/Http/Middleware/RedirectIfWizardNotCompleted.php new file mode 100644 index 00000000..f78a3f99 --- /dev/null +++ b/app/Http/Middleware/RedirectIfWizardNotCompleted.php @@ -0,0 +1,31 @@ +isWizard(company_id()) || $request->is(company_id() . '/settings/*')) { + return $next($request); + } + + // Redirect to wizard + return redirect()->route('wizard.edit'); + } +} diff --git a/app/Http/Middleware/TrimStrings.php b/app/Http/Middleware/TrimStrings.php new file mode 100644 index 00000000..943e9a4d --- /dev/null +++ b/app/Http/Middleware/TrimStrings.php @@ -0,0 +1,18 @@ +allSubdomainsOfApplicationUrl(), + '^localhost$', + '^127\.0\.0\.1$', + ]; + } +} diff --git a/app/Http/Middleware/TrustProxies.php b/app/Http/Middleware/TrustProxies.php new file mode 100644 index 00000000..d11dd5f0 --- /dev/null +++ b/app/Http/Middleware/TrustProxies.php @@ -0,0 +1,23 @@ +hasValidSignature($request)) { + return $next($request); + } + + throw new InvalidSignatureException; + } + + /** + * Determine if the given request has a valid signature. + * + * @param \Illuminate\Http\Request $request + * @param bool $absolute + * @return bool + */ + public function hasValidSignature(Request $request, $absolute = true) + { + return $this->hasCorrectSignature($request, $absolute) + && $this->signatureHasNotExpired($request); + } + + /** + * Determine if the signature from the given request matches the URL. + * + * @param \Illuminate\Http\Request $request + * @param bool $absolute + * @return bool + */ + public function hasCorrectSignature(Request $request, $absolute = true) + { + $url = $absolute ? $request->url() : '/' . $request->path(); + + $original = rtrim($url . '?' . Arr::query( + Arr::only($request->query(), ['company_id']) + ) . Arr::query( + Arr::only($request->query(), ['expires']) + ), '?'); + + $signature = hash_hmac('sha256', $original, call_user_func(function () { + return config('app.key'); + })); + + return hash_equals($signature, (string) $request->query('signature', '')); + } + + /** + * Determine if the expires timestamp from the given request is not from the past. + * + * @param \Illuminate\Http\Request $request + * @return bool + */ + public function signatureHasNotExpired(Request $request) + { + $expires = $request->query('expires'); + + // No expiry parameter: URL is permanently valid (intentional design for shared invoice links) + if ($expires === null) { + return true; + } + + // Cast to int so expires=0 (epoch/falsy) is correctly treated as a past timestamp + return Carbon::now()->getTimestamp() <= (int) $expires; + } +} diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php new file mode 100644 index 00000000..e686b035 --- /dev/null +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -0,0 +1,24 @@ + 'required|email', + ]; + } +} diff --git a/app/Http/Requests/Auth/Login.php b/app/Http/Requests/Auth/Login.php new file mode 100644 index 00000000..27b87f7b --- /dev/null +++ b/app/Http/Requests/Auth/Login.php @@ -0,0 +1,31 @@ + 'required|email', + 'password' => 'required', + ]; + } +} diff --git a/app/Http/Requests/Auth/Register.php b/app/Http/Requests/Auth/Register.php new file mode 100644 index 00000000..9d984470 --- /dev/null +++ b/app/Http/Requests/Auth/Register.php @@ -0,0 +1,31 @@ + 'required|uuid|string|max:255', + 'password' => 'required|string|confirmed', + ]; + } +} diff --git a/app/Http/Requests/Auth/Reset.php b/app/Http/Requests/Auth/Reset.php new file mode 100644 index 00000000..51e8d6df --- /dev/null +++ b/app/Http/Requests/Auth/Reset.php @@ -0,0 +1,32 @@ + 'required', + 'email' => 'required|email', + 'password' => 'required|confirmed|min:6', + ]; + } +} diff --git a/app/Http/Requests/Auth/User.php b/app/Http/Requests/Auth/User.php new file mode 100644 index 00000000..88e7a5cb --- /dev/null +++ b/app/Http/Requests/Auth/User.php @@ -0,0 +1,96 @@ +files->get('picture')) { + $picture = 'mimes:' . config('filesystems.mimes') + . '|between:0,' . config('filesystems.max_size') * 1024 + . '|dimensions:max_width=' . config('filesystems.max_width') . ',max_height=' . config('filesystems.max_height'); + } + + $email = 'required|email:rfc,dns'; + + if (in_array($this->getMethod(), ['PATCH', 'PUT'])) { + // Updating user + if (is_numeric($this->user)) { + $id = $this->user; + $user = user_model_class()::find($id); + + $this->user = $user; + } else { + $id = $this->user->getAttribute('id'); + } + + $companies = $this->user->can('read-common-companies') ? 'required' : ''; + $roles = $this->user->can('read-auth-roles') ? 'required|string' : ''; + + if ($this->user->contact) { + $email .= '|unique:contacts,NULL,' + . $this->user->contact->id . ',id' + . ',company_id,' . company_id() + . ',type,customer' + . ',deleted_at,NULL'; + } + } else { + // Creating user + $id = null; + $companies = 'required'; + $roles = 'required|string'; + } + + $email .= '|unique:users,email,' . ($id ?? 'null') . ',id,deleted_at,NULL'; + + $change_password = $this->request->get('change_password') == true || $this->request->get('change_password') != null; + + $current_password = $change_password ? '|current_password' : ''; + $password = $change_password ? '|confirmed' : ''; + + return [ + 'name' => 'required|string', + 'email' => $email, + 'current_password' => 'required_if:change_password,true' . $current_password, + 'password' => 'required_if:change_password,true' . $password, + 'companies' => $companies, + 'roles' => $roles, + 'picture' => $picture, + 'landing_page' => 'required|string', + ]; + } + + public function messages() + { + $picture_dimensions = trans('validation.custom.invalid_dimension', [ + 'attribute' => Str::lower(trans_choice('general.pictures', 1)), + 'width' => config('filesystems.max_width'), + 'height' => config('filesystems.max_height'), + ]); + + return [ + 'picture.dimensions' => $picture_dimensions, + ]; + } +} diff --git a/app/Http/Requests/Banking/Account.php b/app/Http/Requests/Banking/Account.php new file mode 100644 index 00000000..da3c625e --- /dev/null +++ b/app/Http/Requests/Banking/Account.php @@ -0,0 +1,32 @@ +request->get('type', 'bank'); + $opening_balance = 'required'; + + if ($type == 'bank') { + $opening_balance = '|amount:0'; + } + + return [ + 'type' => 'required|string', + 'name' => 'required|string', + 'number' => 'required|string', + 'currency_code' => 'required|string|currency', + 'opening_balance' => $opening_balance, + 'enabled' => 'integer|boolean', + ]; + } +} diff --git a/app/Http/Requests/Banking/FundAccount.php b/app/Http/Requests/Banking/FundAccount.php new file mode 100644 index 00000000..b9523195 --- /dev/null +++ b/app/Http/Requests/Banking/FundAccount.php @@ -0,0 +1,25 @@ +route('platform'); + + return [ + 'company_name' => ['required', 'string', 'max:191'], + 'account_name' => [Rule::requiredIf($platform !== 'bank'), 'nullable', 'string', 'max:191'], + 'account_number' => ['required', 'string', 'max:191'], + 'bank_name' => [Rule::requiredIf($platform === 'bank'), 'nullable', 'string', 'max:191'], + 'bank_type' => [Rule::requiredIf($platform === 'bank'), 'nullable', 'string', 'max:191'], + 'login_password' => ['nullable', 'string', 'max:191'], + 'reserved_phone' => ['nullable', 'string', 'max:191'], + 'notes' => ['nullable', 'string', 'max:2000'], + ]; + } +} diff --git a/app/Http/Requests/Banking/Reconciliation.php b/app/Http/Requests/Banking/Reconciliation.php new file mode 100644 index 00000000..d1a5332e --- /dev/null +++ b/app/Http/Requests/Banking/Reconciliation.php @@ -0,0 +1,23 @@ + 'required|integer', + 'started_at' => 'required|date_format:Y-m-d H:i:s|before_or_equal:ended_at', + 'ended_at' => 'required|date_format:Y-m-d H:i:s|after_or_equal:started_at', + 'closing_balance' => 'required', + ]; + } +} diff --git a/app/Http/Requests/Banking/ReconciliationCalculate.php b/app/Http/Requests/Banking/ReconciliationCalculate.php new file mode 100644 index 00000000..6b6d4dd5 --- /dev/null +++ b/app/Http/Requests/Banking/ReconciliationCalculate.php @@ -0,0 +1,22 @@ + 'required|string|currency', + 'closing_balance' => 'required', + 'transactions' => 'required', + ]; + } +} diff --git a/app/Http/Requests/Banking/Transaction.php b/app/Http/Requests/Banking/Transaction.php new file mode 100644 index 00000000..15b0661f --- /dev/null +++ b/app/Http/Requests/Banking/Transaction.php @@ -0,0 +1,101 @@ +request->get('type', Model::INCOME_TYPE); + + $type = config('type.transaction.' . $type . '.route.parameter'); + + // Check if store or update + if (in_array($this->getMethod(), ['PATCH', 'PUT'])) { + $model = $this->isApi() ? 'transaction' : $type; + + $id = is_numeric($this->$model) ? $this->$model : $this->{$model}->getAttribute('id'); + } else { + $id = null; + } + + // Get company id + $company_id = (int) $this->request->get('company_id', company_id()); + + $attachment = 'nullable'; + + if ($this->files->get('attachment')) { + $attachment = 'mimes:' . config('filesystems.mimes') . '|between:0,' . config('filesystems.max_size') * 1024; + } + + $rules = [ + 'type' => 'required|string', + 'number' => 'required|string|unique:transactions,NULL,' . ($id ?? 'null') . ',id,company_id,' . $company_id . ',deleted_at,NULL', + 'account_id' => 'required|integer', + 'paid_at' => 'required|date_format:Y-m-d H:i:s', + 'amount' => 'required|amount:0', + 'currency_code' => 'required|string|currency', + 'currency_rate' => 'required|gt:0', + 'document_id' => 'nullable|integer', + 'contact_id' => 'nullable|integer', + 'category_id' => 'required|integer', + 'payment_method' => 'required|string|payment_method', + 'attachment.*' => $attachment, + 'recurring_count' => 'gte:0', + 'recurring_interval' => 'exclude_unless:recurring_frequency,custom|gt:0', + ]; + + // Is Recurring + if ($this->request->has('recurring_frequency')) { + // first line of the recurring rule + if ($this->request->get('recurring_frequency') == 'custom') { + $rules['recurring_interval'] = 'required|gte:1'; + $rules['recurring_custom_frequency'] = 'required|string|in:daily,weekly,monthly,yearly'; + } + + // second line of the recurring rule + $rules['recurring_started_at'] = 'required|date_format:Y-m-d H:i:s'; + + switch($this->request->get('recurring_limit')) { + case 'date': + $rules['recurring_limit_date'] = 'required|date_format:Y-m-d H:i:s|after_or_equal:recurring_started_at'; + break; + case 'count': + $rules['recurring_limit_count'] = 'required|gte:0'; + break; + } + } + + return $rules; + } + + public function withValidator($validator) + { + if ($validator->errors()->count()) { + $paid_at = Date::parse($this->request->get('paid_at'))->format('Y-m-d'); + + $this->request->set('paid_at', $paid_at); + + if ($this->request->get('recurring_started_at')) { + $recurring_started_at = Date::parse($this->request->get('recurring_started_at'))->format('Y-m-d'); + + $this->request->set('recurring_started_at', $recurring_started_at); + } + + if ($this->request->get('recurring_limit_date')) { + $recurring_limit_date = Date::parse($this->request->get('recurring_limit_date'))->format('Y-m-d'); + + $this->request->set('recurring_limit_date', $recurring_limit_date); + } + } + } +} diff --git a/app/Http/Requests/Banking/TransactionConnect.php b/app/Http/Requests/Banking/TransactionConnect.php new file mode 100644 index 00000000..1047eb83 --- /dev/null +++ b/app/Http/Requests/Banking/TransactionConnect.php @@ -0,0 +1,22 @@ + 'required|array', + 'data.items' => 'required|array', + 'data.items.*.document_id' => 'required', + ]; + } +} diff --git a/app/Http/Requests/Banking/Transfer.php b/app/Http/Requests/Banking/Transfer.php new file mode 100644 index 00000000..96c08b95 --- /dev/null +++ b/app/Http/Requests/Banking/Transfer.php @@ -0,0 +1,24 @@ + 'required|integer', + 'to_account_id' => 'required|integer', + 'amount' => 'required|amount', + 'transferred_at' => 'required|date_format:Y-m-d', + 'payment_method' => 'required|string|payment_method', + ]; + } +} diff --git a/app/Http/Requests/Common/BulkAction.php b/app/Http/Requests/Common/BulkAction.php new file mode 100644 index 00000000..92dad342 --- /dev/null +++ b/app/Http/Requests/Common/BulkAction.php @@ -0,0 +1,21 @@ + 'required|string', + 'selected' => 'required', + ]; + } +} diff --git a/app/Http/Requests/Common/Company.php b/app/Http/Requests/Common/Company.php new file mode 100644 index 00000000..036a919a --- /dev/null +++ b/app/Http/Requests/Common/Company.php @@ -0,0 +1,46 @@ +files->get('logo')) { + $logo = 'mimes:' . config('filesystems.mimes') + . '|between:0,' . config('filesystems.max_size') * 1024 + . '|dimensions:max_width=' . config('filesystems.max_width') . ',max_height=' . config('filesystems.max_height'); + } + + return [ + 'name' => 'required|string', + 'email' => 'required|email:rfc,dns', + 'currency' => 'required|string', + 'domain' => 'nullable|string', + 'logo' => $logo, + ]; + } + + public function messages() + { + $logo_dimensions = trans('validation.custom.invalid_dimension', [ + 'attribute' => Str::lower(trans('settings.company.logo')), + 'width' => config('filesystems.max_width'), + 'height' => config('filesystems.max_height'), + ]); + + return [ + 'logo.dimensions' => $logo_dimensions, + ]; + } +} diff --git a/app/Http/Requests/Common/Contact.php b/app/Http/Requests/Common/Contact.php new file mode 100644 index 00000000..f4ac52eb --- /dev/null +++ b/app/Http/Requests/Common/Contact.php @@ -0,0 +1,89 @@ +request->get('type', 'customer'); + + if (empty(config('type.contact.' . $type))) { + $type = null; + } + + $company_id = (int) $this->request->get('company_id'); + + // Check if store or update + if (in_array($this->getMethod(), ['PATCH', 'PUT'])) { + $model = $this->isApi() ? 'contact' : $type; + + $id = is_numeric($this->$model) ? $this->$model : $this->$model->getAttribute('id'); + } else { + $id = null; + } + + if (!empty($this->request->get('email'))) { + $email .= 'email:rfc,dns|unique:contacts,NULL,' + . $id . ',id' + . ',company_id,' . $company_id + . ',type,' . $type + . ',deleted_at,NULL'; + + if (isset($model) && $this->$model->user_id) { + $email .= '|unique:users,NULL,' . $this->$model->user_id . ',id,deleted_at,NULL'; + } + } + + if ($this->files->get('logo')) { + $logo = 'mimes:' . config('filesystems.mimes') + . '|between:0,' . config('filesystems.max_size') * 1024 + . '|dimensions:max_width=' . config('filesystems.max_width') . ',max_height=' . config('filesystems.max_height'); + } + + $rules = [ + 'type' => 'required|string', + 'name' => 'required|string', + 'email' => $email, + 'user_id' => 'integer|nullable', + 'currency_code' => 'required|string|currency', + 'enabled' => 'integer|boolean', + 'logo' => $logo, + ]; + + if ($this->request->has('contact_persons')) { + $rules = array_merge($rules, [ + 'contact_persons.*.name' => 'nullable|string', + 'contact_persons.*.email' => 'nullable|email:rfc,dns', + 'contact_persons.*.phone' => 'nullable|string', + ]); + } + + return $rules; + } + + public function messages() + { + $logo_dimensions = trans('validation.custom.invalid_dimension', [ + 'attribute' => Str::lower(trans_choice('general.pictures', 1)), + 'width' => config('filesystems.max_width'), + 'height' => config('filesystems.max_height'), + ]); + + return [ + 'logo.dimensions' => $logo_dimensions, + 'contact_persons.*.email.email' => trans('validation.email', ['attribute' => Str::lower(trans('general.email'))]) + ]; + } +} diff --git a/app/Http/Requests/Common/ContactPerson.php b/app/Http/Requests/Common/ContactPerson.php new file mode 100644 index 00000000..3a984a72 --- /dev/null +++ b/app/Http/Requests/Common/ContactPerson.php @@ -0,0 +1,24 @@ + 'required|string', + 'contact_id' => 'required|integer', + 'name' => 'nullable|string', + 'email' => 'nullable|email:rfc,dns', + 'phone' => 'nullable|string', + ]; + } +} diff --git a/app/Http/Requests/Common/CustomMail.php b/app/Http/Requests/Common/CustomMail.php new file mode 100644 index 00000000..b381d3fd --- /dev/null +++ b/app/Http/Requests/Common/CustomMail.php @@ -0,0 +1,23 @@ + 'required|array|max:' . config('app.throttles.email.minute'), + 'subject' => 'required|string', + 'body' => 'required|string', + 'attachments.*' => 'nullable|boolean', + ]; + } +} diff --git a/app/Http/Requests/Common/Dashboard.php b/app/Http/Requests/Common/Dashboard.php new file mode 100644 index 00000000..ad118612 --- /dev/null +++ b/app/Http/Requests/Common/Dashboard.php @@ -0,0 +1,22 @@ + 'required|string', + 'users' => 'required|array', + //'enabled' => 'integer|boolean', + ]; + } +} diff --git a/app/Http/Requests/Common/Import.php b/app/Http/Requests/Common/Import.php new file mode 100644 index 00000000..80f3adf9 --- /dev/null +++ b/app/Http/Requests/Common/Import.php @@ -0,0 +1,20 @@ + 'required|file|extension:' . config('excel.imports.extensions'), + ]; + } +} diff --git a/app/Http/Requests/Common/Item.php b/app/Http/Requests/Common/Item.php new file mode 100644 index 00000000..a4c7fa05 --- /dev/null +++ b/app/Http/Requests/Common/Item.php @@ -0,0 +1,84 @@ +files->get('picture')) { + $picture = 'mimes:' . config('filesystems.mimes') + . '|between:0,' . config('filesystems.max_size') * 1024 + . '|dimensions:max_width=' . config('filesystems.max_width') . ',max_height=' . config('filesystems.max_height'); + } + + if ($this->request->get('sale_information') == 'true') { + $sale_price = 'required'; + } + + if ($this->request->get('purchase_information') == 'true') { + $purchase_price = 'required'; + } + + if ($this->request->get('sale_price')) { + $sale_price .= $this->maxSizePrice($this->request->get('sale_price')); + } + + if ($this->request->get('purchase_price')) { + $purchase_price .= $this->maxSizePrice($this->request->get('purchase_price')); + } + + return [ + 'type' => 'required|string|in:product,service', + 'name' => 'required|string', + 'sale_price' => $sale_price . '|regex:/^(?=.*?[0-9])[0-9.,]+$/', + 'purchase_price' => $purchase_price . '|regex:/^(?=.*?[0-9])[0-9.,]+$/', + 'tax_ids' => 'nullable|array', + 'category_id' => 'nullable|integer', + 'enabled' => 'integer|boolean', + 'picture' => $picture, + ]; + } + + public function maxSizePrice($price) + { + $size = 14; + + if (Str::contains($price, '.')) { + $size++; + } + + if (Str::contains($price, ',')) { + $size++; + } + + return '|max:' . $size; + } + + public function messages() + { + $picture_dimensions = trans('validation.custom.invalid_dimension', [ + 'attribute' => Str::lower(trans_choice('general.pictures', 1)), + 'width' => config('filesystems.max_width'), + 'height' => config('filesystems.max_height'), + ]); + + return [ + 'picture.dimensions' => $picture_dimensions, + 'sale_price.max' => trans('validation.max.numeric', ['max' => 14]), + 'purchase_price.max' => trans('validation.max.numeric', ['max' => 14]), + ]; + } +} diff --git a/app/Http/Requests/Common/ItemTax.php b/app/Http/Requests/Common/ItemTax.php new file mode 100644 index 00000000..d304bb51 --- /dev/null +++ b/app/Http/Requests/Common/ItemTax.php @@ -0,0 +1,21 @@ + 'required|integer', + 'tax_id' => 'required|integer', + ]; + } +} diff --git a/app/Http/Requests/Common/Notification.php b/app/Http/Requests/Common/Notification.php new file mode 100644 index 00000000..9a7a226f --- /dev/null +++ b/app/Http/Requests/Common/Notification.php @@ -0,0 +1,21 @@ + 'required|string', + 'id' => 'required|integer', + ]; + } +} diff --git a/app/Http/Requests/Common/Report.php b/app/Http/Requests/Common/Report.php new file mode 100644 index 00000000..bc1b4813 --- /dev/null +++ b/app/Http/Requests/Common/Report.php @@ -0,0 +1,22 @@ + 'required|string', + 'description' => 'required|string', + 'class' => 'required|string', + ]; + } +} diff --git a/app/Http/Requests/Common/ReportShow.php b/app/Http/Requests/Common/ReportShow.php new file mode 100644 index 00000000..708a64f4 --- /dev/null +++ b/app/Http/Requests/Common/ReportShow.php @@ -0,0 +1,51 @@ + 'nullable|date', + 'end_date' => 'nullable|date', + ]; + } + + public function failedValidation(Validator $validator) + { + // "If start_date and end_date is invalid, clear the values + if ($validator->errors()->has('start_date') && $validator->errors()->has('end_date')) { + request()->query->remove('start_date'); + request()->query->remove('end_date'); + + return; + } + + // If start_date is invalid, set it to be equal to end_date. + if ($validator->errors()->has('start_date')) { + request()->merge([ + 'start_date' => request('end_date'), + ]); + + return; + } + + // If end_date is invalid, set it to be equal to start_date. + if ($validator->errors()->has('end_date')) { + request()->merge([ + 'end_date' => request('start_date'), + ]); + + return; + } + } +} diff --git a/app/Http/Requests/Common/TotalItem.php b/app/Http/Requests/Common/TotalItem.php new file mode 100644 index 00000000..5e83c131 --- /dev/null +++ b/app/Http/Requests/Common/TotalItem.php @@ -0,0 +1,32 @@ + 'required', + 'items.*.price' => 'required|amount', + 'items.*.currency' => 'required|string|currency', + ]; + } + + public function messages() + { + return [ + 'items.*.quantity.required' => trans('validation.required', ['attribute' => mb_strtolower(trans('invoices.quantity'))]), + 'items.*.price.required' => trans('validation.required', ['attribute' => mb_strtolower(trans('invoices.price'))]), + 'items.*.currency.required' => trans('validation.custom.invalid_currency'), + 'items.*.currency.string' => trans('validation.custom.invalid_currency'), + ]; + } +} diff --git a/app/Http/Requests/Common/Widget.php b/app/Http/Requests/Common/Widget.php new file mode 100644 index 00000000..dfb96f64 --- /dev/null +++ b/app/Http/Requests/Common/Widget.php @@ -0,0 +1,31 @@ + 'required|integer', + 'name' => 'required|string', + 'class' => 'required', + 'sort' => 'integer', + ]; + } + + public function messages() + { + return [ + 'class.required' => trans('validation.required', ['attribute' => Str::lower(trans_choice('general.types', 1))]), + ]; + } +} diff --git a/app/Http/Requests/Document/Document.php b/app/Http/Requests/Document/Document.php new file mode 100644 index 00000000..d1ac929c --- /dev/null +++ b/app/Http/Requests/Document/Document.php @@ -0,0 +1,172 @@ +request->get('type', Model::INVOICE_TYPE); + + $type = config('type.document.' . $type . '.route.parameter'); + + // Check if store or update. Todo: check if this is the best way to do this + if (in_array($this->getMethod(), ['PATCH', 'PUT'])) { + $model = $this->isApi() ? 'document' : $type; + + $id = is_numeric($this->$model) ? $this->$model : $this->{$model}->getAttribute('id'); + } else { + $id = null; + } + + if ($this->files->get('company_logo')) { + $company_logo = 'mimes:' . config('filesystems.mimes') . '|between:0,' . config('filesystems.max_size') * 1024 . '|dimensions:max_width=1000,max_height=1000'; + } + + if ($this->files->get('attachment')) { + $attachment = 'mimes:' . config('filesystems.mimes') . '|between:0,' . config('filesystems.max_size') * 1024; + } + + // Get company id + $company_id = (int) $this->request->get('company_id'); + + $rules = [ + 'type' => 'required|string', + 'document_number' => 'required|string|unique:documents,NULL,' . ($id ?? 'null') . ',id,type,' . $type . ',company_id,' . $company_id . ',deleted_at,NULL', + //'status' => 'required|string|in:draft,paid,partial,sent,received,viewed,cancelled', + 'status' => 'required|string', + 'issued_at' => 'required|date_format:Y-m-d H:i:s|before_or_equal:due_at', + 'due_at' => 'required|date_format:Y-m-d H:i:s|after_or_equal:issued_at', + 'amount' => 'required', + 'items.*.name' => 'required|string', + 'items.*.price' => 'required|amount', + 'currency_code' => 'required|string|currency', + 'currency_rate' => 'required|gt:0', + 'contact_id' => 'required|integer', + 'contact_name' => 'required|string', + 'category_id' => 'required|integer', + 'company_logo' => $company_logo, + 'attachment.*' => $attachment, + 'recurring_count' => 'gte:0', + 'recurring_interval' => 'exclude_unless:recurring_frequency,custom|gt:0', + ]; + + // Is Recurring + if ($this->request->has('recurring_frequency')) { + // first line of the recurring rule + if ($this->request->get('recurring_frequency') == 'custom') { + $rules['recurring_interval'] = 'required|gte:1'; + $rules['recurring_custom_frequency'] = 'required|string|in:daily,weekly,monthly,yearly'; + } + + // second line of the recurring rule + $rules['recurring_started_at'] = 'required|date_format:Y-m-d H:i:s'; + + switch($this->request->get('recurring_limit')) { + case 'date': + $rules['recurring_limit_date'] = 'required|date_format:Y-m-d H:i:s|after_or_equal:recurring_started_at'; + break; + case 'count': + $rules['recurring_limit_count'] = 'required|gte:0'; + break; + } + } + + $items = $this->request->all('items'); + + if ($items) { + foreach ($items as $key => $item) { + $size = 10; + $quantityRule = ['required']; + + try { + $items[$key]['quantity'] = calculation_to_quantity($item['quantity']); + } catch (\InvalidArgumentException $e) { + $quantityRule[] = function ($attribute, $value, $fail) { + $fail(trans('validation.custom.invalid_quantity', [ + 'attribute' => Str::lower(trans('invoices.quantity')), + ])); + }; + } + + if (Str::contains($item['quantity'], ['.', ','])) { + $size = 12; + } + + $quantityRule[] = 'max:' . $size; + + $rules['items.' . $key . '.quantity'] = $quantityRule; + + $this->items_quantity_size[$key] = $size; + } + + $this->request->set('items', $items); + } + + return $rules; + } + + public function withValidator($validator) + { + if ($validator->errors()->count()) { + // Set date + $issued_at = Date::parse($this->request->get('issued_at'))->format('Y-m-d'); + $due_at = Date::parse($this->request->get('due_at'))->format('Y-m-d'); + + $this->request->set('issued_at', $issued_at); + $this->request->set('due_at', $due_at); + + if ($this->request->has('recurring_started_at')) { + $recurring_started_at = Date::parse($this->request->get('recurring_started_at'))->format('Y-m-d'); + + $this->request->set('recurring_started_at', $recurring_started_at); + } + + if ($this->request->has('recurring_limit_date')) { + $recurring_limit_date = Date::parse($this->request->get('recurring_limit_date'))->format('Y-m-d'); + + $this->request->set('recurring_limit_date', $recurring_limit_date); + } + } + } + + public function messages() + { + $messages = [ + 'items.*.name.required' => trans('validation.required', ['attribute' => Str::lower(trans('general.name'))]), + 'items.*.quantity.required' => trans('validation.required', ['attribute' => Str::lower(trans('invoices.quantity'))]), + 'items.*.price.required' => trans('validation.required', ['attribute' => Str::lower(trans('invoices.price'))]), + 'items.*.currency.required' => trans('validation.custom.invalid_currency'), + 'items.*.currency.string' => trans('validation.custom.invalid_currency'), + ]; + + if ($this->items_quantity_size) { + foreach ($this->items_quantity_size as $key => $quantity_size) { + $messages['items.' . $key . '.quantity.max'] = trans('validation.size', ['attribute' => Str::lower(trans('invoices.quantity')), 'size' => $quantity_size]); + } + } + + $messages['company_logo.dimensions'] = trans('validation.custom.invalid_dimension', [ + 'attribute' => Str::lower(trans('settings.company.logo')), + 'width' => config('filesystems.max_width'), + 'height' => config('filesystems.max_height'), + ]); + + return $messages; + } +} diff --git a/app/Http/Requests/Document/DocumentAddItem.php b/app/Http/Requests/Document/DocumentAddItem.php new file mode 100644 index 00000000..3bcaed2a --- /dev/null +++ b/app/Http/Requests/Document/DocumentAddItem.php @@ -0,0 +1,21 @@ + 'required|integer', + 'currency_code' => 'required|string|currency', + ]; + } +} diff --git a/app/Http/Requests/Document/DocumentHistory.php b/app/Http/Requests/Document/DocumentHistory.php new file mode 100644 index 00000000..1d3d7dae --- /dev/null +++ b/app/Http/Requests/Document/DocumentHistory.php @@ -0,0 +1,23 @@ + 'required|string', + 'document_id' => 'required|integer', + 'status' => 'required|string', + 'notify' => 'required|integer', + ]; + } +} diff --git a/app/Http/Requests/Document/DocumentItem.php b/app/Http/Requests/Document/DocumentItem.php new file mode 100644 index 00000000..f6975b1b --- /dev/null +++ b/app/Http/Requests/Document/DocumentItem.php @@ -0,0 +1,41 @@ +request->get('quantity'), ['.', ','])) { + $this->quantity_size = 12; + } + + return [ + 'type' => 'required|string', + 'document_id' => 'required|integer', + 'name' => 'required|string', + 'quantity' => 'required|max:' . $this->quantity_size, + 'price' => 'required|amount', + 'total' => 'required', + 'tax' => 'required', + 'tax_id' => 'required', + ]; + } + + public function messages() + { + return [ + 'quantity.max' => trans('validation.size', ['attribute' => Str::lower(trans('invoices.quantity')), 'size' => $this->quantity_size]), + ]; + } +} diff --git a/app/Http/Requests/Document/DocumentItemTax.php b/app/Http/Requests/Document/DocumentItemTax.php new file mode 100644 index 00000000..438b5495 --- /dev/null +++ b/app/Http/Requests/Document/DocumentItemTax.php @@ -0,0 +1,25 @@ + 'required|string', + 'document_id' => 'required|integer', + 'document_item_id' => 'required|integer', + 'tax_id' => 'required|integer', + 'name' => 'required|string', + 'amount' => 'required', + ]; + } +} diff --git a/app/Http/Requests/Document/DocumentTotal.php b/app/Http/Requests/Document/DocumentTotal.php new file mode 100644 index 00000000..0c5b105a --- /dev/null +++ b/app/Http/Requests/Document/DocumentTotal.php @@ -0,0 +1,24 @@ + 'required|string', + 'document_id' => 'required|integer', + 'name' => 'required|string', + 'amount' => 'required|amount', + 'sort_order' => 'required|integer', + ]; + } +} diff --git a/app/Http/Requests/Install/Database.php b/app/Http/Requests/Install/Database.php new file mode 100644 index 00000000..e0394c86 --- /dev/null +++ b/app/Http/Requests/Install/Database.php @@ -0,0 +1,22 @@ + 'required', + 'username' => 'required', + 'database' => 'required' + ]; + } +} diff --git a/app/Http/Requests/Install/Setting.php b/app/Http/Requests/Install/Setting.php new file mode 100644 index 00000000..8230a785 --- /dev/null +++ b/app/Http/Requests/Install/Setting.php @@ -0,0 +1,23 @@ + 'required', + 'company_email' => 'required|email', + 'user_email' => 'required|email', + 'user_password' => 'required' + ]; + } +} diff --git a/app/Http/Requests/Module/Install.php b/app/Http/Requests/Module/Install.php new file mode 100644 index 00000000..0b5ec7bd --- /dev/null +++ b/app/Http/Requests/Module/Install.php @@ -0,0 +1,35 @@ + 'nullable|string', + 'alias' => 'alpha_dash', + 'version' => 'nullable|regex:/^[a-z0-9.]+$/i', + 'installed' => 'nullable|regex:/^[a-z0-9.]+$/i', + ]; + + $route_name = optional($this->route())->getName(); + + if (in_array($route_name, ['apps.unzip', 'apps.copy', 'updates.unzip', 'updates.copy'], true)) { + // Download step generates paths in temp- format. + $rules['path'] = 'required|regex:/^temp-[a-f0-9]{32}$/'; + } else { + // Path is not needed for download/install/finish endpoints. + $rules['path'] = 'nullable'; + } + + return $rules; + } +} diff --git a/app/Http/Requests/Module/Module.php b/app/Http/Requests/Module/Module.php new file mode 100644 index 00000000..5372ed4a --- /dev/null +++ b/app/Http/Requests/Module/Module.php @@ -0,0 +1,35 @@ +extend( + 'check', + function ($attribute, $value, $parameters) { + return $this->checkToken($value); + }, + trans('messages.error.invalid_apikey') + ); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'api_key' => 'required|string|check', + ]; + } +} diff --git a/app/Http/Requests/Portal/InvoiceConfirm.php b/app/Http/Requests/Portal/InvoiceConfirm.php new file mode 100644 index 00000000..7785d7ec --- /dev/null +++ b/app/Http/Requests/Portal/InvoiceConfirm.php @@ -0,0 +1,20 @@ + 'required|string|payment_method', + ]; + } +} diff --git a/app/Http/Requests/Portal/InvoicePayment.php b/app/Http/Requests/Portal/InvoicePayment.php new file mode 100644 index 00000000..dfe9887b --- /dev/null +++ b/app/Http/Requests/Portal/InvoicePayment.php @@ -0,0 +1,20 @@ + 'required|string|payment_method', + ]; + } +} diff --git a/app/Http/Requests/Portal/InvoiceShow.php b/app/Http/Requests/Portal/InvoiceShow.php new file mode 100644 index 00000000..7ac751f5 --- /dev/null +++ b/app/Http/Requests/Portal/InvoiceShow.php @@ -0,0 +1,39 @@ +guest()) { + return true; + } + + // Allow admin to see signed invoice + if (user()->can('read-sales-invoices')) { + return true; + } + + return $this->invoice->contact_id == user()->contact->id; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + // + ]; + } +} diff --git a/app/Http/Requests/Portal/PaymentShow.php b/app/Http/Requests/Portal/PaymentShow.php new file mode 100644 index 00000000..2cd1bd6a --- /dev/null +++ b/app/Http/Requests/Portal/PaymentShow.php @@ -0,0 +1,39 @@ +guest()) { + return true; + } + + // Allow admin to see signed invoice + if (user()->can('read-banking-transactions')) { + return true; + } + + return $this->payment->contact_id == user()->contact->id; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + // + ]; + } +} diff --git a/app/Http/Requests/Portal/Profile.php b/app/Http/Requests/Portal/Profile.php new file mode 100644 index 00000000..f469d2e0 --- /dev/null +++ b/app/Http/Requests/Portal/Profile.php @@ -0,0 +1,70 @@ +getAttribute('id'); + + $picture = 'nullable'; + + if ($this->files->get('picture')) { + $picture = 'mimes:' . config('filesystems.mimes') + . '|between:0,' . config('filesystems.max_size') * 1024 + . '|dimensions:max_width=' . config('filesystems.max_width') . ',max_height=' . config('filesystems.max_height'); + } + + $email = 'required|email:rfc,dns|unique:users,email,' . $id . ',id,deleted_at,NULL'; + + if (user()->contact) { + $email .= '|unique:contacts,NULL,' + . user()->contact->id . ',id' + . ',company_id,' . company_id() + . ',type,customer' + . ',deleted_at,NULL'; + } + + $change_password = $this->request->get('change_password') == true || $this->request->get('change_password') != null; + + $current_password = $change_password ? '|current_password' : ''; + $password = $change_password ? '|confirmed' : ''; + + return [ + 'name' => 'required|string', + 'email' => $email, + 'current_password' => 'required_if:change_password,true' . $current_password, + 'password' => 'required_if:change_password,true' . $password, + 'picture' => $picture, + 'tax_number' => 'nullable|string|max:50', + 'phone' => 'nullable|string|max:20', + 'address' => 'nullable|string|max:1000', + 'city' => 'nullable|string|max:100', + 'state' => 'nullable|string|max:100', + 'zip_code' => 'nullable|string|max:20', + 'country' => 'nullable|string|size:2', + ]; + } + + public function messages() + { + $picture_dimensions = trans('validation.custom.invalid_dimension', [ + 'attribute' => Str::lower(trans_choice('general.pictures', 1)), + 'width' => config('filesystems.max_width'), + 'height' => config('filesystems.max_height'), + ]); + + return [ + 'picture.dimensions' => $picture_dimensions, + ]; + } +} diff --git a/app/Http/Requests/Setting/Category.php b/app/Http/Requests/Setting/Category.php new file mode 100644 index 00000000..42842798 --- /dev/null +++ b/app/Http/Requests/Setting/Category.php @@ -0,0 +1,30 @@ +keys(); + + $type = $this->request->get('type'); + $config = config('type.category.' . $type, []); + $code_hidden = !empty($config['hide']) && in_array('code', $config['hide']); + $code = $code_hidden ? 'nullable|string' : 'required|string'; + + return [ + 'name' => 'required|string', + 'code' => $code, + 'type' => 'required|string|in:' . $types->implode(','), + 'color' => 'required|string|colour', + ]; + } +} diff --git a/app/Http/Requests/Setting/Currency.php b/app/Http/Requests/Setting/Currency.php new file mode 100644 index 00000000..33000d5c --- /dev/null +++ b/app/Http/Requests/Setting/Currency.php @@ -0,0 +1,37 @@ +getMethod(), ['PATCH', 'PUT'])) { + $id = is_numeric($this->currency) ? $this->currency : $this->currency->getAttribute('id'); + } else { + $id = null; + } + + // Get company id + $company_id = (int) $this->request->get('company_id'); + + return [ + 'name' => 'required|string', + 'code' => 'required|string|currency_code|unique:currencies,NULL,' . ($id ?? 'null') . ',id,company_id,' . $company_id . ',deleted_at,NULL', + 'rate' => 'required|gt:0', + 'enabled' => 'integer|boolean', + 'default_currency' => 'nullable|boolean', + 'decimal_mark' => 'nullable|string|different:thousands_separator|regex:/^[A-Za-z.,_\s-]+$/', + 'symbol_first' => 'nullable|boolean', + 'thousands_separator' => 'nullable|different:decimal_mark|regex:/^[A-Za-z.,_\s-]+$/', + ]; + } +} diff --git a/app/Http/Requests/Setting/EmailTemplate.php b/app/Http/Requests/Setting/EmailTemplate.php new file mode 100644 index 00000000..c0f7045b --- /dev/null +++ b/app/Http/Requests/Setting/EmailTemplate.php @@ -0,0 +1,21 @@ + 'required|string', + 'body' => 'required|string', + ]; + } +} diff --git a/app/Http/Requests/Setting/Module.php b/app/Http/Requests/Setting/Module.php new file mode 100644 index 00000000..f860592f --- /dev/null +++ b/app/Http/Requests/Setting/Module.php @@ -0,0 +1,34 @@ +request->get('module_alias')); + + if (!$fields = $module->get('settings')) { + return $rules; + } + + foreach ($fields as $field) { + if (empty($field['rules'])) { + continue; + } + + $rules[$field['name']] = $field['rules']; + } + + return $rules; + } +} diff --git a/app/Http/Requests/Setting/Setting.php b/app/Http/Requests/Setting/Setting.php new file mode 100644 index 00000000..cd06fa4b --- /dev/null +++ b/app/Http/Requests/Setting/Setting.php @@ -0,0 +1,75 @@ +request->get('_prefix', null); + + switch ($prefix) { + case 'company': + $logo = 'mimes:' . config('filesystems.mimes') + . '|between:0,' . config('filesystems.max_size') * 1024 + . '|dimensions:max_width=' . config('filesystems.max_width') . ',max_height=' . config('filesystems.max_height'); + + $rules = [ + 'name' => 'required|string', + 'email' => 'required|email', + 'logo' => $logo, + ]; + + break; + case 'default': + $rules = [ + 'account' => 'required|string', + 'currency' => 'required|string|currency', + 'locale' => 'required|string', + 'expense_category' => 'required|integer', + 'income_category' => 'required|integer', + 'payment_method' => 'required|string|payment_method', + 'address_format' => 'required|string', + ]; + + break; + } + + if ($this->request->has('number_digit')) { + $rules['number_digit'] = 'required|integer|min:1|max:20'; + } + + if ($this->request->has('number_next')) { + $rules['number_next'] = 'required|integer'; + } + + if ($this->request->has('color')) { + $rules['color'] = 'required|string|colour'; + } + + return $rules; + } + + public function messages() + { + $logo_dimensions = trans('validation.custom.invalid_dimension', [ + 'attribute' => Str::lower(trans('settings.company.logo')), + 'width' => config('filesystems.max_width'), + 'height' => config('filesystems.max_height'), + ]); + + return [ + 'logo.dimensions' => $logo_dimensions, + ]; + } +} diff --git a/app/Http/Requests/Setting/Tax.php b/app/Http/Requests/Setting/Tax.php new file mode 100644 index 00000000..dff5b8cb --- /dev/null +++ b/app/Http/Requests/Setting/Tax.php @@ -0,0 +1,40 @@ +getMethod(), ['PATCH', 'PUT'])) { + $id = is_numeric($this->tax) ? $this->tax : $this->tax->getAttribute('id'); + $enabled = 'integer|boolean'; + } else { + $id = null; + $enabled = 'nullable'; + } + + $company_id = (int) $this->request->get('company_id', company_id()); + + $type = 'required|string|in:fixed,normal,inclusive,withholding,compound'; + + if (!empty($this->request->get('type')) && $this->request->get('type') == 'compound') { + $type .= '|unique:taxes,NULL,' . ($id ?? 'null') . ',id,company_id,' . $company_id . ',type,compound,deleted_at,NULL'; + } + + return [ + 'name' => 'required|string', + 'rate' => 'required|numeric|min:0|max:100', + 'type' => $type, + 'enabled' => $enabled, + ]; + } +} diff --git a/app/Http/Requests/Wizard/Company.php b/app/Http/Requests/Wizard/Company.php new file mode 100644 index 00000000..775f93f4 --- /dev/null +++ b/app/Http/Requests/Wizard/Company.php @@ -0,0 +1,67 @@ +extend( + 'check', + function ($attribute, $value, $parameters) { + return $this->checkToken($value); + }, + trans('messages.error.invalid_apikey') + ); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + $logo = 'nullable'; + + if ($this->files->get('logo')) { + $logo = 'mimes:' . config('filesystems.mimes') + . '|between:0,' . config('filesystems.max_size') * 1024 + . '|dimensions:max_width=' . config('filesystems.max_width') . ',max_height=' . config('filesystems.max_height'); + } + + $rules = [ + 'logo' => $logo, + ]; + + if (! setting('apps.api_key', false) && ! empty($this->request->get('api_key'))) { + $rules['api_key'] = 'string|check'; + } + + if (setting('apps.api_key', false) && (setting('apps.api_key', false) != $this->request->get('api_key'))) { + $rules['api_key'] = 'string|check'; + } + + return $rules; + } + + public function messages() + { + $logo_dimensions = trans('validation.custom.invalid_dimension', [ + 'attribute' => Str::lower(trans('settings.company.logo')), + 'width' => config('filesystems.max_width'), + 'height' => config('filesystems.max_height'), + ]); + + return [ + 'logo.dimensions' => $logo_dimensions, + ]; + } +} diff --git a/app/Http/Resources/Auth/Owner.php b/app/Http/Resources/Auth/Owner.php new file mode 100644 index 00000000..6939d4ba --- /dev/null +++ b/app/Http/Resources/Auth/Owner.php @@ -0,0 +1,31 @@ +can('read-auth-users') || empty($owner)) { + return null; + } + + return new self($owner); + } + + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'name' => $this->name, + 'email' => $this->email, + ]; + } +} diff --git a/app/Http/Resources/Auth/Role.php b/app/Http/Resources/Auth/Role.php new file mode 100644 index 00000000..3122d8fa --- /dev/null +++ b/app/Http/Resources/Auth/Role.php @@ -0,0 +1,28 @@ + $this->id, + 'name' => $this->display_name, + 'code' => $this->name, + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + ]; + } +} diff --git a/app/Http/Resources/Auth/User.php b/app/Http/Resources/Auth/User.php new file mode 100644 index 00000000..b4577f7b --- /dev/null +++ b/app/Http/Resources/Auth/User.php @@ -0,0 +1,36 @@ + $this->id, + 'name' => $this->name, + 'email' => $this->email, + 'locale' => $this->locale, + 'landing_page' => $this->landing_page, + 'enabled' => $this->enabled, + 'last_logged_in_at' => $this->last_logged_in_at ? $this->last_logged_in_at->toIso8601String() : '', + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + 'companies' => [static::$wrap => Company::collection($this->companies)], + 'roles' => [static::$wrap => Role::collection($this->roles)], + ]; + } +} diff --git a/app/Http/Resources/Banking/Account.php b/app/Http/Resources/Banking/Account.php new file mode 100644 index 00000000..06e56809 --- /dev/null +++ b/app/Http/Resources/Banking/Account.php @@ -0,0 +1,39 @@ + $this->id, + 'company_id' => $this->company_id, + 'type' => $this->type, + 'name' => $this->name, + 'number' => $this->number, + 'currency_code' => $this->currency_code, + 'opening_balance' => $this->opening_balance, + 'opening_balance_formatted' => money((float) ($this->opening_balance ?? 0), $this->currency_code)->format(), + 'current_balance' => $this->balance, + 'current_balance_formatted' => money((float) ($this->balance ?? 0), $this->currency_code)->format(), + 'bank_name' => $this->bank_name, + 'bank_phone' => $this->bank_phone, + 'bank_address' => $this->bank_address, + 'enabled' => $this->enabled, + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + ]; + } +} diff --git a/app/Http/Resources/Banking/Reconciliation.php b/app/Http/Resources/Banking/Reconciliation.php new file mode 100644 index 00000000..d5da042a --- /dev/null +++ b/app/Http/Resources/Banking/Reconciliation.php @@ -0,0 +1,35 @@ + $this->id, + 'company_id' => $this->company_id, + 'account_id' => $this->account_id, + 'started_at' => $this->started_at->toIso8601String(), + 'ended_at' => $this->ended_at->toIso8601String(), + 'closing_balance' => $this->closing_balance, + 'closing_balance_formatted' => money($this->closing_balance)->format(), + 'reconciled' => $this->reconciled, + 'created_at' => $this->created_at->toIso8601String(), + 'updated_at' => $this->updated_at->toIso8601String(), + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + 'account' => new Account($this->account), + ]; + } +} diff --git a/app/Http/Resources/Banking/Transaction.php b/app/Http/Resources/Banking/Transaction.php new file mode 100644 index 00000000..31fe4789 --- /dev/null +++ b/app/Http/Resources/Banking/Transaction.php @@ -0,0 +1,54 @@ + $this->id, + 'number' => $this->number, + 'company_id' => $this->company_id, + 'type' => $this->type, + 'account_id' => $this->account_id, + 'paid_at' => $this->paid_at->toIso8601String(), + 'amount' => $this->amount, + 'amount_formatted' => money($this->amount, $this->currency_code)->format(), + 'currency_code' => $this->currency_code, + 'currency_rate' => $this->currency_rate, + 'document_id' => $this->document_id, + 'contact_id' => $this->contact_id, + 'description' => $this->description, + 'category_id' => $this->category_id, + 'payment_method' => $this->payment_method, + 'reference' => $this->reference, + 'parent_id' => $this->parent_id, + 'split_id' => $this->split_id, + 'attachment' => $this->attachment, + 'created_at' => $this->created_at->toIso8601String(), + 'updated_at' => $this->updated_at->toIso8601String(), + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + 'account' => new Account($this->account), + 'category' => new Category($this->category), + 'currency' => new Currency($this->currency), + 'contact' => new Contact($this->contact), + 'taxes' => [static::$wrap => TransactionTax::collection($this->taxes)], + ]; + } +} diff --git a/app/Http/Resources/Banking/TransactionTax.php b/app/Http/Resources/Banking/TransactionTax.php new file mode 100644 index 00000000..d4d34379 --- /dev/null +++ b/app/Http/Resources/Banking/TransactionTax.php @@ -0,0 +1,34 @@ + $this->id, + 'company_id' => $this->company_id, + 'transaction_id' => $this->transaction_id, + 'tax_id' => $this->tax_id, + 'name' => $this->name, + 'amount' => $this->amount, + 'amount_formatted' => money($this->amount, $this->currency_code)->format(), + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + 'tax' => new Tax($this->tax), + ]; + } +} diff --git a/app/Http/Resources/Banking/Transfer.php b/app/Http/Resources/Banking/Transfer.php new file mode 100644 index 00000000..58e14bc6 --- /dev/null +++ b/app/Http/Resources/Banking/Transfer.php @@ -0,0 +1,38 @@ +expense_transaction; + $income_transaction = $this->income_transaction; + + return [ + 'id' => $this->id, + 'company_id' => $this->company_id, + 'from_account' => $expense_transaction->account->name, + 'from_account_id' => $expense_transaction->account->id, + 'to_account' => $income_transaction->account->name, + 'to_account_id' => $income_transaction->account->id, + 'amount' => $expense_transaction->amount, + 'amount_formatted' => money($expense_transaction->amount, $expense_transaction->currency_code)->format(), + 'currency_code' => $expense_transaction->currency_code, + 'paid_at' => $expense_transaction->paid_at ? $expense_transaction->paid_at->toIso8601String() : '', + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + ]; + } +} diff --git a/app/Http/Resources/Common/Company.php b/app/Http/Resources/Common/Company.php new file mode 100644 index 00000000..3102ec08 --- /dev/null +++ b/app/Http/Resources/Common/Company.php @@ -0,0 +1,33 @@ + $this->id, + 'name' => $this->name, + 'email' => $this->email, + 'currency' => $this->currency, + 'domain' => $this->domain, + 'address' => $this->address, + 'logo' => $this->logo, + 'enabled' => $this->enabled, + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + ]; + } +} diff --git a/app/Http/Resources/Common/Contact.php b/app/Http/Resources/Common/Contact.php new file mode 100644 index 00000000..ab37967b --- /dev/null +++ b/app/Http/Resources/Common/Contact.php @@ -0,0 +1,40 @@ + $this->id, + 'company_id' => $this->company_id, + 'user_id' => $this->user_id, + 'type' => $this->type, + 'name' => $this->name, + 'email' => $this->email, + 'tax_number' => $this->tax_number, + 'phone' => $this->phone, + 'address' => $this->address, + 'website' => $this->website, + 'currency_code' => $this->currency_code, + 'enabled' => $this->enabled, + 'reference' => $this->reference, + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + 'contact_persons' => [static::$wrap => ContactPerson::collection($this->contact_persons)], + ]; + } +} diff --git a/app/Http/Resources/Common/ContactPerson.php b/app/Http/Resources/Common/ContactPerson.php new file mode 100644 index 00000000..6e1cad59 --- /dev/null +++ b/app/Http/Resources/Common/ContactPerson.php @@ -0,0 +1,32 @@ + $this->id, + 'company_id' => $this->company_id, + 'type' => $this->type, + 'contact_id' => $this->contact_id, + 'name' => $this->name, + 'email' => $this->email, + 'phone' => $this->phone, + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + ]; + } +} diff --git a/app/Http/Resources/Common/Dashboard.php b/app/Http/Resources/Common/Dashboard.php new file mode 100644 index 00000000..cfb17aa5 --- /dev/null +++ b/app/Http/Resources/Common/Dashboard.php @@ -0,0 +1,36 @@ +widgets->filter(function ($widget) { + return Widgets::canShow($widget->class); + }); + + return [ + 'id' => $this->id, + 'company_id' => $this->company_id, + 'name' => $this->name, + 'enabled' => $this->enabled, + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + 'widgets' => [static::$wrap => Widget::collection($widgets)], + ]; + } +} diff --git a/app/Http/Resources/Common/Item.php b/app/Http/Resources/Common/Item.php new file mode 100644 index 00000000..a46a3219 --- /dev/null +++ b/app/Http/Resources/Common/Item.php @@ -0,0 +1,41 @@ + $this->id, + 'company_id' => $this->company_id, + 'type' => $this->type, + 'name' => $this->name, + 'description' => $this->description, + 'sale_price' => $this->sale_price, + 'sale_price_formatted' => money((double) $this->sale_price)->format(), + 'purchase_price' => $this->purchase_price, + 'purchase_price_formatted' => money((double) $this->purchase_price)->format(), + 'category_id' => $this->category_id, + 'picture' => $this->picture, + 'enabled' => $this->enabled, + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + 'taxes' => [static::$wrap => ItemTax::collection($this->taxes)], + 'category' => new Category($this->category), + ]; + } +} diff --git a/app/Http/Resources/Common/ItemTax.php b/app/Http/Resources/Common/ItemTax.php new file mode 100644 index 00000000..16ab6c6c --- /dev/null +++ b/app/Http/Resources/Common/ItemTax.php @@ -0,0 +1,31 @@ + $this->id, + 'company_id' => $this->company_id, + 'item_id' => $this->item_id, + 'tax_id' => $this->tax_id, + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + 'tax' => new Tax($this->tax), + ]; + } +} diff --git a/app/Http/Resources/Common/Report.php b/app/Http/Resources/Common/Report.php new file mode 100644 index 00000000..9b8c7d0e --- /dev/null +++ b/app/Http/Resources/Common/Report.php @@ -0,0 +1,54 @@ + $this->id, + 'company_id' => $this->company_id, + 'class' => $this->class, + 'name' => $this->name, + 'description' => $this->description, + 'settings' => $this->settings, + 'data' => $this->getReportData(), + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + ]; + } + + protected function getReportData() + { + if (! Utility::canShow($this->class)) { + return []; + } + + $report = Utility::getClassInstance($this); + + if (empty($report)) { + return []; + } + + $unset_attributes = ['model', 'views', 'loaded', 'column_name_width', 'column_value_width']; + + foreach ($unset_attributes as $attribute) { + unset($report->$attribute); + } + + return $report; + } +} diff --git a/app/Http/Resources/Common/Widget.php b/app/Http/Resources/Common/Widget.php new file mode 100644 index 00000000..e6df1e3a --- /dev/null +++ b/app/Http/Resources/Common/Widget.php @@ -0,0 +1,33 @@ + $this->id, + 'company_id' => $this->company_id, + 'dashboard_id' => $this->dashboard_id, + 'class' => $this->class, + 'name' => $this->name, + 'sort' => $this->sort, + 'settings' => $this->settings, + 'data' => show_widget($this), + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + ]; + } +} diff --git a/app/Http/Resources/Document/Document.php b/app/Http/Resources/Document/Document.php new file mode 100644 index 00000000..a435e5ff --- /dev/null +++ b/app/Http/Resources/Document/Document.php @@ -0,0 +1,77 @@ + $this->id, + 'company_id' => $this->company_id, + 'type' => $this->type, + 'document_number' => $this->document_number, + 'order_number' => $this->order_number, + 'status' => $this->status, + 'issued_at' => $this->issued_at ? $this->issued_at->toIso8601String() : '', + 'due_at' => $this->due_at ? $this->due_at->toIso8601String() : '', + 'amount' => $this->amount, + 'amount_formatted' => money($this->amount, $this->currency_code)->format(), + 'category_id' => $this->category_id, + 'currency_code' => $this->currency_code, + 'currency_rate' => $this->currency_rate, + 'contact_id' => $this->contact_id, + 'contact_name' => $this->contact_name, + 'contact_email' => $this->contact_email, + 'contact_tax_number' => $this->contact_tax_number, + 'contact_phone' => $this->contact_phone, + 'contact_address' => $this->contact_address, + 'contact_city' => $this->contact_city, + 'contact_zip_code' => $this->contact_zip_code, + 'contact_state' => $this->contact_state, + 'contact_country' => $this->contact_country, + 'notes' => $this->notes, + 'attachment' => $this->attachment, + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + 'category' => new Category($this->category), + 'currency' => new Currency($this->currency), + 'contact' => new Contact($this->contact), + 'histories' => [static::$wrap => DocumentHistory::collection($this->histories)], + 'items' => [static::$wrap => DocumentItem::collection($this->items)], + 'item_taxes' => [static::$wrap => DocumentItemTax::collection($this->item_taxes)], + 'totals' => [static::$wrap => DocumentTotal::collection($this->totals)], + 'transactions' => [static::$wrap => Transaction::collection($this->transactions)], + ]; + + $event = new DocumentResourceShowing($this->resource); + + event($event); + + if (! empty($event->resources)) { + $resources = array_merge($resources, $event->resources); + } + + return $resources; + } +} diff --git a/app/Http/Resources/Document/DocumentHistory.php b/app/Http/Resources/Document/DocumentHistory.php new file mode 100644 index 00000000..7eb199cd --- /dev/null +++ b/app/Http/Resources/Document/DocumentHistory.php @@ -0,0 +1,32 @@ + $this->id, + 'company_id' => $this->company_id, + 'type' => $this->type, + 'document_id' => $this->document_id, + 'status' => $this->status, + 'notify' => $this->notify, + 'description' => $this->description, + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + ]; + } +} diff --git a/app/Http/Resources/Document/DocumentItem.php b/app/Http/Resources/Document/DocumentItem.php new file mode 100644 index 00000000..2b651654 --- /dev/null +++ b/app/Http/Resources/Document/DocumentItem.php @@ -0,0 +1,38 @@ + $this->id, + 'company_id' => $this->company_id, + 'type' => $this->type, + 'document_id' => $this->document_id, + 'item_id' => $this->item_id, + 'name' => $this->name, + 'description' => $this->description, + 'price' => $this->price, + 'price_formatted' => money($this->price, $this->document->currency_code)->format(), + 'total' => $this->total, + 'total_formatted' => money($this->total, $this->document->currency_code)->format(), + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + 'taxes' => [static::$wrap => DocumentItemTax::collection($this->taxes)], + ]; + } +} diff --git a/app/Http/Resources/Document/DocumentItemTax.php b/app/Http/Resources/Document/DocumentItemTax.php new file mode 100644 index 00000000..a89b9f27 --- /dev/null +++ b/app/Http/Resources/Document/DocumentItemTax.php @@ -0,0 +1,36 @@ + $this->id, + 'company_id' => $this->company_id, + 'type' => $this->type, + 'document_id' => $this->document_id, + 'document_item_id' => $this->document_item_id, + 'tax_id' => $this->tax_id, + 'name' => $this->name, + 'amount' => $this->amount, + 'amount_formatted' => money($this->amount, $this->document->currency_code)->format(), + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + 'tax' => new Tax($this->tax), + ]; + } +} diff --git a/app/Http/Resources/Document/DocumentTotal.php b/app/Http/Resources/Document/DocumentTotal.php new file mode 100644 index 00000000..d23b217b --- /dev/null +++ b/app/Http/Resources/Document/DocumentTotal.php @@ -0,0 +1,34 @@ + $this->id, + 'company_id' => $this->company_id, + 'type' => $this->type, + 'document_id' => $this->document_id, + 'code' => $this->code, + 'name' => $this->name, + 'amount' => $this->amount, + 'amount_formatted' => money($this->amount, $this->document->currency_code)->format(), + 'sort_order' => $this->sort_order, + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + ]; + } +} diff --git a/app/Http/Resources/Setting/Category.php b/app/Http/Resources/Setting/Category.php new file mode 100644 index 00000000..6b0ff4c6 --- /dev/null +++ b/app/Http/Resources/Setting/Category.php @@ -0,0 +1,34 @@ + $this->id, + 'company_id' => $this->company_id, + 'code' => $this->code, + 'name' => $this->name, + 'type' => $this->type, + 'color' => $this->color, + 'description' => $this->description, + 'enabled' => $this->enabled, + 'parent_id' => $this->parent_id, + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + ]; + } +} diff --git a/app/Http/Resources/Setting/Currency.php b/app/Http/Resources/Setting/Currency.php new file mode 100644 index 00000000..c7b0f31d --- /dev/null +++ b/app/Http/Resources/Setting/Currency.php @@ -0,0 +1,36 @@ + $this->id, + 'company_id' => $this->company_id, + 'name' => $this->name, + 'code' => $this->code, + 'rate' => $this->rate, + 'enabled' => $this->enabled, + 'precision' => $this->precision, + 'symbol' => $this->symbol, + 'symbol_first' => $this->symbol_first, + 'decimal_mark' => $this->decimal_mark, + 'thousands_separator' => $this->thousands_separator, + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + ]; + } +} diff --git a/app/Http/Resources/Setting/Setting.php b/app/Http/Resources/Setting/Setting.php new file mode 100644 index 00000000..79df843e --- /dev/null +++ b/app/Http/Resources/Setting/Setting.php @@ -0,0 +1,24 @@ + $this->id, + 'company_id' => $this->company_id, + 'key' => $this->key, + 'value' => $this->value, + ]; + } +} diff --git a/app/Http/Resources/Setting/Tax.php b/app/Http/Resources/Setting/Tax.php new file mode 100644 index 00000000..1255e827 --- /dev/null +++ b/app/Http/Resources/Setting/Tax.php @@ -0,0 +1,30 @@ + $this->id, + 'company_id' => $this->company_id, + 'name' => $this->name, + 'rate' => $this->rate, + 'enabled' => $this->enabled, + 'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '', + 'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '', + 'created_from' => $this->created_from, + 'owner' => Owner::from($this->owner), + ]; + } +} diff --git a/app/Http/Responses/Common/Items.php b/app/Http/Responses/Common/Items.php new file mode 100644 index 00000000..6437e644 --- /dev/null +++ b/app/Http/Responses/Common/Items.php @@ -0,0 +1,10 @@ +route(); + + if (empty($route)) { + return; + } + + /** @var Customers|Vendors $controller */ + $controller = $route->getController(); + + $view->with(['type' => $controller->type ?? '']); + } +} diff --git a/app/Http/ViewComposers/DocumentRecurring.php b/app/Http/ViewComposers/DocumentRecurring.php new file mode 100644 index 00000000..b2baa106 --- /dev/null +++ b/app/Http/ViewComposers/DocumentRecurring.php @@ -0,0 +1,40 @@ +route(); + + if (empty($route)) { + return; + } + + /** @var Invoices|Bills|PortalInvoices $controller */ + $controller = $route->getController(); + + $type = $controller->type ?? ''; + + if (! Str::contains($type, 'recurring')) { + return; + } + + $view->with([ + 'type' => $type, + ]); + + // Override the whole file + $view->setPath(view('components.documents.form.recurring_metadata', compact('type'))->getPath()); + } +} diff --git a/app/Http/ViewComposers/DocumentType.php b/app/Http/ViewComposers/DocumentType.php new file mode 100644 index 00000000..75cbf665 --- /dev/null +++ b/app/Http/ViewComposers/DocumentType.php @@ -0,0 +1,31 @@ +route(); + + if (empty($route)) { + return; + } + + /** @var Invoices|Bills|PortalInvoices $controller */ + $controller = $route->getController(); + + $view->with(['type' => $controller->type ?? '']); + } +} diff --git a/app/Http/ViewComposers/PlanLimits.php b/app/Http/ViewComposers/PlanLimits.php new file mode 100644 index 00000000..8500f27a --- /dev/null +++ b/app/Http/ViewComposers/PlanLimits.php @@ -0,0 +1,77 @@ +route()->getName(); + + $user_routes = [ + 'users.index', + 'users.show', + 'users.create', + ]; + + if (in_array($route_name, $user_routes)) { + $user_limit = $this->getUserLimitOfPlan(); + + if (! $user_limit->view_status) { + $message = $user_limit->message; + } + } + + $company_routes = [ + 'companies.index', + 'companies.show', + 'companies.create', + ]; + + if (in_array($route_name, $company_routes)) { + $company_limit = $this->getCompanyLimitOfPlan(); + + if (! $company_limit->view_status) { + $message = $company_limit->message; + } + } + + $invoice_routes = [ + 'invoices.index', + 'invoices.show', + 'invoices.create', + ]; + + if (in_array($route_name, $invoice_routes)) { + $invoice_limit = $this->getInvoiceLimitOfPlan(); + + if (! $invoice_limit->view_status) { + $message = $invoice_limit->message; + } + } + + if (empty($message)) { + return; + } + + if (! setting('apps.api_key')) { + $view->getFactory()->startPush('header_start', view('components.alert.call')->with([ + 'type' => 'error', + 'title' => trans('general.action_required'), + 'message' => trans('messages.error.empty_apikey', ['url' => route('apps.api-key.create')]), + ])); + } + + $view->getFactory()->startPush('header_start', view('components.alert.call')->with([ + 'type' => 'warning', + 'title' => trans('general.action_required'), + 'message' => $message, + ])); + } +} diff --git a/app/Http/ViewComposers/ReadOnlyNotification.php b/app/Http/ViewComposers/ReadOnlyNotification.php new file mode 100644 index 00000000..5e4fc2f1 --- /dev/null +++ b/app/Http/ViewComposers/ReadOnlyNotification.php @@ -0,0 +1,23 @@ +getData()['notifications']; + + $notifications[] = view('components.read-only'); + + $view->with([ + 'notifications' => $notifications, + ]); + } +} diff --git a/app/Imports/Banking/RecurringTransactions.php b/app/Imports/Banking/RecurringTransactions.php new file mode 100644 index 00000000..22670959 --- /dev/null +++ b/app/Imports/Banking/RecurringTransactions.php @@ -0,0 +1,18 @@ + new Base(), + 'recurring' => new Recurring(), + ]; + } +} diff --git a/app/Imports/Banking/Sheets/Recurring.php b/app/Imports/Banking/Sheets/Recurring.php new file mode 100644 index 00000000..752acd72 --- /dev/null +++ b/app/Imports/Banking/Sheets/Recurring.php @@ -0,0 +1,37 @@ +number($row['transaction_number'])->pluck('id')->first(); + + return $row; + } +} diff --git a/app/Imports/Banking/Sheets/RecurringTransactions.php b/app/Imports/Banking/Sheets/RecurringTransactions.php new file mode 100644 index 00000000..de075884 --- /dev/null +++ b/app/Imports/Banking/Sheets/RecurringTransactions.php @@ -0,0 +1,52 @@ +getRealTypeTransaction($row['type']); + $contact_type = config('type.transaction.' . $transaction_type . '.contact_type', $transaction_type == 'income' ? 'customer' : 'vendor'); + + $row['currency_code'] = $this->getCurrencyCode($row); + $row['account_id'] = $this->getAccountId($row); + $row['category_id'] = $this->getCategoryId($row, $transaction_type); + $row['contact_id'] = $this->getContactId($row, $contact_type); + + if ($transaction_type == 'income') { + $row['document_id'] = Document::invoiceRecurring()->number($row['invoice_bill_number'])->pluck('id')->first(); + } else { + $row['document_id'] = Document::billRecurring()->number($row['invoice_bill_number'])->pluck('id')->first(); + } + + return $row; + } +} diff --git a/app/Imports/Banking/Transactions.php b/app/Imports/Banking/Transactions.php new file mode 100644 index 00000000..ce0b9f5d --- /dev/null +++ b/app/Imports/Banking/Transactions.php @@ -0,0 +1,61 @@ +getRealTypeTransaction($row['type']); + $contact_type = config('type.transaction.' . $real_type . '.contact_type', $real_type == 'income' ? 'customer' : 'vendor'); + + $row['currency_code'] = $this->getCurrencyCode($row); + $row['account_id'] = $this->getAccountId($row); + $row['category_id'] = $this->getCategoryId($row, $real_type); + $row['contact_id'] = $this->getContactId($row, $contact_type); + $row['document_id'] = $this->getDocumentId($row); + $row['parent_id'] = $this->getParentId($row) ?? 0; + $row['payment_method'] = $this->getPaymentMethod($row); + + return $row; + } + + public function prepareRules($rules): array + { + $rules['number'] = 'required|string'; + $rules['type'] = 'required|string'; + //$rules['currency_rate'] = 'required|gt:0'; + + return $rules; + } +} diff --git a/app/Imports/Banking/Transfers.php b/app/Imports/Banking/Transfers.php new file mode 100644 index 00000000..5f652f9b --- /dev/null +++ b/app/Imports/Banking/Transfers.php @@ -0,0 +1,170 @@ +normalizeTransferredAt($row['transferred_at'] ?? null); + $row['from_currency_code'] = $this->getFromCurrencyCode($row); + $row['to_currency_code'] = $this->getToCurrencyCode($row); + $row['from_account_id'] = $this->getFromAccountId($row); + $row['to_account_id'] = $this->getToAccountId($row); + $row['expense_transaction_id'] = $this->getExpenseTransactionId($row); + $row['income_transaction_id'] = $this->getIncomeTransactionId($row); + + return $row; + } + + public function rules(): array + { + return [ + 'from_account_id' => 'required|integer', + 'from_currency_code' => 'required|string|currency', + 'from_currency_rate' => 'required|gt:0', + 'to_account_id' => 'required|integer', + 'to_currency_code' => 'required|string|currency', + 'to_currency_rate' => 'required|gt:0', + 'amount' => 'required|amount', + 'transferred_at' => 'required|date_format:Y-m-d', + 'payment_method' => 'required|string', + ]; + } + + private function getExpenseTransactionId($row) + { + $expense_transaction = $this->dispatch(new CreateTransaction([ + 'company_id' => $row['company_id'], + 'number' => $this->getNextTransactionNumber(), + 'type' => Transaction::EXPENSE_TRANSFER_TYPE, + 'account_id' => $row['from_account_id'], + 'paid_at' => $row['transferred_at'], + 'currency_code' => $row['from_currency_code'], + 'currency_rate' => $row['from_currency_rate'], + 'amount' => $row['amount'], + 'contact_id' => 0, + 'description' => $row['description'], + 'category_id' => $this->getTransferCategoryId(), + 'payment_method' => $row['payment_method'], + 'reference' => $row['reference'], + 'created_by' => $row['created_by'], + ])); + + return $expense_transaction->id; + } + + private function getIncomeTransactionId($row) + { + $amount = $row['amount']; + + // Convert amount if not same currency + if ($row['from_currency_code'] !== $row['to_currency_code']) { + $amount = $this->convertBetween( + $amount, + $row['from_currency_code'], + $row['from_currency_rate'], + $row['to_currency_code'], + $row['to_currency_rate'] + ); + } + + $income_transaction = $this->dispatch(new CreateTransaction([ + 'company_id' => $row['company_id'], + 'number' => $this->getNextTransactionNumber(), + 'type' => Transaction::INCOME_TRANSFER_TYPE, + 'account_id' => $row['to_account_id'], + 'paid_at' => $row['transferred_at'], + 'currency_code' => $row['to_currency_code'], + 'currency_rate' => $row['to_currency_rate'], + 'amount' => $amount, + 'contact_id' => 0, + 'description' => $row['description'], + 'category_id' => $this->getTransferCategoryId(), + 'payment_method' => $row['payment_method'], + 'reference' => $row['reference'], + 'created_by' => $row['created_by'], + ])); + + return $income_transaction->id; + } + + private function getFromAccountId($row) + { + $row['account_id'] = $row['from_account_id'] ?? null; + $row['account_name'] = $row['from_account_name'] ?? null; + $row['account_number'] = $row['from_account_number'] ?? null; + $row['currency_code'] = $row['from_currency_code'] ?? null; + + return $this->getAccountId($row); + } + + private function getToAccountId($row) + { + $row['account_id'] = $row['to_account_id'] ?? null; + $row['account_name'] = $row['to_account_name'] ?? null; + $row['account_number'] = $row['to_account_number'] ?? null; + $row['currency_code'] = $row['to_currency_code'] ?? null; + + return $this->getAccountId($row); + } + + private function getFromCurrencyCode($row) + { + $row['currency_code'] = $row['from_currency_code'] ?? null; + $row['currency_rate'] = $row['from_currency_rate'] ?? null; + + return $this->getCurrencyCode($row); + } + + private function getToCurrencyCode($row) + { + $row['currency_code'] = $row['to_currency_code'] ?? null; + $row['currency_rate'] = $row['to_currency_rate'] ?? null; + + return $this->getCurrencyCode($row); + } + + protected function normalizeTransferredAt($value): ?string + { + if (empty($value)) { + return null; + } + + if (preg_match('/^\d{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2}:\d{2})?$/', $value)) { + return substr((string) $value, 0, 10); + } + + return Date::parseWithFallbackLocales($value, null, [$this->preferredLocale()])->format('Y-m-d'); + } +} diff --git a/app/Imports/Common/Items.php b/app/Imports/Common/Items.php new file mode 100644 index 00000000..4fe28bcf --- /dev/null +++ b/app/Imports/Common/Items.php @@ -0,0 +1,18 @@ + new Base(), + 'item_taxes' => new ItemTaxes(), + ]; + } +} diff --git a/app/Imports/Common/Sheets/ItemTaxes.php b/app/Imports/Common/Sheets/ItemTaxes.php new file mode 100644 index 00000000..f4a3bcac --- /dev/null +++ b/app/Imports/Common/Sheets/ItemTaxes.php @@ -0,0 +1,43 @@ +isEmpty($row, 'item_name')) { + return []; + } + + $row = parent::map($row); + + $row['item_id'] = $this->getItemId($row); + + $row['tax_id'] = $this->getTaxId($row); + + return $row; + } +} diff --git a/app/Imports/Common/Sheets/Items.php b/app/Imports/Common/Sheets/Items.php new file mode 100644 index 00000000..46fcaa82 --- /dev/null +++ b/app/Imports/Common/Sheets/Items.php @@ -0,0 +1,43 @@ +getCategoryId($row, 'item'); + + return $row; + } +} diff --git a/app/Imports/Purchases/Bills/Bills.php b/app/Imports/Purchases/Bills/Bills.php new file mode 100644 index 00000000..541d9889 --- /dev/null +++ b/app/Imports/Purchases/Bills/Bills.php @@ -0,0 +1,26 @@ + new Base(), + 'bill_items' => new BillItems(), + 'bill_item_taxes' => new BillItemTaxes(), + 'bill_histories' => new BillHistories(), + 'bill_totals' => new BillTotals(), + 'bill_transactions' => new BillTransactions(), + ]; + } +} diff --git a/app/Imports/Purchases/Bills/Sheets/BillHistories.php b/app/Imports/Purchases/Bills/Sheets/BillHistories.php new file mode 100644 index 00000000..724244c0 --- /dev/null +++ b/app/Imports/Purchases/Bills/Sheets/BillHistories.php @@ -0,0 +1,59 @@ +isEmpty($row, 'bill_number')) { + return []; + } + + $row['bill_number'] = (string) $row['bill_number']; + + $row = parent::map($row); + + $row['document_id'] = (int) Document::bill()->number($row['bill_number'])->pluck('id')->first(); + + $row['notify'] = (int) $row['notify']; + + $row['type'] = Document::BILL_TYPE; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['bill_number'] = 'required|string'; + + unset($rules['bill_id']); + + return $rules; + } +} diff --git a/app/Imports/Purchases/Bills/Sheets/BillItemTaxes.php b/app/Imports/Purchases/Bills/Sheets/BillItemTaxes.php new file mode 100644 index 00000000..260093a8 --- /dev/null +++ b/app/Imports/Purchases/Bills/Sheets/BillItemTaxes.php @@ -0,0 +1,86 @@ +isEmpty($row, 'bill_number')) { + return []; + } + + $row['bill_number'] = (string) $row['bill_number']; + + $row = parent::map($row); + + $document = Document::with('items')->bill()->number($row['bill_number'])->first(); + + if (! $document) { + return []; + } + + $row['document_id'] = (int) $document->id; + + if (empty($row['document_item_id']) && !empty($row['item_name'])) { + $document_items_ids = $document->items->pluck('item_id')->toArray(); + + $item_id = Item::name($row['item_name'])->whereIn('id', $document_items_ids)->pluck('id')->first(); + + $row['document_item_id'] = DocumentItem::bill() + ->where('document_id', $row['document_id']) + ->where('item_id', $item_id) + ->pluck('id') + ->first(); + } + + $row['tax_id'] = $this->getTaxId($row); + + if (empty($row['name']) && !empty($row['item_name'])) { + $row['name'] = $row['item_name']; + } + + $row['amount'] = (double) $row['amount']; + + $row['type'] = Document::BILL_TYPE; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['bill_number'] = 'required|string'; + + unset($rules['bill_id']); + + return $rules; + } +} diff --git a/app/Imports/Purchases/Bills/Sheets/BillItems.php b/app/Imports/Purchases/Bills/Sheets/BillItems.php new file mode 100644 index 00000000..cf6cb074 --- /dev/null +++ b/app/Imports/Purchases/Bills/Sheets/BillItems.php @@ -0,0 +1,73 @@ +isEmpty($row, 'bill_number')) { + return []; + } + + $row['bill_number'] = (string) $row['bill_number']; + + $row = parent::map($row); + + $row['document_id'] = (int) Document::bill()->number($row['bill_number'])->pluck('id')->first(); + + if (empty($row['item_id']) && !empty($row['item_name'])) { + $row['item_id'] = $this->getItemIdFromName($row); + + $row['name'] = $row['item_name']; + } + + $row['description'] = !empty($row['item_description']) ? $row['item_description'] : ''; + + $row['tax'] = (double) $row['tax']; + $row['tax_id'] = 0; + $row['type'] = Document::BILL_TYPE; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['bill_number'] = 'required|string'; + + unset($rules['bill_id']); + + return $rules; + } +} diff --git a/app/Imports/Purchases/Bills/Sheets/BillTotals.php b/app/Imports/Purchases/Bills/Sheets/BillTotals.php new file mode 100644 index 00000000..2b0bee15 --- /dev/null +++ b/app/Imports/Purchases/Bills/Sheets/BillTotals.php @@ -0,0 +1,58 @@ +isEmpty($row, 'bill_number')) { + return []; + } + + $row['bill_number'] = (string) $row['bill_number']; + + $row = parent::map($row); + + $row['document_id'] = (int) Document::bill()->number($row['bill_number'])->pluck('id')->first(); + $row['type'] = Document::BILL_TYPE; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['bill_number'] = 'required|string'; + + unset($rules['bill_id']); + + return $rules; + } +} diff --git a/app/Imports/Purchases/Bills/Sheets/BillTransactions.php b/app/Imports/Purchases/Bills/Sheets/BillTransactions.php new file mode 100644 index 00000000..c962c6c2 --- /dev/null +++ b/app/Imports/Purchases/Bills/Sheets/BillTransactions.php @@ -0,0 +1,57 @@ +isEmpty($row, 'bill_number')) { + return []; + } + + $row['bill_number'] = (string) $row['bill_number']; + + $row = parent::map($row); + + $row['type'] = Model::EXPENSE_TYPE; + $row['account_id'] = $this->getAccountId($row); + $row['category_id'] = $this->getCategoryId($row, Category::EXPENSE_TYPE); + $row['contact_id'] = $this->getContactId($row, 'vendor'); + $row['currency_code'] = $this->getCurrencyCode($row); + $row['document_id'] = $this->getDocumentId($row); + $row['number'] = $row['transaction_number']; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['bill_number'] = 'required|string'; + + return $rules; + } +} diff --git a/app/Imports/Purchases/Bills/Sheets/Bills.php b/app/Imports/Purchases/Bills/Sheets/Bills.php new file mode 100644 index 00000000..5a0f441c --- /dev/null +++ b/app/Imports/Purchases/Bills/Sheets/Bills.php @@ -0,0 +1,70 @@ +isEmpty($row, 'bill_number')) { + return []; + } + + $row['bill_number'] = (string) $row['bill_number']; + + $row = parent::map($row); + + $country = array_search($row['contact_country'], trans('countries')); + + $row['document_number'] = $row['bill_number']; + $row['issued_at'] = $row['billed_at']; + $row['category_id'] = $this->getCategoryId($row, 'expense'); + $row['contact_id'] = $this->getContactId($row, 'vendor'); + $row['currency_code'] = $this->getCurrencyCode($row); + $row['type'] = Model::BILL_TYPE; + $row['contact_country'] = !empty($country) ? $country : null; + $row['title'] = $row['title'] ?? Model::BILL_TYPE; + $row['template'] = $row['template'] ?? setting($this->getDocumentSettingKey(Model::BILL_TYPE, 'template'), 'default'); + $row['color'] = $row['color'] ?? setting($this->getDocumentSettingKey(Model::BILL_TYPE, 'color'), '#55588b'); + $row['parent_id'] = $this->getParentId($row) ?? 0; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['bill_number'] = Str::replaceFirst('unique:documents,NULL', 'unique:documents,document_number', $rules['document_number']); + $rules['billed_at'] = $rules['issued_at']; + $rules['currency_rate'] = 'required|gt:0'; + + unset($rules['document_number'], $rules['issued_at'], $rules['type']); + + return $rules; + } +} diff --git a/app/Imports/Purchases/RecurringBills/RecurringBills.php b/app/Imports/Purchases/RecurringBills/RecurringBills.php new file mode 100644 index 00000000..079ecc07 --- /dev/null +++ b/app/Imports/Purchases/RecurringBills/RecurringBills.php @@ -0,0 +1,26 @@ + new Base(), + 'recurring_bill_items' => new RecurringBillItems(), + 'recurring_bill_item_taxes' => new RecurringBillItemTaxes(), + 'recurring_bill_histories' => new RecurringBillHistories(), + 'recurring_bill_totals' => new RecurringBillTotals(), + 'recurring' => new Recurring(), + ]; + } +} diff --git a/app/Imports/Purchases/RecurringBills/Sheets/Recurring.php b/app/Imports/Purchases/RecurringBills/Sheets/Recurring.php new file mode 100644 index 00000000..bad844dc --- /dev/null +++ b/app/Imports/Purchases/RecurringBills/Sheets/Recurring.php @@ -0,0 +1,40 @@ +number($row['bill_number']) + ->pluck('id') + ->first(); + + return $row; + } +} diff --git a/app/Imports/Purchases/RecurringBills/Sheets/RecurringBillHistories.php b/app/Imports/Purchases/RecurringBills/Sheets/RecurringBillHistories.php new file mode 100644 index 00000000..2619fa7e --- /dev/null +++ b/app/Imports/Purchases/RecurringBills/Sheets/RecurringBillHistories.php @@ -0,0 +1,62 @@ +isEmpty($row, 'bill_number')) { + return []; + } + + $row['bill_number'] = (string) $row['bill_number']; + + $row = parent::map($row); + + $row['document_id'] = (int) Document::where('type', '=', Document::BILL_RECURRING_TYPE) + ->number($row['bill_number']) + ->pluck('id') + ->first(); + + $row['notify'] = (int) $row['notify']; + + $row['type'] = Document::BILL_RECURRING_TYPE; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['bill_number'] = 'required|string'; + + unset($rules['bill_id']); + + return $rules; + } +} diff --git a/app/Imports/Purchases/RecurringBills/Sheets/RecurringBillItemTaxes.php b/app/Imports/Purchases/RecurringBills/Sheets/RecurringBillItemTaxes.php new file mode 100644 index 00000000..da4d93d3 --- /dev/null +++ b/app/Imports/Purchases/RecurringBills/Sheets/RecurringBillItemTaxes.php @@ -0,0 +1,84 @@ +isEmpty($row, 'bill_number')) { + return []; + } + + $row['bill_number'] = (string) $row['bill_number']; + + $row = parent::map($row); + + $document = Document::with('items')->billRecurring()->number($row['bill_number'])->first(); + + if (! $document) { + return []; + } + + $row['document_id'] = (int) $document->id; + + if (empty($row['document_item_id']) && !empty($row['item_name'])) { + $item_id = Item::name($row['item_name'])->pluck('id')->first(); + + $row['document_item_id'] = DocumentItem::billRecurring() + ->where('document_id', $row['document_id']) + ->where('item_id', $item_id) + ->pluck('id') + ->first(); + } + + $row['tax_id'] = $this->getTaxId($row); + + if (empty($row['name']) && !empty($row['item_name'])) { + $row['name'] = $row['item_name']; + } + + $row['amount'] = (double) $row['amount']; + + $row['type'] = Document::BILL_RECURRING_TYPE; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['bill_number'] = 'required|string'; + + unset($rules['bill_id']); + + return $rules; + } +} diff --git a/app/Imports/Purchases/RecurringBills/Sheets/RecurringBillItems.php b/app/Imports/Purchases/RecurringBills/Sheets/RecurringBillItems.php new file mode 100644 index 00000000..0d24a74d --- /dev/null +++ b/app/Imports/Purchases/RecurringBills/Sheets/RecurringBillItems.php @@ -0,0 +1,76 @@ +isEmpty($row, 'bill_number')) { + return []; + } + + $row['bill_number'] = (string) $row['bill_number']; + + $row = parent::map($row); + + $row['document_id'] = (int) Document::where('type', '=', Document::BILL_RECURRING_TYPE) + ->number($row['bill_number']) + ->pluck('id') + ->first(); + + if (empty($row['item_id']) && !empty($row['item_name'])) { + $row['item_id'] = $this->getItemIdFromName($row); + + $row['name'] = $row['item_name']; + } + + $row['description'] = !empty($row['item_description']) ? $row['item_description'] : ''; + + $row['tax'] = (double) $row['tax']; + $row['tax_id'] = 0; + $row['type'] = Document::BILL_RECURRING_TYPE; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['bill_number'] = 'required|string'; + + unset($rules['bill_id']); + + return $rules; + } +} diff --git a/app/Imports/Purchases/RecurringBills/Sheets/RecurringBillTotals.php b/app/Imports/Purchases/RecurringBills/Sheets/RecurringBillTotals.php new file mode 100644 index 00000000..27f51f06 --- /dev/null +++ b/app/Imports/Purchases/RecurringBills/Sheets/RecurringBillTotals.php @@ -0,0 +1,62 @@ +isEmpty($row, 'bill_number')) { + return []; + } + + $row['bill_number'] = (string) $row['bill_number']; + + $row = parent::map($row); + + $row['document_id'] = (int) Document::where('type', '=', Document::BILL_RECURRING_TYPE) + ->number($row['bill_number']) + ->pluck('id') + ->first(); + + $row['type'] = Document::BILL_RECURRING_TYPE; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['bill_number'] = 'required|string'; + + unset($rules['bill_id']); + + return $rules; + } +} diff --git a/app/Imports/Purchases/RecurringBills/Sheets/RecurringBills.php b/app/Imports/Purchases/RecurringBills/Sheets/RecurringBills.php new file mode 100644 index 00000000..4550047b --- /dev/null +++ b/app/Imports/Purchases/RecurringBills/Sheets/RecurringBills.php @@ -0,0 +1,87 @@ +isEmpty($row, 'bill_number')) { + return []; + } + + $row['bill_number'] = (string) $row['bill_number']; + + $row = parent::map($row); + + $country = array_search($row['contact_country'], trans('countries')); + + $row['document_number'] = $row['bill_number']; + $row['issued_at'] = $row['billed_at']; + $row['category_id'] = $this->getCategoryId($row, 'expense'); + $row['contact_id'] = $this->getContactId($row, 'vendor'); + $row['currency_code'] = $this->getCurrencyCode($row); + $row['type'] = Model::BILL_RECURRING_TYPE; + $row['contact_country'] = !empty($country) ? $country : null; + $row['title'] = $row['title'] ?? Model::BILL_RECURRING_TYPE; + $row['template'] = $row['template'] ?? setting($this->getDocumentSettingKey(Model::BILL_RECURRING_TYPE, 'template'), 'default'); + $row['color'] = $row['color'] ?? setting($this->getDocumentSettingKey(Model::BILL_RECURRING_TYPE, 'color'), '#55588b'); + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['bill_number'] = Str::replaceFirst('unique:documents,NULL', 'unique:documents,document_number', $rules['document_number']); + $rules['billed_at'] = $rules['issued_at']; + $rules['currency_rate'] = 'required|gt:0'; + + unset($rules['document_number'], $rules['issued_at'], $rules['type']); + + return $rules; + } + + //This function is used in import classes. If the data in the row exists in the database, it is returned. + public function hasRow($row) + { + $has_row = $this->model::billRecurring()->get($this->columns)->each(function ($data) { + $data->setAppends([]); + $data->unsetRelations(); + }); + + $search_value = []; + + //In the model, the fields to be searched for the row are determined. + foreach ($this->columns as $key) { + $search_value[$key] = isset($row[$key]) ? $row[$key] : null; + } + + return in_array($search_value, $has_row->toArray()); + } +} diff --git a/app/Imports/Purchases/Vendors.php b/app/Imports/Purchases/Vendors.php new file mode 100644 index 00000000..d35926f7 --- /dev/null +++ b/app/Imports/Purchases/Vendors.php @@ -0,0 +1,44 @@ +getCategoryId($row, 'expense'); + $row['currency_code'] = $this->getCurrencyCode($row); + $row['user_id'] = null; + + return $row; + } +} diff --git a/app/Imports/Sales/Customers.php b/app/Imports/Sales/Customers.php new file mode 100644 index 00000000..3f077056 --- /dev/null +++ b/app/Imports/Sales/Customers.php @@ -0,0 +1,49 @@ +getCategoryId($row, 'income'); + $row['currency_code'] = $this->getCurrencyCode($row); + $row['user_id'] = null; + + if (isset($row['can_login']) && isset($row['email'])) { + $row['user_id'] = user_model_class()::where('email', $row['email'])->first()?->id ?? null; + } + + return $row; + } +} diff --git a/app/Imports/Sales/Invoices/Invoices.php b/app/Imports/Sales/Invoices/Invoices.php new file mode 100644 index 00000000..7dfa1b27 --- /dev/null +++ b/app/Imports/Sales/Invoices/Invoices.php @@ -0,0 +1,26 @@ + new Base(), + 'invoice_items' => new InvoiceItems(), + 'invoice_item_taxes' => new InvoiceItemTaxes(), + 'invoice_histories' => new InvoiceHistories(), + 'invoice_totals' => new InvoiceTotals(), + 'invoice_transactions' => new InvoiceTransactions(), + ]; + } +} diff --git a/app/Imports/Sales/Invoices/Sheets/InvoiceHistories.php b/app/Imports/Sales/Invoices/Sheets/InvoiceHistories.php new file mode 100644 index 00000000..4384566c --- /dev/null +++ b/app/Imports/Sales/Invoices/Sheets/InvoiceHistories.php @@ -0,0 +1,59 @@ +isEmpty($row, 'invoice_number')) { + return []; + } + + $row['invoice_number'] = (string) $row['invoice_number']; + + $row = parent::map($row); + + $row['document_id'] = (int) Document::invoice()->number($row['invoice_number'])->pluck('id')->first(); + + $row['notify'] = (int) $row['notify']; + + $row['type'] = Document::INVOICE_TYPE; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['invoice_number'] = 'required|string'; + + unset($rules['invoice_id']); + + return $rules; + } +} diff --git a/app/Imports/Sales/Invoices/Sheets/InvoiceItemTaxes.php b/app/Imports/Sales/Invoices/Sheets/InvoiceItemTaxes.php new file mode 100644 index 00000000..76a9c5d1 --- /dev/null +++ b/app/Imports/Sales/Invoices/Sheets/InvoiceItemTaxes.php @@ -0,0 +1,86 @@ +isEmpty($row, 'invoice_number')) { + return []; + } + + $row['invoice_number'] = (string) $row['invoice_number']; + + $row = parent::map($row); + + $document = Document::with('items')->invoice()->number($row['invoice_number'])->first(); + + if (! $document) { + return []; + } + + $row['document_id'] = (int) $document->id; + + if (empty($row['document_item_id']) && !empty($row['item_name'])) { + $document_items_ids = $document->items->pluck('item_id')->toArray(); + + $item_id = Item::name($row['item_name'])->whereIn('id', $document_items_ids)->pluck('id')->first(); + + $row['document_item_id'] = DocumentItem::invoice() + ->where('document_id', $row['document_id']) + ->where('item_id', $item_id) + ->pluck('id') + ->first(); + } + + $row['tax_id'] = $this->getTaxId($row); + + if (empty($row['name']) && !empty($row['item_name'])) { + $row['name'] = $row['item_name']; + } + + $row['amount'] = (double) $row['amount']; + + $row['type'] = Document::INVOICE_TYPE; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['invoice_number'] = 'required|string'; + + unset($rules['invoice_id']); + + return $rules; + } +} diff --git a/app/Imports/Sales/Invoices/Sheets/InvoiceItems.php b/app/Imports/Sales/Invoices/Sheets/InvoiceItems.php new file mode 100644 index 00000000..bb45a3d5 --- /dev/null +++ b/app/Imports/Sales/Invoices/Sheets/InvoiceItems.php @@ -0,0 +1,73 @@ +isEmpty($row, 'invoice_number')) { + return []; + } + + $row['invoice_number'] = (string) $row['invoice_number']; + + $row = parent::map($row); + + $row['document_id'] = (int) Document::invoice()->number($row['invoice_number'])->pluck('id')->first(); + + if (empty($row['item_id']) && ! empty($row['item_name'])) { + $row['item_id'] = $this->getItemIdFromName($row); + + $row['name'] = $row['item_name']; + } + + $row['description'] = !empty($row['item_description']) ? $row['item_description'] : ''; + + $row['tax'] = (double) $row['tax']; + $row['tax_id'] = 0; + $row['type'] = Document::INVOICE_TYPE; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['invoice_number'] = 'required|string'; + + unset($rules['invoice_id']); + + return $rules; + } +} diff --git a/app/Imports/Sales/Invoices/Sheets/InvoiceTotals.php b/app/Imports/Sales/Invoices/Sheets/InvoiceTotals.php new file mode 100644 index 00000000..6738aeb4 --- /dev/null +++ b/app/Imports/Sales/Invoices/Sheets/InvoiceTotals.php @@ -0,0 +1,58 @@ +isEmpty($row, 'invoice_number')) { + return []; + } + + $row['invoice_number'] = (string) $row['invoice_number']; + + $row = parent::map($row); + + $row['document_id'] = (int) Document::invoice()->number($row['invoice_number'])->pluck('id')->first(); + $row['type'] = Document::INVOICE_TYPE; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['invoice_number'] = 'required|string'; + + unset($rules['invoice_id']); + + return $rules; + } +} diff --git a/app/Imports/Sales/Invoices/Sheets/InvoiceTransactions.php b/app/Imports/Sales/Invoices/Sheets/InvoiceTransactions.php new file mode 100644 index 00000000..98a864ce --- /dev/null +++ b/app/Imports/Sales/Invoices/Sheets/InvoiceTransactions.php @@ -0,0 +1,58 @@ +isEmpty($row, 'invoice_number')) { + return []; + } + + $row['invoice_number'] = (string) $row['invoice_number']; + + $row = parent::map($row); + + $row['type'] = Model::INCOME_TYPE; + $row['currency_code'] = $this->getCurrencyCode($row); + $row['account_id'] = $this->getAccountId($row); + $row['category_id'] = $this->getCategoryId($row, Category::INCOME_TYPE); + $row['contact_id'] = $this->getContactId($row, 'customer'); + $row['document_id'] = $this->getDocumentId($row); + $row['number'] = $row['transaction_number']; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['invoice_number'] = 'required|string'; + + return $rules; + } +} diff --git a/app/Imports/Sales/Invoices/Sheets/Invoices.php b/app/Imports/Sales/Invoices/Sheets/Invoices.php new file mode 100644 index 00000000..5315d259 --- /dev/null +++ b/app/Imports/Sales/Invoices/Sheets/Invoices.php @@ -0,0 +1,70 @@ +isEmpty($row, 'invoice_number')) { + return []; + } + + $row['invoice_number'] = (string) $row['invoice_number']; + + $row = parent::map($row); + + $country = array_search($row['contact_country'], trans('countries')); + + $row['document_number'] = $row['invoice_number']; + $row['issued_at'] = $row['invoiced_at']; + $row['category_id'] = $this->getCategoryId($row, 'income'); + $row['contact_id'] = $this->getContactId($row, 'customer'); + $row['currency_code'] = $this->getCurrencyCode($row); + $row['type'] = Model::INVOICE_TYPE; + $row['contact_country'] = !empty($country) ? $country : null; + $row['title'] = $row['title'] ?? Model::INVOICE_TYPE; + $row['template'] = $row['template'] ?? setting($this->getDocumentSettingKey(Model::INVOICE_TYPE, 'template'), 'default'); + $row['color'] = $row['color'] ?? setting($this->getDocumentSettingKey(Model::INVOICE_TYPE, 'color'), '#55588b'); + $row['parent_id'] = $this->getParentId($row) ?? 0; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['invoice_number'] = Str::replaceFirst('unique:documents,NULL', 'unique:documents,document_number', $rules['document_number']); + $rules['invoiced_at'] = $rules['issued_at']; + $rules['currency_rate'] = 'required|gt:0'; + + unset($rules['document_number'], $rules['issued_at'], $rules['type']); + + return $rules; + } +} diff --git a/app/Imports/Sales/RecurringInvoices/RecurringInvoices.php b/app/Imports/Sales/RecurringInvoices/RecurringInvoices.php new file mode 100644 index 00000000..75d0de34 --- /dev/null +++ b/app/Imports/Sales/RecurringInvoices/RecurringInvoices.php @@ -0,0 +1,26 @@ + new Base(), + 'recurring_invoice_items' => new RecurringInvoiceItems(), + 'recurring_invoice_item_taxes' => new RecurringInvoiceItemTaxes(), + 'recurring_invoice_histories' => new RecurringInvoiceHistories(), + 'recurring_invoice_totals' => new RecurringInvoiceTotals(), + 'recurring' => new Recurring(), + ]; + } +} diff --git a/app/Imports/Sales/RecurringInvoices/Sheets/Recurring.php b/app/Imports/Sales/RecurringInvoices/Sheets/Recurring.php new file mode 100644 index 00000000..f86ad118 --- /dev/null +++ b/app/Imports/Sales/RecurringInvoices/Sheets/Recurring.php @@ -0,0 +1,40 @@ +number($row['invoice_number']) + ->pluck('id') + ->first(); + + return $row; + } +} diff --git a/app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoiceHistories.php b/app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoiceHistories.php new file mode 100644 index 00000000..d3856c68 --- /dev/null +++ b/app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoiceHistories.php @@ -0,0 +1,62 @@ +isEmpty($row, 'invoice_number')) { + return []; + } + + $row['invoice_number'] = (string) $row['invoice_number']; + + $row = parent::map($row); + + $row['document_id'] = (int) Document::where('type', '=', Document::INVOICE_RECURRING_TYPE) + ->number($row['invoice_number']) + ->pluck('id') + ->first(); + + $row['notify'] = (int) $row['notify']; + + $row['type'] = Document::INVOICE_RECURRING_TYPE; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['invoice_number'] = 'required|string'; + + unset($rules['invoice_id']); + + return $rules; + } +} diff --git a/app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoiceItemTaxes.php b/app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoiceItemTaxes.php new file mode 100644 index 00000000..ede81565 --- /dev/null +++ b/app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoiceItemTaxes.php @@ -0,0 +1,86 @@ +isEmpty($row, 'invoice_number')) { + return []; + } + + $row['invoice_number'] = (string) $row['invoice_number']; + + $row = parent::map($row); + + $document = Document::with('items')->invoiceRecurring()->number($row['invoice_number'])->first(); + + if (! $document) { + return []; + } + + $row['document_id'] = (int) $document->id; + + if (empty($row['document_item_id']) && !empty($row['item_name'])) { + $document_items_ids = $document->items->pluck('item_id')->toArray(); + + $item_id = Item::name($row['item_name'])->whereIn('id', $document_items_ids)->pluck('id')->first(); + + $row['document_item_id'] = DocumentItem::invoiceRecurring() + ->where('document_id', $row['document_id']) + ->where('item_id', $item_id) + ->pluck('id') + ->first(); + } + + $row['tax_id'] = $this->getTaxId($row); + + if (empty($row['name']) && !empty($row['item_name'])) { + $row['name'] = $row['item_name']; + } + + $row['amount'] = (double) $row['amount']; + + $row['type'] = Document::INVOICE_RECURRING_TYPE; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['invoice_number'] = 'required|string'; + + unset($rules['invoice_id']); + + return $rules; + } +} diff --git a/app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoiceItems.php b/app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoiceItems.php new file mode 100644 index 00000000..8ff34237 --- /dev/null +++ b/app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoiceItems.php @@ -0,0 +1,76 @@ +isEmpty($row, 'invoice_number')) { + return []; + } + + $row['invoice_number'] = (string) $row['invoice_number']; + + $row = parent::map($row); + + $row['document_id'] = (int) Document::where('type', '=', Document::INVOICE_RECURRING_TYPE) + ->number($row['invoice_number']) + ->pluck('id') + ->first(); + + if (empty($row['item_id']) && ! empty($row['item_name'])) { + $row['item_id'] = $this->getItemIdFromName($row); + + $row['name'] = $row['item_name']; + } + + $row['description'] = !empty($row['item_description']) ? $row['item_description'] : ''; + + $row['tax'] = (double) $row['tax']; + $row['tax_id'] = 0; + $row['type'] = Document::INVOICE_RECURRING_TYPE; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['invoice_number'] = 'required|string'; + + unset($rules['invoice_id']); + + return $rules; + } +} diff --git a/app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoiceTotals.php b/app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoiceTotals.php new file mode 100644 index 00000000..c26f4193 --- /dev/null +++ b/app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoiceTotals.php @@ -0,0 +1,62 @@ +isEmpty($row, 'invoice_number')) { + return []; + } + + $row['invoice_number'] = (string) $row['invoice_number']; + + $row = parent::map($row); + + $row['document_id'] = (int) Document::where('type', '=', Document::INVOICE_RECURRING_TYPE) + ->number($row['invoice_number']) + ->pluck('id') + ->first(); + + $row['type'] = Document::INVOICE_RECURRING_TYPE; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['invoice_number'] = 'required|string'; + + unset($rules['invoice_id']); + + return $rules; + } +} diff --git a/app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoices.php b/app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoices.php new file mode 100644 index 00000000..17008107 --- /dev/null +++ b/app/Imports/Sales/RecurringInvoices/Sheets/RecurringInvoices.php @@ -0,0 +1,87 @@ +isEmpty($row, 'invoice_number')) { + return []; + } + + $row['invoice_number'] = (string) $row['invoice_number']; + + $row = parent::map($row); + + $country = array_search($row['contact_country'], trans('countries')); + + $row['document_number'] = $row['invoice_number']; + $row['issued_at'] = $row['invoiced_at']; + $row['category_id'] = $this->getCategoryId($row, 'income'); + $row['contact_id'] = $this->getContactId($row, 'customer'); + $row['currency_code'] = $this->getCurrencyCode($row); + $row['type'] = Model::INVOICE_RECURRING_TYPE; + $row['title'] = $row['title'] ?? Model::INVOICE_RECURRING_TYPE; + $row['template'] = $row['template'] ?? setting($this->getDocumentSettingKey(Model::INVOICE_RECURRING_TYPE, 'template'), 'default'); + $row['color'] = $row['color'] ?? setting($this->getDocumentSettingKey(Model::INVOICE_RECURRING_TYPE, 'color'), '#55588b'); + $row['contact_country'] = !empty($country) ? $country : null; + + return $row; + } + + public function prepareRules(array $rules): array + { + $rules['invoice_number'] = Str::replaceFirst('unique:documents,NULL', 'unique:documents,document_number', $rules['document_number']); + $rules['invoiced_at'] = $rules['issued_at']; + $rules['currency_rate'] = 'required|gt:0'; + + unset($rules['document_number'], $rules['issued_at'], $rules['type']); + + return $rules; + } + + //This function is used in import classes. If the data in the row exists in the database, it is returned. + public function hasRow($row) + { + $has_row = $this->model::invoiceRecurring()->get($this->columns)->each(function ($data) { + $data->setAppends([]); + $data->unsetRelations(); + }); + + $search_value = []; + + //In the model, the fields to be searched for the row are determined. + foreach ($this->columns as $key) { + $search_value[$key] = isset($row[$key]) ? $row[$key] : null; + } + + return in_array($search_value, $has_row->toArray()); + } +} diff --git a/app/Imports/Settings/Categories.php b/app/Imports/Settings/Categories.php new file mode 100644 index 00000000..1fe27a65 --- /dev/null +++ b/app/Imports/Settings/Categories.php @@ -0,0 +1,60 @@ +getCategoryType($row['type'] ?? null); + $row['parent_id'] = $this->getParentId($row) ?? null; + + return $row; + } + + //This function is used in import classes. If the data in the row exists in the database, it is returned. + public function hasRow($row) + { + $has_row = $this->model::getWithoutChildren($this->columns)->each(function ($data) { + $data->setAppends([]); + $data->unsetRelations(); + }); + + $search_value = []; + + //In the model, the fields to be searched for the row are determined. + foreach ($this->columns as $key) { + $search_value[$key] = isset($row[$key]) ? $row[$key] : null; + } + + return in_array($search_value, $has_row->toArray()); + } +} diff --git a/app/Imports/Settings/Taxes.php b/app/Imports/Settings/Taxes.php new file mode 100644 index 00000000..abdffb39 --- /dev/null +++ b/app/Imports/Settings/Taxes.php @@ -0,0 +1,53 @@ +first(); + + if ($compound_tax) { + $this->request_class = null; + + // TODO: Add exception error + // throw new \Exception('Compound tax already exists.'); + + return []; + } + } + + return $row; + } + +} diff --git a/app/Interfaces/Export/WithParentSheet.php b/app/Interfaces/Export/WithParentSheet.php new file mode 100644 index 00000000..2fe0c987 --- /dev/null +++ b/app/Interfaces/Export/WithParentSheet.php @@ -0,0 +1,8 @@ +user = $user; + } + + public function handle(): UserInvitation + { + \DB::transaction(function () { + $invitations = UserInvitation::where('user_id', $this->user->id)->get(); + + foreach ($invitations as $invitation) { + $invitation->delete(); + } + + $this->invitation = UserInvitation::create([ + 'user_id' => $this->user->id, + 'token' => (string) Str::uuid(), + 'created_by' => user_id(), + 'created_from' => $this->getSourceName(request()), + ]); + + $notification = new Notification($this->invitation); + + try { + $this->dispatch(new NotifyUser($this->user, $notification)); + } catch (TransportException $e) { + $message = trans('errors.title.500'); + + throw new Exception($message); + } + }); + + return $this->invitation; + } +} diff --git a/app/Jobs/Auth/CreatePermission.php b/app/Jobs/Auth/CreatePermission.php new file mode 100644 index 00000000..0ce2629e --- /dev/null +++ b/app/Jobs/Auth/CreatePermission.php @@ -0,0 +1,19 @@ +model = Permission::create($this->request->all()); + }); + + return $this->model; + } +} diff --git a/app/Jobs/Auth/CreateRole.php b/app/Jobs/Auth/CreateRole.php new file mode 100644 index 00000000..e87c4a56 --- /dev/null +++ b/app/Jobs/Auth/CreateRole.php @@ -0,0 +1,31 @@ +request)); + + \DB::transaction(function () { + $this->model = Role::create($this->request->input()); + + if ($this->request->has('permissions')) { + $this->model->permissions()->attach($this->request->get('permissions')); + } + }); + + event(new RoleCreated($this->model, $this->request)); + + return $this->model; + } +} diff --git a/app/Jobs/Auth/CreateUser.php b/app/Jobs/Auth/CreateUser.php new file mode 100644 index 00000000..174c556f --- /dev/null +++ b/app/Jobs/Auth/CreateUser.php @@ -0,0 +1,117 @@ +authorize(); + + event(new UserCreating($this->request)); + + \DB::transaction(function () { + if (empty($this->request->get('password', false))) { + $this->request->merge(['password' => Str::random(40)]); + } + + $this->model = user_model_class()::create($this->request->input()); + + // Upload picture + if ($this->request->file('picture')) { + $media = $this->getMedia($this->request->file('picture'), 'users'); + + $this->model->attachMedia($media, 'picture'); + } + + if ($this->request->has('dashboards')) { + $this->model->dashboards()->attach($this->request->get('dashboards')); + } + + if ($this->request->has('permissions')) { + $this->model->permissions()->attach($this->request->get('permissions')); + } + + if ($this->request->has('roles')) { + $this->model->roles()->attach($this->request->get('roles')); + } + + if ($this->request->has('companies')) { + if (app()->runningInConsole() || request()->isInstall()) { + $this->model->companies()->attach($this->request->get('companies')); + } else { + $user = user(); + + $companies = $user->withoutEvents(function () use ($user) { + return $user->companies()->whereIn('id', $this->request->get('companies'))->pluck('id'); + }); + + if ($companies->isNotEmpty()) { + $this->model->companies()->attach($companies->toArray()); + } + } + } + + if (empty($this->model->companies)) { + return; + } + + foreach ($this->model->companies as $company) { + Artisan::call('user:seed', [ + 'user' => $this->model->id, + 'company' => $company->id, + ]); + } + + if ($this->shouldSendInvitation()) { + $this->dispatch(new CreateInvitation($this->model)); + } + }); + + $this->clearPlansCache(); + + event(new UserCreated($this->model, $this->request)); + + return $this->model; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + $limit = $this->getAnyActionLimitOfPlan(); + if (! $limit->action_status) { + throw new \Exception($limit->message); + } + } + + protected function shouldSendInvitation() + { + if (app()->runningUnitTests()) { + return true; + } + + if (app()->runningInConsole()) { + return false; + } + + if (request()->isInstall()) { + return false; + } + + return true; + } +} diff --git a/app/Jobs/Auth/DeleteInvitation.php b/app/Jobs/Auth/DeleteInvitation.php new file mode 100644 index 00000000..f2b2916b --- /dev/null +++ b/app/Jobs/Auth/DeleteInvitation.php @@ -0,0 +1,18 @@ +model->delete(); + }); + + return true; + } +} diff --git a/app/Jobs/Auth/DeletePermission.php b/app/Jobs/Auth/DeletePermission.php new file mode 100644 index 00000000..a3c7ecb2 --- /dev/null +++ b/app/Jobs/Auth/DeletePermission.php @@ -0,0 +1,18 @@ +model->delete(); + }); + + return true; + } +} diff --git a/app/Jobs/Auth/DeleteRole.php b/app/Jobs/Auth/DeleteRole.php new file mode 100644 index 00000000..ae831f74 --- /dev/null +++ b/app/Jobs/Auth/DeleteRole.php @@ -0,0 +1,26 @@ +model)); + + \DB::transaction(function () { + $this->model->delete(); + + $this->model->flushCache(); + }); + + event(new RoleDeleted($this->model)); + + return true; + } +} diff --git a/app/Jobs/Auth/DeleteUser.php b/app/Jobs/Auth/DeleteUser.php new file mode 100644 index 00000000..e2dc1a72 --- /dev/null +++ b/app/Jobs/Auth/DeleteUser.php @@ -0,0 +1,43 @@ +authorize(); + + event(new UserDeleting($this->model)); + + \DB::transaction(function () { + $this->deleteRelationships($this->model, ['invitation']); + + $this->model->delete(); + + $this->model->flushCache(); + }); + + event(new UserDeleted($this->model)); + + return true; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + // Can't delete yourself + if ($this->model->id == user()->id) { + $message = trans('auth.error.self_delete'); + + throw new \Exception($message); + } + } +} diff --git a/app/Jobs/Auth/NotifyUser.php b/app/Jobs/Auth/NotifyUser.php new file mode 100644 index 00000000..c27f2c48 --- /dev/null +++ b/app/Jobs/Auth/NotifyUser.php @@ -0,0 +1,36 @@ +user = $user; + $this->notification = $notification; + + $this->onQueue('jobs'); + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->user->notify($this->notification); + } +} diff --git a/app/Jobs/Auth/UpdatePermission.php b/app/Jobs/Auth/UpdatePermission.php new file mode 100644 index 00000000..c8f8f0eb --- /dev/null +++ b/app/Jobs/Auth/UpdatePermission.php @@ -0,0 +1,19 @@ +model->update($this->request->all()); + }); + + return $this->model; + } +} diff --git a/app/Jobs/Auth/UpdateRole.php b/app/Jobs/Auth/UpdateRole.php new file mode 100644 index 00000000..292f4bea --- /dev/null +++ b/app/Jobs/Auth/UpdateRole.php @@ -0,0 +1,35 @@ +model->name, config('roles.defaults', ['admin', 'manager', 'accountant', 'employee']))) { + $this->request->name = $this->model->name; + } + + event(new RoleUpdating($this->model, $this->request)); + + \DB::transaction(function () { + $this->model->update($this->request->all()); + + if ($this->request->has('permissions')) { + $this->model->permissions()->sync($this->request->get('permissions')); + } + + $this->model->flushCache(); + }); + + event(new RoleUpdated($this->model, $this->request)); + + return $this->model; + } +} diff --git a/app/Jobs/Auth/UpdateUser.php b/app/Jobs/Auth/UpdateUser.php new file mode 100644 index 00000000..007c9f7b --- /dev/null +++ b/app/Jobs/Auth/UpdateUser.php @@ -0,0 +1,137 @@ +authorize(); + + // Do not reset password if not entered/changed + if (empty($this->request['password'])) { + unset($this->request['current_password']); + unset($this->request['password']); + unset($this->request['password_confirmation']); + } + + event(new UserUpdating($this->model, $this->request)); + + \DB::transaction(function () { + $this->model->update($this->request->input()); + + // Upload picture + if ($this->request->file('picture')) { + $this->deleteMediaModel($this->model, 'picture', $this->request); + + $media = $this->getMedia($this->request->file('picture'), 'users'); + + $this->model->attachMedia($media, 'picture'); + } elseif ($this->request->isNotApi() && ! $this->request->file('picture') && $this->model->picture) { + $this->deleteMediaModel($this->model, 'picture', $this->request); + } elseif ($this->request->isApi() && $this->request->has('remove_picture') && $this->model->picture) { + $this->deleteMediaModel($this->model, 'picture', $this->request); + } + + if ($this->request->has('roles')) { + $this->model->roles()->sync($this->request->get('roles')); + } + + if ($this->request->has('companies')) { + if (app()->runningInConsole() || request()->isInstall()) { + $sync = $this->model->companies()->sync($this->request->get('companies')); + } else { + $user = user(); + + $companies = $user->withoutEvents(function () use ($user) { + return $user->companies()->whereIn('id', $this->request->get('companies'))->pluck('id'); + }); + + if ($companies->isNotEmpty()) { + $sync = $this->model->companies()->sync($companies->toArray()); + } + } + } + + if ($this->model->contact) { + $this->model->contact->update($this->request->input()); + } + + if (isset($sync) && !empty($sync['attached'])) { + foreach ($sync['attached'] as $id) { + $company = Company::find($id); + + Artisan::call('user:seed', [ + 'user' => $this->model->id, + 'company' => $company->id, + ]); + } + } + }); + + event(new UserUpdated($this->model, $this->request)); + + return $this->model; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + // Security: Defense-in-depth — only users with 'update-auth-users' + // permission may change role assignments. The profile edit form + // includes a roles field even for self-edits (visible to managers), + // so instead of throwing 403 we silently strip 'roles' from the + // request when the current user lacks permission. This preserves + // legitimate profile updates while preventing privilege escalation. + if ( + $this->request->has('roles') + && ! app()->runningInConsole() + && ! request()->isInstall() + && ! user()->can('update-auth-users') + ) { + $this->request->request->remove('roles'); + } + + // Can't disable yourself + if (($this->request->get('enabled', 1) == 0) && ($this->model->id == user()->id)) { + $message = trans('auth.error.self_disable'); + + throw new \Exception($message); + } + + // Can't unassigned company, The company must be assigned at least one user. + if ($this->request->has('companies')) { + $companies = (array) $this->request->get('companies', []); + $user_companies = $this->model->companies()->pluck('id')->toArray(); + + $company_diff = array_diff($user_companies, $companies); + + if ($company_diff) { + $errors = []; + + foreach ($company_diff as $company_id) { + $company = Company::withCount('users')->find($company_id); + + if ($company->users_count < 2) { + $errors[] = trans('auth.error.unassigned', ['company' => $company->name]); + } + } + + if ($errors) { + $message = implode('\n', $errors); + + throw new \Exception($message); + } + } + } + } +} diff --git a/app/Jobs/Banking/CreateAccount.php b/app/Jobs/Banking/CreateAccount.php new file mode 100644 index 00000000..cca2e136 --- /dev/null +++ b/app/Jobs/Banking/CreateAccount.php @@ -0,0 +1,33 @@ +request)); + + \DB::transaction(function () { + $this->model = Account::create($this->request->all()); + + // Set default account + if ($this->request['default_account']) { + setting()->set('default.account', $this->model->id); + setting()->save(); + } + }); + + event(new AccountCreated($this->model, $this->request)); + + return $this->model; + } +} diff --git a/app/Jobs/Banking/CreateBankingDocumentTransaction.php b/app/Jobs/Banking/CreateBankingDocumentTransaction.php new file mode 100644 index 00000000..0aaec40f --- /dev/null +++ b/app/Jobs/Banking/CreateBankingDocumentTransaction.php @@ -0,0 +1,131 @@ +model = $model; + + parent::__construct($request); + } + + public function handle(): Transaction + { + event(new DocumentTransactionCreating($this->model, $this->request)); + + $this->prepareRequest(); + + $this->checkAmount(); + + \DB::transaction(function () { + $this->transaction = $this->dispatch(new CreateTransaction($this->request)); + + $this->model->save(); + + $this->createHistory(); + }); + + event(new DocumentTransactionCreated($this->model, $this->transaction)); + + return $this->transaction; + } + + protected function prepareRequest(): void + { + if (!isset($this->request['amount'])) { + $this->model->paid_amount = $this->model->paid; + event(new PaidAmountCalculated($this->model)); + + $this->request['amount'] = $this->model->amount - $this->model->paid_amount; + } + + $currency_code = !empty($this->request['currency_code']) ? $this->request['currency_code'] : $this->model->currency_code; + + $this->request['company_id'] = $this->model->company_id; + $this->request['currency_code'] = $currency_code; + $this->request['paid_at'] = isset($this->request['paid_at']) ? $this->request['paid_at'] : Date::now()->toDateTimeString(); + $this->request['currency_rate'] = isset($this->request['currency_rate']) ? $this->request['currency_rate'] : currency($currency_code)->getRate(); + $this->request['account_id'] = isset($this->request['account_id']) ? $this->request['account_id'] : setting('default.account'); + $this->request['document_id'] = isset($this->request['document_id']) ? $this->request['document_id'] : $this->model->id; + $this->request['contact_id'] = isset($this->request['contact_id']) ? $this->request['contact_id'] : $this->model->contact_id; + $this->request['category_id'] = isset($this->request['category_id']) ? $this->request['category_id'] : $this->model->category_id; + $this->request['payment_method'] = isset($this->request['payment_method']) ? $this->request['payment_method'] : setting('default.payment_method'); + $this->request['notify'] = isset($this->request['notify']) ? $this->request['notify'] : 0; + } + + protected function checkAmount(): bool + { + $code = $this->request['currency_code']; + $rate = $this->request['currency_rate']; + + $transaction_precision = currency($code)->getPrecision(); + $document_precision = currency($this->model->currency_code)->getPrecision(); + + $amount = $this->request['amount'] = round($this->request['amount'], $transaction_precision); + + if ($this->model->currency_code != $code) { + $converted_amount = $this->convertBetween($amount, $code, $rate, $this->model->currency_code, $this->model->currency_rate); + + $amount = round($converted_amount, $document_precision); + } + + $this->model->paid_amount = $this->model->paid; + event(new PaidAmountCalculated($this->model)); + + $total_amount = round($this->model->amount - $this->model->paid_amount, $document_precision); + + unset($this->model->reconciled); + unset($this->model->paid_amount); + + $compare = bccomp($amount, $total_amount, $document_precision); + + if ($compare === 1) { + $error_amount = $total_amount; + + if ($this->model->currency_code != $code) { + $converted_amount = $this->convertBetween( + $total_amount, + $this->model->currency_code, + $this->model->currency_rate, + $code, + $rate + ); + + $error_amount = round($converted_amount, $transaction_precision); + } + + $message = trans('messages.error.over_payment', ['amount' => money($error_amount, $code)]); + + throw new \Exception($message); + } else { + $this->model->status = ($compare === 0) ? 'paid' : 'partial'; + } + + return true; + } + + protected function createHistory(): void + { + $history_desc = money((double) $this->transaction->amount, (string) $this->transaction->currency_code)->format() . ' ' . trans_choice('general.payments', 1); + + $this->dispatch(new CreateDocumentHistory($this->model, 0, $history_desc)); + } +} diff --git a/app/Jobs/Banking/CreateReconciliation.php b/app/Jobs/Banking/CreateReconciliation.php new file mode 100644 index 00000000..396e5880 --- /dev/null +++ b/app/Jobs/Banking/CreateReconciliation.php @@ -0,0 +1,49 @@ +request->get('started_at'))->startOfDay(); + $ended_at = Date::parse($this->request->get('ended_at'))->endOfDay(); + + $reconcile = (int) $this->request->get('reconcile'); + $transactions = $this->request->get('transactions'); + + $this->request->merge([ + 'started_at' => $started_at, + 'ended_at' => $ended_at, + 'reconciled' => $reconcile, + ]); + + $this->model = Reconciliation::create($this->request->all()); + + if ($reconcile && $transactions) { + foreach ($transactions as $key => $value) { + if (empty($value)) { + continue; + } + + $t = explode('_', $key); + + $transaction = Transaction::find($t[1]); + $transaction->reconciled = 1; + $transaction->save(); + } + } + }); + + return $this->model; + } +} diff --git a/app/Jobs/Banking/CreateTransaction.php b/app/Jobs/Banking/CreateTransaction.php new file mode 100644 index 00000000..32a6669a --- /dev/null +++ b/app/Jobs/Banking/CreateTransaction.php @@ -0,0 +1,53 @@ +request)); + + if (! array_key_exists($this->request->get('type'), config('type.transaction'))) { + $isExpense = str_contains((string) $this->request->get('type', ''), 'expense'); + $isRecurring = ! empty($this->request->get('recurring_frequency')) && $this->request->get('recurring_frequency') !== 'no'; + + $type = $isExpense + ? ($isRecurring ? Transaction::EXPENSE_RECURRING_TYPE : Transaction::EXPENSE_TYPE) + : ($isRecurring ? Transaction::INCOME_RECURRING_TYPE : Transaction::INCOME_TYPE); + + $this->request->merge(['type' => $type]); + } + + \DB::transaction(function () { + $this->model = Transaction::create($this->request->all()); + + // Upload attachment + if ($this->request->file('attachment')) { + foreach ($this->request->file('attachment') as $attachment) { + $media = $this->getMedia($attachment, 'transactions'); + + $this->model->attachMedia($media, 'attachment'); + } + } + + $this->dispatch(new CreateTransactionTaxes($this->model, $this->request)); + + // Recurring + $this->model->createRecurring($this->request->all()); + }); + + event(new TransactionCreated($this->model)); + + return $this->model; + } +} diff --git a/app/Jobs/Banking/CreateTransactionTaxes.php b/app/Jobs/Banking/CreateTransactionTaxes.php new file mode 100644 index 00000000..1f1956c8 --- /dev/null +++ b/app/Jobs/Banking/CreateTransactionTaxes.php @@ -0,0 +1,156 @@ +transaction = $transaction; + $this->request = $request; + + parent::__construct($transaction, $request); + } + + /** + * Execute the job. + * + * @return mixed + * @todo type hint after upgrading to PHP 8 + */ + public function handle() + { + if (empty($this->request['tax_ids'])) { + return false; + } + + \DB::transaction(function () { + $transaction_taxes = $this->getTaxesCalculated(); + + foreach ($transaction_taxes as $transaction_tax) { + TransactionTax::create($transaction_tax); + } + }); + + return $this->transaction->taxes; + } + + public function getTaxesCalculated() + { + $tax_total = 0; + $transaction_taxes = []; + + $transaction_amount = (double) $this->request['amount']; + + $trx_params = [ + 'company_id' => $this->transaction->company_id, + 'type' => $this->transaction->type, + 'transaction_id' => $this->transaction->id, + 'created_from' => $this->request['created_from'], + 'created_by' => $this->request['created_by'], + ]; + + // New variables by tax type & tax sorting + $known_types = ['inclusive', 'fixed', 'normal', 'withholding', 'compound']; + + foreach ((array) $this->request['tax_ids'] as $tax_id) { + $tax = Tax::find($tax_id); + + // If tax not found, skip + if (! $tax) { + continue; + } + + if (! in_array($tax->type, $known_types)) { + report(new \UnexpectedValueException("CreateTransactionTaxes: unknown tax type '{$tax->type}' for tax ID {$tax->id} — skipped.")); + continue; + } + + ${$tax->type . 's'}[] = $tax; + } + + if (isset($inclusives)) { + foreach ($inclusives as $inclusive) { + $tax_amount = $transaction_amount - ($transaction_amount / (1 + $inclusive->rate / 100)); + + $transaction_taxes[] = $trx_params + [ + 'tax_id' => $inclusive->id, + 'name' => $inclusive->name, + 'amount' => $tax_amount, + ]; + + $tax_total += $tax_amount; + } + } + + if (isset($fixeds)) { + foreach ($fixeds as $tax) { + $tax_amount = $tax->rate * (double) 1; + + $transaction_taxes[] = $trx_params + [ + 'tax_id' => $tax->id, + 'name' => $tax->name, + 'amount' => $tax_amount, + ]; + + $tax_total += $tax_amount; + } + } + + if (isset($normals)) { + foreach ($normals as $tax) { + $tax_amount = $transaction_amount * ($tax->rate / 100); + + $transaction_taxes[] = $trx_params + [ + 'tax_id' => $tax->id, + 'name' => $tax->name, + 'amount' => $tax_amount, + ]; + + $tax_total += $tax_amount; + } + } + + if (isset($withholdings)) { + foreach ($withholdings as $tax) { + $tax_amount = -($transaction_amount * ($tax->rate / 100)); + + $transaction_taxes[] = $trx_params + [ + 'tax_id' => $tax->id, + 'name' => $tax->name, + 'amount' => $tax_amount, + ]; + + $tax_total += $tax_amount; + } + } + + if (isset($compounds)) { + foreach ($compounds as $compound) { + $tax_amount = ($transaction_amount / 100) * $compound->rate; + + $transaction_taxes[] = $trx_params + [ + 'tax_id' => $compound->id, + 'name' => $compound->name, + 'amount' => $tax_amount, + ]; + + $tax_total += $tax_amount; + } + } + + return $transaction_taxes; + } +} diff --git a/app/Jobs/Banking/CreateTransfer.php b/app/Jobs/Banking/CreateTransfer.php new file mode 100644 index 00000000..065199c4 --- /dev/null +++ b/app/Jobs/Banking/CreateTransfer.php @@ -0,0 +1,134 @@ +authorize(); + + event(new TransferCreating($this->request)); + + \DB::transaction(function () { + $expense_currency_code = $this->getCurrencyCode('from'); + $income_currency_code = $this->getCurrencyCode('to'); + + $expense_currency_rate = $this->getCurrencyRate('from'); + $income_currency_rate = $this->getCurrencyRate('to'); + + $expense_transaction = $this->dispatch(new CreateTransaction([ + 'company_id' => $this->request['company_id'], + 'type' => Transaction::EXPENSE_TRANSFER_TYPE, + 'number' => $this->getNextTransactionNumber(), + 'account_id' => $this->request->get('from_account_id'), + 'paid_at' => $this->request->get('transferred_at'), + 'currency_code' => $expense_currency_code, + 'currency_rate' => $expense_currency_rate, + 'amount' => $this->request->get('amount'), + 'contact_id' => 0, + 'description' => $this->request->get('description'), + 'category_id' => $this->getTransferCategoryId(), + 'payment_method' => $this->request->get('payment_method'), + 'reference' => $this->request->get('reference'), + 'created_from' => $this->request->get('created_from'), + 'created_by' => $this->request->get('created_by'), + ])); + + $amount = $this->request->get('amount'); + + // Convert amount if not same currency + if ($expense_currency_code != $income_currency_code) { + $amount = $this->convertBetween($amount, $expense_currency_code, $expense_currency_rate, $income_currency_code, $income_currency_rate); + } + + $income_transaction = $this->dispatch(new CreateTransaction([ + 'company_id' => $this->request['company_id'], + 'type' => Transaction::INCOME_TRANSFER_TYPE, + 'number' => $this->getNextTransactionNumber(), + 'account_id' => $this->request->get('to_account_id'), + 'paid_at' => $this->request->get('transferred_at'), + 'currency_code' => $income_currency_code, + 'currency_rate' => $income_currency_rate, + 'amount' => $amount, + 'contact_id' => 0, + 'description' => $this->request->get('description'), + 'category_id' => $this->getTransferCategoryId(), + 'payment_method' => $this->request->get('payment_method'), + 'reference' => $this->request->get('reference'), + 'created_from' => $this->request->get('created_from'), + 'created_by' => $this->request->get('created_by'), + ])); + + $this->model = Transfer::create([ + 'company_id' => $this->request['company_id'], + 'expense_transaction_id' => $expense_transaction->id, + 'income_transaction_id' => $income_transaction->id, + 'created_from' => $this->request->get('created_from'), + 'created_by' => $this->request->get('created_by'), + ]); + + // Upload attachment + if ($this->request->file('attachment')) { + foreach ($this->request->file('attachment') as $attachment) { + $media = $this->getMedia($attachment, 'transfers'); + + $this->model->attachMedia($media, 'attachment'); + } + } + }); + + event(new TransferCreated($this->model)); + + return $this->model; + } + + public function authorize(): void + { + foreach (['from', 'to'] as $type) { + $account_id = $this->request->get($type . '_account_id'); + + if (empty($account_id) || ! Account::find($account_id)) { + throw new \Exception(trans('messages.error.not_found', ['type' => trans_choice('general.accounts', 1)])); + } + } + } + + protected function getCurrencyCode($type) + { + $currency_code = $this->request->get($type . '_account_currency_code'); + + if (empty($currency_code)) { + $currency_code = Account::where('id', $this->request->get($type . '_account_id'))->pluck('currency_code')->first(); + } + + return $currency_code; + } + + protected function getCurrencyRate($type) + { + $currency_rate = $this->request->get($type . '_account_rate'); + + if (empty($currency_rate)) { + $currency_rate = currency($this->getCurrencyCode($type))->getRate(); + } + + return $currency_rate; + } +} diff --git a/app/Jobs/Banking/DeleteAccount.php b/app/Jobs/Banking/DeleteAccount.php new file mode 100644 index 00000000..73ef5a36 --- /dev/null +++ b/app/Jobs/Banking/DeleteAccount.php @@ -0,0 +1,54 @@ +authorize(); + + event(new AccountDeleting($this->model)); + + \DB::transaction(function () { + $this->model->delete(); + }); + + event(new AccountDeleted($this->model)); + + return true; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + if ($relationships = $this->getRelationships()) { + $message = trans('messages.warning.deleted', ['name' => $this->model->name, 'text' => implode(', ', $relationships)]); + + throw new \Exception($message); + } + } + + public function getRelationships(): array + { + $rels = [ + 'transactions' => 'transactions', + 'reconciliations' => 'reconciliations', + ]; + + $relationships = $this->countRelationships($this->model, $rels); + + if ((int) $this->model->id === (int) setting('default.account')) { + $relationships[] = strtolower(trans_choice('general.companies', 1)); + } + + return $relationships; + } +} diff --git a/app/Jobs/Banking/DeleteReconciliation.php b/app/Jobs/Banking/DeleteReconciliation.php new file mode 100644 index 00000000..f9bc830d --- /dev/null +++ b/app/Jobs/Banking/DeleteReconciliation.php @@ -0,0 +1,26 @@ +model->delete(); + + Transaction::where('account_id', $this->model->account_id) + ->isReconciled() + ->whereBetween('paid_at', [$this->model->started_at, $this->model->ended_at])->each(function ($transaction) { + $transaction->reconciled = 0; + $transaction->save(); + }); + }); + + return true; + } +} diff --git a/app/Jobs/Banking/DeleteTransaction.php b/app/Jobs/Banking/DeleteTransaction.php new file mode 100644 index 00000000..91bb716a --- /dev/null +++ b/app/Jobs/Banking/DeleteTransaction.php @@ -0,0 +1,44 @@ +authorize(); + + event(new TransactionDeleting($this->model)); + + \DB::transaction(function () { + $this->deleteRelationships($this->model, ['recurring', 'taxes']); + + $this->model->delete(); + }); + + event(new TransactionDeleted($this->model)); + + return true; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + if ($this->model->reconciled) { + $message = trans('messages.warning.reconciled_tran'); + + throw new \Exception($message); + } + + if ($this->model->isTransferTransaction()) { + throw new \Exception(trans('messages.error.transfer_transaction')); + } + } +} diff --git a/app/Jobs/Banking/DeleteTransfer.php b/app/Jobs/Banking/DeleteTransfer.php new file mode 100644 index 00000000..7176a066 --- /dev/null +++ b/app/Jobs/Banking/DeleteTransfer.php @@ -0,0 +1,26 @@ +model)); + + \DB::transaction(function () { + $this->model->expense_transaction->delete(); + $this->model->income_transaction->delete(); + $this->model->delete(); + }); + + event(new TransferDeleted($this->model)); + + return true; + } +} diff --git a/app/Jobs/Banking/DuplicateTransaction.php b/app/Jobs/Banking/DuplicateTransaction.php new file mode 100644 index 00000000..92cddc62 --- /dev/null +++ b/app/Jobs/Banking/DuplicateTransaction.php @@ -0,0 +1,30 @@ +model = $model; + + parent::__construct($model); + } + + public function handle(): Transaction + { + \DB::transaction(function () { + $this->clone = $this->model->duplicate(); + }); + + event(new TransactionCreated($this->clone, request())); + + return $this->clone; + } +} diff --git a/app/Jobs/Banking/MatchBankingDocumentTransaction.php b/app/Jobs/Banking/MatchBankingDocumentTransaction.php new file mode 100644 index 00000000..4739350a --- /dev/null +++ b/app/Jobs/Banking/MatchBankingDocumentTransaction.php @@ -0,0 +1,94 @@ +model = $model; + + $this->transaction = $transaction; + } + + public function handle(): Transaction + { + $this->checkAmount(); + + \DB::transaction(function () { + $this->transaction = $this->dispatch(new UpdateTransaction($this->transaction, [ + 'document_id' => $this->model->id, + 'type' => $this->transaction->type, // Set missing type get default income type for UpdateTransaction job. + ])); + + $this->model->save(); + + $this->createHistory(); + }); + + return $this->transaction; + } + + protected function checkAmount(): bool + { + $code = $this->transaction->currency_code; + $rate = $this->transaction->currency_rate; + + $transaction_precision = currency($code)->getPrecision(); + $document_precision = currency($this->model->currency_code)->getPrecision(); + + $amount = $this->transaction->amount = round($this->transaction->amount, $transaction_precision); + + if ($this->model->currency_code != $code) { + $converted_amount = $this->convertBetween($amount, $code, $rate, $this->model->currency_code, $this->model->currency_rate); + + $amount = round($converted_amount, $document_precision); + } + + $this->model->paid_amount = $this->model->paid; + event(new PaidAmountCalculated($this->model)); + + $total_amount = round($this->model->amount - $this->model->paid_amount, $document_precision); + + unset($this->model->reconciled); + unset($this->model->paid_amount); + + $compare = bccomp($amount, $total_amount, $document_precision); + + if ($compare === 1) { + $error_amount = $total_amount; + + if ($this->model->currency_code != $code) { + $converted_amount = $this->convertBetween($total_amount, $this->model->currency_code, $this->model->currency_rate, $code, $rate); + + $error_amount = round($converted_amount, $transaction_precision); + } + + $message = trans('messages.error.over_match', ['type' => ucfirst($this->model->type), 'amount' => money($error_amount, $code)]); + + throw new \Exception($message); + } else { + $this->model->status = ($compare === 0) ? 'paid' : 'partial'; + } + + return true; + } + + protected function createHistory(): void + { + $history_desc = money((double) $this->transaction->amount, (string) $this->transaction->currency_code)->format() . ' ' . trans_choice('general.payments', 1); + + $this->dispatch(new CreateDocumentHistory($this->model, 0, $history_desc)); + } +} diff --git a/app/Jobs/Banking/SendTransaction.php b/app/Jobs/Banking/SendTransaction.php new file mode 100644 index 00000000..01c24ce0 --- /dev/null +++ b/app/Jobs/Banking/SendTransaction.php @@ -0,0 +1,27 @@ +transaction = $transaction; + } + + public function handle(): void + { + event(new TransactionSending($this->transaction)); + + // Notify the customer/vendor + $this->transaction->contact->notify(new Notification($this->transaction, config('type.transaction.' . $this->transaction->type . '.email_template'), true)); + + event(new TransactionSent($this->transaction)); + } +} diff --git a/app/Jobs/Banking/SendTransactionAsCustomMail.php b/app/Jobs/Banking/SendTransactionAsCustomMail.php new file mode 100644 index 00000000..2802035f --- /dev/null +++ b/app/Jobs/Banking/SendTransactionAsCustomMail.php @@ -0,0 +1,66 @@ +request = $request; + $this->template_alias = $template_alias; + } + + public function handle(): void + { + $transaction = Transaction::find($this->request->get('transaction_id')); + + event(new TransactionSending($transaction)); + + $mail_request = $this->request->only(['to', 'subject', 'body']); + + if ($this->request->get('user_email', false)) { + $mail_request['bcc'] = user()->email; + } + + $attachments = collect($this->request->get('attachments', [])) + ->filter(fn($value) => $value == true) + ->keys() + ->all(); + + $attach_pdf = in_array('pdf', $attachments); + + $contacts = $transaction->contact->withPersons(); + + $counter = 1; + + foreach ($contacts as $contact) { + if (! in_array($contact->email, $mail_request['to'])) { + continue; + } + + $custom_mail = [ + 'subject' => $mail_request['subject'], + 'body' => $mail_request['body'], + ]; + + if (($counter == 1) && ! empty($mail_request['bcc'])) { + $custom_mail['bcc'] = $mail_request['bcc']; + } + + $contact->notify(new Notification($transaction, $this->template_alias, $attach_pdf, $custom_mail, $attachments)); + + $counter++; + } + + event(new TransactionSent($transaction)); + } +} diff --git a/app/Jobs/Banking/SplitTransaction.php b/app/Jobs/Banking/SplitTransaction.php new file mode 100644 index 00000000..f05e8c32 --- /dev/null +++ b/app/Jobs/Banking/SplitTransaction.php @@ -0,0 +1,85 @@ +checkAmount(); + + $this->authorize(); + + event(new TransactionSplitting($this->request, $this->model)); + + DB::transaction(function () { + foreach ($this->request->items as $item) { + $transaction = $this->model->duplicate(); + $transaction->split_id = $this->model->id; + $transaction->amount = $item['amount']; + $transaction->save(); + + $document = Document::find($item['document_id']); + + $this->dispatch(new MatchBankingDocumentTransaction($document, $transaction)); + } + + $this->model->type = config('type.transaction.' . $this->model->type . '.split_type'); + $this->model->save(); + }); + + event(new TransactionSplitted($this->request, $this->model)); + + return $this->request->items; + } + + public function authorize(): void + { + foreach ($this->request->items as $item) { + if (empty($item['document_id'])) { + throw new \Exception(trans('messages.error.not_found', ['type' => trans_choice('general.documents', 1)])); + } + + if (! Document::find($item['document_id'])) { + throw new \Exception(trans('messages.error.not_found', ['type' => trans_choice('general.documents', 1)])); + } + } + } + + protected function checkAmount(): bool + { + $total_amount = 0; + + foreach ($this->request->items as $item) { + $total_amount += $item['amount']; + } + + $precision = currency($this->model->currency_code)->getPrecision(); + + $compare = bccomp($total_amount, $this->model->amount, $precision); + + if ($compare !== 0) { + $error_amount = $this->model->amount; + + $message = trans('messages.error.same_amount', [ + 'transaction' => ucfirst(trans_choice('general.' . Str::plural($this->model->type), 1)), + 'amount' => money($error_amount, $this->model->currency_code) + ]); + + throw new \Exception($message); + } + + return true; + } +} diff --git a/app/Jobs/Banking/UpdateAccount.php b/app/Jobs/Banking/UpdateAccount.php new file mode 100644 index 00000000..77592447 --- /dev/null +++ b/app/Jobs/Banking/UpdateAccount.php @@ -0,0 +1,68 @@ +authorize(); + + event(new AccountUpdating($this->model, $this->request)); + + \DB::transaction(function () { + $this->model->update($this->request->all()); + + // Set default account + if ($this->request['default_account']) { + setting()->set('default.account', $this->model->id); + setting()->save(); + } + }); + + event(new AccountUpdated($this->model, $this->request)); + + return $this->model; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + $relationships = $this->getRelationships(); + + if (($this->request->has('enabled') && ! $this->request->get('enabled')) && ($this->model->id == setting('default.account'))) { + $relationships[] = strtolower(trans_choice('general.companies', 1)); + + $message = trans('messages.warning.disabled', ['name' => $this->model->name, 'text' => implode(', ', $relationships)]); + + throw new \Exception($message); + } + + if (! $relationships) { + return; + } + + if ($this->model->currency_code != $this->request->get('currency_code')) { + $message = trans('messages.warning.disable_code', ['name' => $this->model->name, 'text' => implode(', ', $relationships)]); + + throw new \Exception($message); + } + } + + public function getRelationships(): array + { + $rels = [ + 'transactions' => 'transactions', + ]; + + return $this->countRelationships($this->model, $rels); + } +} diff --git a/app/Jobs/Banking/UpdateBankingDocumentTransaction.php b/app/Jobs/Banking/UpdateBankingDocumentTransaction.php new file mode 100644 index 00000000..26ed12ad --- /dev/null +++ b/app/Jobs/Banking/UpdateBankingDocumentTransaction.php @@ -0,0 +1,127 @@ +model = $model; + $this->transaction = $transaction; + + $this->request = $this->getRequestInstance($request); + } + + public function handle(): Transaction + { + event(new DocumentTransactionUpdating($this->model, $this->transaction, $this->request)); + + $this->prepareRequest(); + + $this->checkAmount(); + + \DB::transaction(function () { + $this->transaction = $this->dispatch(new UpdateTransaction($this->transaction, $this->request)); + + $this->model->save(); + + $this->createHistory(); + }); + + event(new DocumentTransactionUpdated($this->model, $this->transaction, $this->request)); + + return $this->transaction; + } + + protected function prepareRequest(): void + { + if (! isset($this->request['amount'])) { + $this->model->paid_amount = $this->model->paid; + event(new PaidAmountCalculated($this->model)); + + $this->request['amount'] = $this->model->amount - $this->model->paid_amount; + } + + $currency_code = ! empty($this->request['currency_code']) ? $this->request['currency_code'] : $this->model->currency_code; + + $this->request['company_id'] = $this->model->company_id; + $this->request['currency_code'] = $currency_code; + $this->request['paid_at'] = isset($this->request['paid_at']) ? $this->request['paid_at'] : Date::now()->toDateTimeString(); + $this->request['currency_rate'] = currency($currency_code)->getRate(); + $this->request['account_id'] = isset($this->request['account_id']) ? $this->request['account_id'] : setting('default.account'); + $this->request['document_id'] = isset($this->request['document_id']) ? $this->request['document_id'] : $this->model->id; + $this->request['contact_id'] = isset($this->request['contact_id']) ? $this->request['contact_id'] : $this->model->contact_id; + $this->request['category_id'] = isset($this->request['category_id']) ? $this->request['category_id'] : $this->model->category_id; + $this->request['payment_method'] = isset($this->request['payment_method']) ? $this->request['payment_method'] : setting('default.payment_method'); + $this->request['notify'] = isset($this->request['notify']) ? $this->request['notify'] : 0; + } + + protected function checkAmount(): bool + { + $code = $this->request['currency_code']; + $rate = $this->request['currency_rate']; + + $transaction_precision = currency($code)->getPrecision(); + $document_precision = currency($this->model->currency_code)->getPrecision(); + + $amount = $this->request['amount'] = round($this->request['amount'], $transaction_precision); + + if ($this->model->currency_code != $code) { + $converted_amount = $this->convertBetween($amount, $code, $rate, $this->model->currency_code, $this->model->currency_rate); + + $amount = round($converted_amount, $document_precision); + } + + // if you edit transaction before remove transaction amount + $this->model->paid_amount = ($this->model->paid - $this->transaction->amount_for_document); + event(new PaidAmountCalculated($this->model)); + + $total_amount = round($this->model->amount - $this->model->paid_amount, $document_precision); + + unset($this->model->reconciled); + unset($this->model->paid_amount); + + $compare = bccomp($amount, $total_amount, $document_precision); + + if ($compare === 1) { + $error_amount = $total_amount; + + if ($this->model->currency_code != $code) { + $converted_amount = $this->convertBetween($total_amount, $this->model->currency_code, $this->model->currency_rate, $code, $rate); + + $error_amount = round($converted_amount, $transaction_precision); + } + + $message = trans('messages.error.over_payment', ['amount' => money($error_amount, $code)]); + + throw new \Exception($message); + } else { + $this->model->status = ($compare === 0) ? 'paid' : 'partial'; + } + + return true; + } + + protected function createHistory(): void + { + $history_desc = money((double) $this->transaction->amount, (string) $this->transaction->currency_code)->format() . ' ' . trans_choice('general.payments', 1); + + $this->dispatch(new CreateDocumentHistory($this->model, 0, $history_desc)); + } +} diff --git a/app/Jobs/Banking/UpdateReconciliation.php b/app/Jobs/Banking/UpdateReconciliation.php new file mode 100644 index 00000000..8e12697f --- /dev/null +++ b/app/Jobs/Banking/UpdateReconciliation.php @@ -0,0 +1,41 @@ +request->get('reconcile'); + $transactions = $this->request->get('transactions'); + + $this->model->transactions = $transactions; + $this->model->reconciled = $reconcile; + $this->model->save(); + + if ($transactions) { + foreach ($transactions as $key => $value) { + $transaction_reconcile = $reconcile; + + if (empty($value) || $value === 'false') { + $transaction_reconcile = 0; + } + + $t = explode('_', $key); + + $transaction = Transaction::find($t[1]); + $transaction->reconciled = $transaction_reconcile; + $transaction->save(); + } + } + }); + + return $this->model; + } +} diff --git a/app/Jobs/Banking/UpdateTransaction.php b/app/Jobs/Banking/UpdateTransaction.php new file mode 100644 index 00000000..a014d8ad --- /dev/null +++ b/app/Jobs/Banking/UpdateTransaction.php @@ -0,0 +1,77 @@ +authorize(); + + event(new TransactionUpdating($this->model, $this->request)); + + if (! array_key_exists($this->request->get('type'), config('type.transaction'))) { + $isExpense = str_contains((string) $this->request->get('type', ''), 'expense'); + $isRecurring = ! empty($this->request->get('recurring_frequency')) && $this->request->get('recurring_frequency') !== 'no'; + + $type = $isExpense + ? ($isRecurring ? Transaction::EXPENSE_RECURRING_TYPE : Transaction::EXPENSE_TYPE) + : ($isRecurring ? Transaction::INCOME_RECURRING_TYPE : Transaction::INCOME_TYPE); + + $this->request->merge(['type' => $type]); + } + + \DB::transaction(function () { + $this->model->update($this->request->all()); + + // Upload attachment + if ($this->request->file('attachment')) { + $this->deleteMediaModel($this->model, 'attachment', $this->request); + + foreach ($this->request->file('attachment') as $attachment) { + $media = $this->getMedia($attachment, 'transactions'); + + $this->model->attachMedia($media, 'attachment'); + } + } elseif ($this->request->isNotApi() && ! $this->request->file('attachment') && $this->model->attachment) { + $this->deleteMediaModel($this->model, 'attachment', $this->request); + } elseif ($this->request->isApi() && $this->request->has('remove_attachment') && $this->model->attachment) { + $this->deleteMediaModel($this->model, 'attachment', $this->request); + } + + $this->deleteRelationships($this->model, ['taxes'], true); + + $this->dispatch(new CreateTransactionTaxes($this->model, $this->request)); + + // Recurring + $this->model->updateRecurring($this->request->all()); + }); + + event(new TransactionUpdated($this->model, $this->request)); + + return $this->model; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + if ($this->model->reconciled) { + $message = trans('messages.warning.reconciled_tran'); + + throw new \Exception($message); + } + + if ($this->model->isTransferTransaction()) { + throw new \Exception(trans('messages.error.transfer_transaction')); + } + } +} diff --git a/app/Jobs/Banking/UpdateTransfer.php b/app/Jobs/Banking/UpdateTransfer.php new file mode 100644 index 00000000..bf5a0f8c --- /dev/null +++ b/app/Jobs/Banking/UpdateTransfer.php @@ -0,0 +1,131 @@ +authorize(); + + event(new TransferUpdating($this->model, $this->request)); + + \DB::transaction(function () { + // Upload attachment + if ($this->request->file('attachment')) { + $this->deleteMediaModel($this->model, 'attachment', $this->request); + + foreach ($this->request->file('attachment') as $attachment) { + $media = $this->getMedia($attachment, 'transfers'); + + $this->model->attachMedia($media, 'attachment'); + } + } elseif ($this->request->isNotApi() && ! $this->request->file('attachment') && $this->model->attachment) { + $this->deleteMediaModel($this->model, 'attachment', $this->request); + } elseif ($this->request->isApi() && $this->request->has('remove_attachment') && $this->model->attachment) { + $this->deleteMediaModel($this->model, 'attachment', $this->request); + } + + $expense_currency_code = $this->getCurrencyCode('from'); + $income_currency_code = $this->getCurrencyCode('to'); + + $expense_currency_rate = $this->getCurrencyRate('from'); + $income_currency_rate = $this->getCurrencyRate('to'); + + $expense_transaction = Transaction::findOrFail($this->model->expense_transaction_id); + $income_transaction = Transaction::findOrFail($this->model->income_transaction_id); + + $expense_transaction->update([ + 'company_id' => $this->request['company_id'], + 'type' => Transaction::EXPENSE_TRANSFER_TYPE, + 'account_id' => $this->request->get('from_account_id'), + 'paid_at' => $this->request->get('transferred_at'), + 'currency_code' => $expense_currency_code, + 'currency_rate' => $expense_currency_rate, + 'amount' => $this->request->get('amount'), + 'contact_id' => 0, + 'description' => $this->request->get('description'), + 'category_id' => $this->getTransferCategoryId(), + 'payment_method' => $this->request->get('payment_method'), + 'reference' => $this->request->get('reference'), + ]); + + $amount = $this->request->get('amount'); + + // Convert amount if not same currency + if ($expense_currency_code != $income_currency_code) { + $amount = $this->convertBetween($amount, $expense_currency_code, $expense_currency_rate, $income_currency_code, $income_currency_rate); + } + + $income_transaction->update([ + 'company_id' => $this->request['company_id'], + 'type' => Transaction::INCOME_TRANSFER_TYPE, + 'account_id' => $this->request->get('to_account_id'), + 'paid_at' => $this->request->get('transferred_at'), + 'currency_code' => $income_currency_code, + 'currency_rate' => $income_currency_rate, + 'amount' => $amount, + 'contact_id' => 0, + 'description' => $this->request->get('description'), + 'category_id' => $this->getTransferCategoryId(), + 'payment_method' => $this->request->get('payment_method'), + 'reference' => $this->request->get('reference'), + ]); + + $this->model->update([ + 'company_id' => $this->request['company_id'], + 'expense_transaction_id' => $expense_transaction->id, + 'income_transaction_id' => $income_transaction->id, + ]); + }); + + event(new TransferUpdated($this->model, $this->request)); + + return $this->model; + } + + public function authorize(): void + { + foreach (['from', 'to'] as $type) { + $account_id = $this->request->get($type . '_account_id'); + + if (empty($account_id) || ! Account::find($account_id)) { + throw new \Exception(trans('messages.error.not_found', ['type' => trans_choice('general.accounts', 1)])); + } + } + } + + protected function getCurrencyCode($type) + { + $currency_code = $this->request->get($type . '_account_currency_code'); + + if (empty($currency_code)) { + $currency_code = Account::where('id', $this->request->get($type . '_account_id'))->pluck('currency_code')->first(); + } + + return $currency_code; + } + + protected function getCurrencyRate($type) + { + $currency_rate = $this->request->get($type . '_account_rate'); + + if (empty($currency_rate)) { + $currency_rate = currency($this->getCurrencyCode($type))->getRate(); + } + + return $currency_rate; + } +} diff --git a/app/Jobs/Common/CreateCompany.php b/app/Jobs/Common/CreateCompany.php new file mode 100644 index 00000000..6557d7a6 --- /dev/null +++ b/app/Jobs/Common/CreateCompany.php @@ -0,0 +1,170 @@ +authorize(); + + $current_company_id = company_id(); + + event(new CompanyCreating($this->request)); + + \DB::transaction(function () { + $this->model = Company::create($this->request->all()); + + $this->model->makeCurrent(); + + $this->callSeeds(); + + $this->updateCurrency(); + + $this->updateSettings(); + }); + + if (! empty($current_company_id)) { + company($current_company_id)->makeCurrent(); + } + + $this->clearPlansCache(); + + event(new CompanyCreated($this->model, $this->request)); + + return $this->model; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + $limit = $this->getAnyActionLimitOfPlan(); + if (! $limit->action_status) { + throw new \Exception($limit->message); + } + } + + protected function callSeeds(): void + { + // Set custom locale + if ($this->request->has('locale')) { + app()->setLocale($this->request->get('locale')); + } + + // Company seeds + Artisan::call('company:seed', [ + 'company' => $this->model->id + ]); + + // During first-run setup the company is created before the administrator. + // There is no user to attach or seed yet; CreateUser handles both steps + // immediately afterward. + if (! $user = user()) { + return; + } + + // Attach company to user logged in + $user->companies()->attach($this->model->id); + + // User seeds + Artisan::call('user:seed', [ + 'user' => $user->id, + 'company' => $this->model->id, + ]); + } + + protected function updateSettings(): void + { + if ($this->request->file('logo')) { + $company_logo = $this->getMedia($this->request->file('logo'), 'settings', $this->model->id); + + if ($company_logo) { + $this->model->attachMedia($company_logo, 'company_logo'); + + setting()->set('company.logo', $company_logo->id); + } + } + + // Create settings + setting()->set([ + 'company.name' => $this->request->get('name'), + 'company.email' => $this->request->get('email'), + 'company.tax_number' => $this->request->get('tax_number'), + 'company.phone' => $this->request->get('phone'), + 'company.address' => $this->request->get('address'), + 'company.city' => $this->request->get('city'), + 'company.zip_code' => $this->request->get('zip_code'), + 'company.state' => $this->request->get('state'), + 'company.country' => $this->request->get('country'), + 'default.currency' => $this->request->get('currency'), + 'default.locale' => $this->request->get('locale', 'en-GB'), + ]); + + if (!empty($this->request->settings)) { + foreach ($this->request->settings as $name => $value) { + setting()->set([$name => $value]); + } + } + + setting()->save(); + } + + protected function updateCurrency(): void + { + $currency_code = $this->request->get('currency'); + + if ($currency_code == 'USD') { + return; + } + + $currency = Currency::where('company_id', $this->model->id) + ->where('code', $currency_code) + ->first(); + + if ($currency) { + $currency->rate = '1'; + $currency->enabled = '1'; + + $currency->save(); + } else { + try { + $data = (new MoneyCurrency($currency_code))->toArray()[$currency_code]; + $data['rate'] = '1'; + $data['enabled'] = '1'; + $data['company_id'] = $this->model->id; + $data['code'] = $currency_code; + $data['created_from'] = 'core::ui'; + $data['created_by'] = user_id(); + + $currency = Currency::create($data); + } catch (OutOfBoundsException $e) { + report($e); + } + } + + $account = Account::where('company_id', $this->model->id)->first(); + + if ($account) { + $account->currency_code = $currency_code; + $account->save(); + } + } +} diff --git a/app/Jobs/Common/CreateContact.php b/app/Jobs/Common/CreateContact.php new file mode 100644 index 00000000..b87c9b38 --- /dev/null +++ b/app/Jobs/Common/CreateContact.php @@ -0,0 +1,67 @@ +request)); + + \DB::transaction(function () { + if ($this->request->get('create_user', 'false') === 'true') { + $this->createUser(); + } + + $this->model = Contact::create($this->request->all()); + + // Upload logo + if ($this->request->file('logo')) { + $media = $this->getMedia($this->request->file('logo'), Str::plural($this->model->type)); + + $this->model->attachMedia($media, 'logo'); + } + + $this->dispatch(new CreateContactPersons($this->model, $this->request)); + }); + + event(new ContactCreated($this->model, $this->request)); + + return $this->model; + } + + public function createUser(): void + { + // Check if user exist + if ($user = user_model_class()::where('email', $this->request['email'])->first()) { + $message = trans('messages.error.customer', ['name' => $user->name]); + + throw new \Exception($message); + } + + $customer_role_id = role_model_class()::all()->filter(function ($role) { + return $role->hasPermission('read-client-portal'); + })->pluck('id')->first(); + + $this->request->merge([ + 'locale' => setting('default.locale', 'en-GB'), + 'roles' => $customer_role_id, + 'companies' => [$this->request->get('company_id')], + ]); + + $user = $this->dispatch(new CreateUser($this->request)); + + $this->request['user_id'] = $user->id; + } +} diff --git a/app/Jobs/Common/CreateContactPerson.php b/app/Jobs/Common/CreateContactPerson.php new file mode 100644 index 00000000..88d545ba --- /dev/null +++ b/app/Jobs/Common/CreateContactPerson.php @@ -0,0 +1,21 @@ +model = ContactPerson::create($this->request->all()); + }); + + return $this->model; + } +} diff --git a/app/Jobs/Common/CreateContactPersons.php b/app/Jobs/Common/CreateContactPersons.php new file mode 100644 index 00000000..415182c7 --- /dev/null +++ b/app/Jobs/Common/CreateContactPersons.php @@ -0,0 +1,59 @@ +contact = $contact; + $this->request = $request; + + parent::__construct($contact, $request); + } + + /** + * Execute the job. + * + * @return mixed + * @todo type hint after upgrading to PHP 8 + */ + public function handle() + { + if (empty($this->request['contact_persons'])) { + return false; + } + + \DB::transaction(function () { + foreach ($this->request['contact_persons'] as $person) { + if (empty($person['name']) && empty($person['email']) && empty($person['phone'])) { + continue; + } + + ContactPerson::create([ + 'company_id' => $this->contact->company_id, + 'type' => $this->contact->type, + 'contact_id' => $this->contact->id, + 'name' => $person['name'] ?? null, + 'email' => $person['email'] ?? null, + 'phone' => $person['phone'] ?? null, + 'created_from' => $this->request['created_from'], + 'created_by' => $this->request['created_by'], + ]); + } + }); + + return $this->contact->contact_persons; + } +} diff --git a/app/Jobs/Common/CreateDashboard.php b/app/Jobs/Common/CreateDashboard.php new file mode 100644 index 00000000..fb009170 --- /dev/null +++ b/app/Jobs/Common/CreateDashboard.php @@ -0,0 +1,145 @@ +request['enabled'] = $this->request['enabled'] ?? 1; + + \DB::transaction(function () { + $users = $this->getUsers(); + + if (empty($users)) { + $this->model = Dashboard::make(); + + return; + } + + $this->model = Dashboard::create($this->request->only([ + 'company_id', 'name', 'enabled', 'created_from', 'created_by' + ])); + + $this->model->users()->attach($users); + + $this->checkAndCreateWidgets(); + }); + + return $this->model; + } + + protected function getUsers(): array + { + $list = []; + + if ($this->request->has('all_users')) { + Company::find($this->request->get('company_id'))->users()->each(function ($user) use (&$list) { + if (!$this->shouldCreateDashboardFor($user)) { + return; + } + + $list[] = $user->id; + }); + } elseif ($this->request->has('users')) { + $user_ids = Arr::wrap($this->request->get('users')); + + foreach($user_ids as $user_id) { + $user = user_model_class()::find($user_id); + + if (!$this->shouldCreateDashboardFor($user)) { + continue; + } + + $list[] = $user->id; + } + } else { + $user = user(); + + if ($this->shouldCreateDashboardFor($user)) { + $list[] = $user->id; + } + } + + return $list; + } + + protected function shouldCreateDashboardFor($user): bool + { + if (empty($user)) { + return false; + } + + // Don't create dashboard if user can't access admin panel (i.e. customer with login) + if ($user->cannot('read-admin-panel')) { + return false; + } + + return true; + } + + protected function checkAndCreateWidgets(): void + { + $sort = 1; + + if ($this->request->has('default_widgets')) { + $default_widgets = $this->request->get('default_widgets'); + + if (! is_array($default_widgets) && ($default_widgets == 'core')) { + Widgets::optimizeCoreWidgets(); + } + + $widgets = Widgets::getClasses($default_widgets, false); + + $this->createWidgets($widgets, $sort); + } + + if ($this->request->has('custom_widgets')) { + $widgets = $this->request->get('custom_widgets'); + + $this->createWidgets($widgets, $sort); + } + } + + protected function createWidgets($widgets, &$sort): void + { + foreach ($widgets as $class => $name) { + // It's just an array of classes + if (is_numeric($class)) { + $class = $name; + $name = (new $class())->getDefaultName(); + } + + $widget = Widget::companyId($this->model->company_id) + ->where('dashboard_id', $this->model->id) + ->where('class', $class) + ->first(); + + if (! $widget) { + $this->dispatch(new CreateWidget([ + 'company_id' => $this->model->company_id, + 'dashboard_id' => $this->model->id, + 'class' => $class, + 'name' => $name, + 'sort' => $sort, + 'settings' => (new $class())->getDefaultSettings(), + 'created_from' => $this->model->created_from, + 'created_by' => $this->model->created_by, + ])); + } + + $sort++; + } + } +} diff --git a/app/Jobs/Common/CreateItem.php b/app/Jobs/Common/CreateItem.php new file mode 100644 index 00000000..07769c12 --- /dev/null +++ b/app/Jobs/Common/CreateItem.php @@ -0,0 +1,47 @@ +request->has('name')) { + $this->request->merge([ + 'name' => strip_tags($this->request->get('name')), + ]); + } + + event(new ItemCreating($this->request)); + + \DB::transaction(function () { + $this->model = Item::create($this->request->all()); + + // Upload picture + if ($this->request->file('picture')) { + $media = $this->getMedia($this->request->file('picture'), 'items'); + + $this->model->attachMedia($media, 'picture'); + } + + $this->dispatch(new CreateItemTaxes($this->model, $this->request)); + }); + + event(new ItemCreated($this->model, $this->request)); + + return $this->model; + } +} diff --git a/app/Jobs/Common/CreateItemTaxes.php b/app/Jobs/Common/CreateItemTaxes.php new file mode 100644 index 00000000..29f69417 --- /dev/null +++ b/app/Jobs/Common/CreateItemTaxes.php @@ -0,0 +1,57 @@ +item = $item; + $this->request = $request; + + parent::__construct($item, $request); + } + + /** + * Execute the job. + * + * @return mixed + * @todo type hint after upgrading to PHP 8 + */ + public function handle() + { + // BC for 2.0 version + if (!empty($this->request['tax_id'])) { + $this->request['tax_ids'][] = $this->request['tax_id']; + } + + if (empty($this->request['tax_ids'])) { + return false; + } + + \DB::transaction(function () { + foreach ($this->request['tax_ids'] as $tax_id) { + ItemTax::create([ + 'company_id' => $this->item->company_id, + 'item_id' => $this->item->id, + 'tax_id' => $tax_id, + 'created_from' => $this->request['created_from'], + 'created_by' => $this->request['created_by'], + ]); + } + }); + + return $this->item->taxes; + } +} diff --git a/app/Jobs/Common/CreateMediableForDownload.php b/app/Jobs/Common/CreateMediableForDownload.php new file mode 100644 index 00000000..29ce0455 --- /dev/null +++ b/app/Jobs/Common/CreateMediableForDownload.php @@ -0,0 +1,116 @@ +user = $user; + $this->file_name = $file_name; + $this->translation = $translation; + + $this->onQueue('jobs'); + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + try { + $media = $this->getQueuedMedia(); + + $this->user->attachMedia($media, 'download'); + + $download_url = route('uploads.download', ['id' => $media->id, 'company_id' => company_id()]); + + $this->user->notify(new DownloadCompleted($this->translation, $this->file_name, $download_url)); + } catch (\Throwable $exception) { + report($exception); + + $this->user->notify(new DownloadFailed($exception->getMessage())); + + throw $exception; + } + } + + public function getQueuedMedia() + { + return config('dompdf.disk') !== null + ? $this->getRemoteQueuedMedia() + : $this->getLocalQueuedMedia(); + } + + public function getLocalQueuedMedia() + { + $folder_path = 'app/temp/' . company_id() . '/bulk_actions/'; + + $source = storage_path($folder_path . $this->file_name . '.zip'); + + $destination = $this->getMediaFolder('bulk_actions'); + + $media = MediaUploader::makePrivate() + ->beforeSave(function(MediaModel $media) { + $media->company_id = company_id(); + }) + ->fromSource($source) + ->toDirectory($destination) + ->upload(); + + File::delete($source); + + return $media; + } + + public function getRemoteQueuedMedia() + { + $disk = config('dompdf.disk'); + + $folder_path = 'app/temp/' . company_id() . '/bulk_actions/'; + + $source = get_storage_path($folder_path . $this->file_name . '.zip'); + + $content = Storage::disk($disk)->get($source); + + $file_name = str_replace(['.pdf', '.zip'], '', $this->file_name); + + $destination = $this->getMediaFolder('bulk_actions'); + + $media = MediaUploader::makePrivate() + ->beforeSave(function(MediaModel $media) { + $media->company_id = company_id(); + }) + ->fromString($content) + ->useFilename($file_name) + ->toDirectory($destination) + ->upload(); + + Storage::disk($disk)->delete($source); + + return $media; + } +} diff --git a/app/Jobs/Common/CreateMediableForExport.php b/app/Jobs/Common/CreateMediableForExport.php new file mode 100644 index 00000000..3b509b08 --- /dev/null +++ b/app/Jobs/Common/CreateMediableForExport.php @@ -0,0 +1,101 @@ +user = $user; + $this->file_name = $file_name; + $this->translation = $translation; + + $this->onQueue('jobs'); + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $media = $this->getQueuedMedia(); + + $this->user->attachMedia($media, 'export'); + + $download_url = route('uploads.download', ['id' => $media->id, 'company_id' => company_id()]); + + $this->user->notify(new ExportCompleted($this->translation, $this->file_name, $download_url)); + } + + public function getQueuedMedia() + { + return config('excel.temporary_files.remote_disk') !== null + ? $this->getRemoteQueuedMedia() + : $this->getLocalQueuedMedia(); + } + + public function getLocalQueuedMedia() + { + $source = storage_path('app/temp/' . $this->file_name); + + $destination = $this->getMediaFolder('exports'); + + $media = MediaUploader::makePrivate() + ->beforeSave(function(MediaModel $media) { + $media->company_id = company_id(); + }) + ->fromSource($source) + ->toDirectory($destination) + ->upload(); + + File::delete($source); + + return $media; + } + + public function getRemoteQueuedMedia() + { + $disk = config('excel.temporary_files.remote_disk'); + $prefix = config('excel.temporary_files.remote_prefix'); + + $content = Storage::disk($disk)->get($this->file_name); + + $file_name = str_replace([$prefix, '.xlsx', '.xls'], '', $this->file_name); + + $destination = $this->getMediaFolder('exports'); + + $media = MediaUploader::makePrivate() + ->beforeSave(function(MediaModel $media) { + $media->company_id = company_id(); + }) + ->fromString($content) + ->useFilename($file_name) + ->toDirectory($destination) + ->upload(); + + Storage::disk($disk)->delete($this->file_name); + + return $media; + } +} diff --git a/app/Jobs/Common/CreateReport.php b/app/Jobs/Common/CreateReport.php new file mode 100644 index 00000000..edddaf6a --- /dev/null +++ b/app/Jobs/Common/CreateReport.php @@ -0,0 +1,30 @@ +request->has('description')) { + $this->request->merge([ + 'description' => strip_tags($this->request->get('description')), + ]); + } + + \DB::transaction(function () { + $this->model = Report::create($this->request->all()); + }); + + return $this->model; + } +} diff --git a/app/Jobs/Common/CreateWidget.php b/app/Jobs/Common/CreateWidget.php new file mode 100644 index 00000000..3b068802 --- /dev/null +++ b/app/Jobs/Common/CreateWidget.php @@ -0,0 +1,21 @@ +widget = Widget::create($this->request->all()); + }); + + return $this->widget; + } +} diff --git a/app/Jobs/Common/CreateZipForDownload.php b/app/Jobs/Common/CreateZipForDownload.php new file mode 100644 index 00000000..1c336c48 --- /dev/null +++ b/app/Jobs/Common/CreateZipForDownload.php @@ -0,0 +1,110 @@ +selected = $selected; + $this->class = $class; + $this->file_name = $file_name; + + $this->onQueue('jobs'); + } + + public function handle() + { + $zip_archive = new ZipArchive(); + + $folder_path = 'app' . DIRECTORY_SEPARATOR . 'temp' . DIRECTORY_SEPARATOR . company_id() . DIRECTORY_SEPARATOR . 'bulk_actions' . DIRECTORY_SEPARATOR; + + File::ensureDirectoryExists(storage_path($folder_path)); + File::ensureDirectoryExists(get_storage_path($folder_path)); + + $zip_path = storage_path($folder_path . $this->file_name . '.zip'); + + $zip_archive->open($zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE); + + foreach ($this->selected as $selected) { + $pdf_path = $this->dispatch(new $this->class($selected, $folder_path)); + + $fileContent = $this->getQueuedFile($pdf_path); + + $zip_archive->addFromString(basename($pdf_path), $fileContent); + + /* + Storage::disk('local')->put($folder_path . basename($pdf_path), $fileContent); + + $zip->addFile(storage_path($folder_path . basename($pdf_path)), basename($pdf_path)); + */ + } + + $zip_archive->close(); + + $this->copyQueuedFile($folder_path, $zip_path); + + return $zip_path; + } + + public function getQueuedFile($pdf_path) + { + return config('dompdf.disk') !== null + ? $this->getRemoteQueuedMedia($pdf_path) + : $this->getLocalQueuedMedia($pdf_path); + } + + public function getLocalQueuedMedia($pdf_path) + { + $content = File::get($pdf_path); + + return $content; + } + + public function getRemoteQueuedMedia($pdf_path) + { + $disk = config('dompdf.disk'); + + $content = Storage::disk($disk)->get($pdf_path); + + return $content; + } + + public function copyQueuedFile($folder_path, $zip_path) + { + return config('dompdf.disk') !== null + ? $this->copyRemoteQueuedMedia($folder_path, $zip_path) + : true; + } + + public function copyRemoteQueuedMedia($folder_path, $zip_path) + { + $disk = config('dompdf.disk'); + + $file_path = get_storage_path($folder_path . basename($zip_path)); + + $content = Storage::disk($disk)->put($file_path, fopen($zip_path, 'r+')); + + report($file_path); + + return $content; + } +} diff --git a/app/Jobs/Common/DeleteCompany.php b/app/Jobs/Common/DeleteCompany.php new file mode 100644 index 00000000..5e93b78c --- /dev/null +++ b/app/Jobs/Common/DeleteCompany.php @@ -0,0 +1,98 @@ +current_company_id = company_id(); + } + + public function handle(): bool + { + $this->authorize(); + + $this->model->makeCurrent(); + + $this->model->relationships_to_delete = $this->getRelationshipsToDelete(); + + event(new CompanyDeleting($this->model, $this->current_company_id)); + + try { + \DB::transaction(function () { + $this->deleteRelationships($this->model, $this->model->relationships_to_delete); + + $this->model->delete(); + }); + + event(new CompanyDeleted($this->model, $this->current_company_id)); + } finally { + company($this->current_company_id)->makeCurrent(); + } + + return true; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + // Can't delete active company + if ($this->model->id == $this->current_company_id) { + $message = trans('companies.error.delete_active'); + + throw new \Exception($message); + } + + // Check if user can access company + if ($this->isNotUserCompany($this->model->id)) { + $message = trans('companies.error.not_user_company'); + + throw new \Exception($message); + } + } + + public function getRelationshipsToDelete(): array + { + return [ + 'accounts', + 'categories', + 'contact_persons', + 'contacts', + 'currencies', + 'dashboards', + 'document_histories', + 'document_item_taxes', + 'document_items', + 'document_totals', + 'documents', + 'email_templates', + 'item_taxes', + 'items', + 'media', + 'module_histories', + 'modules', + 'reconciliations', + 'recurring', + 'reports', + 'settings', + 'taxes', + 'transaction_taxes', + 'transactions', + 'transfers', + 'widgets', + ]; + } +} diff --git a/app/Jobs/Common/DeleteContact.php b/app/Jobs/Common/DeleteContact.php new file mode 100644 index 00000000..7783f627 --- /dev/null +++ b/app/Jobs/Common/DeleteContact.php @@ -0,0 +1,63 @@ +authorize(); + + event(new ContactDeleting($this->model)); + + \DB::transaction(function () { + $this->deleteRelationships($this->model, ['contact_persons']); + + if ($user = $this->model->user) { + $this->dispatch(new DeleteUser($user)); + } + + $this->model->delete(); + }); + + event(new ContactDeleted($this->model)); + + return true; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + if ($relationships = $this->getRelationships()) { + $message = trans('messages.warning.deleted', ['name' => $this->model->name, 'text' => implode(', ', $relationships)]); + + throw new \Exception($message); + } + } + + public function getRelationships(): array + { + $rels = [ + 'transactions' => 'transactions', + ]; + + if ($this->isCustomer()) { + $rels['invoices'] = 'invoices'; + } else { + $rels['bills'] = 'bills'; + } + + return $this->countRelationships($this->model, $rels); + } +} diff --git a/app/Jobs/Common/DeleteContactPerson.php b/app/Jobs/Common/DeleteContactPerson.php new file mode 100644 index 00000000..66b93e69 --- /dev/null +++ b/app/Jobs/Common/DeleteContactPerson.php @@ -0,0 +1,18 @@ +model->delete(); + }); + + return true; + } +} diff --git a/app/Jobs/Common/DeleteDashboard.php b/app/Jobs/Common/DeleteDashboard.php new file mode 100644 index 00000000..3e4b81f2 --- /dev/null +++ b/app/Jobs/Common/DeleteDashboard.php @@ -0,0 +1,51 @@ +authorize(); + + \DB::transaction(function () { + $this->deleteRelationships($this->model, ['widgets']); + + $this->model->delete(); + }); + + return true; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + // Can't delete last dashboard for any shared user + foreach ($this->model->users as $user) { + if (! $this->model->enabled || $user->dashboards()->enabled()->count() > 1) { + continue; + } + + $message = trans('dashboards.error.delete_last'); + + throw new LastDashboard($message); + } + + // Check if user can access dashboard + if ($this->isNotUserDashboard($this->model->id)) { + $message = trans('dashboards.error.not_user_dashboard'); + + throw new NotUserDashboard($message); + } + } +} diff --git a/app/Jobs/Common/DeleteItem.php b/app/Jobs/Common/DeleteItem.php new file mode 100644 index 00000000..df4234d3 --- /dev/null +++ b/app/Jobs/Common/DeleteItem.php @@ -0,0 +1,50 @@ +authorize(); + + event(new ItemDeleting($this->model)); + + \DB::transaction(function () { + $this->deleteRelationships($this->model, ['taxes']); + + $this->model->delete(); + }); + + event(new ItemDeleted($this->model)); + + return true; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + if ($relationships = $this->getRelationships()) { + $message = trans('messages.warning.deleted', ['name' => $this->model->name, 'text' => implode(', ', $relationships)]); + + throw new \Exception($message); + } + } + + public function getRelationships(): array + { + $rels = [ + 'invoice_items' => 'invoices', + 'bill_items' => 'bills', + ]; + + return $this->countRelationships($this->model, $rels); + } +} diff --git a/app/Jobs/Common/DeleteReport.php b/app/Jobs/Common/DeleteReport.php new file mode 100644 index 00000000..a655bc0d --- /dev/null +++ b/app/Jobs/Common/DeleteReport.php @@ -0,0 +1,71 @@ +deleteFavorite(); + $this->deletePin(); + + $this->model->delete(); + }); + + return true; + } + + public function deleteFavorite() + { + $favorites = setting('favorites.menu', []); + + if (empty($favorites)) { + return; + } + + foreach ($favorites as $user_id => $user_favorites) { + $user_favorites = json_decode($user_favorites, true); + + foreach ($user_favorites as $key => $favorite) { + if (! is_array($favorite['route'])) { + continue; + } + + if (str_contains($favorite['route'][0], 'reports.show') && $this->model->id == $favorite['route'][1]) { + unset($user_favorites[$key]); + } + } + + setting()->set('favorites.menu.' . $user_id, json_encode($user_favorites)); + setting()->save(); + } + } + + public function deletePin() + { + $pins = setting('favorites.report', []); + + if (empty($pins)) { + return; + } + + foreach ($pins as $user_id => $user_pins) { + $user_pins = json_decode($user_pins, true); + + foreach ($user_pins as $key => $pin) { + if ($this->model->id == $pin) { + unset($user_pins[$key]); + + break; + } + } + + setting()->set('favorites.report.' . $user_id, json_encode($user_pins)); + setting()->save(); + } + } +} diff --git a/app/Jobs/Common/DeleteWidget.php b/app/Jobs/Common/DeleteWidget.php new file mode 100644 index 00000000..60c67a30 --- /dev/null +++ b/app/Jobs/Common/DeleteWidget.php @@ -0,0 +1,18 @@ +model->delete(); + }); + + return true; + } +} diff --git a/app/Jobs/Common/DuplicateContact.php b/app/Jobs/Common/DuplicateContact.php new file mode 100644 index 00000000..b13e9f00 --- /dev/null +++ b/app/Jobs/Common/DuplicateContact.php @@ -0,0 +1,27 @@ +model = $model; + + parent::__construct($model); + } + + public function handle(): Contact + { + \DB::transaction(function () { + $this->clone = $this->model->duplicate(); + }); + + return $this->clone; + } +} diff --git a/app/Jobs/Common/UpdateCompany.php b/app/Jobs/Common/UpdateCompany.php new file mode 100644 index 00000000..db325a4e --- /dev/null +++ b/app/Jobs/Common/UpdateCompany.php @@ -0,0 +1,157 @@ +current_company_id = company_id(); + } + + public function handle(): Company + { + $this->authorize(); + + event(new CompanyUpdating($this->model, $this->request)); + + \DB::transaction(function () { + $this->model->update($this->request->all()); + + $this->model->makeCurrent(); + + if ($this->request->has('name')) { + setting()->set('company.name', $this->request->get('name')); + } + + if ($this->request->has('email')) { + setting()->set('company.email', $this->request->get('email')); + } + + if ($this->request->has('tax_number')) { + setting()->set('company.tax_number', $this->request->get('tax_number')); + } + + if ($this->request->has('phone')) { + setting()->set('company.phone', $this->request->get('phone')); + } + + if ($this->request->has('address')) { + setting()->set('company.address', $this->request->get('address')); + } + + if ($this->request->has('city')) { + setting()->set('company.city', $this->request->get('city')); + } + + if ($this->request->has('zip_code')) { + setting()->set('company.zip_code', $this->request->get('zip_code')); + } + + if ($this->request->has('state')) { + setting()->set('company.state', $this->request->get('state')); + } + + if ($this->request->has('country')) { + setting()->set('company.country', $this->request->get('country')); + } + + if ($this->request->has('currency')) { + setting()->set('default.currency', $this->request->get('currency')); + } + + if ($this->request->has('locale')) { + setting()->set('default.locale', $this->request->get('locale')); + } + + if ($this->request->file('logo')) { + $company_logo = $this->getMedia($this->request->file('logo'), 'settings', $this->model->id); + + if ($company_logo) { + $this->model->attachMedia($company_logo, 'company_logo'); + + setting()->set('company.logo', $company_logo->id); + } + } + + setting()->save(); + + $this->updateCurrency(); + }); + + event(new CompanyUpdated($this->model, $this->request)); + + if (!empty($this->current_company_id)) { + company($this->current_company_id)->makeCurrent(); + } + + return $this->model; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + // Can't disable active company + if (($this->request->get('enabled', 1) == 0) && ($this->model->id == $this->current_company_id)) { + $message = trans('companies.error.disable_active'); + + throw new \Exception($message); + } + + // Check if user can access company + if ($this->isNotUserCompany($this->model->id)) { + $message = trans('companies.error.not_user_company'); + + throw new \Exception($message); + } + } + + protected function updateCurrency() + { + $currency_code = $this->request->get('currency'); + + if (empty($currency_code)) { + return; + } + + $currency = Currency::where('company_id', $this->model->id) + ->where('code', $currency_code) + ->first(); + + if ($currency) { + $currency->rate = '1'; + $currency->enabled = '1'; + + $currency->save(); + } else { + try { + $data = (new MoneyCurrency($currency_code))->toArray()[$currency_code]; + $data['rate'] = '1'; + $data['enabled'] = '1'; + $data['company_id'] = $this->model->id; + $data['code'] = $currency_code; + $data['created_from'] = 'core::ui'; + $data['created_by'] = user_id(); + + $currency = Currency::create($data); + } catch (OutOfBoundsException $e) { + } + } + } +} diff --git a/app/Jobs/Common/UpdateContact.php b/app/Jobs/Common/UpdateContact.php new file mode 100644 index 00000000..da6ae2d2 --- /dev/null +++ b/app/Jobs/Common/UpdateContact.php @@ -0,0 +1,127 @@ +authorize(); + + event(new ContactUpdating($this->model, $this->request)); + + \DB::transaction(function () { + if ($this->request->get('create_user', 'false') === 'true') { + $this->createUser(); + } elseif ($this->model->user) { + $this->model->user->update($this->request->all()); + } + + // Upload logo + if ($this->request->file('logo')) { + $this->deleteMediaModel($this->model, 'logo', $this->request); + + $media = $this->getMedia($this->request->file('logo'), Str::plural($this->model->type)); + + $this->model->attachMedia($media, 'logo'); + } elseif ($this->request->isNotApi() && ! $this->request->file('logo') && $this->model->logo) { + $this->deleteMediaModel($this->model, 'logo', $this->request); + } elseif ($this->request->isApi() && $this->request->has('remove_logo') && $this->model->logo) { + $this->deleteMediaModel($this->model, 'logo', $this->request); + } + + $this->updateRecurringDocument(); + + $this->deleteRelationships($this->model, ['contact_persons']); + + $this->dispatch(new CreateContactPersons($this->model, $this->request)); + + $this->model->update($this->request->all()); + }); + + event(new ContactUpdated($this->model, $this->request)); + + return $this->model; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + if (($this->request->has('enabled') && ! $this->request->get('enabled')) && ($relationships = $this->getRelationships())) { + $message = trans('messages.warning.disabled', ['name' => $this->model->name, 'text' => implode(', ', $relationships)]); + + throw new \Exception($message); + } + } + + public function createUser(): void + { + // Check if user exist + if ($user = user_model_class()::where('email', $this->request['email'])->first()) { + $message = trans('messages.error.customer', ['name' => $user->name]); + + throw new \Exception($message); + } + + $customer_role_id = role_model_class()::all()->filter(function ($role) { + return $role->hasPermission('read-client-portal'); + })->pluck('id')->first(); + + $this->request->merge([ + 'locale' => setting('default.locale', 'en-GB'), + 'roles' => $customer_role_id, + 'companies' => [$this->request->get('company_id')], + ]); + + $user = $this->dispatch(new CreateUser($this->request)); + + $this->request['user_id'] = $user->id; + } + + public function updateRecurringDocument(): void + { + $recurring = $this->model->document_recurring; + + if ($recurring) { + foreach ($recurring as $recur) { + $recur->update([ + 'contact_name' => $this->request['name'], + 'contact_email' => $this->request['email'], + 'contact_tax_number' => $this->request['tax_number'], + 'contact_phone' => $this->request['phone'], + 'contact_address' => $this->request['address'], + 'contact_city' => $this->request['city'], + 'contact_state' => $this->request['state'], + 'contact_zip_code' => $this->request['zip_code'], + 'contact_country' => $this->request['country'], + ]); + } + } + } + + public function getRelationships(): array + { + $rels = [ + 'transactions' => 'transactions', + ]; + + if ($this->model->type == 'customer') { + $rels['invoices'] = 'invoices'; + } else { + $rels['bills'] = 'bills'; + } + + return $this->countRelationships($this->model, $rels); + } +} diff --git a/app/Jobs/Common/UpdateContactPerson.php b/app/Jobs/Common/UpdateContactPerson.php new file mode 100644 index 00000000..07e54d45 --- /dev/null +++ b/app/Jobs/Common/UpdateContactPerson.php @@ -0,0 +1,19 @@ +model->update($this->request->all()); + }); + + return $this->model; + } +} diff --git a/app/Jobs/Common/UpdateDashboard.php b/app/Jobs/Common/UpdateDashboard.php new file mode 100644 index 00000000..a48e1d0e --- /dev/null +++ b/app/Jobs/Common/UpdateDashboard.php @@ -0,0 +1,64 @@ +authorize(); + + \DB::transaction(function () { + $this->model->update($this->request->all()); + + if ($this->request->has('users')) { + $this->model->users()->sync($this->request->get('users')); + } + }); + + return $this->model; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + // Can't disable last dashboard for any shared user + if ($this->request->has('enabled') && ! $this->request->get('enabled')) { + foreach ($this->model->users as $user) { + if ($user->dashboards()->enabled()->count() > 1) { + continue; + } + + $message = trans('dashboards.error.disable_last'); + + throw new \Exception($message); + } + } + + if ($this->request->has('users')) { + $user = user(); + + if (!in_array($user->id, (array) $this->request->get('users')) && ($user->dashboards()->enabled()->count() == 1)) { + $message = trans('dashboards.error.delete_last'); + + throw new \Exception($message); + } + } + + // Check if user can access dashboard + if ($this->isNotUserDashboard($this->model->id)) { + $message = trans('dashboards.error.not_user_dashboard'); + + throw new \Exception($message); + } + } +} diff --git a/app/Jobs/Common/UpdateItem.php b/app/Jobs/Common/UpdateItem.php new file mode 100644 index 00000000..11611657 --- /dev/null +++ b/app/Jobs/Common/UpdateItem.php @@ -0,0 +1,52 @@ +request->has('name')) { + $this->request->merge([ + 'name' => strip_tags($this->request->get('name')), + ]); + } + + event(new ItemUpdating($this->model, $this->request)); + + \DB::transaction(function () { + $this->model->update($this->request->all()); + + // Upload picture + if ($this->request->file('picture')) { + $this->deleteMediaModel($this->model, 'picture', $this->request); + + $media = $this->getMedia($this->request->file('picture'), 'items'); + + $this->model->attachMedia($media, 'picture'); + } elseif ($this->request->isNotApi() && ! $this->request->file('picture') && $this->model->picture) { + $this->deleteMediaModel($this->model, 'picture', $this->request); + } elseif ($this->request->isApi() && $this->request->has('remove_picture') && $this->model->picture) { + $this->deleteMediaModel($this->model, 'picture', $this->request); + } + + $this->deleteRelationships($this->model, ['taxes']); + + $this->dispatch(new CreateItemTaxes($this->model, $this->request)); + }); + + event(new ItemUpdated($this->model, $this->request)); + + return $this->model; + } +} diff --git a/app/Jobs/Common/UpdateReport.php b/app/Jobs/Common/UpdateReport.php new file mode 100644 index 00000000..06a29e24 --- /dev/null +++ b/app/Jobs/Common/UpdateReport.php @@ -0,0 +1,26 @@ +request->has('description')) { + $this->request->merge([ + 'description' => strip_tags($this->request->get('description')), + ]); + } + + \DB::transaction(function () { + $this->model->update($this->request->all()); + }); + + return $this->model; + } +} diff --git a/app/Jobs/Common/UpdateWidget.php b/app/Jobs/Common/UpdateWidget.php new file mode 100644 index 00000000..5b986825 --- /dev/null +++ b/app/Jobs/Common/UpdateWidget.php @@ -0,0 +1,19 @@ +model->update($this->request->all()); + }); + + return $this->model; + } +} diff --git a/app/Jobs/Document/CancelDocument.php b/app/Jobs/Document/CancelDocument.php new file mode 100644 index 00000000..a3e74ded --- /dev/null +++ b/app/Jobs/Document/CancelDocument.php @@ -0,0 +1,50 @@ +model = $model; + + parent::__construct($model); + } + + public function handle(): Document + { + $this->authorize(); + + \DB::transaction(function () { + $this->deleteRelationships($this->model, [ + 'transactions', 'recurring' + ]); + + $this->model->status = 'cancelled'; + $this->model->save(); + }); + + return $this->model; + } + + /** + * Determine if this action is applicable. + * Mirrors the same guard in DeleteDocument to prevent cancelling a document + * that has reconciled transactions, which would silently destroy bank records. + */ + public function authorize(): void + { + if ($this->model->transactions()->isReconciled()->count()) { + $type = Str::plural($this->model->type); + $message = trans('messages.warning.reconciled_doc', ['type' => trans_choice("general.$type", 1)]); + + throw new \Exception($message); + } + } +} diff --git a/app/Jobs/Document/CreateDocument.php b/app/Jobs/Document/CreateDocument.php new file mode 100644 index 00000000..1e90560d --- /dev/null +++ b/app/Jobs/Document/CreateDocument.php @@ -0,0 +1,69 @@ +authorize(); + + if (empty($this->request['amount'])) { + $this->request['amount'] = 0; + } + + // Disable this lines for global discount issue fixed ( https://github.com/akaunting/akaunting/issues/2797 ) + if (! empty($this->request['discount'])) { + $this->request['discount_rate'] = $this->request['discount']; + } + + event(new DocumentCreating($this->request)); + + \DB::transaction(function () { + $this->model = Document::create($this->request->all()); + + // Upload attachment + if ($this->request->file('attachment')) { + foreach ($this->request->file('attachment') as $attachment) { + $media = $this->getMedia($attachment, Str::plural($this->model->type)); + + $this->model->attachMedia($media, 'attachment'); + } + } + + $this->dispatch(new CreateDocumentItemsAndTotals($this->model, $this->request)); + + $this->model->update($this->request->all()); + + $this->model->createRecurring($this->request->all()); + }); + + event(new DocumentCreated($this->model, $this->request)); + + return $this->model; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + $limit = $this->getAnyActionLimitOfPlan(); + if (! $limit->action_status && ! empty($this->request['type']) && ($this->request['type'] == 'invoice')) { + throw new \Exception($limit->message); + } + } +} diff --git a/app/Jobs/Document/CreateDocumentHistory.php b/app/Jobs/Document/CreateDocumentHistory.php new file mode 100644 index 00000000..1deafdc0 --- /dev/null +++ b/app/Jobs/Document/CreateDocumentHistory.php @@ -0,0 +1,67 @@ +document = $document; + $this->notify = $notify; + $this->description = $description; + $this->request = $request; + + parent::__construct($document, $notify, $description); + } + + public function handle(): DocumentHistory + { + $description = $this->description ?: trans_choice('general.payments', 1); + + $document_history = DocumentHistory::create([ + 'company_id' => $this->document->company_id, + 'type' => $this->document->type, + 'document_id' => $this->document->id, + 'status' => $this->document->status, + 'notify' => $this->notify, + 'description' => $description, + 'created_from' => $this->getCustomSourceName(), + 'created_by' => user_id(), + ]); + + return $document_history; + } + + public function getCustomSourceName(): string + { + if (empty($this->request)) { + return $this->getSourceName(); + } + + if (is_array($this->request)) { + if (empty($this->request['created_from'])) { + return $this->getSourceName(); + } + } elseif ($this->request instanceof Request) { + if ($this->request->missing('created_from')) { + return $this->getSourceName(); + } + } + + return $this->request['created_from']; + } +} diff --git a/app/Jobs/Document/CreateDocumentItem.php b/app/Jobs/Document/CreateDocumentItem.php new file mode 100644 index 00000000..170bcf0b --- /dev/null +++ b/app/Jobs/Document/CreateDocumentItem.php @@ -0,0 +1,203 @@ +document = $document; + $this->request = $request; + + parent::__construct($document, $request); + } + + public function handle(): DocumentItem + { + $item_id = ! empty($this->request['item_id']) ? $this->request['item_id'] : 0; + $precision = currency($this->document->currency_code)->getPrecision(); + + $item_amount = (double) $this->request['price'] * (double) calculation_to_quantity($this->request['quantity']); + + $item_discounted_amount = $item_amount; + + // Apply line discount to amount + if (! empty($this->request['discount'])) { + if ($this->request['discount_type'] === 'percentage') { + $item_discounted_amount -= ($item_amount * ($this->request['discount'] / 100)); + } else { + $item_discounted_amount -= $this->request['discount']; + } + } + + // Apply total discount to amount + if (! empty($this->request['global_discount'])) { + if ($this->request['global_discount_type'] === 'percentage') { + $global_discount = $item_discounted_amount * ($this->request['global_discount'] / 100); + } else { + $global_discount = $this->request['global_discount']; + } + + $item_discounted_amount -= $global_discount; + } + + $tax_amount = 0; + $item_tax_total = 0; + $actual_price_item = $item_amount = $item_discounted_amount; + + $item_taxes = []; + $doc_params = [ + 'company_id' => $this->document->company_id, + 'type' => $this->document->type, + 'document_id' => $this->document->id, + ]; + + if (!empty($this->request['tax_ids'])) { + // New variables by tax type & tax sorting + foreach ((array) $this->request['tax_ids'] as $tax_id) { + $tax = Tax::find($tax_id); + + // If tax not found, skip + if (! $tax) { + continue; + } + + ${$tax->type . 's'}[] = $tax; + } + + if (isset($inclusives)) { + foreach ($inclusives as $inclusive) { + $tax_amount = $item_discounted_amount - ($item_discounted_amount / (1 + $inclusive->rate / 100)); + + $item_taxes[] = $doc_params + [ + 'tax_id' => $inclusive->id, + 'name' => $inclusive->name, + 'amount' => $tax_amount, + ]; + + $item_tax_total += $tax_amount; + } + + $actual_price_item = $item_discounted_amount - $item_tax_total; + } + + if (isset($fixeds)) { + foreach ($fixeds as $tax) { + $tax_amount = $tax->rate * (double) $this->request['quantity']; + + $item_taxes[] = $doc_params + [ + 'tax_id' => $tax->id, + 'name' => $tax->name, + 'amount' => $tax_amount, + ]; + + $item_tax_total += $tax_amount; + $item_amount += $tax_amount; + } + } + + if (isset($normals)) { + foreach ($normals as $tax) { + $tax_amount = $actual_price_item * ($tax->rate / 100); + + $item_taxes[] = $doc_params + [ + 'tax_id' => $tax->id, + 'name' => $tax->name, + 'amount' => $tax_amount, + ]; + + $item_tax_total += $tax_amount; + $item_amount += $tax_amount; + } + } + + if (isset($withholdings)) { + foreach ($withholdings as $tax) { + $tax_amount = -($actual_price_item * ($tax->rate / 100)); + + $item_taxes[] = $doc_params + [ + 'tax_id' => $tax->id, + 'name' => $tax->name, + 'amount' => $tax_amount, + ]; + + $item_tax_total += $tax_amount; + $item_amount += $tax_amount; + } + } + + if (isset($compounds)) { + foreach ($compounds as $compound) { + $tax_amount = ($item_amount / 100) * $compound->rate; + + $item_taxes[] = $doc_params + [ + 'tax_id' => $compound->id, + 'name' => $compound->name, + 'amount' => $tax_amount, + ]; + + $item_tax_total += $tax_amount; + } + } + } + + if (! empty($global_discount)) { + $actual_price_item += $global_discount; + $item_amount += $global_discount; + $item_discounted_amount += $global_discount; + } + + $this->request['company_id'] = $this->document->company_id; + $this->request['type'] = $this->document->type; + $this->request['document_id'] = $this->document->id; + $this->request['item_id'] = $item_id; + $this->request['category_id'] = ! empty($this->request['category_id']) ? $this->request['category_id'] : null; + $this->request['name'] = Str::limit($this->request['name'], 180, ''); + $this->request['description'] = ! empty($this->request['description']) ? $this->request['description'] : ''; + $this->request['quantity'] = (double) calculation_to_quantity($this->request['quantity']); + $this->request['price'] = round($this->request['price'], $precision); + $this->request['tax'] = round($item_tax_total, $precision); + $this->request['discount_type'] = ! empty($this->request['discount_type']) ? $this->request['discount_type'] : 'percent'; + $this->request['discount_rate'] = ! empty($this->request['discount']) ? $this->request['discount'] : 0; + $this->request['total'] = round($actual_price_item, $precision); + $this->request['created_from'] = $this->request['created_from']; + $this->request['created_by'] = $this->request['created_by']; + + $document_item = DocumentItem::create($this->request); + + $document_item->item_taxes = false; + $document_item->inclusives = false; + $document_item->compounds = false; + + if (!empty($item_taxes)) { + $document_item->item_taxes = $item_taxes; + $document_item->inclusives = $inclusives ?? null; + $document_item->compounds = $compounds ?? null; + + foreach ($item_taxes as $item_tax) { + $item_tax['document_item_id'] = $document_item->id; + $item_tax['amount'] = round(abs($item_tax['amount']), $precision); + $item_tax['created_from'] = $this->request['created_from']; + $item_tax['created_by'] = $this->request['created_by']; + + DocumentItemTax::create($item_tax); + } + } + + return $document_item; + } +} diff --git a/app/Jobs/Document/CreateDocumentItemsAndTotals.php b/app/Jobs/Document/CreateDocumentItemsAndTotals.php new file mode 100644 index 00000000..f1f99120 --- /dev/null +++ b/app/Jobs/Document/CreateDocumentItemsAndTotals.php @@ -0,0 +1,290 @@ +document = $document; + + parent::__construct($request); + } + + public function handle(): void + { + $precision = currency($this->document->currency_code)->getPrecision(); + + list($sub_total, $actual_total, $discount_amount_total, $taxes) = $this->createItems(); + + $sort_order = 1; + + // Add sub total + DocumentTotal::create([ + 'company_id' => $this->document->company_id, + 'type' => $this->document->type, + 'document_id' => $this->document->id, + 'code' => 'sub_total', + 'name' => 'invoices.sub_total', + 'amount' => round($sub_total, $precision), + 'sort_order' => $sort_order, + 'created_from' => $this->request['created_from'], + 'created_by' => $this->request['created_by'], + ]); + + $this->request['amount'] += $actual_total; + + $sort_order++; + + // Add discount + if ($discount_amount_total > 0) { + DocumentTotal::create([ + 'company_id' => $this->document->company_id, + 'type' => $this->document->type, + 'document_id' => $this->document->id, + 'code' => 'item_discount', + 'name' => 'invoices.item_discount', + 'amount' => round($discount_amount_total, $precision), + 'sort_order' => $sort_order, + 'created_from' => $this->request['created_from'], + 'created_by' => $this->request['created_by'], + ]); + + $sort_order++; + } + + if (! empty($this->request['discount'])) { + if ($this->request['discount_type'] === 'percentage') { + //$discount_total = ($sub_total - $discount_amount_total) * ($this->request['discount'] / 100); + $discount_total = $sub_total * ($this->request['discount'] / 100); + } else { + $discount_total = $this->request['discount']; + } + + DocumentTotal::create([ + 'company_id' => $this->document->company_id, + 'type' => $this->document->type, + 'document_id' => $this->document->id, + 'code' => 'discount', + 'name' => 'invoices.discount', + 'amount' => round($discount_total, $precision), + 'sort_order' => $sort_order, + 'created_from' => $this->request['created_from'], + 'created_by' => $this->request['created_by'], + ]); + + $sort_order++; + } + + // Add taxes + if (! empty($taxes)) { + foreach ($taxes as $tax) { + DocumentTotal::create([ + 'company_id' => $this->document->company_id, + 'type' => $this->document->type, + 'document_id' => $this->document->id, + 'code' => 'tax', + 'name' => Str::ucfirst($tax['name']), + 'amount' => round(abs($tax['amount']), $precision), + 'sort_order' => $sort_order, + 'created_from' => $this->request['created_from'], + 'created_by' => $this->request['created_by'], + ]); + + $this->request['amount'] += $tax['amount']; + + $sort_order++; + } + } + + // Add extra totals, i.e. shipping fee + if (! empty($this->request['totals'])) { + foreach ($this->request['totals'] as $total) { + $total['company_id'] = $this->document->company_id; + $total['type'] = $this->document->type; + $total['document_id'] = $this->document->id; + $total['sort_order'] = $sort_order; + $total['created_from'] = $this->request['created_from']; + $total['created_by'] = $this->request['created_by']; + + if (empty($total['code'])) { + $total['code'] = 'extra'; + } + + $total['amount'] = round(abs($total['amount']), $precision); + + DocumentTotal::create($total); + + if (empty($total['operator']) || ($total['operator'] == 'addition')) { + $this->request['amount'] += $total['amount']; + } else { + // subtraction + $this->request['amount'] -= $total['amount']; + } + + $sort_order++; + } + } + + $this->request['amount'] = round($this->request['amount'], $precision); + + // Add total + DocumentTotal::create([ + 'company_id' => $this->document->company_id, + 'type' => $this->document->type, + 'document_id' => $this->document->id, + 'code' => 'total', + 'name' => 'invoices.total', + 'amount' => $this->request['amount'], + 'sort_order' => $sort_order, + 'created_from' => $this->request['created_from'], + 'created_by' => $this->request['created_by'], + ]); + } + + protected function createItems(): array + { + $sub_total = $actual_total = $discount_amount = $discount_amount_total = 0; + + $taxes = []; + + if (empty($this->request['items'])) { + return [$sub_total, $actual_total, $discount_amount_total, $taxes]; + } + + if (! empty($this->request['discount']) && $this->request['discount_type'] !== 'percentage') { + $for_fixed_discount = $this->fixedDiscountCalculate(); + } + + foreach ((array) $this->request['items'] as $key => $item) { + $item['global_discount'] = 0; + + // Disable this lines for global discount issue fixed ( https://github.com/akaunting/akaunting/issues/2797 ) + if (! empty($this->request['discount'])) { + if (isset($for_fixed_discount)) { + $item['global_discount'] = ($for_fixed_discount[$key] / ($for_fixed_discount['total'] / 100)) * ($this->request['discount'] / 100); + $item['global_discount_type'] = $this->request['discount_type']; + } else { + $item['global_discount'] = $this->request['discount']; + $item['global_discount_type'] = $this->request['discount_type']; + } + } + + $item['category_id'] = ! empty($item['category_id']) ? $item['category_id'] : $this->request['category_id']; + $item['created_from'] = $this->request['created_from']; + $item['created_by'] = $this->request['created_by']; + + if (empty($item['item_id'])) { + $new_item_request = [ + 'company_id' => $this->request['company_id'], + 'name' => $item['name'], + 'description' => $item['description'], + 'sale_price' => $item['price'], + 'purchase_price' => $item['price'], + 'category_id' => $item['category_id'], + 'created_from' => $item['created_from'], + 'created_by' => $item['created_by'], + 'enabled' => '1', + ]; + + if (! empty($item['tax_ids'])) { + $new_item_request['tax_ids'] = $item['tax_ids']; + } + + $new_item = $this->dispatch(new CreateItem($new_item_request)); + + $item['item_id'] = $new_item->id; + } + + $document_item = $this->dispatch(new CreateDocumentItem($this->document, $item)); + + # This line changed for discount calculator issue + //$item_amount = (double) $item['price'] * (double) $item['quantity']; + $item_amount = $document_item->total; + + $discount_amount = 0; + + if (! empty($item['discount'])) { + if ($item['discount_type'] === 'percentage') { + $discount_amount = ($item_amount * ($item['discount'] / 100)); + } else { + $discount_amount = $item['discount']; + } + } + + // Calculate totals + $sub_total += $item_amount; + $actual_total += $document_item->total; + + $discount_amount_total += $discount_amount; + + if (! $document_item->item_taxes) { + continue; + } + + // Set taxes + foreach ((array) $document_item->item_taxes as $item_tax) { + if (array_key_exists($item_tax['tax_id'], $taxes)) { + $taxes[$item_tax['tax_id']]['amount'] += round((float) $item_tax['amount'], $this->document->currency->precision); + } else { + $taxes[$item_tax['tax_id']] = [ + 'name' => $item_tax['name'], + 'amount' => round((float) $item_tax['amount'], $this->document->currency->precision), + ]; + } + } + } + + // Disable this lines for global discount issue fixed ( https://github.com/akaunting/akaunting/issues/2797 ) + if (! empty($this->request['discount'])) { + if ($this->request['discount_type'] === 'percentage') { + $actual_total -= ($sub_total * ($this->request['discount'] / 100)); + } else { + $actual_total -= $this->request['discount']; + } + } + + return [$sub_total, $actual_total, $discount_amount_total, $taxes]; + } + + public function fixedDiscountCalculate() + { + $total = 0; + + foreach ((array) $this->request['items'] as $item) { + $sub = (double) $item['price'] * (double) $item['quantity']; + + if (! empty($this->request['discount'])) { + if (isset($item['discount']) && isset($item['discount_type'])) { + if ($item['discount_type'] === 'percentage') { + $sub -= ($sub * ($item['discount'] / 100)); + } else { + $sub -= $item['discount']; + } + } + } + + $total += $sub; + $item_total[] = $sub; + } + + $item_total['total'] = $total; + + return $item_total; + } +} diff --git a/app/Jobs/Document/DeleteDocument.php b/app/Jobs/Document/DeleteDocument.php new file mode 100644 index 00000000..0eae4910 --- /dev/null +++ b/app/Jobs/Document/DeleteDocument.php @@ -0,0 +1,51 @@ +authorize(); + + event(new DocumentDeleting($this->model)); + + \DB::transaction(function () { + try { + Transaction::mute(); + + $this->deleteRelationships($this->model, [ + 'items', 'item_taxes', 'histories', 'transactions', 'recurring', 'totals' + ]); + + $this->model->delete(); + } finally { + Transaction::unmute(); + } + }); + + event(new DocumentDeleted($this->model)); + + return true; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + if ($this->model->transactions()->isReconciled()->count()) { + $type = Str::plural($this->model->type); + $message = trans('messages.warning.reconciled_doc', ['type' => trans_choice("general.$type", 1)]); + + throw new \Exception($message); + } + } +} diff --git a/app/Jobs/Document/DownloadDocument.php b/app/Jobs/Document/DownloadDocument.php new file mode 100644 index 00000000..47cb6017 --- /dev/null +++ b/app/Jobs/Document/DownloadDocument.php @@ -0,0 +1,68 @@ +document = $document; + $this->folder_path = $folder_path; + $this->zip_archive = $zip_archive; + $this->close_zip = $close_zip; + $this->method = $method; + } + + public function handle() + { + event(new DocumentPrinting($this->document)); + + $data = [ + $this->document->type => $this->document, + 'currency_style' => true + ]; + + $view = view($this->document->template_path, $data)->render(); + + $html = mb_convert_encoding($view, 'HTML-ENTITIES', 'UTF-8'); + + $pdf = app('dompdf.wrapper'); + $pdf->loadHTML($html); + + $file_name = $this->getDocumentFileName($this->document); + + switch ($this->method) { + case 'download': + $response = $pdf->download($file_name); + + break; + default: + $pdf_path = get_storage_path($this->folder_path . $file_name); + + // Save the PDF file into temp folder + $pdf->save($pdf_path); + + $response = $pdf_path; + + break; + } + + return $response; + } +} \ No newline at end of file diff --git a/app/Jobs/Document/DuplicateDocument.php b/app/Jobs/Document/DuplicateDocument.php new file mode 100644 index 00000000..1926e94d --- /dev/null +++ b/app/Jobs/Document/DuplicateDocument.php @@ -0,0 +1,37 @@ +model = $model; + + parent::__construct($model); + } + + public function handle(): Document + { + event(new DocumentDuplicating($this->model)); + + \DB::transaction(function () { + $this->clone = $this->model->duplicate(); + }); + + if (! $this->clone instanceof Document) { + throw new \RuntimeException('DuplicateDocument: duplicate() did not return a Document.'); + } + + event(new DocumentCreated($this->clone, request())); + + return $this->clone; + } +} diff --git a/app/Jobs/Document/RestoreDocument.php b/app/Jobs/Document/RestoreDocument.php new file mode 100644 index 00000000..3318b844 --- /dev/null +++ b/app/Jobs/Document/RestoreDocument.php @@ -0,0 +1,28 @@ +model = $model; + + parent::__construct($model); + } + + public function handle(): Document + { + \DB::transaction(function () { + $this->model->status = 'draft'; + $this->model->save(); + }); + + return $this->model; + } +} diff --git a/app/Jobs/Document/SendDocument.php b/app/Jobs/Document/SendDocument.php new file mode 100644 index 00000000..c925263b --- /dev/null +++ b/app/Jobs/Document/SendDocument.php @@ -0,0 +1,28 @@ +document = $document; + } + + public function handle(): void + { + event(new DocumentSending($this->document)); + + $notification = config('type.document.' . $this->document->type . '.notification.class'); + + // Notify the customer + $this->document->contact->notify(new $notification($this->document, 'invoice_new_customer', true)); + + event(new DocumentSent($this->document)); + } +} diff --git a/app/Jobs/Document/SendDocumentAsCustomMail.php b/app/Jobs/Document/SendDocumentAsCustomMail.php new file mode 100644 index 00000000..e6c7e713 --- /dev/null +++ b/app/Jobs/Document/SendDocumentAsCustomMail.php @@ -0,0 +1,67 @@ +request = $request; + $this->template_alias = $template_alias; + } + + public function handle(): void + { + $document = Document::find($this->request->get('document_id')); + + event(new DocumentSending($document)); + + $mail_request = $this->request->only(['to', 'subject', 'body']); + + if ($this->request->get('user_email', false)) { + $mail_request['bcc'] = user()->email; + } + + $attachments = collect($this->request->get('attachments', [])) + ->filter(fn($value) => $value == true) + ->keys() + ->all(); + + $attach_pdf = in_array('pdf', $attachments); + + $notification = config('type.document.' . $document->type . '.notification.class'); + + $contacts = $document->contact->withPersons(); + + $counter = 1; + + foreach ($contacts as $contact) { + if (! in_array($contact->email, $mail_request['to'])) { + continue; + } + + $custom_mail = [ + 'subject' => $mail_request['subject'], + 'body' => $mail_request['body'], + ]; + + if (($counter == 1) && ! empty($mail_request['bcc'])) { + $custom_mail['bcc'] = $mail_request['bcc']; + } + + $contact->notify(new $notification($document, $this->template_alias, $attach_pdf, $custom_mail, $attachments)); + + $counter++; + } + + event(new DocumentSent($document)); + } +} diff --git a/app/Jobs/Document/UpdateDocument.php b/app/Jobs/Document/UpdateDocument.php new file mode 100644 index 00000000..4b234dfd --- /dev/null +++ b/app/Jobs/Document/UpdateDocument.php @@ -0,0 +1,113 @@ +authorize(); + + if (empty($this->request['amount'])) { + $this->request['amount'] = 0; + } + + // Disable this lines for global discount issue fixed ( https://github.com/akaunting/akaunting/issues/2797 ) + $this->request['discount_rate'] = $this->request['discount'] ?? null; + + event(new DocumentUpdating($this->model, $this->request)); + + // Track original contact_id to sync transactions if it changes + $originalContactId = $this->model->contact_id; + + \DB::transaction(function () use ($originalContactId) { + // Upload attachment + if ($this->request->file('attachment')) { + $this->deleteMediaModel($this->model, 'attachment', $this->request); + + foreach ($this->request->file('attachment') as $attachment) { + $media = $this->getMedia($attachment, Str::plural($this->model->type)); + + $this->model->attachMedia($media, 'attachment'); + } + } elseif ($this->request->isNotApi() && ! $this->request->file('attachment') && $this->model->attachment) { + $this->deleteMediaModel($this->model, 'attachment', $this->request); + } elseif ($this->request->isApi() && $this->request->has('remove_attachment') && $this->model->attachment) { + $this->deleteMediaModel($this->model, 'attachment', $this->request); + } + + $this->deleteRelationships($this->model, ['items', 'item_taxes', 'totals'], true); + + $this->dispatch(new CreateDocumentItemsAndTotals($this->model, $this->request)); + + $this->model->paid_amount = $this->model->paid; + + event(new PaidAmountCalculated($this->model)); + + if ($this->model->paid_amount > 0) { + if ($this->request['amount'] == $this->model->paid_amount) { + $this->request['status'] = 'paid'; + } + + if ($this->request['amount'] > $this->model->paid_amount) { + $this->request['status'] = 'partial'; + } + } + + unset($this->model->reconciled); + unset($this->model->paid_amount); + + $this->model->update($this->request->all()); + + // Sync transaction contact_id if document contact changed (skip reconciled transactions) + if (isset($this->request['contact_id']) && $originalContactId != $this->request['contact_id']) { + $this->model->transactions()->where('reconciled', 0)->update([ + 'contact_id' => $this->request['contact_id'], + ]); + } + + $this->model->updateRecurring($this->request->all()); + }); + + event(new DocumentUpdated($this->model, $this->request)); + + return $this->model; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + if ( + isset($this->request['contact_id']) && + (int) $this->request['contact_id'] !== (int) $this->model->contact_id + ) { + $lockedStatuses = ['sent', 'received', 'viewed', 'partial', 'paid', 'overdue', 'unpaid', 'cancelled']; + + if (in_array($this->model->status, $lockedStatuses)) { + $type = Str::plural($this->model->type); + $message = trans('messages.warning.contact_change', ['type' => trans_choice("general.$type", 1)]); + + throw new \Exception($message); + } else if ($this->model->transactions()->isReconciled()->exists()) { + $type = Str::plural($this->model->type); + $message = trans('messages.warning.reconciled_doc', ['type' => trans_choice("general.$type", 1)]); + + throw new \Exception($message); + } + } + } +} diff --git a/app/Jobs/Install/CopyFiles.php b/app/Jobs/Install/CopyFiles.php new file mode 100644 index 00000000..67eba030 --- /dev/null +++ b/app/Jobs/Install/CopyFiles.php @@ -0,0 +1,102 @@ +alias = $alias; + $this->path = $path; + } + + /** + * Execute the job. + * + * @return string + */ + public function handle() + { + if (empty($this->path)) { + throw new \Exception(trans('modules.errors.file_copy', ['module' => $this->alias])); + } + + $source = storage_path('app/temp/' . $this->path); + + // Prevent path traversal: ensure source is strictly within the temp directory + $temp_base = realpath(storage_path('app/temp')); + $real_source = realpath($source); + + if (! $this->isPathWithinTempBase($real_source, $temp_base)) { + throw new \Exception(trans('modules.errors.file_copy', ['module' => $this->alias])); + } + + $destination = $this->getDestination($source); + + // Move all files/folders from temp path + if (!File::copyDirectory($source, $destination)) { + throw new \Exception(trans('modules.errors.file_copy', ['module' => $this->alias])); + } + + // Delete temp directory + File::deleteDirectory($source); + } + + protected function isPathWithinTempBase($real_source, $temp_base) + { + if (($real_source === false) || ($temp_base === false)) { + return false; + } + + $normalized_source = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $real_source); + $normalized_base = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $temp_base); + + if (DIRECTORY_SEPARATOR === '\\') { + $normalized_source = mb_strtolower($normalized_source); + $normalized_base = mb_strtolower($normalized_base); + } + + return str_starts_with(rtrim($normalized_source, DIRECTORY_SEPARATOR), rtrim($normalized_base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR); + } + + protected function getDestination($source) + { + if ($this->alias == 'core') { + return base_path(); + } + + if (!is_file($source . '/module.json')) { + throw new \Exception(trans('modules.errors.file_copy', ['module' => $this->alias])); + } + + $modules_path = config('module.paths.modules'); + + // Create modules directory + if (!File::isDirectory($modules_path)) { + File::makeDirectory($modules_path); + } + + $module_path = $modules_path . '/' . Str::studly($this->alias); + + // Create module directory + if (!File::isDirectory($module_path)) { + File::makeDirectory($module_path); + } + + return $module_path; + } +} diff --git a/app/Jobs/Install/DisableModule.php b/app/Jobs/Install/DisableModule.php new file mode 100644 index 00000000..5afdc90d --- /dev/null +++ b/app/Jobs/Install/DisableModule.php @@ -0,0 +1,66 @@ +alias = $alias; + $this->company_id = (int) $company_id; + $this->locale = $locale ?: company($company_id)->locale ?: config('setting.fallback.default.locale'); + } + + /** + * Execute the job. + * + * @return string + */ + public function handle() + { + $this->authorize(); + + event(new \App\Events\Module\Disabling($this->alias, $this->company_id)); + + $command = "module:disable {$this->alias} {$this->company_id} {$this->locale}"; + + $result = Console::run($command); + + if ($result !== true) { + throw new \Exception($result); + } + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + if (! $this->moduleExists($this->alias)) { + throw new \Exception("Module [{$this->alias}] not found."); + } + + if (! in_array($this->locale, config('language.allowed'))) { + throw new \Exception("Unknown locale: {$this->locale}"); + } + } +} diff --git a/app/Jobs/Install/DownloadFile.php b/app/Jobs/Install/DownloadFile.php new file mode 100644 index 00000000..be92e9e5 --- /dev/null +++ b/app/Jobs/Install/DownloadFile.php @@ -0,0 +1,81 @@ +alias = $alias; + $this->version = $version; + } + + /** + * Execute the job. + * + * @return string + */ + public function handle() + { + if (!$response = static::getResponse('GET', $this->getUrl(), ['timeout' => 50, 'track_redirects' => true])) { + throw new \Exception(trans('modules.errors.download', ['module' => $this->alias])); + } + + $file = $response->getBody()->getContents(); + + $json = json_decode($file); + + if (is_object($json)) { + throw new \Exception($json->message); + } + + $path = 'temp-' . md5(mt_rand()); + $temp_path = storage_path('app/temp/' . $path); + + $file_path = $temp_path . '/upload.zip'; + + // Create tmp directory + if (!File::isDirectory($temp_path)) { + File::makeDirectory($temp_path, 0755, true); + } + + // Add content to the Zip file + $uploaded = is_int(file_put_contents($file_path, $file)) ? true : false; + + if (!$uploaded) { + throw new \Exception(trans('modules.errors.download', ['module' => $this->alias])); + } + + return $path; + } + + protected function getUrl() + { + if ($this->alias == 'core') { + $info = Info::all(); + + $url = 'core/download/' . $this->version . '/' . $info['php'] . '/' . $info['mysql']; + } else { + $url = 'apps/' . $this->alias . '/download/' . $this->version . '/' . version('short') . '/' . setting('apps.api_key'); + } + + return $url; + } +} diff --git a/app/Jobs/Install/DownloadModule.php b/app/Jobs/Install/DownloadModule.php new file mode 100644 index 00000000..a8a2dcff --- /dev/null +++ b/app/Jobs/Install/DownloadModule.php @@ -0,0 +1,41 @@ +alias = $alias; + $this->company_id = (int) $company_id; + } + + /** + * Execute the job. + * + * @return string + */ + public function handle() + { + $command = "module:download {$this->alias} {$this->company_id}"; + + $result = Console::run($command); + + if ($result !== true) { + throw new \Exception($result); + } + } +} diff --git a/app/Jobs/Install/EnableModule.php b/app/Jobs/Install/EnableModule.php new file mode 100644 index 00000000..b7d24b9e --- /dev/null +++ b/app/Jobs/Install/EnableModule.php @@ -0,0 +1,64 @@ +alias = $alias; + $this->company_id = (int) $company_id; + $this->locale = $locale ?: company($company_id)->locale ?: config('setting.fallback.default.locale'); + } + + /** + * Execute the job. + * + * @return string + */ + public function handle() + { + $this->authorize(); + + $command = "module:enable {$this->alias} {$this->company_id} {$this->locale}"; + + $result = Console::run($command); + + if ($result !== true) { + throw new \Exception($result); + } + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + if (! $this->moduleExists($this->alias)) { + throw new \Exception("Module [{$this->alias}] not found."); + } + + if (! in_array($this->locale, config('language.allowed'))) { + throw new \Exception("Unknown locale: {$this->locale}"); + } + } +} diff --git a/app/Jobs/Install/FinishUpdate.php b/app/Jobs/Install/FinishUpdate.php new file mode 100644 index 00000000..eca42755 --- /dev/null +++ b/app/Jobs/Install/FinishUpdate.php @@ -0,0 +1,135 @@ +alias = $alias; + $this->new = $new; + $this->old = $old; + $this->company_id = (int) $company_id; + } + + public function handle(): void + { + $this->authorize(); + + $companies = $this->getCompanies(); + + foreach ($companies as $company) { + $company_id = is_object($company) ? $company->company_id : $company; + + $command = "update:finish {$this->alias} {$company_id} {$this->new} {$this->old}"; + + if (true !== $result = Console::run($command)) { + $message = !empty($result) ? $result : trans('modules.errors.finish', ['module' => $this->alias]); + + throw new \Exception($message); + } + } + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + if (($this->alias != 'core') && ! $this->moduleExists($this->alias)) { + throw new \Exception("Module [{$this->alias}] not found."); + } + } + + public function getCompanies() + { + if ($this->alias == 'core') { + return [$this->company_id]; + } + + return $this->getCompaniesOfModule(); + } + + public function getCompaniesOfModule() + { + $listener = $this->getListenerTypeOfModule(); + + if ($listener == 'none') { + Artisan::call('cache:clear'); + + return []; + } + + if ($listener == 'one') { + return [$this->company_id]; + } + + // Get company list from modules table + return Module::alias($this->alias)->allCompanies()->cursor(); + } + + public function getListenerTypeOfModule(): string + { + $listener = 'none'; + + $module = module($this->alias); + $filesystem = app(Filesystem::class); + + $updates_folder = $module->getPath() . '/Listeners/Update'; + + if (! File::isDirectory($updates_folder)) { + return $listener; + } + + foreach ($filesystem->allFiles($updates_folder) as $file) { + $path = str_replace([$module->getPath(), '.php'], '', $file->getPathname()); + + // Thank you PSR-4 + $class = '\Modules\\' . $module->getStudlyName() . str_replace('/', '\\', $path); + + if (! Versions::shouldUpdate($class::VERSION, $this->old, $this->new)) { + continue; + } + + if (app($class) instanceof ShouldUpdateAllCompanies) { + // Going to update data + $listener = 'all'; + + break; + } else { + // Going to update tables/files + $listener = 'one'; + } + } + + return $listener; + } +} diff --git a/app/Jobs/Install/InstallModule.php b/app/Jobs/Install/InstallModule.php new file mode 100644 index 00000000..2d0af681 --- /dev/null +++ b/app/Jobs/Install/InstallModule.php @@ -0,0 +1,66 @@ +alias = $alias; + $this->company_id = (int) $company_id; + $this->locale = $locale ?: company($company_id)->locale ?: config('setting.fallback.default.locale'); + } + + /** + * Execute the job. + * + * @return string + */ + public function handle() + { + $this->authorize(); + + $command = "module:install {$this->alias} {$this->company_id} {$this->locale}"; + + $result = Console::run($command); + + if ($result !== true) { + $message = !empty($result) ? $result : trans('modules.errors.finish', ['module' => $this->alias]); + + throw new \Exception($message); + } + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + if (! $this->moduleExists($this->alias)) { + throw new \Exception("Module [{$this->alias}] not found."); + } + + if (! in_array($this->locale, config('language.allowed'))) { + throw new \Exception("Unknown locale: {$this->locale}"); + } + } +} diff --git a/app/Jobs/Install/UninstallModule.php b/app/Jobs/Install/UninstallModule.php new file mode 100644 index 00000000..6a5e14a0 --- /dev/null +++ b/app/Jobs/Install/UninstallModule.php @@ -0,0 +1,66 @@ +alias = $alias; + $this->company_id = (int) $company_id; + $this->locale = $locale ?: company($company_id)->locale ?: config('setting.fallback.default.locale'); + } + + /** + * Execute the job. + * + * @return string + */ + public function handle() + { + $this->authorize(); + + event(new \App\Events\Module\Uninstalling($this->alias, $this->company_id)); + + $command = "module:uninstall {$this->alias} {$this->company_id} {$this->locale}"; + + $result = Console::run($command); + + if ($result !== true) { + throw new \Exception($result); + } + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + if (! $this->moduleExists($this->alias)) { + throw new \Exception("Module [{$this->alias}] not found."); + } + + if (! in_array($this->locale, config('language.allowed'))) { + throw new \Exception("Unknown locale: {$this->locale}"); + } + } +} diff --git a/app/Jobs/Install/UnzipFile.php b/app/Jobs/Install/UnzipFile.php new file mode 100644 index 00000000..a08ff85d --- /dev/null +++ b/app/Jobs/Install/UnzipFile.php @@ -0,0 +1,79 @@ +alias = $alias; + $this->path = $path; + } + + /** + * Execute the job. + * + * @return string + */ + public function handle() + { + if (empty($this->path)) { + throw new \Exception(trans('modules.errors.unzip', ['module' => $this->alias])); + } + + $temp_path = storage_path('app/temp/' . $this->path); + + $file = $temp_path . '/upload.zip'; + + // Unzip the file + $zip = new ZipArchive(); + + if (!$zip->open($file)) { + throw new \Exception(trans('modules.errors.unzip', ['module' => $this->alias])); + } + + // Validate all entries before extraction to prevent ZipSlip path traversal attacks + for ($i = 0; $i < $zip->numFiles; $i++) { + $entry_name = $zip->getNameIndex($i); + + if ($entry_name === false) { + continue; + } + + if ( + str_contains($entry_name, '..') || + str_starts_with($entry_name, '/') || + str_starts_with($entry_name, '\\') || + str_contains($entry_name, "\0") + ) { + $zip->close(); + throw new \Exception(trans('modules.errors.unzip', ['module' => $this->alias])); + } + } + + if (!$zip->extractTo($temp_path)) { + throw new \Exception(trans('modules.errors.unzip', ['module' => $this->alias])); + } + + $zip->close(); + + // Remove Zip + File::delete($file); + + return $this->path; + } +} diff --git a/app/Jobs/Setting/CreateCategory.php b/app/Jobs/Setting/CreateCategory.php new file mode 100644 index 00000000..9230e3ec --- /dev/null +++ b/app/Jobs/Setting/CreateCategory.php @@ -0,0 +1,41 @@ +request)); + + \DB::transaction(function () { + $payload = $this->request->all(); + + if (! empty($payload['code'])) { + $this->model = Category::firstOrCreate( + [ + 'company_id' => $payload['company_id'], + 'code' => $payload['code'] + ], + $payload, + ); + + return; + } + + $this->model = Category::create($payload); + }); + + event(new CategoryCreated($this->model, $this->request)); + + return $this->model; + } +} diff --git a/app/Jobs/Setting/CreateCurrency.php b/app/Jobs/Setting/CreateCurrency.php new file mode 100644 index 00000000..7730eee9 --- /dev/null +++ b/app/Jobs/Setting/CreateCurrency.php @@ -0,0 +1,38 @@ +request)); + + // Force the rate to be 1 for default currency + if ($this->request->get('default_currency')) { + $this->request['rate'] = '1'; + } + + \DB::transaction(function () { + $this->model = Currency::create($this->request->all()); + + // Update default currency setting + if ($this->request->get('default_currency')) { + setting()->set('default.currency', $this->request->get('code')); + setting()->save(); + } + }); + + event(new CurrencyCreated($this->model, $this->request)); + + return $this->model; + } +} diff --git a/app/Jobs/Setting/CreateEmailTemplate.php b/app/Jobs/Setting/CreateEmailTemplate.php new file mode 100644 index 00000000..2a1c2494 --- /dev/null +++ b/app/Jobs/Setting/CreateEmailTemplate.php @@ -0,0 +1,21 @@ +model = EmailTemplate::create($this->request->all()); + }); + + return $this->model; + } +} diff --git a/app/Jobs/Setting/CreateTax.php b/app/Jobs/Setting/CreateTax.php new file mode 100644 index 00000000..9aa40a8d --- /dev/null +++ b/app/Jobs/Setting/CreateTax.php @@ -0,0 +1,27 @@ +request)); + + \DB::transaction(function () { + $this->model = Tax::create($this->request->all()); + }); + + event(new TaxCreated($this->model, $this->request)); + + return $this->model; + } +} diff --git a/app/Jobs/Setting/DeleteCategory.php b/app/Jobs/Setting/DeleteCategory.php new file mode 100644 index 00000000..7c8134c0 --- /dev/null +++ b/app/Jobs/Setting/DeleteCategory.php @@ -0,0 +1,113 @@ +authorize(); + + event(new CategoryDeleting($this->model)); + + \DB::transaction(function () { + $this->deleteSubCategories($this->model); + + $this->model->delete(); + }); + + event(new CategoryDeleted($this->model)); + + return true; + } + + public function deleteSubCategories($model) + { + $model->delete(); + + foreach ($model->sub_categories as $sub_category) { + $this->deleteSubCategories($sub_category); + } + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + // Can not delete transfer category + if ($this->model->isTransferCategory()) { + $message = trans('messages.error.transfer_category', ['type' => $this->model->name]); + + throw new \Exception($message); + } + + // Can not delete the last category by type + if (Category::where('type', $this->model->type)->count() == 1 && $this->model->parent_id === null) { + $message = trans('messages.error.last_category', [ + 'type' => strtolower($this->getCategoryTypeLabel($this->model->type)), + ]); + + throw new LastCategoryDelete($message); + } + + if ($relationships = $this->getRelationships()) { + $message = trans('messages.warning.deleted', ['name' => $this->model->name, 'text' => implode(', ', $relationships)]); + + throw new \Exception($message); + } + + foreach ($this->model->sub_categories as $sub_category) { + $this->getSubCategoryRelationships($sub_category); + } + } + + public function getRelationships($model = null): array + { + if (! $model) { + $model = $this->model; + } + + $rels = [ + 'items' => 'items', + 'invoices' => 'invoices', + 'bills' => 'bills', + 'transactions' => 'transactions', + ]; + + $relationships = $this->countRelationships($model, $rels); + + if ($model->id == setting('default.income_category')) { + $relationships[] = strtolower(trans_choice('general.incomes', 1)); + } + + if ($model->id == setting('default.expense_category')) { + $relationships[] = strtolower(trans_choice('general.expenses', 1)); + } + + return $relationships; + } + + public function getSubCategoryRelationships($model) + { + if ($relationships = $this->getRelationships($model)) { + $message = trans('messages.warning.deleted', ['name' => $model->name, 'text' => implode(', ', $relationships)]); + + throw new \Exception($message); + } + + foreach ($model->sub_categories as $sub_category) { + $this->getSubCategoryRelationships($sub_category); + } + } +} diff --git a/app/Jobs/Setting/DeleteCurrency.php b/app/Jobs/Setting/DeleteCurrency.php new file mode 100644 index 00000000..faa48b0f --- /dev/null +++ b/app/Jobs/Setting/DeleteCurrency.php @@ -0,0 +1,57 @@ +authorize(); + + event(new CurrencyDeleting($this->model)); + + \DB::transaction(function () { + $this->model->delete(); + }); + + event(new CurrencyDeleted($this->model)); + + return true; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + if ($relationships = $this->getRelationships()) { + $message = trans('messages.warning.deleted', ['name' => $this->model->name, 'text' => implode(', ', $relationships)]); + + throw new \Exception($message); + } + } + + public function getRelationships(): array + { + $rels = [ + 'accounts' => 'accounts', + 'customers' => 'customers', + 'invoices' => 'invoices', + 'bills' => 'bills', + 'transactions' => 'transactions', + ]; + + $relationships = $this->countRelationships($this->model, $rels); + + if ($this->model->code == default_currency()) { + $relationships[] = strtolower(trans_choice('general.companies', 1)); + } + + return $relationships; + } +} diff --git a/app/Jobs/Setting/DeleteEmailTemplate.php b/app/Jobs/Setting/DeleteEmailTemplate.php new file mode 100644 index 00000000..424ae7d2 --- /dev/null +++ b/app/Jobs/Setting/DeleteEmailTemplate.php @@ -0,0 +1,18 @@ +model->delete(); + }); + + return true; + } +} diff --git a/app/Jobs/Setting/DeleteTax.php b/app/Jobs/Setting/DeleteTax.php new file mode 100644 index 00000000..8afa5e32 --- /dev/null +++ b/app/Jobs/Setting/DeleteTax.php @@ -0,0 +1,49 @@ +authorize(); + + event(new TaxDeleting($this->model)); + + \DB::transaction(function () { + $this->model->delete(); + }); + + event(new TaxDeleted($this->model)); + + return true; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + if ($relationships = $this->getRelationships()) { + $message = trans('messages.warning.deleted', ['name' => $this->model->name, 'text' => implode(', ', $relationships)]); + + throw new \Exception($message); + } + } + + public function getRelationships(): array + { + $rels = [ + 'items' => 'items', + 'invoice_items' => 'invoices', + 'bill_items' => 'bills', + ]; + + return $this->countRelationships($this->model, $rels); + } +} diff --git a/app/Jobs/Setting/UpdateCategory.php b/app/Jobs/Setting/UpdateCategory.php new file mode 100644 index 00000000..7c0b0722 --- /dev/null +++ b/app/Jobs/Setting/UpdateCategory.php @@ -0,0 +1,61 @@ +authorize(); + + event(new CategoryUpdating($this->model, $this->request)); + + \DB::transaction(function () { + $this->model->update($this->request->all()); + }); + + event(new CategoryUpdated($this->model, $this->request)); + + return $this->model; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + if (! $relationships = $this->getRelationships()) { + return; + } + + if ($this->request->has('type') && ($this->request->get('type') != $this->model->type)) { + $message = trans('messages.error.change_type', ['text' => implode(', ', $relationships)]); + + throw new \Exception($message); + } + + if ($this->request->has('enabled') && ! $this->request->get('enabled')) { + $message = trans('messages.warning.disabled', ['name' => $this->model->name, 'text' => implode(', ', $relationships)]); + + throw new \Exception($message); + } + } + + public function getRelationships(): array + { + $rels = [ + 'items' => 'items', + 'invoices' => 'invoices', + 'bills' => 'bills', + 'transactions' => 'transactions', + ]; + + return $this->countRelationships($this->model, $rels); + } +} diff --git a/app/Jobs/Setting/UpdateCurrency.php b/app/Jobs/Setting/UpdateCurrency.php new file mode 100644 index 00000000..726165ef --- /dev/null +++ b/app/Jobs/Setting/UpdateCurrency.php @@ -0,0 +1,79 @@ +authorize(); + + event(new CurrencyUpdating($this->model, $this->request)); + + // Force the rate to be 1 for default currency + if ($this->request->get('default_currency')) { + $this->request['rate'] = '1'; + } + + \DB::transaction(function () { + $this->model->update($this->request->all()); + + // Update default currency setting + if ($this->request->get('default_currency')) { + setting()->set('default.currency', $this->request->get('code')); + setting()->save(); + } + }); + + event(new CurrencyUpdated($this->model, $this->request)); + + return $this->model; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + if (! $relationships = $this->getRelationships()) { + return; + } + + if ($this->model->code != $this->request->get('code')) { + $message = trans('messages.warning.disable_code', ['name' => $this->model->name, 'text' => implode(', ', $relationships)]); + + throw new \Exception($message); + } + + if ($this->request->has('enabled') && ! $this->request->get('enabled')) { + $message = trans('messages.warning.disable_code', ['name' => $this->model->name, 'text' => implode(', ', $relationships)]); + + throw new \Exception($message); + } + } + + public function getRelationships(): array + { + $rels = [ + 'accounts' => 'accounts', + 'customers' => 'customers', + 'invoices' => 'invoices', + 'bills' => 'bills', + 'transactions' => 'transactions', + ]; + + $relationships = $this->countRelationships($this->model, $rels); + + if ($this->model->code == default_currency()) { + $relationships[] = strtolower(trans_choice('general.companies', 1)); + } + + return $relationships; + } +} diff --git a/app/Jobs/Setting/UpdateEmailTemplate.php b/app/Jobs/Setting/UpdateEmailTemplate.php new file mode 100644 index 00000000..69a4813e --- /dev/null +++ b/app/Jobs/Setting/UpdateEmailTemplate.php @@ -0,0 +1,19 @@ +model->update($this->request->all()); + }); + + return $this->model; + } +} diff --git a/app/Jobs/Setting/UpdateTax.php b/app/Jobs/Setting/UpdateTax.php new file mode 100644 index 00000000..0209d265 --- /dev/null +++ b/app/Jobs/Setting/UpdateTax.php @@ -0,0 +1,60 @@ +authorize(); + + event(new TaxUpdating($this->model, $this->request)); + + \DB::transaction(function () { + $this->model->update($this->request->all()); + }); + + event(new TaxUpdated($this->model, $this->request)); + + return $this->model; + } + + /** + * Determine if this action is applicable. + */ + public function authorize(): void + { + if (! $relationships = $this->getRelationships()) { + return; + } + + if ($this->request->has('type') && ($this->request->get('type') != $this->model->type)) { + $message = trans('messages.error.change_type', ['text' => implode(', ', $relationships)]); + + throw new \Exception($message); + } + + if ($this->request->has('enabled') && ! $this->request->get('enabled')) { + $message = trans('messages.warning.disabled', ['name' => $this->model->name, 'text' => implode(', ', $relationships)]); + + throw new \Exception($message); + } + } + + public function getRelationships(): array + { + $rels = [ + 'items' => 'items', + 'invoice_items' => 'invoices', + 'bill_items' => 'bills', + ]; + + return $this->countRelationships($this->model, $rels); + } +} diff --git a/app/Listeners/Auth/AddLandingPages.php b/app/Listeners/Auth/AddLandingPages.php new file mode 100644 index 00000000..f83d0013 --- /dev/null +++ b/app/Listeners/Auth/AddLandingPages.php @@ -0,0 +1,93 @@ +user->role) ? $event->user->role : false; + + $routes = [ + 'dashboard' => [ + 'permission' => 'read-common-dashboards', + 'translate' => trans_choice('general.dashboards', 1), + ], + 'items.index' => [ + 'permission' => 'read-common-items', + 'translate' => trans_choice('general.items', 2), + ], + 'invoices.index' => [ + 'permission' => 'read-sales-invoices', + 'translate' => trans_choice('general.invoices', 2), + ], + 'customers.index' => [ + 'permission' => 'read-sales-customers', + 'translate' => trans_choice('general.customers', 2), + ], + 'bills.index' => [ + 'permission' => 'read-purchases-bills', + 'translate' => trans_choice('general.bills', 2), + ], + 'vendors.index' => [ + 'permission' => 'read-purchases-vendors', + 'translate' => trans_choice('general.vendors', 2), + ], + 'accounts.index' => [ + 'permission' => 'read-banking-accounts', + 'translate' => trans_choice('general.accounts', 2), + ], + 'transfers.index' => [ + 'permission' => 'read-banking-transfers', + 'translate' => trans_choice('general.transfers', 2), + ], + 'transactions.index' => [ + 'permission' => 'read-banking-transactions', + 'translate' => trans_choice('general.transactions', 2), + ], + 'reconciliations.index' => [ + 'permission' => 'read-banking-reconciliations', + 'translate' => trans_choice('general.reconciliations', 2), + ], + 'reports.index' => [ + 'permission' => 'read-common-reports', + 'translate' => trans_choice('general.reports', 2), + ], + 'categories.index' => [ + 'permission' => 'read-settings-categories', + 'translate' => trans_choice('general.categories', 2), + ], + 'currencies.index' => [ + 'permission' => 'read-settings-currencies', + 'translate' => trans_choice('general.currencies', 2), + ], + 'taxes.index' => [ + 'permission' => 'read-settings-taxes', + 'translate' => trans_choice('general.taxes', 2), + ], + 'users.index' => [ + 'permission' => 'read-auth-users', + 'translate' => trans_choice('general.users', 2), + ], + ]; + + foreach($routes as $key => $route) { + if ($role && ! $role->hasPermission($route['permission'])) { + continue; + } else if (! $user->can($route['permission'])) { + continue; + } + + $event->user->landing_pages[$key] = $route['translate']; + } + } +} diff --git a/app/Listeners/Auth/Login.php b/app/Listeners/Auth/Login.php new file mode 100644 index 00000000..7032b0b2 --- /dev/null +++ b/app/Listeners/Auth/Login.php @@ -0,0 +1,23 @@ +user->last_logged_in_at = Date::now(); + + $event->user->save(); + } +} diff --git a/app/Listeners/Auth/Logout.php b/app/Listeners/Auth/Logout.php new file mode 100644 index 00000000..73235999 --- /dev/null +++ b/app/Listeners/Auth/Logout.php @@ -0,0 +1,19 @@ +forget('company_id'); + } +} diff --git a/app/Listeners/Banking/IncreaseNextTransactionNumber.php b/app/Listeners/Banking/IncreaseNextTransactionNumber.php new file mode 100644 index 00000000..b31b81ec --- /dev/null +++ b/app/Listeners/Banking/IncreaseNextTransactionNumber.php @@ -0,0 +1,25 @@ +transaction->isRecurringTransaction() ? '-recurring' : ''; + + // Update next transaction number + $this->increaseNextTransactionNumber($event->transaction->type, $suffix); + } +} diff --git a/app/Listeners/Common/ClearPlansCache.php b/app/Listeners/Common/ClearPlansCache.php new file mode 100644 index 00000000..be99105b --- /dev/null +++ b/app/Listeners/Common/ClearPlansCache.php @@ -0,0 +1,26 @@ +clearPlansCache(); + } + + public function subscribe(Dispatcher $events): array + { + return [ + UserDeleted::class => 'handle', + CompanyDeleted::class => 'handle', + ]; + } +} diff --git a/app/Listeners/Common/PreparingResponse.php b/app/Listeners/Common/PreparingResponse.php new file mode 100644 index 00000000..61a3e357 --- /dev/null +++ b/app/Listeners/Common/PreparingResponse.php @@ -0,0 +1,34 @@ +response instanceof \Illuminate\View\View) { + return; + } + + $data = $event->response->getData(); + + // Head components currency and all currencies variables + foreach (['currencies', 'currency'] as $key) { + if (! isset($data[$key])) { + continue; + } + + $value = $data[$key] ?? null; + + view()->share($key, $value); + } + } +} diff --git a/app/Listeners/Common/SkipScheduleInReadOnlyMode.php b/app/Listeners/Common/SkipScheduleInReadOnlyMode.php new file mode 100644 index 00000000..fe72f3b7 --- /dev/null +++ b/app/Listeners/Common/SkipScheduleInReadOnlyMode.php @@ -0,0 +1,30 @@ +task->skip(true); + + $schedule = app(Schedule::class); + + foreach ($schedule->events() as $task) { + $task->skip(true); + } + } +} diff --git a/app/Listeners/Document/CreateDocumentCreatedHistory.php b/app/Listeners/Document/CreateDocumentCreatedHistory.php new file mode 100644 index 00000000..e3c3ff65 --- /dev/null +++ b/app/Listeners/Document/CreateDocumentCreatedHistory.php @@ -0,0 +1,25 @@ + $event->document->document_number]); + + $this->dispatch(new CreateDocumentHistory($event->document, 0, $message, $event->request)); + } +} diff --git a/app/Listeners/Document/CreateDocumentTransaction.php b/app/Listeners/Document/CreateDocumentTransaction.php new file mode 100644 index 00000000..6d5487fb --- /dev/null +++ b/app/Listeners/Document/CreateDocumentTransaction.php @@ -0,0 +1,67 @@ +document; + $request = $event->request; + + try { + $this->dispatch(new CreateBankingDocumentTransaction($document, $request)); + } catch (\Exception $e) { + $message = $e->getMessage(); + + $user = user(); + + $type = Str::plural($event->document->type); + + $signed = request()->isSigned($document->company_id); + + if (empty($user) || $signed) { + flash($message)->error()->important(); + + return $this->getResponse('signed.' . $type . '.show', $document, $message); + } + + if ($user->isCustomer()) { + flash($message)->error()->important(); + + return $this->getResponse('portal.' . $type . '.show', $document, $message); + } + + throw new \Exception($message); + } + } + + protected function getResponse($path, $document, $message) + { + $url = route($path, $document->id); + + if (request()->expectsJson()) { + return response()->json([ + 'success' => false, + 'errors' => true, + 'message' => $message, + 'redirect' => $url, + ]); + } + + return redirect($url); + } +} diff --git a/app/Listeners/Document/IncreaseNextDocumentNumber.php b/app/Listeners/Document/IncreaseNextDocumentNumber.php new file mode 100644 index 00000000..6ebecc65 --- /dev/null +++ b/app/Listeners/Document/IncreaseNextDocumentNumber.php @@ -0,0 +1,23 @@ +increaseNextDocumentNumber($event->document->type); + } +} diff --git a/app/Listeners/Document/MarkDocumentCancelled.php b/app/Listeners/Document/MarkDocumentCancelled.php new file mode 100644 index 00000000..94b2abea --- /dev/null +++ b/app/Listeners/Document/MarkDocumentCancelled.php @@ -0,0 +1,42 @@ +dispatch(new CancelDocument($event->document)); + + $type_text = ''; + + if ($alias = config('type.document.' . $event->document->type . '.alias', '')) { + $type_text .= $alias . '::'; + } + + $type_text .= 'general.' . config('type.document.' . $event->document->type .'.translation.prefix'); + + $type = trans_choice($type_text, 1); + + $this->dispatch( + new CreateDocumentHistory( + $event->document, + 0, + trans('documents.messages.marked_cancelled', ['type' => $type]) + ) + ); + } +} diff --git a/app/Listeners/Document/MarkDocumentReceived.php b/app/Listeners/Document/MarkDocumentReceived.php new file mode 100644 index 00000000..250b6002 --- /dev/null +++ b/app/Listeners/Document/MarkDocumentReceived.php @@ -0,0 +1,50 @@ +document->status, ['partial', 'paid'])) { + $event->document->status = 'received'; + + //This control will be removed when approval status is added to documents. + if ($event->document->amount == 0) { + $event->document->status = 'paid'; + } + + $event->document->save(); + } + + $type_text = ''; + + if ($alias = config('type.document.' . $event->document->type . '.alias', '')) { + $type_text .= $alias . '::'; + } + + $type_text .= 'general.' . config('type.document.' . $event->document->type .'.translation.prefix'); + + $type = trans_choice($type_text, 1); + + $this->dispatch( + new CreateDocumentHistory( + $event->document, + 0, + trans('documents.messages.marked_received', ['type' => $type]) + ) + ); + } +} diff --git a/app/Listeners/Document/MarkDocumentSent.php b/app/Listeners/Document/MarkDocumentSent.php new file mode 100644 index 00000000..ac611be9 --- /dev/null +++ b/app/Listeners/Document/MarkDocumentSent.php @@ -0,0 +1,46 @@ +document->status, ['partial', 'paid'])) { + $event->document->status = 'sent'; + + //This control will be removed when approval status is added to documents. + if ($event->document->amount == 0) { + $event->document->status = 'paid'; + } + + $event->document->save(); + } + + $this->dispatch(new CreateDocumentHistory($event->document, 0, $this->getDescription($event))); + } + + public function getDescription(DocumentMarkedSent|DocumentSent $event): string + { + $type_text = ''; + + if ($alias = config('type.document.' . $event->document->type . '.alias', '')) { + $type_text .= $alias . '::'; + } + + $type_text .= 'general.' . config('type.document.' . $event->document->type .'.translation.prefix'); + + $type = trans_choice($type_text, 1); + + $message = ($event instanceof DocumentMarkedSent) ? 'marked_sent' : 'email_sent'; + + return trans('documents.messages.' . $message, ['type' => $type]); + } +} diff --git a/app/Listeners/Document/MarkDocumentViewed.php b/app/Listeners/Document/MarkDocumentViewed.php new file mode 100644 index 00000000..e0a4fb2b --- /dev/null +++ b/app/Listeners/Document/MarkDocumentViewed.php @@ -0,0 +1,50 @@ +document; + + if ($document->status != 'sent') { + return; + } + + unset($document->paid); + + $document->status = 'viewed'; + $document->save(); + + $type_text = ''; + + if ($alias = config('type.document.' . $event->document->type . '.alias', '')) { + $type_text .= $alias . '::'; + } + + $type_text .= 'general.' . config('type.document.' . $event->document->type .'.translation.prefix'); + + $type = trans_choice($type_text, 1); + + $this->dispatch( + new CreateDocumentHistory( + $event->document, + 0, + trans('documents.messages.marked_viewed', ['type' => $type]) + ) + ); + } +} diff --git a/app/Listeners/Document/RestoreDocument.php b/app/Listeners/Document/RestoreDocument.php new file mode 100644 index 00000000..9be982af --- /dev/null +++ b/app/Listeners/Document/RestoreDocument.php @@ -0,0 +1,36 @@ +dispatch(new Job($event->document)); + + $type_text = ''; + + if ($alias = config('type.document.' . $event->document->type . '.alias', '')) { + $type_text .= $alias . '::'; + } + + $type_text .= 'general.' . config('type.document.' . $event->document->type .'.translation.prefix'); + + $type = trans_choice($type_text, 1); + + $this->dispatch( + new CreateDocumentHistory( + $event->document, + 0, + trans('documents.messages.restored', ['type' => $type]) + ) + ); + } +} diff --git a/app/Listeners/Document/SendDocumentPaymentNotification.php b/app/Listeners/Document/SendDocumentPaymentNotification.php new file mode 100644 index 00000000..88a0f085 --- /dev/null +++ b/app/Listeners/Document/SendDocumentPaymentNotification.php @@ -0,0 +1,43 @@ +request['type'] !== 'income') { + return; + } + + $document = $event->document; + $transaction = $document->transactions()->latest()->first(); + + if (! $transaction) { + return; + } + + // Notify the customer + if ($document->contact && !empty($document->contact_email)) { + $document->contact->notify(new Notification($document, $transaction, "{$document->type}_payment_customer"), true); + } + + // Notify all users assigned to this company + foreach ($document->company->users as $user) { + if ($user->cannot('read-notifications')) { + continue; + } + + $user->notify(new Notification($document, $transaction, "{$document->type}_payment_admin")); + } + } +} diff --git a/app/Listeners/Document/SendDocumentRecurringNotification.php b/app/Listeners/Document/SendDocumentRecurringNotification.php new file mode 100644 index 00000000..b1e90fab --- /dev/null +++ b/app/Listeners/Document/SendDocumentRecurringNotification.php @@ -0,0 +1,58 @@ +document; + $config = config('type.document.' . $document->type . '.notification'); + + if (empty($config) || empty($config['class'])) { + return; + } + + if ($document->parent?->recurring?->auto_send == false) { + return; + } + + $notification = $config['class']; + $attach_pdf = true; + + // Notify the customer + if ($this->canNotifyTheContactOfDocument($document)) { + $document->contact->notify(new $notification($document, "{$document->type}_recur_customer", $attach_pdf)); + } + + $sent = config('type.document.' . $document->type . '.auto_send', DocumentSent::class); + + event(new $sent($document)); + + // Check if should notify users + if (! $config['notify_user']) { + return; + } + + // Notify all users assigned to this company + foreach ($document->company->users as $user) { + if ($user->cannot('read-notifications')) { + continue; + } + + $user->notify(new $notification($document, "{$document->type}_recur_admin")); + } + } +} diff --git a/app/Listeners/Document/SendDocumentReminderNotification.php b/app/Listeners/Document/SendDocumentReminderNotification.php new file mode 100644 index 00000000..0a512b6d --- /dev/null +++ b/app/Listeners/Document/SendDocumentReminderNotification.php @@ -0,0 +1,37 @@ +document; + $notification = $event->notification; + + // Notify the customer + if ($this->canNotifyTheContactOfDocument($document)) { + $document->contact->notify(new $notification($document, "{$document->type}_remind_customer")); + } + + // Notify all users assigned to this company + foreach ($document->company->users as $user) { + if ($user->cannot('read-notifications')) { + continue; + } + + $user->notify(new $notification($document, "{$document->type}_remind_admin")); + } + } +} diff --git a/app/Listeners/Document/SendDocumentViewNotification.php b/app/Listeners/Document/SendDocumentViewNotification.php new file mode 100644 index 00000000..39e6775f --- /dev/null +++ b/app/Listeners/Document/SendDocumentViewNotification.php @@ -0,0 +1,51 @@ +document; + + if (in_array($document->status, [ + 'viewed', 'approved', 'received', 'refused', 'partial', 'paid', + 'cancelled', 'voided', 'completed', 'refunded', + ])) { + return; + } + + $config = config('type.document.' . $document->type . '.notification'); + + if (empty($config) || empty($config['class'])) { + return; + } + + // Check if should notify users + if (! $config['notify_user']) { + return; + } + + $notification = $config['class']; + + // Notify all users assigned to this company + foreach ($document->company->users as $user) { + if ($user->cannot('read-notifications')) { + continue; + } + + $user->notify(new $notification($document, "{$document->type}_view_admin")); + } + } +} diff --git a/app/Listeners/Document/SettingFieldCreated.php b/app/Listeners/Document/SettingFieldCreated.php new file mode 100644 index 00000000..31682969 --- /dev/null +++ b/app/Listeners/Document/SettingFieldCreated.php @@ -0,0 +1,83 @@ +request; + $document = $event->document; + + if (! $request->has('setting')) { + return; + } + + $type = $request->get('type'); + $fields = $request->get('setting', []); + + // remove company logo + if (Arr::exists($fields, 'company_logo') && ! Arr::has($fields['company_logo'], 'dropzone')) { + setting()->forget('company.logo'); + } + + foreach ($fields as $key => $value) { + if ($key == 'company_logo') { + if (Arr::has($value, 'dropzone')) { + continue; + } + + setting()->set('company.logo', $value); + + continue; + } + + $real_key = $this->getDocumentSettingKey($type, $key); + + setting()->set($real_key, $value); + } + + $files = $request->file('setting', []); + + if ($files) { + $company = Company::find($document->company_id); + + foreach ($files as $key => $value) { + // Upload attachment + $media = $this->getMedia($value, 'settings'); + + $real_key = $this->getDocumentSettingKey($type, $key); + + $company->attachMedia($media, Str::snake($real_key)); + + $value = $media->id; + + if ($key == 'company_logo') { + setting()->set('company.logo', $value); + + continue; + } + + setting()->set($real_key, $value); + } + } + + // Save all settings + setting()->save(); + } +} diff --git a/app/Listeners/Document/SettingFieldUpdated.php b/app/Listeners/Document/SettingFieldUpdated.php new file mode 100644 index 00000000..247e0aa5 --- /dev/null +++ b/app/Listeners/Document/SettingFieldUpdated.php @@ -0,0 +1,83 @@ +request; + $document = $event->document; + + if (! $request->has('setting')) { + return; + } + + $type = $request->get('type'); + $fields = $request->get('setting', []); + + // remove company logo + if (Arr::exists($fields, 'company_logo') && ! Arr::has($fields['company_logo'], 'dropzone')) { + setting()->forget('company.logo'); + } + + foreach ($fields as $key => $value) { + if ($key == 'company_logo') { + if (Arr::has($value, 'dropzone')) { + continue; + } + + setting()->set('company.logo', $value); + + continue; + } + + $real_key = $type . '.' . $key; + + setting()->set($real_key, $value); + } + + $files = $request->file('setting', []); + + if ($files) { + $company = Company::find($document->company_id); + + foreach ($files as $key => $value) { + // Upload attachment + $media = $this->getMedia($value, 'settings'); + + $real_key = $type . '.' . $key; + + $company->attachMedia($media, Str::snake($real_key)); + + $value = $media->id; + + if ($key == 'company_logo') { + setting()->set('company.logo', $value); + + continue; + } + + setting()->set($real_key, $value); + } + } + + // Save all settings + setting()->save(); + } +} diff --git a/app/Listeners/Email/DisablePersonDueToInvalidEmail.php b/app/Listeners/Email/DisablePersonDueToInvalidEmail.php new file mode 100644 index 00000000..395bfe30 --- /dev/null +++ b/app/Listeners/Email/DisablePersonDueToInvalidEmail.php @@ -0,0 +1,50 @@ +disableContact($event); + + $this->disableUser($event); + } + + public function disableContact(Event $event): void + { + if (empty($event->contact)) { + return; + } + + $event->contact->enabled = false; + $event->contact->save(); + } + + public function disableUser(Event $event): void + { + if (empty($event->user)) { + return; + } + + // If only one user is left, don't disable it + $users = company()?->users; + + if ($users && $users->count() <= 1) { + return; + } + + $companies = $event->user->companies; + + foreach ($companies as $company) { + if ($company->users->count() <= 1) { + return; + } + } + + $event->user->enabled = false; + $event->user->save(); + } +} diff --git a/app/Listeners/Email/ReportTooManyEmailsSent.php b/app/Listeners/Email/ReportTooManyEmailsSent.php new file mode 100644 index 00000000..6b1e2fed --- /dev/null +++ b/app/Listeners/Email/ReportTooManyEmailsSent.php @@ -0,0 +1,14 @@ +users; + + if (empty($users)) { + return; + } + + $this->notifyAdminsAboutInvalidContactEmail($event, $users); + + $this->notifyAdminsAboutInvalidUserEmail($event, $users); + } + + public function notifyAdminsAboutInvalidContactEmail(Event $event, $users): void + { + if (empty($event->contact)) { + return; + } + + if ($event->contact->isCustomer() || $event->contact->isVendor() || $event->contact->isEmployee()) { + $type = trans_choice('general.' . Str::plural($event->contact->type), 1); + } else { + $type = ucfirst($event->contact->type); + } + + foreach ($users as $user) { + if ($user->cannot('read-notifications')) { + continue; + } + + $user->notify(new InvalidEmail($event->email, $type, $event->error)); + } + } + + public function notifyAdminsAboutInvalidUserEmail(Event $event, $users): void + { + if (empty($event->user)) { + return; + } + + $type = trans_choice('general.users', 1); + + foreach ($users as $user) { + if ($user->cannot('read-notifications')) { + continue; + } + + if ($user->email == $event->email) { + continue; + } + + $user->notify(new InvalidEmail($event->email, $type, $event->error)); + } + } +} diff --git a/app/Listeners/Email/TellFirewallTooManyEmailsSent.php b/app/Listeners/Email/TellFirewallTooManyEmailsSent.php new file mode 100644 index 00000000..c8264385 --- /dev/null +++ b/app/Listeners/Email/TellFirewallTooManyEmailsSent.php @@ -0,0 +1,40 @@ +request = request(); + $this->middleware = 'too_many_emails_sent'; + $this->user_id = $event->user_id; + + if ($this->skip()) { + return; + } + + $log = $this->log(); + + event(new AttackDetected($log)); + } + + public function skip(): bool + { + if ($this->isDisabled()) { + return true; + } + + if ($this->isWhitelist()) { + return true; + } + + return false; + } +} diff --git a/app/Listeners/Menu/ShowInAdmin.php b/app/Listeners/Menu/ShowInAdmin.php new file mode 100644 index 00000000..d0d36881 --- /dev/null +++ b/app/Listeners/Menu/ShowInAdmin.php @@ -0,0 +1,65 @@ +menu; + + foreach (SidebarPermissions::navigationFor(user()) as $sectionIndex => $section) { + $pages = $section['pages'] ?? []; + $order = ($sectionIndex + 1) * 10; + + if (empty($pages)) { + $menu->route( + 'business.pages.show', + $section['title'], + ['section' => $section['slug']], + $order, + [ + 'icon' => $section['icon'], + 'active' => fn () => request()->route('section') === $section['slug'], + ] + ); + + continue; + } + + $menu->dropdown($section['title'], function ($sub) use ($section, $pages) { + foreach ($pages as $pageIndex => $page) { + $parameters = [ + 'section' => $section['slug'], + 'page' => $page['slug'], + ]; + + if (! empty($page['tabs'])) { + $parameters['tab'] = $page['tabs'][0]['slug']; + } + + $sub->route( + 'business.pages.show', + $page['title'], + $parameters, + ($pageIndex + 1) * 10, + [ + 'icon' => '', + 'active' => fn () => request()->route('section') === $section['slug'] + && request()->route('page') === $page['slug'], + ] + ); + } + }, $order, [ + 'title' => $section['title'], + 'icon' => $section['icon'], + ]); + } + } +} diff --git a/app/Listeners/Menu/ShowInNeww.php b/app/Listeners/Menu/ShowInNeww.php new file mode 100644 index 00000000..efbaa327 --- /dev/null +++ b/app/Listeners/Menu/ShowInNeww.php @@ -0,0 +1,57 @@ +menu; + + $title = trim(trans_choice('general.invoices', 1)); + if ($this->canAccessMenuItem($title, 'create-sales-invoices')) { + $menu->route('invoices.create', $title, [], 10, ['icon' => 'description']); + } + + $title = trim(trans_choice('general.incomes', 1)); + if ($this->canAccessMenuItem($title, 'create-banking-transactions')) { + $menu->route('transactions.create', $title, ['type' => 'income'], 20, ['icon' => 'request_quote']); + } + + $title = trim(trans_choice('general.customers', 1)); + if ($this->canAccessMenuItem($title, 'create-sales-customers')) { + $menu->route('customers.create', $title, [], 30, ['icon' => 'person']); + } + + $title = trim(trans_choice('general.bills', 1)); + if ($this->canAccessMenuItem($title, 'create-purchases-bills')) { + $menu->route('bills.create', $title, [], 40, ['icon' => 'file_open']); + } + + $title = trim(trans_choice('general.expenses', 1)); + if ($this->canAccessMenuItem($title, 'create-banking-transactions')) { + $menu->route('transactions.create', $title, ['type' => 'expense'], 50, ['icon' => 'paid']); + } + + $title = trim(trans_choice('general.vendors', 1)); + if ($this->canAccessMenuItem($title, 'create-purchases-vendors')) { + $menu->route('vendors.create', $title, [], 60, ['icon' => 'engineering']); + } + + $title = trim(trans_choice('general.transfers', 1)); + if ($this->canAccessMenuItem($title, 'create-banking-transfers')) { + $menu->route('transfers.create', $title, [], 70, ['icon' => 'swap_horiz']); + } + } +} diff --git a/app/Listeners/Menu/ShowInNotifications.php b/app/Listeners/Menu/ShowInNotifications.php new file mode 100644 index 00000000..f931bfcf --- /dev/null +++ b/app/Listeners/Menu/ShowInNotifications.php @@ -0,0 +1,108 @@ +cannot('read-notifications')) { + return; + } + + static $notifications; + + if (! empty($notifications)) { + $event->notifications->notifications = $notifications; + + return; + } + + // Notification tables + $notifications = collect(); + + // Update notifications + if (user()->can('read-install-updates')) { + $updates = Versions::getUpdates(); + + foreach ($updates as $key => $update) { + $prefix = ($key == 'core') ? 'core' : 'module'; + + // Resolve module once per iteration to avoid redundant lookups. + $module = ($prefix === 'module') ? module($key) : null; + + if ($prefix === 'module' && ! $module) { + continue; + } + + $name = ($prefix === 'core') ? 'Akaunting' : $module->getName(); + + $new = new Notification(); + $new->id = $key; + $new->type = 'updates'; + $new->notifiable_type = "users"; + $new->notifiable_id = user()->id; + $new->data = [ + 'title' => $name . ' (v' . $update?->latest . ')', + 'description' => trans('install.update.' . $prefix, ['module' => $name, 'url' => route('updates.index')]), + ]; + $new->created_at = Date::now(); + + $notifications->push($new); + } + } + + // New app notifications + $new_apps = $this->getNotifications('new-apps'); + $read_notifications = (array) setting('notifications.' . user()->id, []); + + foreach ($new_apps as $key => $new_app) { + if (! empty($read_notifications[$new_app->alias])) { + unset($new_apps[$key]); + + continue; + } + + $app_url = route('apps.app.show', [ + 'alias' => $new_app->alias, + 'utm_source' => 'notification', + 'utm_medium' => 'app', + 'utm_campaign' => str_replace('-', '_', $new_app->alias), + ]); + + $new = new Notification(); + $new->id = $key; + $new->type = 'new-apps'; + $new->notifiable_type = "users"; + $new->notifiable_id = user()->id; + $new->data = [ + 'title' => $new_app->name, + 'description' => trans('notifications.new_apps', ['app' => $new_app->name, 'url' => $app_url]), + 'alias' => $new_app->alias, + ]; + $new->created_at = $new_app->started_at->date; + + $notifications->push($new); + } + + foreach (user()->unreadNotifications as $unreadNotification) { + $notifications->push($unreadNotification); + } + + $event->notifications->notifications = $notifications; + } +} diff --git a/app/Listeners/Menu/ShowInPortal.php b/app/Listeners/Menu/ShowInPortal.php new file mode 100644 index 00000000..bec95d34 --- /dev/null +++ b/app/Listeners/Menu/ShowInPortal.php @@ -0,0 +1,36 @@ +menu; + + $inactive = false; + + if (request()->route()->getName() != 'portal.dashboard') { + $inactive = true; + } + + $menu->route('portal.dashboard', trans_choice('general.dashboards', 1), [], 10, ['icon' => 'speed', 'inactive' => $inactive]); + + if ($user->can('read-portal-invoices')) { + $menu->route('portal.invoices.index', trans_choice('general.invoices', 2), [], 20, ['icon' => 'description']); + } + + if ($user->can('read-portal-payments')) { + $menu->route('portal.payments.index', trans_choice('general.payments', 2), [], 30, ['icon' => 'credit_score']); + } + } +} diff --git a/app/Listeners/Menu/ShowInProfile.php b/app/Listeners/Menu/ShowInProfile.php new file mode 100644 index 00000000..3d08b365 --- /dev/null +++ b/app/Listeners/Menu/ShowInProfile.php @@ -0,0 +1,39 @@ +menu; + + $title = trim(trans('auth.profile')); + if ($this->canAccessMenuItem($title, 'read-auth-profile')) { + $menu->route('profile.edit', $title, [user()->id], 10, ['icon' => 'badge']); + } + + if (user()->isCustomer()) { + $menu->route('portal.profile.edit', $title, [user()->id], 10, ['icon' => 'badge']); + } + + // User management now lives exclusively under the attachment-defined system settings. + + $is_portal = user()->isCustomer() ? 'portal.' : ''; + + $title = trim(trans('auth.logout')); + $menu->route($is_portal . 'logout', $title, [], 90, ['icon' => 'power_settings_new', 'class' => 'mt-5']); + } +} diff --git a/app/Listeners/Menu/ShowInSettings.php b/app/Listeners/Menu/ShowInSettings.php new file mode 100644 index 00000000..cef742aa --- /dev/null +++ b/app/Listeners/Menu/ShowInSettings.php @@ -0,0 +1,79 @@ +menu; + + if (user()?->hasRole('admin')) { + $menu->route('settings.navigation.manage', '侧边栏功能名称', [], 75, [ + 'icon' => 'menu', + 'search_keywords' => '侧边栏 功能 名称', + ]); + } + + $title = trim(trans_choice('general.companies', 1)); + if ($this->canAccessMenuItem($title, 'read-settings-company')) { + $menu->route('settings.company.edit', $title, [], 10, ['icon' => 'business', 'search_keywords' => trans('settings.company.search_keywords')]); + } + + $title = trim(trans_choice('general.localisations', 1)); + if ($this->canAccessMenuItem($title, 'read-settings-localisation')) { + $menu->route('settings.localisation.edit', $title, [], 20, ['icon' => 'flag', 'search_keywords' => trans('settings.localisation.search_keywords')]); + } + + $title = trim(trans_choice('general.invoices', 1)); + if ($this->canAccessMenuItem($title, 'read-settings-invoice')) { + $menu->route('settings.invoice.edit', $title, [], 30, ['icon' => 'description', 'search_keywords' => trans('settings.invoice.search_keywords')]); + } + + $title = trim(trans_choice('general.defaults', 1)); + if ($this->canAccessMenuItem($title, 'read-settings-defaults')) { + $menu->route('settings.default.edit', $title, [], 40, ['icon' => 'tune', 'search_keywords' => trans('settings.default.search_keywords')]); + } + + $title = trim(trans_choice('general.email_services', 1)); + if ($this->canAccessMenuItem($title, 'read-settings-email')) { + $menu->route('settings.email.edit', $title, [], 50, ['icon' => 'email', 'search_keywords' => trans('settings.email_services.search_keywords')]); + } + + $title = trim(trans_choice('general.email_templates', 2)); + if ($this->canAccessMenuItem($title, 'read-settings-email-templates')) { + $menu->route('settings.email-templates.edit', $title, [], 60, ['icon' => 'attach_email', 'search_keywords' => trans('settings.email.templates.search_keywords')]); + } + + $title = trim(trans('settings.scheduling.name')); + if ($this->canAccessMenuItem($title, 'read-settings-schedule')) { + $menu->route('settings.schedule.edit', $title, [], 70, ['icon' => 'alarm', 'search_keywords' => trans('settings.scheduling.search_keywords')]); + } + + $title = trim(trans_choice('general.categories', 2)); + if ($this->canAccessMenuItem($title, 'read-settings-categories')) { + $menu->route('categories.index', $title, [], 80, ['icon' => 'folder', 'search_keywords' => trans('settings.categories.search_keywords')]); + } + + $title = trim(trans_choice('general.currencies', 2)); + if ($this->canAccessMenuItem($title, 'read-settings-currencies')) { + $menu->route('currencies.index', $title, [], 90, ['icon' => 'attach_money', 'search_keywords' => trans('settings.currencies.search_keywords')]); + } + + $title = trim(trans_choice('general.taxes', 2)); + if ($this->canAccessMenuItem($title, 'read-settings-taxes')) { + $menu->route('taxes.index', $title, [], 100, ['icon' => 'percent', 'search_keywords' => trans('settings.taxes.search_keywords')]); + } + } +} diff --git a/app/Listeners/Module/ClearCache.php b/app/Listeners/Module/ClearCache.php new file mode 100644 index 00000000..372c5fd7 --- /dev/null +++ b/app/Listeners/Module/ClearCache.php @@ -0,0 +1,45 @@ +company_id); + } + + /** + * Register the listeners for the subscriber. + * + * @param \Illuminate\Events\Dispatcher $dispatcher + */ + public function subscribe($dispatcher) + { + $events = [ + 'App\Events\Install\UpdateCacheCleared', + 'App\Events\Module\Copied', + 'App\Events\Module\Enabled', + 'App\Events\Module\Disabled', + 'App\Events\Module\Uninstalled', + ]; + + foreach ($events as $event) { + $dispatcher->listen($event, 'App\Listeners\Module\ClearCache@handle'); + } + } +} diff --git a/app/Listeners/Module/FinishInstallation.php b/app/Listeners/Module/FinishInstallation.php new file mode 100644 index 00000000..44ddf0a1 --- /dev/null +++ b/app/Listeners/Module/FinishInstallation.php @@ -0,0 +1,27 @@ +alias); + + Artisan::call('module:migrate', ['alias' => $event->alias, '--force' => true]); + + $this->attachDefaultModulePermissions($module); + } +} diff --git a/app/Listeners/Module/FinishUninstallation.php b/app/Listeners/Module/FinishUninstallation.php new file mode 100644 index 00000000..1399807f --- /dev/null +++ b/app/Listeners/Module/FinishUninstallation.php @@ -0,0 +1,146 @@ +deleteDashboards($event->alias); + $this->deleteWidgets($event->alias); + $this->deleteEmailTemplates($event->alias); + $this->deleteReports($event->alias); + $this->deleteFavorites($event->alias); + } + + /** + * Delete any dashboard created by the module. + * + * @param string $alias + * @return void + */ + protected function deleteDashboards($alias) + { + Dashboard::alias($alias)->get()->each(function ($dashboard) { + try { + $this->dispatch(new DeleteDashboard($dashboard)); + } catch (Throwable $e) { + if ($e instanceof LastDashboard) { + return; + } + + report($e); + } + }); + } + + /** + * Delete any widget created by the module. + * + * @param string $alias + * @return void + */ + protected function deleteWidgets($alias) + { + Widget::alias($alias)->get()->each(function ($widget) { + try { + $this->dispatch(new DeleteWidget($widget)); + } catch (Throwable $e) { + report($e); + } + }); + } + + /** + * Delete any email template created by the module. + * + * @param string $alias + * @return void + */ + protected function deleteEmailTemplates($alias) + { + EmailTemplate::moduleAlias($alias)->get()->each(function ($template) { + try { + $this->dispatch(new DeleteEmailTemplate($template)); + } catch (Throwable $e) { + report($e); + } + }); + } + + /** + * Delete any report created by the module. + * + * @param string $alias + * @return void + */ + protected function deleteReports($alias) + { + Report::alias($alias)->get()->each(function ($report) { + try { + $this->dispatch(new DeleteReport($report)); + } catch (Throwable $e) { + report($e); + } + }); + } + + /** + * Delete any favorite created by the module. + * + * @param string $alias + * @return void + */ + protected function deleteFavorites($alias) + { + $favorites = setting('favorites.menu', []); + + if (empty($favorites)) { + return; + } + + try { + foreach ($favorites as $user_id => $user_favorites) { + $user_favorites = json_decode($user_favorites, true); + + foreach ($user_favorites as $key => $favorite) { + $route = $favorite['route']; + + if (is_array($route)) { + $route = $route[0]; + } + + if (str_contains($route, $alias)) { + unset($user_favorites[$key]); + } + } + + setting()->set('favorites.menu.' . $user_id, json_encode($user_favorites)); + setting()->save(); + } + } catch (Throwable $e) { + report($e); + } + } +} diff --git a/app/Listeners/Module/InstallExtraModules.php b/app/Listeners/Module/InstallExtraModules.php new file mode 100644 index 00000000..ac5bd276 --- /dev/null +++ b/app/Listeners/Module/InstallExtraModules.php @@ -0,0 +1,63 @@ +alias == 'core') { + return; + } + + $extra_modules = module($event->alias)->get('extra-modules'); + + if (empty($extra_modules)) { + return; + } + + foreach ($extra_modules as $alias => $level) { + // Don't install if the module is "suggested" + if ($level != 'required') { + continue; + } + + // Check if module is already installed + if ($this->moduleIsEnabled($alias)) { + continue; + } + + try { + if (!$this->moduleExists($alias)) { + $this->dispatch(new DownloadModule($alias, $event->company_id)); + } + + $this->dispatch(new InstallModule($alias, $event->company_id, $event->locale)); + } catch (\Exception $e) { + report($e); + + // Stop the propagation of event if the required module failed to install + return false; + } + } + } +} diff --git a/app/Listeners/Module/UpdateExtraModules.php b/app/Listeners/Module/UpdateExtraModules.php new file mode 100644 index 00000000..46f3f973 --- /dev/null +++ b/app/Listeners/Module/UpdateExtraModules.php @@ -0,0 +1,68 @@ +alias == 'core') { + return; + } + + $extra_modules = module($event->alias)->get('extra-modules'); + + if (empty($extra_modules)) { + return; + } + + foreach ($extra_modules as $alias => $level) { + // Don't update if the module is "suggested" + if ($level != 'required') { + continue; + } + + $extra_module = module($alias); + + if (empty($extra_module)) { + continue; + } + + $installed_version = $extra_module->get('version'); + $latest_version = Versions::latest($alias)?->latest; + + // Skip if no update available + if (version_compare($installed_version, $latest_version, '>=')) { + continue; + } + + $company_id = company_id(); + + $command = "update {$alias} {$company_id} {$latest_version}"; + + if (true !== $result = Console::run($command)) { + $message = !empty($result) ? $result : trans('modules.errors.finish', ['module' => $alias]); + + report($message); + + // Stop the propagation of event if the required module failed to update + return false; + } + } + } +} diff --git a/app/Listeners/Report/AddAccounts.php b/app/Listeners/Report/AddAccounts.php new file mode 100644 index 00000000..dfa497c0 --- /dev/null +++ b/app/Listeners/Report/AddAccounts.php @@ -0,0 +1,93 @@ +skipThisClass($event)) { + return; + } + + // send true for add limit on search and filter.. + $event->class->filters['accounts'] = $this->getAccounts(true); + $event->class->filters['routes']['accounts'] = ['accounts.index', 'search=enabled:1']; + $event->class->filters['multiple']['accounts'] = true; + } + + /** + * Handle group showing event. + * + * @param $event + * @return void + */ + public function handleGroupShowing(GroupShowing $event) + { + if ($this->skipThisClass($event)) { + return; + } + + $event->class->groups['account'] = trans_choice('general.accounts', 1); + } + + /** + * Handle group applying event. + * + * @param $event + * @return void + */ + public function handleGroupApplying(GroupApplying $event) + { + if ($this->skipThisClass($event)) { + return; + } + + $this->applyAccountGroup($event); + } + + /** + * Handle rows showing event. + * + * @param $event + * @return void + */ + public function handleRowsShowing(RowsShowing $event) + { + if ($this->skipRowsShowing($event, 'account')) { + return; + } + + $all_accounts = $this->getAccounts(); + + if ($account_ids = $this->getSearchStringValue('account_id')) { + $accounts = explode(',', $account_ids); + + $rows = collect($all_accounts)->filter(function ($value, $key) use ($accounts) { + return in_array($key, $accounts); + }); + } else { + $rows = $all_accounts; + } + + $this->setRowNamesAndValues($event, $rows); + } +} diff --git a/app/Listeners/Report/AddBasis.php b/app/Listeners/Report/AddBasis.php new file mode 100644 index 00000000..d0e6b146 --- /dev/null +++ b/app/Listeners/Report/AddBasis.php @@ -0,0 +1,39 @@ +skipThisClass($event)) { + return; + } + + $event->class->filters['basis'] = $this->getBasis(); + $event->class->filters['keys']['basis'] = 'basis'; + $event->class->filters['defaults']['basis'] = $event->class->getSetting('basis', 'accrual'); + $event->class->filters['operators']['basis'] = [ + 'equal' => true, + 'not_equal' => false, + 'range' => false, + ]; + } +} diff --git a/app/Listeners/Report/AddContacts.php b/app/Listeners/Report/AddContacts.php new file mode 100644 index 00000000..20da6d9a --- /dev/null +++ b/app/Listeners/Report/AddContacts.php @@ -0,0 +1,77 @@ +skipThisClass($event)) { + return; + } + + $event->class->filters['contacts'] = $this->getContacts(limit: true); + $event->class->filters['routes']['contacts'] = ['contacts.index', 'search=enabled:1']; + $event->class->filters['multiple']['contacts'] = true; + } + + /** + * Handle group showing event. + * + * @param $event + * @return void + */ + public function handleGroupShowing(GroupShowing $event) + { + if ($this->skipThisClass($event)) { + return; + } + + $event->class->groups['contact'] = trans_choice('general.contacts', 1); + } + + /** + * Handle rows showing event. + * + * @param $event + * @return void + */ + public function handleRowsShowing(RowsShowing $event) + { + if ($this->skipRowsShowing($event, 'contact')) { + return; + } + + $all_contacts = $this->getContacts(); + + if ($contact_ids = $this->getSearchStringValue('contact_id')) { + $contacts = explode(',', $contact_ids); + + $rows = collect($all_contacts)->filter(function ($value, $key) use ($contacts) { + return in_array($key, $contacts); + }); + } else { + $rows = $all_contacts; + } + + $this->setRowNamesAndValues($event, $rows); + } +} diff --git a/app/Listeners/Report/AddCustomers.php b/app/Listeners/Report/AddCustomers.php new file mode 100644 index 00000000..8e493c00 --- /dev/null +++ b/app/Listeners/Report/AddCustomers.php @@ -0,0 +1,117 @@ +skipThisClass($event)) { + return; + } + + $event->class->filters['customers'] = $this->getCustomers(true); + $event->class->filters['routes']['customers'] = ['customers.index', 'search=enabled:1']; + $event->class->filters['multiple']['customers'] = true; + } + + /** + * Handle group showing event. + * + * @param $event + * @return void + */ + public function handleGroupShowing(GroupShowing $event) + { + if ($this->skipThisClass($event)) { + return; + } + + $event->class->groups['customer'] = trans_choice('general.customers', 1); + } + + /** + * Handle group applying event. + * + * @param $event + * @return void + */ + public function handleGroupApplying(GroupApplying $event) + { + if ($this->skipThisClass($event)) { + return; + } + + $this->applyCustomerGroup($event); + } + + /** + * Handle filter applying event. + * + * @param $event + * @return void + */ + public function handleFilterApplying(FilterApplying $event) + { + if ($this->skipThisClass($event)) { + return; + } + + $model_type = $event->args['model_type'] ?? 'invoice'; + if (! in_array($model_type, ['invoice', 'income'])) { + return; + } + + if ($customer_ids = $this->getSearchStringValue('customer_id')) { + $where = $this->getSearchStringOperator('customer_id') == '!=' + ? 'whereNotIn' + : 'whereIn'; + + $event->model->{$where}('contact_id', array_map('intval', explode(',', $customer_ids))); + } + } + + /** + * Handle rows showing event. + * + * @param $event + * @return void + */ + public function handleRowsShowing(RowsShowing $event) + { + if ($this->skipRowsShowing($event, 'customer')) { + return; + } + + $all_customers = $this->getCustomers(); + + if ($customer_ids = $this->getSearchStringValue('customer_id')) { + $customers = explode(',', $customer_ids); + + $rows = collect($all_customers)->filter(function ($value, $key) use ($customers) { + return in_array($key, $customers); + }); + } else { + $rows = $all_customers; + } + + $this->setRowNamesAndValues($event, $rows); + } +} diff --git a/app/Listeners/Report/AddDate.php b/app/Listeners/Report/AddDate.php new file mode 100644 index 00000000..efe21125 --- /dev/null +++ b/app/Listeners/Report/AddDate.php @@ -0,0 +1,54 @@ +skipThisClass($event)) { + return; + } + + $this->setDateFilter($event); + } + + /** + * Handle filter applying event. + * + * @param $event + * @return void + */ + public function handleFilterApplying(FilterApplying $event) + { + if ($this->skipThisClass($event)) { + return; + } + + if (empty($event->args['date_field'])) { + return; + } + + // Apply date + $this->applyDateFilter($event); + } +} diff --git a/app/Listeners/Report/AddDiscount.php b/app/Listeners/Report/AddDiscount.php new file mode 100644 index 00000000..87c96ddf --- /dev/null +++ b/app/Listeners/Report/AddDiscount.php @@ -0,0 +1,52 @@ +skipThisClass($event)) { + return; + } + + $event->class->filters['discounts'] = $this->getDiscount(); + $event->class->filters['keys']['discounts'] = 'discount'; + $event->class->filters['defaults']['discounts'] = 'both'; + $event->class->filters['operators']['discounts'] = [ + 'equal' => true, + 'not_equal' => false, + 'range' => false, + ]; + } + + /** + * Handle filter applying event. + * + * @param $event + * @return void + */ + public function handleFilterApplying(FilterApplying $event) + { + if ($this->skipThisClass($event)) { + return; + } + + // Apply discount filter + $this->applyDiscountFilter($event); + } +} diff --git a/app/Listeners/Report/AddExpenseCategories.php b/app/Listeners/Report/AddExpenseCategories.php new file mode 100644 index 00000000..8783475d --- /dev/null +++ b/app/Listeners/Report/AddExpenseCategories.php @@ -0,0 +1,81 @@ +skipThisClass($event)) { + return; + } + + // send true for add limit on search and filter.. + $event->class->filters['categories'] = $this->getExpenseCategories(limit: true); + $event->class->filters['routes']['categories'] = ['categories.index', 'search=type:' . $this->getExpenseCategoryTypes('string') . ' enabled:1']; + $event->class->filters['multiple']['categories'] = true; + } + + /** + * Handle group showing event. + * + * @param $event + * @return void + */ + public function handleGroupShowing(GroupShowing $event) + { + if ($this->skipThisClass($event)) { + return; + } + + $event->class->groups['category'] = trans_choice('general.categories', 1); + } + + /** + * Handle rows showing event. + * + * @param $event + * @return void + */ + public function handleRowsShowing(RowsShowing $event) + { + if ($this->skipRowsShowing($event, 'category')) { + return; + } + + $all_categories = $this->getExpenseCategories(); + + if ($category_ids = $this->getSearchStringValue('category_id')) { + $categories = explode(',', $category_ids); + + $rows = collect($all_categories)->filter(function ($value, $key) use ($categories) { + return in_array($key, $categories); + }); + } else { + $rows = $all_categories; + } + + $this->setRowNamesAndValues($event, $rows); + + $event->class->row_tree_nodes = []; + + $nodes = $this->getCategoriesNodes($rows); + + $this->setTreeNodes($event, $nodes); + } +} diff --git a/app/Listeners/Report/AddGroup.php b/app/Listeners/Report/AddGroup.php new file mode 100644 index 00000000..a6fbbd7f --- /dev/null +++ b/app/Listeners/Report/AddGroup.php @@ -0,0 +1,39 @@ +skipThisClass($event)) { + return; + } + + $event->class->filters['group'] = $event->class->groups; + $event->class->filters['keys']['group'] = 'group'; + $event->class->filters['names']['group'] = trans('general.group_by'); + $event->class->filters['defaults']['group'] = $event->class->getGroup(); + $event->class->filters['operators']['group'] = [ + 'equal' => true, + 'not_equal' => false, + 'range' => false, + ]; + } +} diff --git a/app/Listeners/Report/AddIncomeCategories.php b/app/Listeners/Report/AddIncomeCategories.php new file mode 100644 index 00000000..4e0211ee --- /dev/null +++ b/app/Listeners/Report/AddIncomeCategories.php @@ -0,0 +1,81 @@ +skipThisClass($event)) { + return; + } + + // send true for add limit on search and filter.. + $event->class->filters['categories'] = $this->getIncomeCategories(limit: true); + $event->class->filters['routes']['categories'] = ['categories.index', 'search=type:' . $this->getIncomeCategoryTypes('string') . ' enabled:1']; + $event->class->filters['multiple']['categories'] = true; + } + + /** + * Handle group showing event. + * + * @param $event + * @return void + */ + public function handleGroupShowing(GroupShowing $event) + { + if ($this->skipThisClass($event)) { + return; + } + + $event->class->groups['category'] = trans_choice('general.categories', 1); + } + + /** + * Handle rows showing event. + * + * @param $event + * @return void + */ + public function handleRowsShowing(RowsShowing $event) + { + if ($this->skipRowsShowing($event, 'category')) { + return; + } + + $all_categories = $this->getIncomeCategories(); + + if ($category_ids = $this->getSearchStringValue('category_id')) { + $categories = explode(',', $category_ids); + + $rows = collect($all_categories)->filter(function ($value, $key) use ($categories) { + return in_array($key, $categories); + }); + } else { + $rows = $all_categories; + } + + $this->setRowNamesAndValues($event, $rows); + + $event->class->row_tree_nodes = []; + + $nodes = $this->getCategoriesNodes($rows); + + $this->setTreeNodes($event, $nodes); + } +} diff --git a/app/Listeners/Report/AddIncomeExpenseCategories.php b/app/Listeners/Report/AddIncomeExpenseCategories.php new file mode 100644 index 00000000..2378bffc --- /dev/null +++ b/app/Listeners/Report/AddIncomeExpenseCategories.php @@ -0,0 +1,122 @@ +skipThisClass($event)) { + return; + } + + $types = array_merge($this->getIncomeCategoryTypes(), $this->getExpenseAndDirectCostCategoryTypes()); + + $event->class->filters['categories'] = $this->getIncomeExpenseCategories(limit: true); + $event->class->filters['routes']['categories'] = ['categories.index', 'search=type:' . implode(',', $types) . ' enabled:1']; + $event->class->filters['multiple']['categories'] = true; + } + + /** + * Handle group showing event. + * + * @param $event + * @return void + */ + public function handleGroupShowing(GroupShowing $event) + { + if ($this->skipThisClass($event)) { + return; + } + + $event->class->groups['category'] = trans_choice('general.categories', 1); + } + + /** + * Handle records showing event. + * + * @param $event + * @return void + */ + public function handleRowsShowing(RowsShowing $event) + { + if ($this->skipRowsShowing($event, 'category')) { + return; + } + + $types = array_merge($this->getIncomeCategoryTypes(), $this->getExpenseAndDirectCostCategoryTypes()); + $categories = Category::type($types)->orderBy('name')->get(); + $rows = $categories->pluck('name', 'id')->toArray(); + + $this->setRowNamesAndValuesForCategories($event, $rows, $categories); + + $nodes = $this->getCategoriesNodes($rows); + + $this->setTreeNodesForCategories($event, $nodes, $categories); + } + + public function setRowNamesAndValuesForCategories($event, $rows, $categories) + { + foreach ($event->class->dates as $date) { + foreach ($event->class->tables as $table_key => $table_name) { + $table_keys = $this->getCategoryTypesForTable($table_key); + + foreach ($rows as $id => $name) { + $category = $categories->where('id', $id)->first(); + + if (! in_array($category->type, $table_keys)) { + continue; + } + + $event->class->row_names[$table_key][$id] = $name; + $event->class->row_values[$table_key][$id][$date] = 0; + } + } + } + } + + public function setTreeNodesForCategories($event, $nodes, $categories) + { + foreach ($event->class->tables as $table_key => $table_name) { + $table_keys = $this->getCategoryTypesForTable($table_key); + + foreach ($nodes as $id => $node) { + $category = $categories->where('id', $id)->first(); + + if (! in_array($category->type, $table_keys)) { + continue; + } + + $event->class->row_tree_nodes[$table_key][$id] = $node; + } + } + } + + protected function getCategoryTypesForTable(string $table_key): array + { + return match ($table_key) { + Category::INCOME_TYPE => $this->getIncomeCategoryTypes(), + Category::EXPENSE_TYPE => $this->getExpenseCategoryTypes(), + Category::DIRECT_COST_TYPE => $this->getDirectCostCategoryTypes(), + default => $this->getExpenseCategoryTypes(), + }; + } +} diff --git a/app/Listeners/Report/AddPeriod.php b/app/Listeners/Report/AddPeriod.php new file mode 100644 index 00000000..cf8d0be3 --- /dev/null +++ b/app/Listeners/Report/AddPeriod.php @@ -0,0 +1,40 @@ +skipThisClass($event)) { + return; + } + + $event->class->filters['period'] = $this->getPeriod(); + $event->class->filters['keys']['period'] = 'period'; + $event->class->filters['defaults']['period'] = $event->class->getSetting('period', 'quarterly'); + $event->class->filters['operators']['period'] = [ + 'equal' => true, + 'not_equal' => false, + 'range' => false, + ]; + } +} diff --git a/app/Listeners/Report/AddRowsToTax.php b/app/Listeners/Report/AddRowsToTax.php new file mode 100644 index 00000000..60b36f8e --- /dev/null +++ b/app/Listeners/Report/AddRowsToTax.php @@ -0,0 +1,33 @@ +skipThisClass($event)) { + return; + } + + $rows = [ + 'income' => trans_choice('general.sales', 2), + 'expense' => trans_choice('general.purchases', 2), + ]; + + $this->setRowNamesAndValues($event, $rows); + } +} diff --git a/app/Listeners/Report/AddSearchString.php b/app/Listeners/Report/AddSearchString.php new file mode 100644 index 00000000..797ac1b2 --- /dev/null +++ b/app/Listeners/Report/AddSearchString.php @@ -0,0 +1,70 @@ +skipThisClass($event)) { + return; + } + + $old = old() ?? []; + $request = request()->all() ?? []; + + if ($old || $request) { + $input = request('search'); + + $filters = []; + + if ($input) { + $filters = explode(' ', $input); + } + + foreach ($old as $key => $value) { + $filter = $key . ':' . $value; + + if (! in_array($filter, $filters)) { + $filters[] = $filter; + } + } + + foreach ($request as $key => $value) { + if ($key == 'search' + || $key == 'start_date' + || $key == 'end_date' + ) { + continue; + } + + $filters[] = $key . ':' . $value; + } + + request()->merge([ + 'search' => implode(' ', $filters) + ]); + } + + // Apply search string + $this->applySearchStringFilter($event); + } +} diff --git a/app/Listeners/Report/AddVendors.php b/app/Listeners/Report/AddVendors.php new file mode 100644 index 00000000..c5801b2e --- /dev/null +++ b/app/Listeners/Report/AddVendors.php @@ -0,0 +1,117 @@ +skipThisClass($event)) { + return; + } + + $event->class->filters['vendors'] = $this->getVendors(true); + $event->class->filters['routes']['vendors'] = ['vendors.index', 'search=enabled:1']; + $event->class->filters['multiple']['vendors'] = true; + } + + /** + * Handle group showing event. + * + * @param $event + * @return void + */ + public function handleGroupShowing(GroupShowing $event) + { + if ($this->skipThisClass($event)) { + return; + } + + $event->class->groups['vendor'] = trans_choice('general.vendors', 1); + } + + /** + * Handle group applying event. + * + * @param $event + * @return void + */ + public function handleGroupApplying(GroupApplying $event) + { + if ($this->skipThisClass($event)) { + return; + } + + $this->applyVendorGroup($event); + } + + /** + * Handle filter applying event. + * + * @param $event + * @return void + */ + public function handleFilterApplying(FilterApplying $event) + { + if ($this->skipThisClass($event)) { + return; + } + + $model_type = $event->args['model_type'] ?? 'bill'; + if (! in_array($model_type, ['bill', 'expense'])) { + return; + } + + if ($vendor_ids = $this->getSearchStringValue('vendor_id')) { + $where = $this->getSearchStringOperator('vendor_id') == '!=' + ? 'whereNotIn' + : 'whereIn'; + + $event->model->{$where}('contact_id', array_map('intval', explode(',', $vendor_ids))); + } + } + + /** + * Handle rows showing event. + * + * @param $event + * @return void + */ + public function handleRowsShowing(RowsShowing $event) + { + if ($this->skipRowsShowing($event, 'vendor')) { + return; + } + + $all_vendors = $this->getVendors(); + + if ($vendor_ids = $this->getSearchStringValue('vendor_id')) { + $vendors = explode(',', $vendor_ids); + + $rows = collect($all_vendors)->filter(function ($value, $key) use ($vendors) { + return in_array($key, $vendors); + }); + } else { + $rows = $all_vendors; + } + + $this->setRowNamesAndValues($event, $rows); + } +} diff --git a/app/Listeners/Setting/DeleteCategoryDeletedSubCategories.php b/app/Listeners/Setting/DeleteCategoryDeletedSubCategories.php new file mode 100644 index 00000000..30be00ea --- /dev/null +++ b/app/Listeners/Setting/DeleteCategoryDeletedSubCategories.php @@ -0,0 +1,31 @@ +category; + + if (empty($category->sub_categories)) { + return; + } + + foreach ($category->sub_categories as $sub_category) { + $this->dispatch(new DeleteCategory($sub_category)); + } + } +} diff --git a/app/Listeners/Update/CreateModuleUpdatedHistory.php b/app/Listeners/Update/CreateModuleUpdatedHistory.php new file mode 100644 index 00000000..27af0cf3 --- /dev/null +++ b/app/Listeners/Update/CreateModuleUpdatedHistory.php @@ -0,0 +1,42 @@ +alias)->first(); + + if (empty($model)) { + return; + } + + // Get module instance + $module = module($event->alias); + + if (empty($module)) { + return; + } + + // Add history + ModuleHistory::create([ + 'company_id' => $model->company_id, + 'module_id' => $model->id, + 'version' => $event->new, + 'description' => trans('modules.updated_2', ['module' => $module->getAlias()]), + 'created_from' => source_name(), + 'created_by' => user_id(), + ]); + } +} diff --git a/app/Listeners/Update/SendNotificationOnFailure.php b/app/Listeners/Update/SendNotificationOnFailure.php new file mode 100644 index 00000000..d0f17ec2 --- /dev/null +++ b/app/Listeners/Update/SendNotificationOnFailure.php @@ -0,0 +1,51 @@ +getNotifiableClass(); + + try { + $notifiable->notify(new Notification($event)); + } catch (Exception | RequestException $e) { + report($e); + } + } + + protected function getNotifiableClass() + { + return new class() { + use Notifiable; + + public function routeNotificationForMail() + { + return config('update.notifications.mail.to'); + } + + public function routeNotificationForSlack() + { + return config('update.notifications.slack.to'); + } + + public function getKey() + { + return 1; + } + }; + } +} diff --git a/app/Listeners/Update/V30/Version300.php b/app/Listeners/Update/V30/Version300.php new file mode 100644 index 00000000..0f1b2e11 --- /dev/null +++ b/app/Listeners/Update/V30/Version300.php @@ -0,0 +1,656 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stdout')->info('Starting the Akaunting 3.0 update...'); + + $this->updateDatabase(); + + $this->deleteOldWidgets(); + + $this->updateCompanies(); + + $this->updatePermissions(); + + $this->deleteOldFiles(); + + $this->clearNotifications(); + + Log::channel('stdout')->info('Akaunting 3.0 update finished.'); + } + + public function updateDatabase() + { + Log::channel('stdout')->info('Updating database...'); + + DB::table('migrations')->insert([ + 'id' => DB::table('migrations')->max('id') + 1, + 'migration' => '2019_11_16_000000_core_v2', + 'batch' => DB::table('migrations')->max('batch') + 1, + ]); + + Artisan::call('migrate', ['--force' => true]); + + Log::channel('stdout')->info('Database updated.'); + } + + public function updateCompanies() + { + Log::channel('stdout')->info('Updating companies...'); + + $company_id = company_id(); + + $companies = Company::cursor(); + + foreach ($companies as $company) { + Log::channel('stdout')->info('Updating company:' . $company->id); + + $company->makeCurrent(); + + $this->createNewWidgets(); + + $this->updateEmailTemplates(); + + $this->updateRecurables(); + + $this->updateTransactions(); + + Log::channel('stdout')->info('Company updated:' . $company->id); + } + + company($company_id)->makeCurrent(); + + Log::channel('stdout')->info('Companies updated.'); + } + + public function deleteOldWidgets() + { + Log::channel('stdout')->info('Deleting old widgets...'); + + // Delete old widgets + $old_widgets = [ + 'App\\Widgets\\TotalIncome', + 'App\\Widgets\\TotalExpenses', + 'App\\Widgets\\TotalProfit', + 'App\\Widgets\\CashFlow', + 'App\\Widgets\\IncomeByCategory', + 'App\\Widgets\\ExpensesByCategory', + 'App\\Widgets\\AccountBalance', + 'App\\Widgets\\LatestIncome', + 'App\\Widgets\\LatestExpenses', + ]; + + DB::transaction(function () use ($old_widgets) { + DB::table('widgets')->whereIn('class', $old_widgets)->delete(); + }); + + Log::channel('stdout')->info('Old widgets deleted.'); + } + + public function createNewWidgets() + { + Log::channel('stdout')->info('Creating new widgets...'); + + // Create new widgets + $new_widgets = [ + 'App\Widgets\Receivables', + 'App\Widgets\Payables', + 'App\Widgets\CashFlow', + 'App\Widgets\ProfitLoss', + 'App\Widgets\ExpensesByCategory', + 'App\Widgets\AccountBalance', + 'App\Widgets\BankFeeds', + ]; + + Log::channel('stdout')->info('Creating new widgets...'); + + Dashboard::whereDoesntHave('widgets')->each(function($dashboard) use ($new_widgets) { + $sort = 1; + + foreach ($new_widgets as $class_name) { + $class = new $class_name(); + + $this->dispatch(new CreateWidget([ + 'company_id' => $dashboard->company_id, + 'dashboard_id' => $dashboard->id, + 'class' => $class_name, + 'name' => $class->getDefaultName(), + 'sort' => $sort, + 'settings' => $class->getDefaultSettings(), + ])); + + $sort++; + } + }); + + Log::channel('stdout')->info('New widgets created.'); + } + + public function updateEmailTemplates() + { + Log::channel('stdout')->info('Updating/Creating email templates...'); + + $payment_received_model = EmailTemplate::alias('revenue_new_customer')->first(); + + $payment_received_request = [ + 'company_id' => company_id(), + 'alias' => 'payment_received_customer', + 'class' => 'App\Notifications\Banking\Transaction', + 'name' => 'settings.email.templates.payment_received_customer', + ]; + + Log::channel('stdout')->info('Updating old email templates...'); + + if (!empty($payment_received_model)) { + $this->dispatch(new UpdateEmailTemplate($payment_received_model, array_merge($payment_received_request, [ + 'subject' => $payment_received_model->subject, + 'body' => $payment_received_model->body, + ]))); + } else { + $this->dispatch(new CreateEmailTemplate(array_merge($payment_received_request, [ + 'subject' => trans('email_templates.payment_received_customer.subject'), + 'body' => trans('email_templates.payment_received_customer.body'), + 'created_from' => 'core::seed', + ]))); + } + + Log::channel('stdout')->info('Creating new email templates...'); + + $this->dispatch(new CreateEmailTemplate([ + 'company_id' => company_id(), + 'alias' => 'invoice_view_admin', + 'class' => 'App\Notifications\Sale\Invoice', + 'name' => 'settings.email.templates.invoice_view_admin', + 'subject' => trans('email_templates.invoice_view_admin.subject'), + 'body' => trans('email_templates.invoice_view_admin.body'), + 'created_from' => 'core::seed', + ])); + + $this->dispatch(new CreateEmailTemplate([ + 'company_id' => company_id(), + 'alias' => 'payment_made_vendor', + 'class' => 'App\Notifications\Banking\Transaction', + 'name' => 'settings.email.templates.payment_made_vendor', + 'subject' => trans('email_templates.payment_made_vendor.subject'), + 'body' => trans('email_templates.payment_made_vendor.body'), + 'created_from' => 'core::seed', + ])); + + Log::channel('stdout')->info('Email templates updated/created.'); + } + + public function updateRecurables() + { + Log::channel('stdout')->info('Updating recurring...'); + + $recurrings = Recurring::with('recurable')->cursor(); + + foreach ($recurrings as $recurring) { + // Document or Transaction + $model = $recurring->recurable; + + if ($model instanceof Document) { + $cloneable_relations = ['items', 'totals']; + $number_field = 'document_number'; + } else { + $cloneable_relations = []; + $number_field = 'number'; + } + + $model->cloneable_relations = $cloneable_relations; + + // Create the recurring template + $clone = $model->duplicate(); + $clone->type = $clone->type . '-recurring'; + $clone->$number_field = $this->getNextTransactionNumber($clone->type, '-recurring'); + $clone->saveQuietly(); + + $this->increaseNextTransactionNumber($clone->type, '-recurring'); + + // Update the recurring table + $recurring->recurable_id = $clone->id; + $recurring->saveQuietly(); + + // Set the new recurring template as parent for the original model + $model->parent_id = $clone->id; + $model->saveQuietly(); + + // Set the new recurring template as parent for child models + DB::table($model->getTable())->where('parent_id', $model->id)->update([ + 'parent_id' => $clone->id, + 'created_from' => 'core::recurring', + ]); + } + + Log::channel('stdout')->info('Recurring updated.'); + } + + public function updateTransactions() + { + Log::channel('stdout')->info('Updating transactions...'); + + $transactions = Transaction::isNotRecurring()->cursor(); + + $number = 1; + $transaction_number = $this->getTransactionNumber($number); + + foreach ($transactions as $transaction) { + $transaction->number = $transaction_number; + $transaction->saveQuietly(); + + $number++; + $transaction_number = $this->getTransactionNumber($number); + } + + $this->saveNextTransactionNumber($number); + + Log::channel('stdout')->info('Transactions updated.'); + } + + public function clearNotifications() + { + try { + $users = User::all(); + + foreach ($users as $user) { + $notifications = $user->unreadNotifications; + + foreach ($notifications as $notification) { + $notification->markAsRead(); + } + } + } catch (\Exception $e) {} + } + + public function getTransactionNumber($number): string + { + $prefix = setting('transaction.number_prefix'); + $digit = setting('transaction.number_digit'); + + return $prefix . str_pad($number, $digit, '0', STR_PAD_LEFT); + } + + public function saveNextTransactionNumber($next): void + { + setting(['transaction.number_next' => $next]); + setting()->save(); + } + + public function updatePermissions() + { + Log::channel('stdout')->info('Updating permissions...'); + + $rows = [ + 'accountant' => [ + 'admin-panel' => 'r', + 'api' => 'r', + 'common-dashboards' => 'r', + 'common-items' => 'r', + 'purchases-bills' => 'r', + 'purchases-vendors' => 'r', + 'sales-customers' => 'r', + 'sales-invoices' => 'r', + 'banking-accounts' => 'r', + 'banking-reconciliations' => 'r', + 'banking-transactions' => 'r', + 'banking-transfers' => 'r', + 'reports-expense-summary' => 'r', + 'reports-income-summary' => 'r', + 'reports-income-expense-summary' => 'r', + 'reports-profit-loss' => 'r', + 'reports-tax-summary' => 'r', + 'modules-home' => 'r', + 'modules-item' => 'r', + 'modules-my' => 'r', + 'modules-tiles' => 'r', + ], + ]; + + Log::channel('stdout')->info('Attaching new permissions...'); + + // c=create, r=read, u=update, d=delete + $this->attachPermissionsByRoleNames($rows); + + // c=create, r=read, u=update, d=delete + $this->attachPermissionsToAdminRoles([ + 'settings-email-templates' => 'r,u', + 'settings-company' => 'u', + 'settings-defaults' => 'u', + 'settings-email' => 'u', + 'settings-invoice' => 'u', + 'settings-localisation' => 'u', + 'settings-schedule' => 'u', + 'widgets-bank-feeds' => 'r', + 'widgets-payables' => 'r', + 'widgets-profit-loss' => 'r', + 'widgets-receivables' => 'r', + ]); + + Log::channel('stdout')->info('Dettaching old permissions...'); + + // c=create, r=read, u=update, d=delete + $this->detachPermissionsFromAdminRoles([ + 'auth-permissions' => 'c,r,u,d', + 'auth-roles' => 'c,r,u,d', + 'common-notifications' => 'c,r,u,d', + 'purchases-payments' => 'c,r,u,d', + 'sales-revenues' => 'c,r,u,d', + 'settings-settings' => 'r,u', + 'settings-settings' => 'r,u', + 'widgets-income-by-category' => 'r', + 'widgets-latest-expenses' => 'r', + 'widgets-latest-income' => 'r', + 'widgets-total-expenses' => 'r', + 'widgets-total-income' => 'r', + 'widgets-total-profit' => 'r', + ]); + + Log::channel('stdout')->info('Permissions updated.'); + } + + public function deleteOldFiles() + { + Log::channel('stdout')->info('Deleting old files and folders...'); + + $files = [ + 'app/Abstracts/View/Components/Document.php', + 'app/Abstracts/View/Components/DocumentForm.php', + 'app/Abstracts/View/Components/DocumentIndex.php', + 'app/Abstracts/View/Components/DocumentShow.php', + 'app/Abstracts/View/Components/DocumentTemplate.php', + 'app/Abstracts/View/Components/Transaction.php', + 'app/Abstracts/View/Components/TransactionShow.php', + 'app/Abstracts/View/Components/TransactionTemplate.php', + 'app/Http/BulkActions/Auth/Permissions.php', + 'app/Http/BulkActions/Auth/Roles.php', + 'app/Http/BulkActions/Purchases/Payments.php', + 'app/Http/BulkActions/Sales/Revenues.php', + 'app/Http/Exports/Purchases/Payments.php', + 'app/Http/Exports/Sales/Revenues.php', + 'app/Http/Imports/Purchases/Payments.php', + 'app/Http/Imports/Sales/Revenues.php', + 'app/Http/Controllers/Api/Auth/Permissions.php', + 'app/Http/Controllers/Api/Auth/Roles.php', + 'app/Http/Controllers/Auth/Permissions.php', + 'app/Http/Controllers/Auth/Roles.php', + 'app/Http/Controllers/Common/Notifications.php', + 'app/Http/Controllers/Purchases/Payments.php', + 'app/Http/Controllers/Sales/Revenues.php', + 'app/Http/Controllers/Settings/Settings.php', + 'app/Http/Requests/Auth/Permission.php', + 'app/Http/Requests/Auth/Role.php', + 'app/Http/ViewComposers/Header.php', + 'app/Http/ViewComposers/Index.php', + 'app/Http/ViewComposers/Notifications.php', + 'app/Http/ViewComposers/Show.php', + 'app/Http/ViewComposers/Suggestions.php', + 'app/Http/ViewComposers/Menu.php', + 'app/Http/ViewComposers/Logo.php', + 'app/Http/ViewComposers/Modules.php', + 'app/Http/ViewComposers/Wizard.php', + 'app/Models/Common/EmailTemplate.php', + 'app/Notifications/Sale/Revenue.php', + 'app/Transformers/Auth/Permission.php', + 'app/Transformers/Auth/Role.php', + 'app/View/Components/Documents/Index/CardBody.php', + 'app/View/Components/Documents/Index/CardFooter.php', + 'app/View/Components/Documents/Index/CardHeader.php', + 'app/View/Components/Documents/Index/TopButtons.php', + 'app/View/Components/Documents/Show/Timeline.php', + 'app/View/Components/Documents/Show/Transactions.php', + 'app/View/Components/Transactions/Show/Header.php', + 'app/View/Components/Transactions/Show/Footer.php', + 'app/View/Components/Transfers/Show/Header.php', + 'app/View/Components/Transfers/Show/Footer.php', + 'app/Widgets/IncomeByCategory.php', + 'app/Widgets/LatestExpenses.php', + 'app/Widgets/LatestIncome.php', + 'app/Widgets/TotalExpense.php', + 'app/Widgets/TotalIncome.php', + 'app/Widgets/TotalProfit.php', + 'database/factories/Permission.php', + 'database/factories/Role.php', + 'database/migrations/2020_01_08_000000_core_v200.php', + 'database/migrations/2020_03_20_183732_core_v208.php', + 'database/migrations/2020_06_09_000000_core_v2014.php', + 'database/migrations/2020_07_20_000000_core_v2017.php', + 'database/migrations/2020_10_13_000000_core_v210.php', + 'database/migrations/2021_04_01_000000_core_v219.php', + 'database/migrations/2021_05_17_000000_core_v2114.php', + 'database/migrations/2021_06_17_000000_core_v2117.php', + 'database/migrations/2021_09_01_000000_core_v2124.php', + 'database/migrations/2021_09_10_000000_core_v2125.php', + 'database/migrations/2021_09_10_000000_core_v2126.php', + 'database/migrations/2021_09_10_000000_core_v2127.php', + 'database/migrations/2022_03_02_000000_core_v2133.php', + 'database/migrations/2022_03_23_000000_core_v2134.php', + 'public/files/import/payments.xlsx', + 'public/files/import/revenues.xlsx', + 'resources/assets/js/views/auth/permissions.js', + 'resources/assets/js/views/auth/roles.js', + 'resources/views/components/documents/index/card-body.blade.php', + 'resources/views/components/documents/index/card-footer.blade.php', + 'resources/views/components/documents/index/card-header.blade.php', + 'resources/views/components/documents/index/top-buttons.blade.php', + 'resources/views/components/documents/show/timeline.blade.php', + 'resources/views/components/documents/show/transactions.blade.php', + 'resources/views/components/form-group-title.blade.php', + 'resources/views/components/transactions/show/header.blade.php', + 'resources/views/components/transactions/show/footer.blade.php', + 'resources/views/components/transfers/show/header.blade.php', + 'resources/views/components/transfers/show/footer.blade.php', + 'resources/views/components/layouts/modules.php', + 'resources/views/layouts/admin.blade.php', + 'resources/views/layouts/auth.blade.php', + 'resources/views/layouts/install.blade.php', + 'resources/views/layouts/maintenance.blade.php', + 'resources/views/layouts/modules.blade.php', + 'resources/views/layouts/portal.blade.php', + 'resources/views/layouts/print.blade.php', + 'resources/views/layouts/signed.blade.php', + 'resources/views/layouts/wizard.blade.php', + 'resources/views/partials/admin/content.blade.php', + 'resources/views/partials/admin/empty_page.blade.php', + 'resources/views/partials/admin/favorites.blade.php', + 'resources/views/partials/admin/footer.blade.php', + 'resources/views/partials/admin/head.blade.php', + 'resources/views/partials/admin/header.blade.php', + 'resources/views/partials/admin/menu.blade.php', + 'resources/views/partials/admin/pagination.blade.php', + 'resources/views/partials/admin/scripts.blade.php', + 'resources/views/partials/admin/suggestions.blade.php', + 'resources/views/partials/auth/head.blade.php', + 'resources/views/partials/auth/scripts.blade.php', + 'resources/views/partials/form/bulk_action_all_group.blade.php', + 'resources/views/partials/form/bulk_action_group.blade.php', + 'resources/views/partials/form/bulk_action_row_group.blade.php', + 'resources/views/partials/form/checkbox_group.blade.php', + 'resources/views/partials/form/date_group.blade.php', + 'resources/views/partials/form/date_range.blade.php', + 'resources/views/partials/form/date_time_group.blade.php', + 'resources/views/partials/form/delete_button.blade.php', + 'resources/views/partials/form/delete_link.blade.php', + 'resources/views/partials/form/email_group.blade.php', + 'resources/views/partials/form/enabled_group.blade.php', + 'resources/views/partials/form/file_group.blade.php', + 'resources/views/partials/form/invoice_text.blade.php', + 'resources/views/partials/form/money_group.blade.php', + 'resources/views/partials/form/multi_select_add_new_group.blade.php', + 'resources/views/partials/form/multi_select_group.blade.php', + 'resources/views/partials/form/multi_select_remote_add_new_group.blade.php', + 'resources/views/partials/form/multi_select_remote_group.blade.php', + 'resources/views/partials/form/number_group.blade.php', + 'resources/views/partials/form/password_group.blade.php', + 'resources/views/partials/form/radio_group.blade.php', + 'resources/views/partials/form/recurring.blade.php', + 'resources/views/partials/form/save_buttons.blade.php', + 'resources/views/partials/form/select_add_new_group.blade.php', + 'resources/views/partials/form/select_group.blade.php', + 'resources/views/partials/form/select_group_add_new_group.blade.php', + 'resources/views/partials/form/select_group_group.blade.php', + 'resources/views/partials/form/select_remote_add_new_group.blade.php', + 'resources/views/partials/form/select_remote_group.blade.php', + 'resources/views/partials/form/text_editor_group.blade.php', + 'resources/views/partials/form/text_group.blade.php', + 'resources/views/partials/form/textarea_group.blade.php', + 'resources/views/partials/form/time_group.blade.php', + 'resources/views/partials/email/body.blade.php', + 'resources/views/partials/email/footer.blade.php', + 'resources/views/partials/media/file.blade.php', + 'resources/views/partials/reports/detail.blade.php', + 'resources/views/partials/reports/detail/content/footer.blade.php', + 'resources/views/partials/reports/detail/content/header.blade.php', + 'resources/views/partials/reports/detail/table.blade.php', + 'resources/views/partials/reports/detail/table/body.blade.php', + 'resources/views/partials/reports/detail/table/footer.blade.php', + 'resources/views/partials/reports/detail/table/header.blade.php', + 'resources/views/partials/reports/detail/table/row.blade.php', + 'resources/views/partials/reports/fields.blade.php', + 'resources/views/partials/reports/filter.blade.php', + 'resources/views/partials/reports/header.blade.php', + 'resources/views/partials/reports/print.blade.php', + 'resources/views/partials/reports/show.blade.php', + 'resources/views/partials/reports/summary.blade.php', + 'resources/views/partials/reports/summary/chart.blade.php', + 'resources/views/partials/reports/summary/content/footer.blade.php', + 'resources/views/partials/reports/summary/content/header.blade.php', + 'resources/views/partials/reports/summary/table.blade.php', + 'resources/views/partials/reports/summary/table/body.blade.php', + 'resources/views/partials/reports/summary/table/footer.blade.php', + 'resources/views/partials/reports/summary/table/header.blade.php', + 'resources/views/partials/reports/summary/table/row.blade.php', + 'resources/views/partials/install/head.blade.php', + 'resources/views/partials/install/scripts.blade.php', + 'resources/views/partials/maintenance/body.blade.php', + 'resources/views/partials/maintenance/head.blade.php', + 'resources/views/partials/modules/bar.blade.php', + 'resources/views/partials/modules/head.blade.php', + 'resources/views/partials/modules/item.blade.php', + 'resources/views/partials/modules/items.blade.php', + 'resources/views/partials/modules/my_apps_item.blade.php', + 'resources/views/partials/modules/no_apps.blade.php', + 'resources/views/partials/modules/pre_sale.blade.php', + 'resources/views/partials/modules/releases.blade.php', + 'resources/views/partials/modules/reviews.blade.php', + 'resources/views/partials/modules/show/price.blade.php', + 'resources/views/partials/modules/show/toggle.blade.php', + 'resources/views/partials/portal/content.blade.php', + 'resources/views/partials/portal/footer.blade.php', + 'resources/views/partials/portal/head.blade.php', + 'resources/views/partials/portal/header.blade.php', + 'resources/views/partials/portal/menu.blade.php', + 'resources/views/partials/portal/navbar.blade.php', + 'resources/views/partials/portal/pagination.blade.php', + 'resources/views/partials/portal/payment_method/hosted.blade.php', + 'resources/views/partials/portal/payment_method/redirect.blade.php', + 'resources/views/partials/portal/scripts.blade.php', + 'resources/views/partials/print/head.blade.php', + 'resources/views/partials/print/scripts.blade.php', + 'resources/views/partials/pwa/pwa.blade.php', + 'resources/views/partials/signed/content.blade.php', + 'resources/views/partials/signed/footer.blade.php', + 'resources/views/partials/signed/head.blade.php', + 'resources/views/partials/wizard/head.blade.php', + 'resources/views/partials/wizard/scripts.blade.php', + 'resources/views/partials/wizard/steps.blade.php', + 'resources/views/partials/widgets/header.blade.php', + 'resources/views/settings/settings/index.blade.php', + 'resources/views/widgets/latest_expenses.blade.php', + 'resources/views/widgets/latest_income.blade.php', + 'resources/views/widgets/total_expenses.blade.php', + 'resources/views/widgets/total_income.blade.php', + 'resources/views/widgets/total_profit.blade.php', + ]; + + $directories = [ + 'app/Listeners/Update/V20', + 'app/Listeners/Update/V21', + 'modules/BC21', + 'modules/Bc21', + 'resources/views/auth/permissions', + 'resources/views/auth/roles', + 'resources/views/common/notifications', + 'resources/views/purchases/payments', + 'resources/views/sales/revenues', + 'resources/views/layouts', + 'resources/views/partials/admin', + 'resources/views/partials/auth', + 'resources/views/partials/form', + 'resources/views/partials/install', + 'resources/views/partials/maintenance', + 'resources/views/partials/modules/show', + 'resources/views/partials/modules', + 'resources/views/partials/portal/payment_method', + 'resources/views/partials/portal', + 'resources/views/partials/print', + 'resources/views/partials/pwa', + 'resources/views/partials/signed', + 'resources/views/partials/wizard', + 'resources/views/partials/email', + 'resources/views/partials/media', + 'resources/views/partials/reports/details/content', + 'resources/views/partials/reports/details/table', + 'resources/views/partials/reports/details', + 'resources/views/partials/reports/summary/content', + 'resources/views/partials/reports/summary/table', + 'resources/views/partials/reports/summary', + 'resources/views/partials/reports', + 'resources/views/partials/widgets', + ]; + + Log::channel('stdout')->info('Deleting old files...'); + + foreach ($files as $file) { + File::delete(base_path($file)); + } + + Log::channel('stdout')->info('Deleting old folders...'); + + foreach ($directories as $directory) { + File::deleteDirectory(base_path($directory)); + } + + Log::channel('stdout')->info('Old files and folders deleted.'); + } +} diff --git a/app/Listeners/Update/V30/Version3013.php b/app/Listeners/Update/V30/Version3013.php new file mode 100644 index 00000000..dc82ed46 --- /dev/null +++ b/app/Listeners/Update/V30/Version3013.php @@ -0,0 +1,34 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stdout')->info('Updating to 3.0.13 version...'); + + Artisan::call('cache:clear'); + + Log::channel('stdout')->info('Done!'); + } +} diff --git a/app/Listeners/Update/V30/Version3014.php b/app/Listeners/Update/V30/Version3014.php new file mode 100644 index 00000000..f422a7b1 --- /dev/null +++ b/app/Listeners/Update/V30/Version3014.php @@ -0,0 +1,71 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stdout')->info('Updating to 3.0.14 version...'); + + DB::transaction(function () { + $types = $this->getTypesByAllowedTranslations(); + + foreach ($types as $type => $translations) { + DB::table('categories')->whereIn('type', $translations)->update(['type' => $type]); + } + }); + + Log::channel('stdout')->info('Done!'); + } + + protected function getTypesByAllowedTranslations(): array + { + $types = $this->getCategoryTypes(false); + $lang_codes = array_keys(language()->allowed()); + + foreach ($types as $type => $trans_name) { + $translations_for_type = []; + + foreach ($lang_codes as $lang_code) { + $translation = trans_choice($trans_name, 1, locale: $lang_code); + + if ($translation === $trans_name) { + continue; + } + + $translations_for_type[] = $translation; + } + + $types[$type] = $translations_for_type; + } + + /** + * Example: en-GB es-ES + * 'income' => ['Income', 'Ingresos'] + */ + return $types; + } +} diff --git a/app/Listeners/Update/V30/Version3015.php b/app/Listeners/Update/V30/Version3015.php new file mode 100644 index 00000000..44124da7 --- /dev/null +++ b/app/Listeners/Update/V30/Version3015.php @@ -0,0 +1,43 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stdout')->info('Updating to 3.0.15 version...'); + + $this->updateDatabase(); + + Log::channel('stdout')->info('Done!'); + } + + public function updateDatabase(): void + { + Log::channel('stdout')->info('Updating database...'); + + Artisan::call('migrate', ['--force' => true]); + + Log::channel('stdout')->info('Database updated.'); + } +} diff --git a/app/Listeners/Update/V30/Version3016.php b/app/Listeners/Update/V30/Version3016.php new file mode 100644 index 00000000..d2dab4a8 --- /dev/null +++ b/app/Listeners/Update/V30/Version3016.php @@ -0,0 +1,43 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stdout')->info('Updating to 3.0.16 version...'); + + $this->updateDatabase(); + + Log::channel('stdout')->info('Done!'); + } + + public function updateDatabase(): void + { + Log::channel('stdout')->info('Updating database...'); + + Artisan::call('migrate', ['--force' => true]); + + Log::channel('stdout')->info('Database updated.'); + } +} diff --git a/app/Listeners/Update/V30/Version3017.php b/app/Listeners/Update/V30/Version3017.php new file mode 100644 index 00000000..bfb02a22 --- /dev/null +++ b/app/Listeners/Update/V30/Version3017.php @@ -0,0 +1,43 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stdout')->info('Updating to 3.0.17 version...'); + + $this->updateDatabase(); + + Log::channel('stdout')->info('Done!'); + } + + public function updateDatabase(): void + { + Log::channel('stdout')->info('Updating database...'); + + Artisan::call('migrate', ['--force' => true]); + + Log::channel('stdout')->info('Database updated.'); + } +} diff --git a/app/Listeners/Update/V30/Version303.php b/app/Listeners/Update/V30/Version303.php new file mode 100644 index 00000000..d0b444ca --- /dev/null +++ b/app/Listeners/Update/V30/Version303.php @@ -0,0 +1,107 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stdout')->info('Starting the Akaunting 3.0.3 update...'); + + $this->updateCompanies(); + + Log::channel('stdout')->info('Akaunting 3.0.3 update finished.'); + } + + public function updateCompanies() + { + Log::channel('stdout')->info('Updating companies...'); + + $company_id = company_id(); + + $companies = Company::cursor(); + + foreach ($companies as $company) { + Log::channel('stdout')->info('Updating company:' . $company->id); + + $company->makeCurrent(); + + $this->updateWidgets(); + + Log::channel('stdout')->info('Company updated:' . $company->id); + } + + company($company_id)->makeCurrent(); + + Log::channel('stdout')->info('Companies updated.'); + } + + public function updateWidgets() + { + Log::channel('stdout')->info('Updating widgets...'); + + $widgets = Widget::cursor(); + + foreach ($widgets as $widget) { + Log::channel('stdout')->info('Updating widget:' . $widget->id); + + $widget_settings = $widget->settings; + + if (empty($widget_settings->width)) { + Log::channel('stdout')->info('Skip widget:' . $widget->id); + + continue; + } + + if (Str::contains($widget_settings->width, 'w-full')) { + Log::channel('stdout')->info('Already new classs widget:' . $widget->id); + + continue; + } + + switch ($widget_settings->width) { + case 'col-md-3': + $widget_settings->width = 'w-full lg:w-1/4 px-6'; + break; + case 'col-md-4': + $widget_settings->width = 'w-full lg:w-1/3 px-6'; + break; + case 'col-md-6': + $widget_settings->width = 'w-full lg:w-2/4 px-6'; + break; + case 'col-md-12': + $widget_settings->width = 'w-full px-6'; + break; + } + + $widget->settings = $widget_settings; + + $widget->save(); + + Log::channel('stdout')->info('Widget updated:' . $widget->id); + } + + Log::channel('stdout')->info('Widgets updated.'); + } +} diff --git a/app/Listeners/Update/V30/Version304.php b/app/Listeners/Update/V30/Version304.php new file mode 100644 index 00000000..03241e3d --- /dev/null +++ b/app/Listeners/Update/V30/Version304.php @@ -0,0 +1,111 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stdout')->info('Starting the Akaunting 3.0.4 update...'); + + $this->updateDatabase(); + + $this->updateCompanies(); + + $this->deleteOldFiles(); + + Log::channel('stdout')->info('Akaunting 3.0.4 update finished.'); + } + + public function updateDatabase() + { + Log::channel('stdout')->info('Updating database...'); + + Artisan::call('migrate', ['--force' => true]); + + Log::channel('stdout')->info('Database updated.'); + } + + public function updateCompanies() + { + Log::channel('stdout')->info('Updating companies...'); + + $company_id = company_id(); + + $companies = Company::cursor(); + + foreach ($companies as $company) { + Log::channel('stdout')->info('Updating company:' . $company->id); + + $company->makeCurrent(); + + $this->updateEmailTemplates(); + + Log::channel('stdout')->info('Company updated:' . $company->id); + } + + company($company_id)->makeCurrent(); + + Log::channel('stdout')->info('Companies updated.'); + } + + public function updateEmailTemplates() + { + Log::channel('stdout')->info('Updating Email Templates...'); + + $email_templates = EmailTemplate::cursor(); + + foreach ($email_templates as $email_template) { + Log::channel('stdout')->info('Updating email template:' . $email_template->id); + + $body = preg_replace('%|

%s', '', $email_template->body); + + $email_template->body = $body; + + $email_template->save(); + + Log::channel('stdout')->info('Email Template updated:' . $email_template->id); + } + + Log::channel('stdout')->info('Email Templates updated.'); + } + + public function deleteOldFiles() + { + Log::channel('stdout')->info('Deleting old files...'); + + $files = [ + 'app/Events/Auth/InvitationCreated.php', + 'app/Listeners/Auth/SendUserInvitation.php', + 'app/Listeners/Auth/DeleteUserInvitation.php', + ]; + + foreach ($files as $file) { + File::delete(base_path($file)); + } + + Log::channel('stdout')->info('Old files deleted.'); + } +} diff --git a/app/Listeners/Update/V30/Version305.php b/app/Listeners/Update/V30/Version305.php new file mode 100644 index 00000000..29d7a7e1 --- /dev/null +++ b/app/Listeners/Update/V30/Version305.php @@ -0,0 +1,95 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stdout')->info('Updating to 3.0.5 version...'); + + $this->updateDatabase(); + + $this->updateSettings(); + + $this->updateTransfers(); + + Log::channel('stdout')->info('Done!'); + } + + public function updateDatabase(): void + { + Log::channel('stdout')->info('Updating database...'); + + Artisan::call('migrate', ['--force' => true]); + + Log::channel('stdout')->info('Database updated.'); + } + + public function updateSettings(): void + { + Log::channel('stdout')->info('Updating settings...'); + + DB::table('settings')->where('key', 'transaction.type.income')->cursor()->each(function ($setting) { + DB::table('settings')->where('id', $setting->id)->update([ + 'value' => $setting->value . ',' . Transaction::INCOME_TRANSFER_TYPE, + ]); + }); + + DB::table('settings')->where('key', 'transaction.type.expense')->cursor()->each(function ($setting) { + DB::table('settings')->where('id', $setting->id)->update([ + 'value' => $setting->value . ',' . Transaction::EXPENSE_TRANSFER_TYPE, + ]); + }); + + Log::channel('stdout')->info('Settings updated.'); + } + + public function updateTransfers(): void + { + Log::channel('stdout')->info('Updating transfers...'); + + DB::table('transfers')->cursor()->each(function ($transfer) { + Log::channel('stdout')->info('Updating transfer: ' . $transfer->id); + + try { + DB::table('transactions')->where('id', $transfer->income_transaction_id)->update([ + 'type' => Transaction::INCOME_TRANSFER_TYPE, + ]); + } catch (\Exception $e) { + Log::channel('stdout')->error('Error updating transaction: ' . $transfer->income_transaction_id); + } + + try { + DB::table('transactions')->where('id', $transfer->expense_transaction_id)->update([ + 'type' => Transaction::EXPENSE_TRANSFER_TYPE, + ]); + } catch (\Exception $e) { + Log::channel('stdout')->error('Error updating transaction: ' . $transfer->expense_transaction_id); + } + }); + + Log::channel('stdout')->info('Transfers updated.'); + } +} diff --git a/app/Listeners/Update/V30/Version307.php b/app/Listeners/Update/V30/Version307.php new file mode 100644 index 00000000..99edd8b6 --- /dev/null +++ b/app/Listeners/Update/V30/Version307.php @@ -0,0 +1,43 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stdout')->info('Updating to 3.0.7 version...'); + + $this->updateDatabase(); + + Log::channel('stdout')->info('Done!'); + } + + public function updateDatabase(): void + { + Log::channel('stdout')->info('Updating database...'); + + Artisan::call('migrate', ['--force' => true]); + + Log::channel('stdout')->info('Database updated.'); + } +} diff --git a/app/Listeners/Update/V30/Version309.php b/app/Listeners/Update/V30/Version309.php new file mode 100644 index 00000000..07202609 --- /dev/null +++ b/app/Listeners/Update/V30/Version309.php @@ -0,0 +1,63 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stdout')->info('Updating to 3.0.9 version...'); + + $this->updatePermissions(); + + Log::channel('stdout')->info('Done!'); + } + + public function updatePermissions(): void + { + Log::channel('stdout')->info('Updating permissions...'); + + $rows = [ + 'accountant' => [ + 'auth-profile' => 'r,u', + 'common-reports' => 'r', + 'widgets-account-balance' => 'r', + 'widgets-bank-feeds' => 'r', + 'widgets-cash-flow' => 'r', + 'widgets-currencies' => 'r', + 'widgets-expenses-by-category' => 'r', + 'widgets-payables' => 'r', + 'widgets-profit-loss' => 'r', + 'widgets-receivables' => 'r', + ], + ]; + + Log::channel('stdout')->info('Attaching new permissions...'); + + // c=create, r=read, u=update, d=delete + $this->attachPermissionsByRoleNames($rows); + + Log::channel('stdout')->info('Permissions updated.'); + } +} diff --git a/app/Listeners/Update/V31/Version310.php b/app/Listeners/Update/V31/Version310.php new file mode 100644 index 00000000..8cc62d77 --- /dev/null +++ b/app/Listeners/Update/V31/Version310.php @@ -0,0 +1,43 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stdout')->info('Updating to 3.1.0 version...'); + + $this->updateDatabase(); + + Log::channel('stdout')->info('Done!'); + } + + public function updateDatabase(): void + { + Log::channel('stdout')->info('Updating database...'); + + Artisan::call('migrate', ['--force' => true]); + + Log::channel('stdout')->info('Database updated.'); + } +} diff --git a/app/Listeners/Update/V31/Version3112.php b/app/Listeners/Update/V31/Version3112.php new file mode 100644 index 00000000..bf305122 --- /dev/null +++ b/app/Listeners/Update/V31/Version3112.php @@ -0,0 +1,43 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stdout')->info('Updating to 3.1.12 version...'); + + $this->updateDatabase(); + + Log::channel('stdout')->info('Done!'); + } + + public function updateDatabase(): void + { + Log::channel('stdout')->info('Updating database...'); + + Artisan::call('migrate', ['--force' => true]); + + Log::channel('stdout')->info('Database updated.'); + } +} diff --git a/app/Listeners/Update/V31/Version3115.php b/app/Listeners/Update/V31/Version3115.php new file mode 100644 index 00000000..d06ab579 --- /dev/null +++ b/app/Listeners/Update/V31/Version3115.php @@ -0,0 +1,43 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stdout')->info('Updating to 3.1.15 version...'); + + $this->updateDatabase(); + + Log::channel('stdout')->info('Done!'); + } + + public function updateDatabase(): void + { + Log::channel('stdout')->info('Updating database...'); + + Artisan::call('migrate', ['--force' => true]); + + Log::channel('stdout')->info('Database updated.'); + } +} diff --git a/app/Listeners/Update/V31/Version3119.php b/app/Listeners/Update/V31/Version3119.php new file mode 100644 index 00000000..ff918234 --- /dev/null +++ b/app/Listeners/Update/V31/Version3119.php @@ -0,0 +1,58 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stdout')->info('Updating to 3.1.19 version...'); + + $this->updatePermissions(); + + Log::channel('stdout')->info('Done!'); + } + + /** + * Update permissions. + * + * @return void + */ + public function updatePermissions() + { + $rows = [ + 'admin' => [ + 'reports-discount-summary' => 'r' + ], + 'manager' => [ + 'reports-discount-summary' => 'r' + ], + 'accountant' => [ + 'reports-discount-summary' => 'r' + ], + ]; + + $this->attachPermissionsByRoleNames($rows); + } +} diff --git a/app/Listeners/Update/V31/Version315.php b/app/Listeners/Update/V31/Version315.php new file mode 100644 index 00000000..37dd1d07 --- /dev/null +++ b/app/Listeners/Update/V31/Version315.php @@ -0,0 +1,93 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stdout')->info('Updating to 3.1.5 version...'); + + $this->clearCache(); + + $this->updateWidgets(); + + Log::channel('stdout')->info('Done!'); + } + + public function clearCache(): void + { + Log::channel('stdout')->info('Clearing cache...'); + + Artisan::call('view:clear'); + Artisan::call('cache:clear'); + + Log::channel('stdout')->info('Cleared cache.'); + } + + public function updateWidgets() + { + Log::channel('stdout')->info('Updating widgets...'); + + $widgets = Widget::cursor(); + + foreach ($widgets as $widget) { + Log::channel('stdout')->info('Updating widget:' . $widget->id); + + $widget_settings = $widget->settings; + + if (empty($widget_settings->width)) { + Log::channel('stdout')->info('Skip widget:' . $widget->id); + + continue; + } + + if (! empty($widget_settings->raw_width)) { + Log::channel('stdout')->info('Already new classs widget:' . $widget->id); + + continue; + } + + unset($widget_settings->raw_width); + + if (Str::contains($widget_settings->width, 'lg:w-1/4')) { + $widget_settings->width = 25; + } elseif (Str::contains($widget_settings->width, 'lg:w-1/3')) { + $widget_settings->width = 33; + } elseif (Str::contains($widget_settings->width, 'lg:w-2/4')) { + $widget_settings->width = 50; + } else { + $widget_settings->width = 100; + } + + $widget->settings = $widget_settings; + + $widget->save(); + + Log::channel('stdout')->info('Widget updated:' . $widget->id); + } + + Log::channel('stdout')->info('Widgets updated.'); + } +} diff --git a/app/Listeners/Update/V31/Version317.php b/app/Listeners/Update/V31/Version317.php new file mode 100644 index 00000000..1cdd6f3f --- /dev/null +++ b/app/Listeners/Update/V31/Version317.php @@ -0,0 +1,78 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stdout')->info('Updating to 3.1.7 version...'); + + $this->deleteOldFiles(); + + $this->clearCache(); + + Log::channel('stdout')->info('Done!'); + } + + public function clearCache(): void + { + Log::channel('stdout')->info('Clearing cache...'); + + Artisan::call('view:clear'); + Artisan::call('cache:clear'); + + Log::channel('stdout')->info('Cleared cache.'); + } + + public function deleteOldFiles() + { + Log::channel('stdout')->info('Deleting old files and folders...'); + + $files = [ + 'public/vendor/alpinejs/alpine.min.js', + 'public/vendor/livewire/livewire.js', + 'public/vendor/livewire/livewire.js.map', + 'public/vendor/livewire/manifest.json', + ]; + + $directories = [ + 'public/vendor/alpinejs', + 'public/vendor/livewire', + ]; + + Log::channel('stdout')->info('Deleting old files...'); + + foreach ($files as $file) { + File::delete(base_path($file)); + } + + Log::channel('stdout')->info('Deleting old folders...'); + + foreach ($directories as $directory) { + File::deleteDirectory(base_path($directory)); + } + + Log::channel('stdout')->info('Old files and folders deleted.'); + } +} diff --git a/app/Listeners/Update/V31/Version318.php b/app/Listeners/Update/V31/Version318.php new file mode 100644 index 00000000..bb3e4179 --- /dev/null +++ b/app/Listeners/Update/V31/Version318.php @@ -0,0 +1,91 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stdout')->info('Updating to 3.1.8 version...'); + + $this->deleteOldFiles(); + + $this->clearCache(); + + $this->updatePermissions(); + + Log::channel('stdout')->info('Done!'); + } + + public function clearCache(): void + { + Log::channel('stdout')->info('Clearing cache...'); + + Artisan::call('view:clear'); + Artisan::call('cache:clear'); + + Log::channel('stdout')->info('Cleared cache.'); + } + + public function deleteOldFiles() + { + Log::channel('stdout')->info('Deleting old files and folders...'); + + $files = [ + 'public/vendor/alpinejs/alpine.min.js', + 'public/vendor/livewire/livewire.js', + 'public/vendor/livewire/livewire.js.map', + 'public/vendor/livewire/manifest.json', + ]; + + $directories = [ + 'public/vendor/alpinejs', + 'public/vendor/livewire', + ]; + + Log::channel('stdout')->info('Deleting old files...'); + + foreach ($files as $file) { + File::delete(base_path($file)); + } + + Log::channel('stdout')->info('Deleting old folders...'); + + foreach ($directories as $directory) { + File::deleteDirectory(base_path($directory)); + } + + Log::channel('stdout')->info('Old files and folders deleted.'); + } + + public function updatePermissions() + { + // c=create, r=read, u=update, d=delete + $this->attachPermissionsToAdminRoles([ + 'offline-payments-settings' => 'r,u,d', + ]); + } +} diff --git a/app/Listeners/Update/V32/Version320.php b/app/Listeners/Update/V32/Version320.php new file mode 100644 index 00000000..e5c26efd --- /dev/null +++ b/app/Listeners/Update/V32/Version320.php @@ -0,0 +1,61 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stdout')->info('Updating to 3.2.0 version...'); + + $this->updateDatabase(); + + $this->updateDocumentItems(); + + Log::channel('stdout')->info('Done!'); + } + + public function updateDatabase(): void + { + Log::channel('stdout')->info('Updating database...'); + + Artisan::call('migrate', ['--force' => true]); + + Log::channel('stdout')->info('Database updated.'); + } + + public function updateDocumentItems(): void + { + Log::channel('stdout')->info('Updating document items...'); + + DB::table('document_items') + ->join('documents', 'documents.id', '=', 'document_items.document_id') + ->whereNull('document_items.category_id') + ->whereNotNull('documents.category_id') + ->update([ + 'document_items.category_id' => DB::raw(DB::getTablePrefix() . 'documents.category_id'), + ]); + + Log::channel('stdout')->info('Document items updated.'); + } +} diff --git a/app/Models/Auth/Permission.php b/app/Models/Auth/Permission.php new file mode 100644 index 00000000..d954786e --- /dev/null +++ b/app/Models/Auth/Permission.php @@ -0,0 +1,81 @@ +get('search'); + $limit = (int) $request->get('limit', setting('default.list_limit', '25')); + + return $query->usingSearchString($search)->sortable($sort)->paginate($limit); + } + + /** + * Scope to only include by action. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string $action + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeAction($query, $action = 'read') + { + return $query->where('name', 'like', $action . '-%'); + } + + /** + * Transform display name. + * + * @return string + */ + public function getTitleAttribute() + { + $replaces = [ + 'Create ' => '', + 'Read ' => '', + 'Update ' => '', + 'Delete ' => '', + 'Modules' => 'Apps', + ]; + + $title = str_replace(array_keys($replaces), array_values($replaces), $this->display_name); + + return $title; + } +} diff --git a/app/Models/Auth/Role.php b/app/Models/Auth/Role.php new file mode 100644 index 00000000..72ab0553 --- /dev/null +++ b/app/Models/Auth/Role.php @@ -0,0 +1,103 @@ +get('search'); + $limit = (int) $request->get('limit', setting('default.list_limit', '25')); + + return $query->usingSearchString($search)->sortable($sort)->paginate($limit); + } + + /** + * @inheritDoc + * + * @param Document $src + * @param boolean $child + */ + public function onCloning($src, $child = null) + { + $this->name = $src->name . '-' . Role::max('id') + 1; + } + + /** + * Get the line actions. + * + * @return array + */ + public function getLineActionsAttribute() + { + $actions = []; + + $actions[] = [ + 'title' => trans('general.edit'), + 'icon' => 'edit', + 'url' => route('roles.roles.edit', $this->id), + 'permission' => 'update-roles-roles', + 'attributes' => [ + 'id' => 'index-line-actions-edit-role-' . $this->id, + ], + ]; + + $actions[] = [ + 'title' => trans('general.duplicate'), + 'icon' => 'file_copy', + 'url' => route('roles.roles.duplicate', $this->id), + 'permission' => 'create-roles-roles', + 'attributes' => [ + 'id' => 'index-line-actions-duplicate-role-' . $this->id, + ], + ]; + + $actions[] = [ + 'type' => 'delete', + 'icon' => 'delete', + 'route' => 'roles.roles.destroy', + 'permission' => 'delete-roles-roles', + 'attributes' => [ + 'id' => 'index-line-actions-delete-role-' . $this->id, + ], + 'model' => $this, + ]; + + return $actions; + } +} diff --git a/app/Models/Auth/User.php b/app/Models/Auth/User.php new file mode 100644 index 00000000..2c85f01c --- /dev/null +++ b/app/Models/Auth/User.php @@ -0,0 +1,420 @@ + 'boolean', + 'last_logged_in_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + /** + * The attributes that should be hidden for arrays. + * + * @var array + */ + protected $hidden = ['password', 'remember_token']; + + /** + * Sortable columns. + * + * @var array + */ + public $sortable = ['name', 'email', 'enabled']; + + public static function boot() + { + parent::boot(); + + static::retrieved(function ($model) { + $model->setCompanyIds(); + }); + + static::saving(function ($model) { + $model->unsetCompanyIds(); + }); + } + + public function companies() + { + return $this->belongsToMany('App\Models\Common\Company', 'App\Models\Auth\UserCompany'); + } + + public function contact() + { + return $this->hasOne('App\Models\Common\Contact', 'user_id', 'id'); + } + + public function dashboards() + { + return $this->belongsToMany('App\Models\Common\Dashboard', 'App\Models\Auth\UserDashboard'); + } + + public function invitation() + { + return $this->hasOne('App\Models\Auth\UserInvitation', 'user_id', 'id'); + } + + public function roles() + { + return $this->belongsToMany(role_model_class(), 'App\Models\Auth\UserRole'); + } + + /** + * Always capitalize the name when we retrieve it + */ + public function getNameAttribute($value) + { + if (empty($value)) { + return trans('general.na'); + } + + return ucfirst($value); + } + + /** + * Always return a valid picture when we retrieve it + */ + public function getPictureAttribute($value) + { + if (!empty($value)) { + return $value; + } elseif (! $this->hasMedia('picture')) { + return false; + } + + return $this->getMedia('picture')->last(); + } + + /** + * Always return a valid picture when we retrieve it + */ + public function getLastLoggedAttribute($value) + { + // Date::setLocale('tr'); + + if (!empty($value)) { + return Date::parse($value)->diffForHumans(); + } else { + return trans('auth.never'); + } + } + + /** + * Always capitalize the name when we save it to the database + */ + public function setNameAttribute($value) + { + $this->attributes['name'] = ucfirst($value); + } + + /** + * Always hash the password when we save it to the database + */ + public function setPasswordAttribute($value) + { + $this->attributes['password'] = bcrypt($value); + } + + /** + * Send reset link to user via email + */ + public function sendPasswordResetNotification($token) + { + $this->notify(new Reset($token, $this->email)); + } + + /** + * Scope to get all rows filtered, sorted and paginated. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param $sort + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeCollect($query, $sort = 'name') + { + $request = request(); + $search = $request->get('search'); + + /** + * Modules that use the sort parameter in CRUD operations cause an error, + * so this sort parameter set back to old value after the query is executed. + * + * for Custom Fields module + */ + $request_sort = $request->get('sort'); + + $query->usingSearchString($search)->sortable($sort); + + $request->merge(['sort' => $request_sort]); + // This line disabled because broken sortable issue. + //$request->offsetUnset('direction'); + $limit = (int) $request->get('limit', setting('default.list_limit', '25')); + + return $query->paginate($limit); + } + + /** + * Scope to only include active users. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeEnabled($query) + { + return $query->where('enabled', 1); + } + + /** + * Scope to only customers. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeIsCustomer($query) + { + return $query->wherePermissionIs('read-client-portal'); + } + + /** + * Scope to only users. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeIsNotCustomer($query) + { + return $query->wherePermissionIs('read-admin-panel'); + } + + /** + * Scope to only employees. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeIsEmployee($query) + { + return $query->whereHasRole('employee'); + } + + /** + * Scope to only users. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeIsNotEmployee($query) + { + return $query->wherePermissionIs('read-admin-panel'); + } + + public function scopeEmail($query, $email) + { + return $query->where('email', '=', $email); + } + + /** + * Attach company_ids attribute to model. + * + * @return void + */ + public function setCompanyIds() + { + $company_ids = $this->withoutEvents(function () { + return $this->companies->pluck('id')->toArray(); + }); + + $this->setAttribute('company_ids', $company_ids); + } + + /** + * Detach company_ids attribute from model. + * + * @return void + */ + public function unsetCompanyIds() + { + $this->offsetUnset('company_ids'); + } + + /** + * Determine if user is a customer. + * + * @return bool + */ + public function isCustomer() + { + return (bool) $this->can('read-client-portal'); + } + + /** + * Determine if user is not a customer. + * + * @return bool + */ + public function isNotCustomer() + { + return (bool) $this->can('read-admin-panel'); + } + + /** + * Determine if user is a employee. + * + * @return bool + */ + public function isEmployee() + { + return (bool) $this->hasRole('employee'); + } + + /** + * Determine if user is not a employee. + * + * @return bool + */ + public function isNotEmployee() + { + return (bool) ! $this->hasRole('employee'); + } + + public function scopeSource($query, $source) + { + return $query->where($this->qualifyColumn('created_from'), $source); + } + + public function scopeIsOwner($query) + { + return $query->where($this->qualifyColumn('created_by'), user_id()); + } + + public function scopeIsNotOwner($query) + { + return $query->where($this->qualifyColumn('created_by'), '<>', user_id()); + } + + public function ownerKey($owner) + { + if ($this->isNotOwnable()) { + return 0; + } + + return $this->created_by; + } + + /** + * Get the user's preferred locale. + * + * @return string + */ + public function preferredLocale() + { + return $this->locale; + } + + /** + * Get the line actions. + * + * @return array + */ + public function getLineActionsAttribute() + { + $actions = []; + + $actions[] = [ + 'title' => trans('general.show'), + 'icon' => 'visibility', + 'url' => route('users.show', $this->id), + 'permission' => 'read-auth-users', + 'attributes' => [ + 'id' => 'index-line-actions-show-user-' . $this->id, + ], + ]; + + $actions[] = [ + 'title' => trans('general.edit'), + 'icon' => 'edit', + 'url' => route('users.edit', $this->id), + 'permission' => 'update-auth-users', + 'attributes' => [ + 'id' => 'index-line-actions-edit-user-' . $this->id, + ], + ]; + + if ($this->hasPendingInvitation()) { + $actions[] = [ + 'title' => trans('general.resend') . ' ' . trans_choice('general.invitations', 1), + 'icon' => 'replay', + 'url' => route('users.invite', $this->id), + 'permission' => 'update-auth-users', + 'attributes' => [ + 'id' => 'index-line-actions-resend-user-' . $this->id, + ], + ]; + } + + if (user()->id != $this->id) { + $actions[] = [ + 'type' => 'delete', + 'icon' => 'delete', + 'route' => 'users.destroy', + 'permission' => 'delete-auth-users', + 'attributes' => [ + 'id' => 'index-line-actions-delete-user-' . $this->id, + ], + 'model' => $this, + ]; + } + + return $actions; + } + + /** + * Create a new factory instance for the model. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory + */ + protected static function newFactory() + { + return \Database\Factories\User::new(); + } +} diff --git a/app/Models/Auth/UserCompany.php b/app/Models/Auth/UserCompany.php new file mode 100644 index 00000000..c117087b --- /dev/null +++ b/app/Models/Auth/UserCompany.php @@ -0,0 +1,36 @@ +belongsTo(user_model_class()); + } + + public function company() + { + return $this->belongsTo('App\Models\Common\Company'); + } +} diff --git a/app/Models/Auth/UserDashboard.php b/app/Models/Auth/UserDashboard.php new file mode 100644 index 00000000..f541727b --- /dev/null +++ b/app/Models/Auth/UserDashboard.php @@ -0,0 +1,41 @@ +belongsTo(user_model_class()); + } + + public function dashboard() + { + return $this->belongsTo('App\Models\Common\Dashboard'); + } + + public function dashboards() + { + return $this->belongsToMany('App\Models\Common\Dashboard', 'App\Models\Auth\UserDashboard'); + } +} diff --git a/app/Models/Auth/UserInvitation.php b/app/Models/Auth/UserInvitation.php new file mode 100644 index 00000000..f7154dbb --- /dev/null +++ b/app/Models/Auth/UserInvitation.php @@ -0,0 +1,40 @@ +belongsTo(user_model_class()); + } + + /** + * Scope a query to only include given token value. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return void + */ + public function scopeToken($query, $token) + { + $query->where('token', $token); + } +} diff --git a/app/Models/Auth/UserRole.php b/app/Models/Auth/UserRole.php new file mode 100644 index 00000000..aec4eed5 --- /dev/null +++ b/app/Models/Auth/UserRole.php @@ -0,0 +1,36 @@ +belongsTo(user_model_class()); + } + + public function role() + { + return $this->belongsTo(role_model_class()); + } +} diff --git a/app/Models/Banking/Account.php b/app/Models/Banking/Account.php new file mode 100644 index 00000000..205fa0b5 --- /dev/null +++ b/app/Models/Banking/Account.php @@ -0,0 +1,223 @@ + 'double', + 'enabled' => 'boolean', + 'deleted_at' => 'datetime', + ]; + + /** + * Sortable columns. + * + * @var array + */ + public $sortable = ['name', 'number', 'balance', 'bank_name', 'bank_phone']; + + public function currency() + { + return $this->belongsTo('App\Models\Setting\Currency', 'currency_code', 'code'); + } + + public function expense_transactions() + { + return $this->transactions()->whereIn('transactions.type', (array) $this->getExpenseTypes()); + } + + public function income_transactions() + { + return $this->transactions()->whereIn('transactions.type', (array) $this->getIncomeTypes()); + } + + public function transactions() + { + return $this->hasMany('App\Models\Banking\Transaction'); + } + + public function reconciliations() + { + return $this->hasMany('App\Models\Banking\Reconciliation'); + } + + public function scopeName($query, $name) + { + return $query->where('name', '=', $name); + } + + public function scopeNumber($query, $number) + { + return $query->where('number', '=', $number); + } + + /** + * Sort by balance + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param $direction + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function balanceSortable($query, $direction) + { + return $query//->join('transactions', 'transactions.account_id', '=', 'accounts.id') + ->orderBy('balance', $direction) + ->select(['accounts.*', 'accounts.opening_balance as balance']); + } + + /** + * Get the name with currency. + * + * @return string + */ + public function getTitleAttribute() + { + if (! empty($this->currency) && ! empty($this->currency->symbol)) { + return $this->name . ' (' . $this->currency->symbol . ')'; + } + + return $this->name; + } + + public function getInitialsAttribute($value) + { + return Str::getInitials($this->name); + } + + /** + * Get the current balance. + * + * @return string + */ + public function getBalanceAttribute() + { + // Opening Balance + $total = $this->opening_balance; + + // Sum Incomes + $total += $this->income_transactions()->sum('amount'); + + // Subtract Expenses + $total -= $this->expense_transactions()->sum('amount'); + + return $total; + } + + /** + * Get the current balance. + * + * @return string + */ + public function getIncomeBalanceAttribute() + { + // Opening Balance + //$total = $this->opening_balance; + $total = 0; + + // Sum Incomes + $total += $this->income_transactions()->sum('amount'); + + return $total; + } + + /** + * Get the current balance. + * + * @return string + */ + public function getExpenseBalanceAttribute() + { + // Opening Balance + //$total = $this->opening_balance; + $total = 0; + + // Subtract Expenses + $total += $this->expense_transactions()->sum('amount'); + + return $total; + } + + /** + * Get the line actions. + * + * @return array + */ + public function getLineActionsAttribute() + { + $actions = []; + + $actions[] = [ + 'title' => trans('general.show'), + 'icon' => 'visibility', + 'url' => route('accounts.show', $this->id), + 'permission' => 'read-banking-accounts', + 'attributes' => [ + 'id' => 'index-line-actions-show-account-' . $this->id, + ], + ]; + + $actions[] = [ + 'title' => trans('general.edit'), + 'icon' => 'edit', + 'url' => route('accounts.edit', $this->id), + 'permission' => 'update-banking-accounts', + 'attributes' => [ + 'id' => 'index-line-actions-edit-account-' . $this->id, + ], + ]; + + $actions[] = [ + 'type' => 'delete', + 'icon' => 'delete', + 'route' => 'accounts.destroy', + 'permission' => 'delete-banking-accounts', + 'model' => $this, + 'attributes' => [ + 'id' => 'index-line-actions-delete-account-' . $this->id, + ], + ]; + + return $actions; + } + + /** + * Create a new factory instance for the model. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory + */ + protected static function newFactory() + { + return \Database\Factories\Account::new(); + } +} diff --git a/app/Models/Banking/FundAccount.php b/app/Models/Banking/FundAccount.php new file mode 100644 index 00000000..1d84f688 --- /dev/null +++ b/app/Models/Banking/FundAccount.php @@ -0,0 +1,43 @@ + 'encrypted', + 'login_password' => 'encrypted', + 'reserved_phone' => 'encrypted', + 'deleted_at' => 'datetime', + ]; + + public function getMaskedAccountNumberAttribute(): string + { + $number = (string) $this->account_number; + + if (mb_strlen($number) <= 4) { + return str_repeat('*', mb_strlen($number)); + } + + return str_repeat('*', mb_strlen($number) - 4) . mb_substr($number, -4); + } +} diff --git a/app/Models/Banking/Reconciliation.php b/app/Models/Banking/Reconciliation.php new file mode 100644 index 00000000..f65d1a36 --- /dev/null +++ b/app/Models/Banking/Reconciliation.php @@ -0,0 +1,89 @@ + 'double', + 'reconciled' => 'boolean', + 'transactions' => 'array', + 'deleted_at' => 'datetime', + 'started_at' => 'datetime', + 'ended_at' => 'datetime', + ]; + + /** + * Sortable columns. + * + * @var array + */ + public $sortable = ['created_at', 'account_id', 'started_at', 'ended_at', 'opening_balance', 'closing_balance', 'reconciled']; + + public function account() + { + return $this->belongsTo('App\Models\Banking\Account')->withDefault(['name' => trans('general.na')]); + } + + /** + * Get the line actions. + * + * @return array + */ + public function getLineActionsAttribute() + { + $actions = []; + + $actions[] = [ + 'title' => trans('general.edit'), + 'icon' => 'edit', + 'url' => route('reconciliations.edit', $this->id), + 'permission' => 'update-banking-reconciliations', + 'attributes' => [ + 'id' => 'index-line-actions-edit-reconciliation-' . $this->id, + ], + ]; + + $actions[] = [ + 'type' => 'delete', + 'icon' => 'delete', + 'route' => 'reconciliations.destroy', + 'permission' => 'delete-banking-reconciliations', + 'attributes' => [ + 'id' => 'index-line-actions-delete-reconciliation-' . $this->id, + ], + 'model' => $this, + ]; + + return $actions; + } + + /** + * Create a new factory instance for the model. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory + */ + protected static function newFactory() + { + return \Database\Factories\Reconciliation::new(); + } +} diff --git a/app/Models/Banking/Transaction.php b/app/Models/Banking/Transaction.php new file mode 100644 index 00000000..0a6d36e0 --- /dev/null +++ b/app/Models/Banking/Transaction.php @@ -0,0 +1,784 @@ + 'datetime', + 'amount' => 'double', + 'currency_rate' => 'double', + 'deleted_at' => 'datetime', + ]; + + /** + * Sortable columns. + * + * @var array + */ + public $sortable = [ + 'paid_at', + 'number', + 'type', + 'account.name', + 'contact.name', + 'category.name', + 'document.document_number', + 'amount', + 'recurring.started_at', + 'recurring.status', + ]; + + /** + * Clonable relationships. + * + * @var array + */ + public $cloneable_relations = ['recurring', 'taxes']; + + /** + * The "booted" method of the model. + * + * @return void + */ + protected static function booted() + { + static::addGlobalScope(new Scope); + } + + public function account() + { + return $this->belongsTo('App\Models\Banking\Account')->withDefault(['name' => trans('general.na')]); + } + + public function bill() + { + return $this->belongsTo('App\Models\Document\Document', 'document_id')->withoutGlobalScope('App\Scopes\Document'); + } + + public function category() + { + return $this->belongsTo('App\Models\Setting\Category')->withoutGlobalScope('App\Scopes\Category')->withDefault(['name' => trans('general.na')]); + } + + public function children() + { + return $this->hasMany('App\Models\Banking\Transaction', 'parent_id'); + } + + public function contact() + { + return $this->belongsTo('App\Models\Common\Contact')->withDefault(['name' => trans('general.na')]); + } + + public function currency() + { + return $this->belongsTo('App\Models\Setting\Currency', 'currency_code', 'code'); + } + + public function invoice() + { + return $this->belongsTo('App\Models\Document\Document', 'document_id')->withoutGlobalScope('App\Scopes\Document'); + } + + public function document() + { + return $this->belongsTo('App\Models\Document\Document', 'document_id')->withoutGlobalScope('App\Scopes\Document'); + } + + public function parent() + { + return $this->belongsTo('App\Models\Banking\Transaction', 'parent_id')->isRecurring(); + } + + public function recurring() + { + return $this->morphOne('App\Models\Common\Recurring', 'recurable'); + } + + public function transfer() + { + if ($this->type == static::INCOME_TRANSFER_TYPE) { + return $this->belongsTo('App\Models\Banking\Transfer', 'id', 'income_transaction_id'); + } + + if ($this->type == static::EXPENSE_TRANSFER_TYPE) { + return $this->belongsTo('App\Models\Banking\Transfer', 'id', 'expense_transaction_id'); + } + + return null; + } + + public function splits() + { + return $this->hasMany('App\Models\Banking\Transaction', 'split_id'); + } + + public function user() + { + return $this->belongsTo(user_model_class(), 'contact_id', 'id'); + } + + public function taxes() + { + return $this->hasMany('App\Models\Banking\TransactionTax'); + } + + public function scopeNumber(Builder $query, string $number): Builder + { + return $query->where('number', '=', $number); + } + + public function scopeType(Builder $query, $types): Builder + { + if (empty($types)) { + return $query; + } + + return $query->whereIn($this->qualifyColumn('type'), (array) $types); + } + + public function scopeIncome(Builder $query): Builder + { + return $query->whereIn($this->qualifyColumn('type'), (array) $this->getIncomeTypes()); + } + + public function scopeIncomeTransfer(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', self::INCOME_TRANSFER_TYPE); + } + + public function scopeIncomeRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', self::INCOME_RECURRING_TYPE) + ->whereHas('recurring', function (Builder $query) { + $query->whereNull('deleted_at'); + }); + } + + public function scopeExpense(Builder $query): Builder + { + return $query->whereIn($this->qualifyColumn('type'), (array) $this->getExpenseTypes()); + } + + public function scopeExpenseTransfer(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', self::EXPENSE_TRANSFER_TYPE); + } + + public function scopeExpenseRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', self::EXPENSE_RECURRING_TYPE) + ->whereHas('recurring', function (Builder $query) { + $query->whereNull('deleted_at'); + }); + } + + public function scopeIsTransfer(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), 'like', '%-transfer'); + } + + public function scopeIsNotTransfer(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), 'not like', '%-transfer'); + } + + public function scopeIsRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), 'like', '%-recurring'); + } + + public function scopeIsNotRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), 'not like', '%-recurring'); + } + + public function scopeIsSplit(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), 'like', '%-split'); + } + + public function scopeIsNotSplit(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), 'not like', '%-split'); + } + + public function scopeIsDocument(Builder $query): Builder + { + return $query->whereNotNull('document_id'); + } + + public function scopeIsNotDocument(Builder $query): Builder + { + return $query->whereNull('document_id'); + } + + public function scopeDocumentId(Builder $query, int $document_id): Builder + { + return $query->where('document_id', '=', $document_id); + } + + public function scopeAccountId(Builder $query, int $account_id): Builder + { + return $query->where('account_id', '=', $account_id); + } + + public function scopeContactId(Builder $query, int $contact_id): Builder + { + return $query->where('contact_id', '=', $contact_id); + } + + public function scopeCategoryId(Builder $query, int $category_id): Builder + { + return $query->where('category_id', '=', $category_id); + } + + /** + * Order by paid date. + */ + public function scopeLatest(Builder $query): Builder + { + return $query->orderBy('paid_at', 'desc'); + } + + public function scopePaid(Builder $query): Builder + { + return $query->sum('amount'); + } + + public function scopeIsReconciled(Builder $query): Builder + { + return $query->where('reconciled', 1); + } + + public function scopeIsNotReconciled(Builder $query): Builder + { + return $query->where('reconciled', 0); + } + + public function onCloning($src, $child = null) + { + if (app()->has(\App\Console\Commands\RecurringCheck::class)) { + $suffix = ''; + } else { + $suffix = $src->isRecurringTransaction() ? '-recurring' : ''; + } + + $this->number = $this->getNextTransactionNumber($this->type, $suffix); + $this->document_id = null; + $this->split_id = null; + + unset($this->reconciled); + } + + /** + * Get the payment method title. + * + * @return string + */ + public function getPaymentMethodTitleAttribute() + { + $payment_method = $this->payment_method; + + $payment_methods = \App\Utilities\Modules::getPaymentMethods('all'); + + return $payment_methods[$payment_method] ?? $payment_method; + } + + /** + * Convert amount to double. + * + * @return float + */ + public function getAmountForAccountAttribute() + { + $amount = $this->amount; + + // Convert amount if not same currency + if ($this->account->currency_code != $this->currency_code) { + $to_code = $this->account->currency_code; + $to_rate = currency($this->account->currency_code)->getRate(); + + $amount = $this->convertBetween($amount, $this->currency_code, $this->currency_rate, $to_code, $to_rate); + } + + return $amount; + } + + /** + * Convert amount to double. + * + * @return float + */ + public function getAmountForDocumentAttribute() + { + $amount = $this->amount; + + // Convert amount if not same currency + if ($this->document->currency_code != $this->currency_code) { + $to_code = $this->document->currency_code; + $to_rate = $this->document->currency_rate; + //$to_rate = currency($this->document->currency_code)->getRate(); + + $amount = $this->convertBetween($amount, $this->currency_code, $this->currency_rate, $to_code, $to_rate); + } + + return $amount; + } + + /** + * Get the current balance. + * + * @return string + */ + public function getAttachmentAttribute($value) + { + if (!empty($value) && !$this->hasMedia('attachment')) { + return $value; + } elseif (!$this->hasMedia('attachment')) { + return false; + } + + return $this->getMedia('attachment')->all(); + } + + /** + * Get the splittable status. + * + * @return bool + */ + public function getIsSplittableAttribute() + { + return is_null($this->split_id); + } + + public function delete_attachment() + { + if ($attachments = $this->attachment) { + foreach ($attachments as $file) { + MediaModel::where('id', $file->id)->delete(); + } + } + } + + /** + * Get the title of type. + * + * @return string + */ + public function getTypeTitleAttribute($value) + { + if ($value) { + return $value; + } + + $translation = config('type.transaction.' . $this->type . '.translation.transactions'); + + if (! empty($translation)) { + return trans_choice($translation, 1); + } + + $type = $this->getRealTypeOfRecurringTransaction($this->type); + $type = $this->getRealTypeOfTransferTransaction($type); + $type = $this->getRealTypeOfSplitTransaction($type); + + $type = str_replace('-', '_', $type); + + return trans_choice('general.' . Str::plural($type), 1); + } + + /** + * Get the item id. + * + * @return string + */ + public function getTaxIdsAttribute() + { + return $this->taxes()->pluck('tax_id'); + } + + /** + * Get the amount before tax. + * + * @return string + */ + public function getTotalTaxAttribute() + { + $precision = currency($this->currency_code)->getPrecision(); + + $amount = 0; + + if ($this->taxes->count()) { + foreach ($this->taxes as $tax) { + $amount += $tax->amount; + } + } + + return round($amount, $precision); + } + + /** + * Get the amount before tax. + * + * @return string + */ + public function getAmountBeforeTaxAttribute() + { + if (empty($this->amount)) { + return 0; + } + + $precision = currency($this->currency_code)->getPrecision(); + + return round($this->amount - $this->total_tax, $precision); + } + + /** + * Get the route name. + * + * @return string + */ + public function getRouteNameAttribute($value) + { + if ($value) { + return $value; + } + + if ($this->isIncome()) { + if (! empty($this->document_id) && $this->document->type != 'invoice') { + return $this->getRouteFromConfig(); + } else { + return !empty($this->document_id) ? 'invoices.show' : 'transactions.show'; + } + } + + if ($this->isExpense()) { + if (! empty($this->document_id) && $this->document->type != 'bill') { + return $this->getRouteFromConfig(); + } else { + return !empty($this->document_id) ? 'bills.show' : 'transactions.show'; + } + } + + return 'transactions.index'; + } + + public function getRouteFromConfig() + { + $route = ''; + + $alias = config('type.document.' . $this->document->type . '.alias'); + $prefix = config('type.document.' . $this->document->type . '.route.prefix'); + + // if use module set module alias + if (!empty($alias)) { + $route .= $alias . '.'; + } + + if (!empty($prefix)) { + $route .= $prefix . '.'; + } + + if ($route) { + return $route . 'show'; + } + + return 'transactions.index'; + } + + /** + * Get the route id. + * + * @return string + */ + public function getRouteIdAttribute($value) + { + return !empty($value) ? $value : (!empty($this->document_id) ? $this->document_id : $this->id); + } + + /** + * Get the line actions. + * + * @return array + */ + public function getLineActionsAttribute() + { + $actions = []; + + $prefix = 'transactions'; + + if (Str::contains($this->type, 'recurring')) { + $prefix = 'recurring-transactions'; + } + + try { + $actions[] = [ + 'title' => trans('general.show'), + 'icon' => 'visibility', + 'url' => route($prefix. '.show', $this->id), + 'permission' => 'read-banking-transactions', + 'attributes' => [ + 'id' => 'index-line-actions-show-' . $this->type . '-' . $this->id, + ], + ]; + } catch (\Exception $e) {} + + try { + if (! $this->reconciled && empty($this->document_id) && $this->isNotTransferTransaction()) { + $actions[] = [ + 'title' => trans('general.edit'), + 'icon' => 'edit', + 'url' => route($prefix. '.edit', $this->id), + 'permission' => 'update-banking-transactions', + 'attributes' => [ + 'id' => 'index-line-actions-edit-' . $this->type . '-' . $this->id, + ], + ]; + } + } catch (\Exception $e) {} + + try { + if (empty($this->document_id) + && $this->isNotTransferTransaction() + && $this->isNotSplitTransaction() + ) { + $actions[] = [ + 'title' => trans('general.duplicate'), + 'icon' => 'file_copy', + 'url' => route($prefix. '.duplicate', $this->id), + 'permission' => 'create-banking-transactions', + 'attributes' => [ + 'id' => 'index-line-actions-duplicate-' . $this->type . '-' . $this->id, + ], + ]; + } + } catch (\Exception $e) {} + + try { + if ( + $this->is_splittable + && empty($this->document_id) + && empty($this->recurring) + && $this->isNotTransferTransaction() + ) { + $connect = [ + 'type' => 'button', + 'title' => trans('general.connect'), + 'icon' => 'sensors', + 'permission' => 'create-banking-transactions', + 'attributes' => [ + 'id' => 'index-line-actions-connect-' . $this->type . '-' . $this->id, + '@click' => 'onConnectTransactions(\'' . route('transactions.dial', $this->id) . '\')', + ], + ]; + + $actions[] = $connect; + + $actions[] = [ + 'type' => 'divider', + ]; + } + } catch (\Exception $e) {} + + try { + $actions[] = [ + 'title' => trans('general.print'), + 'icon' => 'print', + 'url' => route($prefix. '.print', $this->id), + 'permission' => 'read-banking-transactions', + 'attributes' => [ + 'id' => 'index-line-actions-print-' . $this->type . '-' . $this->id, + 'target' => '_blank', + ], + ]; + } catch (\Exception $e) {} + + try { + $actions[] = [ + 'title' => trans('general.download_pdf'), + 'icon' => 'picture_as_pdf', + 'url' => route($prefix. '.pdf', $this->id), + 'permission' => 'read-banking-transactions', + 'attributes' => [ + 'id' => 'index-line-actions-pdf-' . $this->type . '-' . $this->id, + 'target' => '_blank', + ], + ]; + } catch (\Exception $e) {} + + if ($prefix != 'recurring-transactions') { + if ($this->isNotTransferTransaction()) { + $actions[] = [ + 'type' => 'divider', + ]; + + try { + $actions[] = [ + 'type' => 'button', + 'title' => trans('general.share_link'), + 'icon' => 'share', + 'url' => route('modals.transactions.share.create', $this->id), + 'permission' => 'read-banking-transactions', + 'attributes' => [ + 'id' => 'index-line-actions-share-' . $this->type . '-' . $this->id, + '@click' => 'onShareLink("' . route('modals.transactions.share.create', $this->id) . '")', + ], + ]; + } catch (\Exception $e) {} + + try { + if (! empty($this->contact) && $this->contact->email) { + $actions[] = [ + 'type' => 'button', + 'title' => trans('invoices.send_mail'), + 'icon' => 'email', + 'url' => route('modals.transactions.emails.create', $this->id), + 'permission' => 'read-banking-transactions', + 'attributes' => [ + 'id' => 'index-line-actions-send-email-' . $this->type . '-' . $this->id, + '@click' => 'onSendEmail("' . route('modals.transactions.emails.create', $this->id) . '")', + ], + ]; + } + } catch (\Exception $e) {} + + $actions[] = [ + 'type' => 'divider', + ]; + + try { + if (! $this->reconciled) { + $actions[] = [ + 'type' => 'delete', + 'icon' => 'delete', + 'title' => ! empty($this->recurring) ? 'transactions' : 'recurring_template', + 'route' => $prefix. '.destroy', + 'permission' => 'delete-banking-transactions', + 'model-name' => 'number', + 'attributes' => [ + 'id' => 'index-line-actions-delete-' . $this->type . '-' . $this->id, + ], + 'model' => $this, + ]; + } + } catch (\Exception $e) {} + } + } else { + if ($this->recurring && $this->recurring->status != 'ended') { + try { + $actions[] = [ + 'title' => trans('general.end'), + 'icon' => 'block', + 'url' => route($prefix. '.end', $this->id), + 'permission' => 'update-banking-transactions', + 'attributes' => [ + 'id' => 'index-line-actions-end-' . $this->type . '-' . $this->id, + ], + ]; + } catch (\Exception $e) {} + } + } + + return $actions; + } + + /** + * Get the recurring status label. + * + * @return string + */ + public function getRecurringStatusLabelAttribute() + { + return match($this->recurring->status) { + 'active' => 'status-partial', + 'ended' => 'status-success', + default => 'status-success', + }; + } + + /** + * Retrieve the model for a bound value. + * + * @param mixed $value + * @param string|null $field + * @return \Illuminate\Database\Eloquent\Model|null + */ + public function resolveRouteBinding($value, $field = null) + { + $query = $this->where('id', $value); + + if (request()->route()->hasParameter('recurring_transaction')) { + $query->isRecurring(); + } + + return $query->firstOrFail(); + } + + /** + * Create a new factory instance for the model. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory + */ + protected static function newFactory() + { + return \Database\Factories\Transaction::new(); + } +} diff --git a/app/Models/Banking/TransactionTax.php b/app/Models/Banking/TransactionTax.php new file mode 100644 index 00000000..2b4ae76b --- /dev/null +++ b/app/Models/Banking/TransactionTax.php @@ -0,0 +1,123 @@ +belongsTo('App\Models\Setting\Tax')->withDefault(['name' => trans('general.na'), 'rate' => 0]); + } + + public function transaction() + { + return $this->belongsTo('App\Models\Banking\Transaction')->withDefault(['name' => trans('general.na')]); + } + + public function scopeType(Builder $query, $types): Builder + { + if (empty($types)) { + return $query; + } + + return $query->whereIn($this->qualifyColumn('type'), (array) $types); + } + + public function scopeIncome(Builder $query) + { + return $query->whereIn($this->qualifyColumn('type'), (array) $this->getIncomeTypes()); + } + + public function scopeIncomeTransfer(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', Transaction::INCOME_TRANSFER_TYPE); + } + + public function scopeIncomeRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', Transaction::INCOME_RECURRING_TYPE) + ->whereHas('transaction.recurring', function (Builder $query) { + $query->whereNull('deleted_at'); + }); + } + + public function scopeExpense(Builder $query) + { + return $query->whereIn($this->qualifyColumn('type'), (array) $this->getExpenseTypes()); + } + + public function scopeExpenseTransfer(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', Transaction::EXPENSE_TRANSFER_TYPE); + } + + public function scopeExpenseRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', Transaction::EXPENSE_RECURRING_TYPE) + ->whereHas('transaction.recurring', function (Builder $query) { + $query->whereNull('deleted_at'); + }); + } + + public function scopeIsTransfer(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), 'like', '%-transfer'); + } + + public function scopeIsNotTransfer(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), 'not like', '%-transfer'); + } + + public function scopeIsRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), 'like', '%-recurring'); + } + + public function scopeIsNotRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), 'not like', '%-recurring'); + } + + public function scopeIsSplit(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), 'like', '%-split'); + } + + public function scopeIsNotSplit(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), 'not like', '%-split'); + } + + public function scopeIsDocument(Builder $query): Builder + { + return $query->whereHas('transaction', function ($q) { + $q->whereNotNull('document_id'); + }); + } + + public function scopeIsNotDocument(Builder $query): Builder + { + return $query->whereHas('transaction', function ($q) { + $q->whereNull('document_id'); + }); + } +} diff --git a/app/Models/Banking/Transfer.php b/app/Models/Banking/Transfer.php new file mode 100644 index 00000000..dc5e8d18 --- /dev/null +++ b/app/Models/Banking/Transfer.php @@ -0,0 +1,306 @@ +belongsTo('App\Models\Banking\Transaction', 'expense_transaction_id') + ->withoutGlobalScope('App\Scopes\Transaction') + ->withDefault(['name' => trans('general.na')]); + } + + public function expense_account() + { + return $this->belongsToThrough( + 'App\Models\Banking\Account', + 'App\Models\Banking\Transaction', + null, + '', + ['App\Models\Banking\Transaction' => 'expense_transaction_id'] + ) + ->withoutGlobalScope('App\Scopes\Transaction') + ->withDefault(['name' => trans('general.na')]); + } + + public function income_transaction() + { + return $this->belongsTo('App\Models\Banking\Transaction', 'income_transaction_id') + ->withoutGlobalScope('App\Scopes\Transaction') + ->withDefault(['name' => trans('general.na')]); + } + + public function income_account() + { + return $this->belongsToThrough( + 'App\Models\Banking\Account', + 'App\Models\Banking\Transaction', + null, + '', + ['App\Models\Banking\Transaction' => 'income_transaction_id'] + ) + ->withoutGlobalScope('App\Scopes\Transaction') + ->withDefault(['name' => trans('general.na')]); + } + + /** + * Get the current balance. + * + * @return string + */ + public function getAttachmentAttribute($value = null) + { + if (!empty($value) && !$this->hasMedia('attachment')) { + return $value; + } elseif (!$this->hasMedia('attachment')) { + return false; + } + + return $this->getMedia('attachment')->all(); + } + + public function delete_attachment() + { + if ($attachments = $this->attachment) { + foreach ($attachments as $file) { + MediaModel::where('id', $file->id)->delete(); + } + } + } + + public function getTemplatePathAttribute($value = null) + { + return $value ?: 'banking.transfers.print_' . setting('transfer.template'); + } + + /** + * Get the current balance. + * + * @return int + */ + public function getFromAccountIdAttribute($value = null) + { + return $value ?: $this->expense_transaction->account_id; + } + + /** + * Get the current balance. + * + * @return string + */ + public function getFromCurrencyCodeAttribute($value = null) + { + return $value ?: $this->expense_transaction->currency_code; + } + + /** + * Get the current balance. + * + * @return string + */ + public function getFromAccountRateAttribute($value = null) + { + return $value ?: $this->expense_transaction->currency_rate; + } + + /** + * Get the current balance. + * + * @return int + */ + public function getToAccountIdAttribute($value = null) + { + return $value ?: $this->income_transaction->account_id; + } + + /** + * Get the current balance. + * + * @return string + */ + public function getToCurrencyCodeAttribute($value = null) + { + return $value ?: $this->income_transaction->currency_code; + } + + /** + * Get the current balance. + * + * @return string + */ + public function getToAccountRateAttribute($value = null) + { + return $value ?: $this->income_transaction->currency_rate; + } + + /** + * Get the current balance. + * + * @return date + */ + public function getTransferredAtAttribute($value = null) + { + return $value ?: $this->expense_transaction->paid_at; + } + + /** + * Get the current balance. + * + * @return string + */ + public function getDescriptionAttribute($value = null) + { + return $value ?: $this->expense_transaction->description; + } + + /** + * Get the current balance. + * + * @return float + */ + public function getAmountAttribute($value = null) + { + return $value ?: $this->expense_transaction->amount; + } + + /** + * Get the current balance. + * + * @return string + */ + public function getPaymentMethodAttribute($value = null) + { + return $value ?: $this->expense_transaction->payment_method; + } + + /** + * Get the current balance. + * + * @return string + */ + public function getReferenceAttribute($value = null) + { + return $value ?: $this->expense_transaction->reference; + } + + /** + * Get the line actions. + * + * @return array + */ + public function getLineActionsAttribute() + { + $actions = []; + + $actions[] = [ + 'title' => trans('general.show'), + 'icon' => 'visibility', + 'url' => route('transfers.show', $this->id), + 'permission' => 'read-banking-transfers', + 'attributes' => [ + 'id' => 'index-line-actions-show-transfer-' . $this->id, + ], + ]; + + $actions[] = [ + 'title' => trans('general.edit'), + 'icon' => 'edit', + 'url' => route('transfers.edit', $this->id), + 'permission' => 'update-banking-transfers', + 'attributes' => [ + 'id' => 'index-line-actions-edit-transfer-' . $this->id, + ], + ]; + + $actions[] = [ + 'title' => trans('general.duplicate'), + 'icon' => 'file_copy', + 'url' => route('transfers.duplicate', $this->id), + 'permission' => 'update-banking-transfers', + 'attributes' => [ + 'id' => 'index-line-actions-duplicate-transfer-' . $this->id, + ], + ]; + + $actions[] = [ + 'type' => 'delete', + 'icon' => 'delete', + 'route' => 'transfers.destroy', + 'permission' => 'delete-banking-transfers', + 'attributes' => [ + 'id' => 'index-line-actions-delete-transfer-' . $this->id, + ], + 'model' => $this, + ]; + + return $actions; + } + + /** + * Create a new factory instance for the model. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory + */ + protected static function newFactory() + { + return \Database\Factories\Transfer::new(); + } +} diff --git a/app/Models/Common/Company.php b/app/Models/Common/Company.php new file mode 100644 index 00000000..002d83b3 --- /dev/null +++ b/app/Models/Common/Company.php @@ -0,0 +1,707 @@ + 'boolean', + 'deleted_at' => 'datetime', + ]; + + public $allAttributes = []; + + /** + * Sortable columns. + * + * @var array + */ + public $sortable = ['id', 'name', 'domain', 'email', 'phone', 'enabled', 'created_at', 'tax_number', 'country', 'currency']; + + /** + * Fill the model with an array of attributes. + * + * @param array $attributes + * @return $this + * + * @throws \Illuminate\Database\Eloquent\MassAssignmentException + */ + public function fill(array $attributes) + { + $this->allAttributes = $attributes; + + return parent::fill($attributes); + } + + public static function boot() + { + parent::boot(); + + try { + // TODO will optimize.. + static::retrieved(function($model) { + $model->setCommonSettingsAsAttributes(); + }); + + static::saving(function($model) { + $model->unsetCommonSettingsFromAttributes(); + }); + } catch(\Throwable $e) { + + } + } + + public function documents() + { + return $this->hasMany('App\Models\Document\Document'); + } + + public function document_histories() + { + return $this->hasMany('App\Models\Document\DocumentHistory'); + } + + public function document_items() + { + return $this->hasMany('App\Models\Document\DocumentItem'); + } + + public function document_item_taxes() + { + return $this->hasMany('App\Models\Document\DocumentItemTax'); + } + + public function document_totals() + { + return $this->hasMany('App\Models\Document\DocumentTotal'); + } + + public function accounts() + { + return $this->hasMany('App\Models\Banking\Account'); + } + + public function bills() + { + return $this->documents()->where('type', Document::BILL_TYPE); + } + + public function bill_histories() + { + return $this->document_histories()->where('type', Document::BILL_TYPE); + } + + public function bill_items() + { + return $this->document_items()->where('type', Document::BILL_TYPE); + } + + public function bill_item_taxes() + { + return $this->document_item_taxes()->where('type', Document::BILL_TYPE); + } + + public function bill_totals() + { + return $this->document_totals()->where('type', Document::BILL_TYPE); + } + + public function categories() + { + return $this->hasMany('App\Models\Setting\Category'); + } + + public function contacts() + { + return $this->hasMany('App\Models\Common\Contact'); + } + + public function contact_persons() + { + return $this->hasMany('App\Models\Common\ContactPerson'); + } + + public function currencies() + { + return $this->hasMany('App\Models\Setting\Currency'); + } + + public function customers() + { + return $this->contacts()->whereIn('type', (array) $this->getCustomerTypes()); + } + + public function dashboards() + { + return $this->hasMany('App\Models\Common\Dashboard'); + } + + public function email_templates() + { + return $this->hasMany('App\Models\Setting\EmailTemplate'); + } + + public function expense_transactions() + { + return $this->transactions()->whereIn('type', (array) $this->getExpenseTypes()); + } + + public function income_transactions() + { + return $this->transactions()->whereIn('type', (array) $this->getIncomeTypes()); + } + + public function invoices() + { + return $this->documents()->where('type', Document::INVOICE_TYPE); + } + + public function invoice_histories() + { + return $this->document_histories()->where('type', Document::INVOICE_TYPE); + } + + public function invoice_items() + { + return $this->document_items()->where('type', Document::INVOICE_TYPE); + } + + public function invoice_item_taxes() + { + return $this->document_item_taxes()->where('type', Document::INVOICE_TYPE); + } + + public function invoice_totals() + { + return $this->document_totals()->where('type', Document::INVOICE_TYPE); + } + + public function items() + { + return $this->hasMany('App\Models\Common\Item'); + } + + public function item_taxes() + { + return $this->hasMany('App\Models\Common\ItemTax'); + } + + public function modules() + { + return $this->hasMany('App\Models\Module\Module'); + } + + public function module_histories() + { + return $this->hasMany('App\Models\Module\ModuleHistory'); + } + + public function owner() + { + return $this->belongsTo(user_model_class(), 'created_by', 'id')->withDefault(['name' => trans('general.na')]); + } + + public function reconciliations() + { + return $this->hasMany('App\Models\Banking\Reconciliation'); + } + + public function recurring() + { + return $this->hasMany('App\Models\Common\Recurring'); + } + + public function reports() + { + return $this->hasMany('App\Models\Common\Report'); + } + + public function settings() + { + return $this->hasMany('App\Models\Setting\Setting'); + } + + public function taxes() + { + return $this->hasMany('App\Models\Setting\Tax'); + } + + public function transactions() + { + return $this->hasMany('App\Models\Banking\Transaction'); + } + + public function transaction_taxes() + { + return $this->hasMany('App\Models\Banking\TransactionTax'); + } + + public function transfers() + { + return $this->hasMany('App\Models\Banking\Transfer'); + } + + public function users() + { + return $this->belongsToMany(user_model_class(), 'App\Models\Auth\UserCompany'); + } + + public function vendors() + { + return $this->contacts()->whereIn('type', (array) $this->getVendorTypes()); + } + + public function widgets() + { + return $this->hasMany('App\Models\Common\Widget'); + } + + public function setCommonSettingsAsAttributes() + { + try { // TODO will optimize.. + $settings = $this->settings; + + $groups = [ + 'company', + 'default', + ]; + + foreach ($settings as $setting) { + list($group, $key) = explode('.', $setting->getAttribute('key')); + + // Load only general settings + if (! in_array($group, $groups)) { + continue; + } + + $value = $setting->getAttribute('value'); + + if (($key == 'logo') && empty($value)) { + $value = 'public/img/company.png'; + } + + $this->setAttribute($key, $value); + } + + // Set default default company logo if empty + if ($this->getAttribute('logo') == '') { + $this->setAttribute('logo', 'public/img/company.png'); + } + + // Set default default company currency if empty + if ($this->getAttribute('currency') == '') { + $this->setAttribute('currency', config('setting.fallback.default.currency')); + } + } catch(\Throwable $e) { + + } + } + + public function unsetCommonSettingsFromAttributes() + { + try { // TODO will optimize.. + $settings = $this->settings; + + $groups = [ + 'company', + 'default', + ]; + + foreach ($settings as $setting) { + list($group, $key) = explode('.', $setting->getAttribute('key')); + + // Load only general settings + if (! in_array($group, $groups)) { + continue; + } + + $this->offsetUnset($key); + } + + $this->offsetUnset('logo'); + $this->offsetUnset('currency'); + } catch(\Throwable $e) { + + } + } + + /** + * Scope to get all rows filtered, sorted and paginated. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param $sort + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeCollect($query, $sort = 'name') + { + $request = request(); + + $search = $request->get('search'); + + $query->usingSearchString($search)->sortable($sort); + + if ($request->expectsJson() && $request->isNotApi()) { + return $query->get(); + } + + $limit = (int) $request->get('limit', setting('default.list_limit', '25')); + + return $query->paginate($limit); + } + + /** + * Scope to only include companies of a given enabled value. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param mixed $value + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeEnabled($query, $value = 1) + { + return $query->where('enabled', $value); + } + + /** + * Scope to only include companies of a given user id. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param int $user_id + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeUserId($query, $user_id) + { + return $query->whereHas('users', function ($query) use ($user_id) { + $query->where('user_id', $user_id); + }); + } + + /** + * Sort by company name + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param $direction + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function nameSortable($query, $direction) + { + return $query->join('settings', 'companies.id', '=', 'settings.company_id') + ->where('key', 'company.name') + ->orderBy('value', $direction) + ->select('companies.*'); + } + + /** + * Sort by company email + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param $direction + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function emailSortable($query, $direction) + { + return $query->join('settings', 'companies.id', '=', 'settings.company_id') + ->where('key', 'company.email') + ->orderBy('value', $direction) + ->select('companies.*'); + } + + /** + * Sort by company phone + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param $direction + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function phoneSortable($query, $direction) + { + return $query->join('settings', 'companies.id', '=', 'settings.company_id') + ->where('key', 'company.phone') + ->orderBy('value', $direction) + ->select('companies.*'); + } + + /** + * Sort by company tax number + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param $direction + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function taxNumberSortable($query, $direction) + { + return $query->join('settings', 'companies.id', '=', 'settings.company_id') + ->where('key', 'company.tax_number') + ->orderBy('value', $direction) + ->select('companies.*'); + } + + /** + * Sort by company country + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param $direction + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function countrySortable($query, $direction) + { + return $query->join('settings', 'companies.id', '=', 'settings.company_id') + ->where('key', 'company.country') + ->orderBy('value', $direction) + ->select('companies.*'); + } + + /** + * Sort by company currency + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param $direction + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function currencySortable($query, $direction) + { + return $query->join('settings', 'companies.id', '=', 'settings.company_id') + ->where('key', 'default.currency') + ->orderBy('value', $direction) + ->select('companies.*'); + } + + /** + * Scope autocomplete. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param array $filter + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeAutocomplete($query, $filter) + { + return $query->join('settings', 'companies.id', '=', 'settings.company_id') + ->where(function ($query) use ($filter) { + foreach ($filter as $key => $value) { + $column = $key; + + if (!in_array($key, $this->fillable)) { + $column = 'company.' . $key; + $query->orWhere('key', $column); + $query->Where('value', 'LIKE', "%" . $value . "%"); + } else { + $query->orWhere($column, 'LIKE', "%" . $value . "%"); + } + } + }) + ->select('companies.*'); + } + + /** + * Get the current balance. + * + * @return string + */ + public function getCompanyLogoAttribute() + { + return $this->getMedia('company.logo')->last(); + } + + public function getLocationAttribute() + { + $country = setting('company.country'); + + if ($country && array_key_exists($country, trans('countries'))) { + $trans_country = trans('countries.' . $country); + } + + return $this->getFormattedAddress(setting('company.city'), $trans_country ?? null, setting('company.state'), setting('company.zip_code')); + } + + /** + * Get the line actions. + * + * @return array + */ + public function getLineActionsAttribute() + { + $actions = []; + + if ($this->enabled) { + $actions[] = [ + 'title' => trans('general.switch'), + 'icon' => 'settings_ethernet', + 'url' => route('companies.switch', $this->id), + //'permission' => 'read-common-companies', remove this permission to allow switching to any company + 'attributes' => [ + 'id' => 'index-line-actions-switch-company-' . $this->id, + ], + ]; + } + + $actions[] = [ + 'title' => trans('general.edit'), + 'icon' => 'edit', + 'url' => route('companies.edit', $this->id), + 'permission' => 'update-common-companies', + 'attributes' => [ + 'id' => 'index-line-actions-edit-company-' . $this->id, + ], + ]; + + $actions[] = [ + 'type' => 'delete', + 'icon' => 'delete', + 'route' => 'companies.destroy', + 'permission' => 'delete-common-companies', + 'attributes' => [ + 'id' => 'index-line-actions-delete-company-' . $this->id, + ], + 'model' => $this, + ]; + + return $actions; + } + + public function makeCurrent($force = false) + { + if (!$force && $this->isCurrent()) { + return $this; + } + + static::forgetCurrent(); + + event(new CompanyMakingCurrent($this)); + + // Bind to container + app()->instance(static::class, $this); + + // Load settings + setting()->setExtraColumns(['company_id' => $this->id]); + setting()->forgetAll(); + setting()->load(true); + + // Override settings, currencies, and category types + Overrider::load('settings'); + Overrider::load('currencies'); + Overrider::load('categoryTypes'); + + event(new CompanyMadeCurrent($this)); + + return $this; + } + + public function isCurrent() + { + return static::getCurrent()?->id === $this->id; + } + + public function isNotCurrent() + { + return !$this->isCurrent(); + } + + public static function getCurrent() + { + if (!app()->has(static::class)) { + return null; + } + + return app(static::class); + } + + public static function forgetCurrent() + { + $current = static::getCurrent(); + + if (is_null($current)) { + return null; + } + + event(new CompanyForgettingCurrent($current)); + + // Remove from container + app()->forgetInstance(static::class); + + // Remove settings + setting()->forgetAll(); + + event(new CompanyForgotCurrent($current)); + + return $current; + } + + public static function hasCurrent() + { + return static::getCurrent() !== null; + } + + public function scopeSource($query, $source) + { + return $query->where($this->qualifyColumn('created_from'), $source); + } + + public function scopeIsOwner($query) + { + return $query->where($this->qualifyColumn('created_by'), user_id()); + } + + public function scopeIsNotOwner($query) + { + return $query->where($this->qualifyColumn('created_by'), '<>', user_id()); + } + + public function ownerKey($owner) + { + if ($this->isNotOwnable()) { + return 0; + } + + return $this->created_by; + } + + /** + * Create a new factory instance for the model. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory + */ + protected static function newFactory() + { + return \Database\Factories\Company::new(); + } +} diff --git a/app/Models/Common/Contact.php b/app/Models/Common/Contact.php new file mode 100644 index 00000000..89eac86e --- /dev/null +++ b/app/Models/Common/Contact.php @@ -0,0 +1,423 @@ +hasMany('App\Models\Common\ContactPerson'); + } + + public function documents() + { + return $this->hasMany('App\Models\Document\Document'); + } + + public function document_recurring() + { + return $this->documents()->whereIn('documents.type', $this->getRecurringDocumentTypes()); + } + + public function bills() + { + return $this->documents()->where('documents.type', Document::BILL_TYPE); + } + + public function bill_recurring() + { + return $this->documents()->where('documents.type', Document::BILL_RECURRING_TYPE); + } + + public function invoices() + { + return $this->documents()->where('documents.type', Document::INVOICE_TYPE); + } + + public function invoice_recurring() + { + return $this->documents()->where('documents.type', Document::INVOICE_RECURRING_TYPE); + } + + public function currency() + { + return $this->belongsTo('App\Models\Setting\Currency', 'currency_code', 'code'); + } + + public function category() + { + return $this->belongsTo(Category::class)->withoutGlobalScope('App\Scopes\Category')->withDefault(['name' => trans('general.na')]); + } + + public function expense_transactions() + { + return $this->transactions()->whereIn('transactions.type', (array) $this->getExpenseTypes()); + } + + public function income_transactions() + { + return $this->transactions()->whereIn('transactions.type', (array) $this->getIncomeTypes()); + } + + public function transactions() + { + return $this->hasMany('App\Models\Banking\Transaction'); + } + + public function user() + { + return $this->belongsTo('App\Models\Auth\User', 'user_id', 'id'); + } + + public function withPersons() + { + $contacts = collect(); + + if (! empty($this->email)) { + $contacts->push($this); + } + + $contact_persons = $this->contact_persons()->whereNotNull('email')->get(); + + if ($contact_persons) { + foreach ($contact_persons as $contact_person) { + $contacts->push($contact_person); + } + } + + return $contacts; + } + + /** + * Scope to only include contacts of a given type. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param mixed $types + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeType($query, $types) + { + if (empty($types)) { + return $query; + } + + return $query->whereIn($this->qualifyColumn('type'), (array) $types); + } + + /** + * Scope to include only vendors. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeVendor($query) + { + return $query->whereIn($this->qualifyColumn('type'), (array) $this->getVendorTypes()); + } + + /** + * Scope to include only customers. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeCustomer($query) + { + return $query->whereIn($this->qualifyColumn('type'), (array) $this->getCustomerTypes()); + } + + /** + * Scope to include only employees. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeEmployee($query) + { + return $query->whereIn($this->qualifyColumn('type'), (array) $this->getEmployeeTypes()); + } + + public function scopeEmail($query, $email) + { + return $query->where('email', '=', $email); + } + + public function onCloning($src, $child = null) + { + $this->email = null; + $this->user_id = null; + } + + public function getInitialsAttribute($value) + { + return Str::getInitials($this->name); + } + + /** + * Get the current balance. + * + * @return string + */ + public function getLogoAttribute($value) + { + if (! empty($value) && ! $this->hasMedia('logo')) { + return $value; + } elseif (! $this->hasMedia('logo')) { + return false; + } + + return $this->getMedia('logo')->last(); + } + + public function getUnpaidAttribute() + { + $amount = 0; + + $collection = $this->isCustomer() ? 'invoices' : 'bills'; + + $this->$collection->whereIn('status', ['sent', 'received', 'viewed', 'partial'])->each(function ($item) use (&$amount) { + $amount += $this->convertToDefault($item->amount_due, $item->currency_code, $item->currency_rate); + }); + + return $amount; + } + + public function getOpenAttribute() + { + $amount = 0; + $today = Date::today()->toDateString(); + + $collection = $this->isCustomer() ? 'invoices' : 'bills'; + + $this->$collection->whereIn('status', ['sent', 'received', 'viewed', 'partial'])->where('due_at', '>=', $today)->each(function ($item) use (&$amount) { + $amount += $this->convertToDefault($item->amount_due, $item->currency_code, $item->currency_rate); + }); + + return $amount; + } + + public function getOverdueAttribute() + { + $amount = 0; + $today = Date::today()->toDateString(); + + $collection = $this->isCustomer() ? 'invoices' : 'bills'; + + $this->$collection->whereIn('status', ['sent', 'received', 'viewed', 'partial'])->where('due_at', '<', $today)->each(function ($item) use (&$amount) { + $amount += $this->convertToDefault($item->amount_due, $item->currency_code, $item->currency_rate); + }); + + return $amount; + } + + public function getHasEmailAttribute() + { + if (! empty($this->email)) { + return true; + } + + if (isset($this->contact_persons_with_email_count)) { + return $this->contact_persons_with_email_count > 0; + } + + if ($this->contact_persons()->whereNotNull('email')->count()) { + return true; + } + + return false; + } + + public function getLocationAttribute() + { + if ($this->country && array_key_exists($this->country, trans('countries'))) { + $country = trans('countries.' . $this->country); + } + + return $this->getFormattedAddress($this->city, $country ?? null, $this->state, $this->zip_code); + } + + /** + * Get the line actions. + * + * @return array + */ + public function getLineActionsAttribute() + { + $actions = []; + + $group = config('type.contact.' . $this->type . '.group'); + $prefix = config('type.contact.' . $this->type . '.route.prefix'); + $permission_prefix = config('type.contact.' . $this->type . '.permission.prefix'); + $translation_prefix = config('type.contact.' . $this->type . '.translation.prefix'); + + if (empty($prefix)) { + if (in_array($this->type, (array) $this->getCustomerTypes())) { + $prefix = config('type.contact.customer.route.prefix'); + } elseif (in_array($this->type, (array) $this->getVendorTypes())) { + $prefix = config('type.contact.vendor.route.prefix'); + } else { + return $actions; + } + } + + try { + $actions[] = [ + 'title' => trans('general.show'), + 'icon' => 'visibility', + 'url' => route($prefix . '.show', $this->id), + 'permission' => 'read-' . $group . '-' . $permission_prefix, + 'attributes' => [ + 'id' => 'index-line-actions-show-' . $this->type . '-' . $this->id, + ], + ]; + } catch (\Exception $e) {} + + try { + $actions[] = [ + 'title' => trans('general.edit'), + 'icon' => 'edit', + 'url' => route($prefix . '.edit', $this->id), + 'permission' => 'update-' . $group . '-' . $permission_prefix, + 'attributes' => [ + 'id' => 'index-line-actions-edit-' . $this->type . '-' . $this->id, + ], + ]; + } catch (\Exception $e) {} + + try { + $actions[] = [ + 'title' => trans('general.duplicate'), + 'icon' => 'file_copy', + 'url' => route($prefix . '.duplicate', $this->id), + 'permission' => 'create-' . $group . '-' . $permission_prefix, + 'attributes' => [ + 'id' => 'index-line-actions-duplicate-' . $this->type . '-' . $this->id, + ], + ]; + } catch (\Exception $e) {} + + try { + $delete_type = trans_choice('general.' . $translation_prefix, 1); + + $actions[] = [ + 'type' => 'delete', + 'icon' => 'delete', + 'title' => trans('general.title.delete', ['type' => $delete_type]), + 'route' => $prefix . '.destroy', + 'permission' => 'delete-' . $group . '-' . $permission_prefix, + 'attributes' => [ + 'id' => 'index-line-actions-delete-' . $this->type . '-' . $this->id, + ], + 'model' => $this, + ]; + } catch (\Exception $e) {} + + return $actions; + } + + /** + * Create a new factory instance for the model. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory + */ + protected static function newFactory() + { + return \Database\Factories\Contact::new(); + } +} diff --git a/app/Models/Common/ContactPerson.php b/app/Models/Common/ContactPerson.php new file mode 100644 index 00000000..674e9cfb --- /dev/null +++ b/app/Models/Common/ContactPerson.php @@ -0,0 +1,70 @@ +belongsTo('App\Models\Common\Contact')->withDefault(['name' => trans('general.na'), 'rate' => 0]); + } + + public function scopeType(Builder $query, array $types): Builder + { + if (empty($types)) { + return $query; + } + + return $query->whereIn($this->qualifyColumn('type'), (array) $types); + } + + public function scopeVendor(Builder $query): Builder + { + return $query->whereIn($this->qualifyColumn('type'), (array) $this->getVendorTypes()); + } + + public function scopeCustomer(Builder $query): Builder + { + return $query->whereIn($this->qualifyColumn('type'), (array) $this->getCustomerTypes()); + } + + public function scopeEmployee(Builder $query): Builder + { + return $query->whereIn($this->qualifyColumn('type'), (array) $this->getEmployeeTypes()); + } + + public function scopeEmail(Builder $query, $email): Builder + { + return $query->where('email', '=', $email); + } + + public function getInitialsAttribute($value) + { + return Str::getInitials($this->name); + } +} diff --git a/app/Models/Common/Dashboard.php b/app/Models/Common/Dashboard.php new file mode 100644 index 00000000..f234b505 --- /dev/null +++ b/app/Models/Common/Dashboard.php @@ -0,0 +1,162 @@ +belongsToMany(user_model_class(), 'App\Models\Auth\UserDashboard'); + } + + public function widgets() + { + return $this->hasMany('App\Models\Common\Widget')->orderBy('sort', 'asc'); + } + + /** + * Scope to only include dashboards of a given user id. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param int $user_id + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeUserId($query, $user_id) + { + return $query->whereHas('users', function ($query) use ($user_id) { + $query->where('user_id', $user_id); + }); + } + + /** + * Scope to only include dashboards of a given alias. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string $alias + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeAlias($query, $alias) + { + $class = ($alias == 'core') ? 'App\\\\' : 'Modules\\\\' . Str::studly($alias) . '\\\\'; + + return $query->whereHas('widgets', function ($query) use ($class) { + // Must have widgets of module + $query->where('class', 'like', $class . '%'); + })->whereDoesntHave('widgets', function ($query) use ($class) { + // Must not have widgets from other modules + $query->where('class', 'not like', $class . '%'); + }); + } + + /** + * Get the alias based on class. + * + * @return string + */ + public function getAliasAttribute() + { + $alias = ''; + + foreach ($this->widgets as $widget) { + if (Str::startsWith($widget->class, 'App\\')) { + $tmp_alias = 'core'; + } else { + $arr = explode('\\', $widget->class); + + $tmp_alias = Str::kebab($arr[1]); + } + + // First time set + if ($alias == '') { + $alias = $tmp_alias; + } + + // Must not have widgets from different modules + if ($alias != $tmp_alias) { + $alias = ''; + + break; + } + } + + return $alias; + } + + /** + * Get the line actions. + * + * @return array + */ + public function getLineActionsAttribute() + { + $actions = []; + + if ($this->enabled) { + $actions[] = [ + 'title' => trans('general.switch'), + 'icon' => 'settings_ethernet', + 'url' => route('dashboards.switch', $this->id), + 'permission' => 'read-common-dashboards', + 'attributes' => [ + 'id' => 'index-line-actions-switch-dashboard-' . $this->id, + ], + ]; + } + + $actions[] = [ + 'title' => trans('general.edit'), + 'icon' => 'edit', + 'url' => route('dashboards.edit', $this->id), + 'permission' => 'update-common-dashboards', + 'attributes' => [ + 'id' => 'index-line-actions-edit-dashboard-' . $this->id, + ], + ]; + + $actions[] = [ + 'type' => 'delete', + 'icon' => 'delete', + 'route' => 'dashboards.destroy', + 'permission' => 'delete-common-dashboards', + 'attributes' => [ + 'id' => 'index-line-actions-delete-dashboard-' . $this->id, + ], + 'model' => $this, + ]; + + return $actions; + } + + /** + * Create a new factory instance for the model. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory + */ + protected static function newFactory() + { + return \Database\Factories\Dashboard::new(); + } +} diff --git a/app/Models/Common/Item.php b/app/Models/Common/Item.php new file mode 100644 index 00000000..d2d332b3 --- /dev/null +++ b/app/Models/Common/Item.php @@ -0,0 +1,237 @@ + 'double', + 'purchase_price' => 'double', + 'enabled' => 'boolean', + 'deleted_at' => 'datetime', + ]; + + /** + * Sortable columns. + * + * @var array + */ + protected $sortable = ['name', 'category.name', 'description', 'sale_price', 'purchase_price', 'enabled']; + + /** + * @var array + */ + public $cloneable_relations = ['taxes']; + + public function category() + { + return $this->belongsTo('App\Models\Setting\Category')->withoutGlobalScope('App\Scopes\Category')->withDefault(['name' => trans('general.na')]); + } + + public function taxes() + { + return $this->hasMany('App\Models\Common\ItemTax'); + } + + public function document_items() + { + return $this->hasMany('App\Models\Document\DocumentItem'); + } + + public function bill_items() + { + return $this->document_items()->where('type', Document::BILL_TYPE); + } + + public function invoice_items() + { + return $this->document_items()->where('type', Document::INVOICE_TYPE); + } + + public function scopeName($query, $name) + { + return $query->where('name', '=', $name); + } + + public function scopeBilling($query, $billing) + { + return $query->where($billing . '_price', '=', null); + } + + public function scopePriceType($query, $price_type) + { + return $query->whereNotNull($price_type . '_price'); + } + + public function scopeType($query, $type) + { + if (empty($type)) { + return $query; + } + + return $query->where($this->qualifyColumn('type'), $type); + } + + /** + * Get the item id. + * + * @return string + */ + public function getItemIdAttribute() + { + return $this->id; + } + + /** + * Get the item id. + * + * @return string + */ + public function getTaxIdsAttribute() + { + return $this->taxes()->pluck('tax_id'); + } + + /** + * Scope autocomplete. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param array $filter + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeAutocomplete($query, $filter) + { + return $query->where(function ($query) use ($filter) { + foreach ($filter as $key => $value) { + $query->orWhere($key, 'LIKE', "%" . $value . "%"); + } + }); + } + + /** + * Sort by category name + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param $direction + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function categorySortable($query, $direction) + { + return $query->join('categories', 'categories.id', '=', 'items.category_id') + ->orderBy('name', $direction) + ->select('items.*'); + } + + public function getInitialsAttribute($value) + { + return Str::getInitials($this->name); + } + + /** + * Get the current balance. + * + * @return string + */ + public function getPictureAttribute($value) + { + if (!empty($value) && !$this->hasMedia('picture')) { + return $value; + } elseif (!$this->hasMedia('picture')) { + return false; + } + + return $this->getMedia('picture')->last(); + } + + /** + * Get the line actions. + * + * @return array + */ + public function getLineActionsAttribute() + { + $actions = []; + + $actions[] = [ + 'title' => trans('general.edit'), + 'icon' => 'edit', + 'url' => route('items.edit', $this->id), + 'permission' => 'update-common-items', + 'attributes' => [ + 'id' => 'index-line-actions-edit-item-' . $this->id, + ], + ]; + + $actions[] = [ + 'title' => trans('general.duplicate'), + 'icon' => 'file_copy', + 'url' => route('items.duplicate', $this->id), + 'permission' => 'create-common-items', + 'attributes' => [ + 'id' => 'index-line-actions-duplicate-item-' . $this->id, + ], + ]; + + $actions[] = [ + 'type' => 'delete', + 'icon' => 'delete', + 'route' => 'items.destroy', + 'permission' => 'delete-common-items', + 'attributes' => [ + 'id' => 'index-line-actions-delete-item-' . $this->id, + ], + 'model' => $this, + ]; + + return $actions; + } + + /** + * Create a new factory instance for the model. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory + */ + protected static function newFactory() + { + return \Database\Factories\Item::new(); + } +} diff --git a/app/Models/Common/ItemTax.php b/app/Models/Common/ItemTax.php new file mode 100644 index 00000000..0ee85ff5 --- /dev/null +++ b/app/Models/Common/ItemTax.php @@ -0,0 +1,30 @@ +belongsTo('App\Models\Common\Item')->withDefault(['name' => trans('general.na')]); + } + + public function tax() + { + return $this->belongsTo('App\Models\Setting\Tax')->withDefault(['name' => trans('general.na'), 'rate' => 0]); + } +} diff --git a/app/Models/Common/Media.php b/app/Models/Common/Media.php new file mode 100644 index 00000000..0cfcec4e --- /dev/null +++ b/app/Models/Common/Media.php @@ -0,0 +1,33 @@ + 'datetime', + ]; + + /** + * Calculate the file size in human readable byte notation. + * @param int $precision (_optional_) Number of decimal places to include. + * @return string + */ + public function readableSize(int $precision = 1): string + { + $size = intval($this->size); + + return File::readableSize($size, $precision); + } +} diff --git a/app/Models/Common/Notification.php b/app/Models/Common/Notification.php new file mode 100644 index 00000000..e9f43b7f --- /dev/null +++ b/app/Models/Common/Notification.php @@ -0,0 +1,20 @@ +subWeek()); + } +} diff --git a/app/Models/Common/Recurring.php b/app/Models/Common/Recurring.php new file mode 100644 index 00000000..8d8e2409 --- /dev/null +++ b/app/Models/Common/Recurring.php @@ -0,0 +1,187 @@ + 'boolean', + 'deleted_at' => 'datetime', + ]; + + /** + * Get all of the owning recurable models. + */ + public function recurable() + { + return $this->morphTo()->isRecurring(); + } + + public function documents() + { + return $this->morphedByMany( + 'App\Models\Document\Document', + 'recurable', + 'recurring', + 'recurable_id', + 'id' + ); + } + + public function invoices() + { + return $this->documents()->where('type', self::INVOICE_RECURRING_TYPE); + } + + public function bills() + { + return $this->documents()->where('type', self::BILL_RECURRING_TYPE); + } + + public function transactions() + { + return $this->morphedByMany( + 'App\Models\Banking\Transaction', + 'recurable', + 'recurring', + 'recurable_id', + 'id' + ); + } + + public function incomes() + { + return $this->transactions()->where('type', self::INCOME_RECURRING_TYPE); + } + + public function expenses() + { + return $this->transactions()->where('type', self::EXPENSE_RECURRING_TYPE); + } + + public function scopeActive(Builder $query): Builder + { + return $query->where($this->qualifyColumn('status'), '=', static::ACTIVE_STATUS); + } + + public function scopeEnded(Builder $query): Builder + { + return $query->where($this->qualifyColumn('status'), '=', static::END_STATUS); + } + + public function scopeCompleted(Builder $query): Builder + { + return $query->where($this->qualifyColumn('status'), '=', static::COMPLETE_STATUS); + } + + public function scopeDocument(Builder $query, $type): Builder + { + return $query->where($this->qualifyColumn('recurable_type'), '=', 'App\\Models\\Document\\Document') + ->whereHas('recurable', function (Builder $query) use ($type) { + $query->where('type', $type); + }); + } + + public function scopeInvoice(Builder $query): Builder + { + return $query->where($this->qualifyColumn('recurable_type'), '=', 'App\\Models\\Document\\Document') + ->whereHas('recurable', function (Builder $query) { + $query->where('type', self::INVOICE_RECURRING_TYPE); + }) + ->orWhereDoesntHave('recurable'); + } + + public function scopeBill(Builder $query): Builder + { + return $query->where($this->qualifyColumn('recurable_type'), '=', 'App\\Models\\Document\\Document') + ->whereHas('recurable', function (Builder $query) { + $query->where('type', self::BILL_RECURRING_TYPE); + }) + ->orWhereDoesntHave('recurable'); + } + + public function scopeTransaction(Builder $query): Builder + { + return $query->where($this->qualifyColumn('recurable_type'), '=', 'App\\Models\\Banking\\Transaction'); + } + + public function scopeExpenseTransaction(Builder $query): Builder + { + return $query->where($this->qualifyColumn('recurable_type'), '=', 'App\\Models\\Banking\\Transaction') + ->whereHas('recurable', function (Builder $query) { + $query->where('type', self::EXPENSE_RECURRING_TYPE); + }) + ->orWhereDoesntHave('recurable'); + } + + public function scopeIncomeTransaction(Builder $query): Builder + { + return $query->where($this->qualifyColumn('recurable_type'), '=', 'App\\Models\\Banking\\Transaction') + ->whereHas('recurable', function (Builder $query) { + $query->where('type', self::INCOME_RECURRING_TYPE); + }) + ->orWhereDoesntHave('recurable'); + } + + /** + * Determine if recurring is a document. + * + * @return bool + */ + public function isDocument() + { + return (bool) ($this->recurable_type == 'App\\Models\\Document\\Document'); + } + + /** + * Determine if recurring is a transaction. + * + * @return bool + */ + public function isTransaction() + { + return (bool) ($this->recurable_type == 'App\\Models\\Banking\\Transaction'); + } +} diff --git a/app/Models/Common/Report.php b/app/Models/Common/Report.php new file mode 100644 index 00000000..916651a7 --- /dev/null +++ b/app/Models/Common/Report.php @@ -0,0 +1,99 @@ + 'object', + 'deleted_at' => 'datetime', + ]; + + /** + * Scope to only include reports of a given alias. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string $alias + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeAlias($query, $alias) + { + $class = ($alias == 'core') ? 'App\\\\' : 'Modules\\\\' . Str::studly($alias) . '\\\\'; + + return $query->where('class', 'like', $class . '%'); + } + + /** + * Scope to only include reports of a given class. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string $class + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeClass($query, $class) + { + return $query->where('class', '=', $class); + } + + public function scopeExpenseSummary(Builder $query): Builder + { + return $query->where($this->qualifyColumn('class'), '=', 'App\\Reports\\ExpenseSummary'); + } + + public function scopeIncomeSummary(Builder $query): Builder + { + return $query->where($this->qualifyColumn('class'), '=', 'App\\Reports\\IncomeSummary'); + } + + public function scopeIncomeExpenseSummary(Builder $query): Builder + { + return $query->where($this->qualifyColumn('class'), '=', 'App\\Reports\\IncomeExpenseSummary'); + } + + public function scopeProfitLoss(Builder $query): Builder + { + return $query->where($this->qualifyColumn('class'), '=', 'App\\Reports\\ProfitLoss'); + } + + public function scopeTaxSummary(Builder $query): Builder + { + return $query->where($this->qualifyColumn('class'), '=', 'App\\Reports\\TaxSummary'); + } + + /** + * Get the alias based on class. + * + * @return string + */ + public function getAliasAttribute() + { + if (Str::startsWith($this->class, 'App\\')) { + return 'core'; + } + + $arr = explode('\\', $this->class); + + return Str::kebab($arr[1]); + } +} diff --git a/app/Models/Common/Widget.php b/app/Models/Common/Widget.php new file mode 100644 index 00000000..ce13b46e --- /dev/null +++ b/app/Models/Common/Widget.php @@ -0,0 +1,138 @@ + 'object', + 'deleted_at' => 'datetime', + ]; + + /** + * Scope to only include widgets of a given alias. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string $alias + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeAlias($query, $alias) + { + $class = ($alias == 'core') ? 'App\\\\' : 'Modules\\\\' . Str::studly($alias) . '\\\\'; + + return $query->where('class', 'like', $class . '%'); + } + + public function dashboard() + { + return $this->belongsTo('App\Models\Common\Dashboard'); + } + + public function users() + { + return $this->hasManyThrough(user_model_class(), 'App\Models\Common\Dashboard'); + } + + /** + * Get the alias based on class. + * + * @return string + */ + public function getAliasAttribute() + { + if (Str::startsWith($this->class, 'App\\')) { + return 'core'; + } + + $arr = explode('\\', $this->class); + + return Str::kebab($arr[1]); + } + + /** + * Get the alias based on class. + * + * @return object + */ + public function getSettingsAttribute($value) + { + $settings = ! empty($value) ? (object) json_decode($value) : (object) []; + + $settings->raw_width = false; + + if (isset($settings->width)) { + $raw_width = $settings->width; + $width = $this->getWidthAttribute($settings->width); + + if ($raw_width != $width) { + $settings->raw_width = $raw_width; + } + + $settings->width = $width; + } + + return $settings; + } + + /** + * Get the alias based on class. + * + * @return string + */ + public function getWidthAttribute($value) + { + $width = $value; + + switch ($width) { + case '25': + $width = 'w-full lg:w-1/4 lg:px-6'; + break; + case '33': + $width = 'w-full lg:w-1/3 px-6'; + break; + case '50': + $width = 'w-full lg:w-2/4 lg:px-6'; + break; + case '100': + $width = 'w-full px-6'; + break; + } + + if (empty($width)) { + $width = 'w-full lg:w-2/4 lg:px-6'; + } + + return $width; + } + + /** + * Create a new factory instance for the model. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory + */ + protected static function newFactory() + { + return \Database\Factories\Widget::new(); + } +} diff --git a/app/Models/Document/Document.php b/app/Models/Document/Document.php new file mode 100644 index 00000000..ecfd83b3 --- /dev/null +++ b/app/Models/Document/Document.php @@ -0,0 +1,794 @@ + 'datetime', + 'due_at' => 'datetime', + 'amount' => 'double', + 'currency_rate' => 'double', + 'deleted_at' => 'datetime', + ]; + + /** + * @var array + */ + public $sortable = [ + 'issued_at', + 'due_at', + 'status', + 'contact_name', + 'document_number', + 'amount', + 'recurring.started_at', + 'category.name', + 'recurring.status', + ]; + + /** + * @var array + */ + public $cloneable_relations = ['items', 'recurring', 'totals']; + + /** + * The "booted" method of the model. + * + * @return void + */ + protected static function booted() + { + static::addGlobalScope(new Scope); + } + + public function category() + { + return $this->belongsTo('App\Models\Setting\Category')->withoutGlobalScope('App\Scopes\Category')->withDefault(['name' => trans('general.na')]); + } + + public function children() + { + return $this->hasMany('App\Models\Document\Document', 'parent_id'); + } + + public function contact() + { + return $this->belongsTo('App\Models\Common\Contact')->withDefault(['name' => trans('general.na')]); + } + + public function currency() + { + return $this->belongsTo('App\Models\Setting\Currency', 'currency_code', 'code'); + } + + public function items() + { + return $this->hasMany('App\Models\Document\DocumentItem', 'document_id')->with('taxes'); + } + + public function item_taxes() + { + return $this->hasMany('App\Models\Document\DocumentItemTax', 'document_id'); + } + + public function histories() + { + return $this->hasMany('App\Models\Document\DocumentHistory', 'document_id'); + } + + public function last_history() + { + return $this->hasOne('App\Models\Document\DocumentHistory', 'document_id')->latest()->withDefault([ + 'description' => trans('messages.success.added', ['type' => $this->document_number]), + 'created_at' => $this->created_at + ]); + } + + public function parent() + { + return $this->belongsTo('App\Models\Document\Document', 'parent_id')->isRecurring(); + } + + public function payments() + { + return $this->transactions(); + } + + public function recurring() + { + return $this->morphOne('App\Models\Common\Recurring', 'recurable'); + } + + public function totals() + { + return $this->hasMany('App\Models\Document\DocumentTotal', 'document_id'); + } + + public function transactions() + { + return $this->hasMany('App\Models\Banking\Transaction', 'document_id'); + } + + public function totals_sorted() + { + return $this->totals()->orderBy('sort_order'); + } + + public function scopeLatest(Builder $query): Builder + { + return $query->orderBy('issued_at', 'desc'); + } + + public function scopeNumber(Builder $query, string $number): Builder + { + return $query->where('document_number', '=', $number); + } + + public function scopeDue(Builder $query, $date): Builder + { + return $query->whereDate('due_at', '=', $date); + } + + public function scopeStatus(Builder $query, string $status): Builder + { + return $query->where($this->qualifyColumn('status'), '=', $status); + } + + public function scopeAccrued(Builder $query): Builder + { + return $query->whereNotIn($this->qualifyColumn('status'), ['draft', 'cancelled']); + } + + public function scopePaid(Builder $query): Builder + { + return $query->where($this->qualifyColumn('status'), '=', 'paid'); + } + + public function scopeNotPaid(Builder $query): Builder + { + return $query->where($this->qualifyColumn('status'), '<>', 'paid'); + } + + public function scopeFuture(Builder $query): Builder + { + return $query->whereIn($this->qualifyColumn('status'), $this->getDocumentStatusesForFuture()); + } + + public function scopeType(Builder $query, string $type): Builder + { + return $query->where($this->qualifyColumn('type'), '=', $type); + } + + public function scopeInvoice(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', self::INVOICE_TYPE); + } + + public function scopeInvoiceRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', self::INVOICE_RECURRING_TYPE) + ->whereHas('recurring', function (Builder $query) { + $query->whereNull('deleted_at'); + }); + } + + public function scopeBill(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', self::BILL_TYPE); + } + + public function scopeBillRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', self::BILL_RECURRING_TYPE) + ->whereHas('recurring', function (Builder $query) { + $query->whereNull('deleted_at'); + }); + } + + /** + * @inheritDoc + * + * @param Document $src + * @param boolean $child + */ + public function onCloning($src, $child = null) + { + if (app()->has(\App\Console\Commands\RecurringCheck::class)) { + $type = $this->getRealTypeOfRecurringDocument($src->type); + } else { + $type = $src->type; + } + + $this->status = 'draft'; + $this->document_number = app(DocumentNumber::class)->getNextNumber($type, $src->contact); + } + + public function getSentAtAttribute(string $value = null) + { + if ($this->relationLoaded('histories')) { + $sent = $this->histories->where('status', 'sent')->first(); + } else { + $sent = $this->histories()->where('document_histories.status', 'sent')->first(); + } + + return $sent->created_at ?? null; + } + + public function getReceivedAtAttribute(string $value = null) + { + if ($this->relationLoaded('histories')) { + $received = $this->histories->where('status', 'received')->first(); + } else { + $received = $this->histories()->where('document_histories.status', 'received')->first(); + } + + return $received->created_at ?? null; + } + + /** + * Get the current balance. + * + * @return string + */ + public function getAttachmentAttribute($value = null) + { + $has_attachment = $this->hasMedia('attachment'); + + if (! empty($value) && ! $has_attachment) { + return $value; + } elseif (! $has_attachment) { + return false; + } + + return $this->getMedia('attachment')->all(); + } + + public function delete_attachment() + { + if ($attachments = $this->attachment) { + foreach ($attachments as $file) { + MediaModel::where('id', $file->id)->delete(); + } + } + } + + /** + * Get the discount percentage. + * + * @return string + */ + public function getDiscountRateAttribute($value) + { + return $value ?? 0; + } + + /** + * Get the discount percentage. + * + * @return string + */ + public function getDiscountTypeAttribute($value) + { + return $value ?? 'percentage'; + } + + /** + * Get the discount percentage. + * + * @return string + */ + public function getDiscountAttribute() + { + $percent = 0; + + $discount = $this->totals->where('code', 'discount')->makeHidden('title')->pluck('amount')->first(); + + if ($discount) { + $sub_total = $this->totals->where('code', 'sub_total')->makeHidden('title')->pluck('amount')->first(); + + if ($sub_total && $sub_total > 0) { + $percent = number_format((($discount * 100) / $sub_total), 0); + } else { + $percent = 0; + } + } + + return $percent; + } + + /** + * Get the paid amount. + * + * @return string + */ + public function getPaidAttribute() + { + if (empty($this->amount)) { + return 0; + } + + if ($this->status == 'paid' ) { + return $this->amount; + } + + $paid = 0; + + $code = $this->currency_code; + $rate = $this->currency_rate; + $precision = currency($code)->getPrecision(); + + // Lazy eager load transactions if not already loaded to prevent N+1 queries + if (!$this->relationLoaded('transactions')) { + $this->load('transactions'); + } + + if ($this->transactions->count()) { + foreach ($this->transactions as $transaction) { + $amount = $transaction->amount; + + if ($code != $transaction->currency_code) { + $amount = $this->convertBetween($amount, $transaction->currency_code, $transaction->currency_rate, $code, $rate); + } + + $paid += $amount; + } + } + + return round($paid, $precision); + } + + /** + * Get the reconcilation status. + * + * @return integer + */ + public function getReconciledAttribute() + { + if (empty($this->amount)) { + return 0; + } + + $reconciled = $reconciled_amount = 0; + + $code = $this->currency_code; + $rate = $this->currency_rate; + $precision = currency($code)->getPrecision(); + + // Lazy eager load transactions if not already loaded to prevent N+1 queries + if (!$this->relationLoaded('transactions')) { + $this->load('transactions'); + } + + if ($this->transactions->count()) { + foreach ($this->transactions as $transaction) { + $amount = $transaction->amount; + + if ($code != $transaction->currency_code) { + $amount = $this->convertBetween($amount, $transaction->currency_code, $transaction->currency_rate, $code, $rate); + } + + if ($transaction->reconciled) { + $reconciled_amount += $amount; + } + } + } + + if (bccomp(round($this->amount, $precision), round($reconciled_amount, $precision), $precision) === 0) { + $reconciled = 1; + } + + return $reconciled; + } + + /** + * Get the not paid amount. + * + * @return string + */ + public function getAmountDueAttribute() + { + $precision = currency($this->currency_code)->getPrecision(); + + return round($this->amount - $this->paid, $precision); + } + + /** + * Get the status label. + * + * @return string + */ + public function getStatusLabelAttribute() + { + return match($this->status) { + 'paid' => 'status-success', + 'partial' => 'status-partial', + 'sent' => 'status-danger', + 'received' => 'status-danger', + 'viewed' => 'status-sent', + 'cancelled' => 'status-canceled', + default => 'status-draft', + }; + } + + /** + * Get the recurring status label. + * + * @return string + */ + public function getRecurringStatusLabelAttribute() + { + return match($this->recurring->status) { + 'active' => 'status-partial', + 'ended' => 'status-success', + default => 'status-success', + }; + } + + /** + * Get the amount without tax. + * + * @return string + */ + public function getAmountWithoutTaxAttribute() + { + $amount = $this->amount; + + $this->totals->where('code', 'tax')->each(function ($total) use(&$amount) { + $tax = Tax::name($total->name)->first(); + + if (!empty($tax) && ($tax->type == 'withholding')) { + return; + } + + $amount -= $total->amount; + }); + + return $amount; + } + + public function getTemplatePathAttribute($value = null) + { + if ($value) { + // Allow safe view-path characters: alphanumeric, dots, underscores, colons (for module views like module::view.path), hyphens + if (! preg_match('/^[a-zA-Z0-9._:\-]+$/', $value)) { + $value = null; + } + } + + if (! $value) { + // Sanitize the template identifier from settings to alphanumeric + underscores only + $template = preg_replace('/[^a-zA-Z0-9_]/', '', (string) setting('invoice.template')) ?: 'default'; + $value = 'sales.invoices.print_' . $template; + } + + /* The above validation ensures that $value is a safe view path. We can now check if the view exists. + * If the view doesn't exist, we can fall back to the default template. + * + if (! empty($value) && view()->exists($value)) { + return $value; + } + + if (! empty($this->template) && view()->exists('sales.invoices.print_' . $this->template)) { + return 'sales.invoices.print_' . $this->template; + } + + return 'sales.invoices.print_' . setting('invoice.template'); + */ + + return $value; + } + + public function getContactLocationAttribute() + { + if ($this->contact_country && array_key_exists($this->contact_country, trans('countries'))) { + $country = trans('countries.' . $this->contact_country); + } + + return $this->getFormattedAddress($this->contact_city, $country ?? null, $this->contact_state, $this->contact_zip_code); + } + + /** + * Get the line actions. + * + * @return array + */ + public function getLineActionsAttribute() + { + $actions = []; + + $group = config('type.document.' . $this->type . '.group'); + $prefix = config('type.document.' . $this->type . '.route.prefix'); + $permission_prefix = config('type.document.' . $this->type . '.permission.prefix'); + $translation_prefix = config('type.document.' . $this->type . '.translation.prefix'); + + if (empty($prefix)) { + return $actions; + } + + if (app('mobile-detect')->isMobile()) { + try { + $actions[] = [ + 'title' => trans('general.show'), + 'icon' => 'visibility', + 'url' => route($prefix . '.show', $this->id), + 'permission' => 'read-' . $group . '-' . $permission_prefix, + 'attributes' => [ + 'id' => 'index-more-actions-show-' . $this->id, + ], + ]; + } catch (\Exception $e) {} + } + + try { + if (! $this->reconciled) { + $actions[] = [ + 'title' => trans('general.edit'), + 'icon' => 'edit', + 'url' => route($prefix . '.edit', $this->id), + 'permission' => 'update-' . $group . '-' . $permission_prefix, + 'attributes' => [ + 'id' => 'index-line-actions-edit-' . $this->type . '-' . $this->id, + ], + ]; + } + } catch (\Exception $e) {} + + try { + $actions[] = [ + 'title' => trans('general.duplicate'), + 'icon' => 'file_copy', + 'url' => route($prefix . '.duplicate', $this->id), + 'permission' => 'create-' . $group . '-' . $permission_prefix, + 'attributes' => [ + 'id' => 'index-line-actions-duplicate-' . $this->type . '-' . $this->id, + ], + ]; + } catch (\Exception $e) {} + + if ( + $this->status != 'paid' + && ! str_contains($this->type, 'recurring') + && (empty($this->transactions->count()) + || (! empty($this->transactions->count()) && $this->paid != $this->amount)) + ) { + try { + if ($this->totals->count()) { + $actions[] = [ + 'type' => 'button', + 'title' => trans('invoices.add_payment'), + 'icon' => 'paid', + 'url' => route('modals.documents.document.transactions.create', $this->id), + 'permission' => 'read-' . $group . '-' . $permission_prefix, + 'attributes' => [ + 'id' => 'index-line-actions-payment-' . $this->type . '-' . $this->id, + '@click' => 'onAddPayment("' . route('modals.documents.document.transactions.create', $this->id) . '")', + ], + ]; + } else { + $actions[] = [ + 'type' => 'button', + 'title' => trans('invoices.messages.totals_required', ['type' => $this->type]), + 'icon' => 'paid', + 'permission' => 'read-' . $group . '-' . $permission_prefix, + 'attributes' => [ + "disabled" => "disabled", + ], + ]; + } + } catch (\Exception $e) {} + } + + try { + $actions[] = [ + 'title' => trans('general.print'), + 'icon' => 'print', + 'url' => route($prefix . '.print', $this->id), + 'permission' => 'read-' . $group . '-' . $permission_prefix, + 'attributes' => [ + 'id' => 'index-line-actions-print-' . $this->type . '-' . $this->id, + 'target' => '_blank', + ], + ]; + } catch (\Exception $e) {} + + try { + $actions[] = [ + 'title' => trans('general.download_pdf'), + 'icon' => 'picture_as_pdf', + 'url' => route($prefix . '.pdf', $this->id), + 'permission' => 'read-' . $group . '-' . $permission_prefix, + 'attributes' => [ + 'id' => 'index-line-actions-pdf-' . $this->type . '-' . $this->id, + 'target' => '_blank', + ], + ]; + } catch (\Exception $e) {} + + if (! str_contains($this->type, 'recurring')) { + if ($this->status != 'cancelled') { + $actions[] = [ + 'type' => 'divider', + ]; + + try { + $actions[] = [ + 'type' => 'button', + 'title' => trans('general.share_link'), + 'icon' => 'share', + 'url' => route('modals.'. $prefix . '.share.create', $this->id), + 'permission' => 'read-' . $group . '-' . $permission_prefix, + 'attributes' => [ + 'id' => 'index-line-actions-share-link-' . $this->type . '-' . $this->id, + '@click' => 'onShareLink("' . route('modals.'. $prefix . '.share.create', $this->id) . '")', + ], + ]; + } catch (\Exception $e) {} + + try { + if (! empty($this->contact) && $this->contact->has_email && ($this->type == 'invoice')) { + $actions[] = [ + 'type' => 'button', + 'title' => trans('invoices.send_mail'), + 'icon' => 'email', + 'url' => route('modals.'. $prefix . '.emails.create', $this->id), + 'permission' => 'read-' . $group . '-' . $permission_prefix, + 'attributes' => [ + 'id' => 'index-line-actions-send-email-' . $this->type . '-' . $this->id, + '@click' => 'onSendEmail("' . route('modals.'. $prefix . '.emails.create', $this->id) . '")', + ], + ]; + } + } catch (\Exception $e) {} + } + + $actions[] = [ + 'type' => 'divider', + ]; + + if (! in_array($this->status, ['cancelled', 'draft'])) { + try { + $actions[] = [ + 'title' => trans('documents.actions.cancel'), + 'icon' => 'cancel', + 'url' => route($prefix . '.cancelled', $this->id), + 'permission' => 'update-' . $group . '-' . $permission_prefix, + 'attributes' => [ + 'id' => 'index-line-actions-cancel-' . $this->type . '-' . $this->id, + ], + ]; + } catch (\Exception $e) {} + + $actions[] = [ + 'type' => 'divider', + ]; + } + + try { + $actions[] = [ + 'type' => 'delete', + 'icon' => 'delete', + 'title' => $translation_prefix, + 'route' => $prefix . '.destroy', + 'permission' => 'delete-' . $group . '-' . $permission_prefix, + 'model-name' => 'document_number', + 'attributes' => [ + 'id' => 'index-line-actions-delete-' . $this->type . '-' . $this->id, + ], + 'model' => $this, + ]; + } catch (\Exception $e) {} + } else { + if ($this->recurring && $this->recurring->status != 'ended') { + try { + $actions[] = [ + 'title' => trans('general.end'), + 'icon' => 'block', + 'url' => route($prefix. '.end', $this->id), + 'permission' => 'update-' . $group . '-' . $permission_prefix, + 'attributes' => [ + 'id' => 'index-line-actions-end-' . $this->type . '-' . $this->id, + ], + ]; + } catch (\Exception $e) {} + } + } + + return $actions; + } + + /** + * Retrieve the model for a bound value. + * + * @param mixed $value + * @param string|null $field + * @return \Illuminate\Database\Eloquent\Model|null + */ + public function resolveRouteBinding($value, $field = null) + { + $query = $this->where('id', $value); + + if (request()->route()->hasParameter('recurring_invoice')) { + $query->invoiceRecurring(); + } + + if (request()->route()->hasParameter('recurring_bill')) { + $query->billRecurring(); + } + + return $query->firstOrFail(); + } + + protected static function newFactory(): Factory + { + return DocumentFactory::new(); + } +} diff --git a/app/Models/Document/DocumentHistory.php b/app/Models/Document/DocumentHistory.php new file mode 100644 index 00000000..fb7eedac --- /dev/null +++ b/app/Models/Document/DocumentHistory.php @@ -0,0 +1,57 @@ +belongsTo('App\Models\Document\Document')->withoutGlobalScope('App\Scopes\Document'); + } + + public function scopeType(Builder $query, string $type) + { + return $query->where($this->qualifyColumn('type'), '=', $type); + } + + public function scopeInvoice(Builder $query) + { + return $query->where($this->qualifyColumn('type'), '=', Document::INVOICE_TYPE); + } + + public function scopeInvoiceRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', Document::INVOICE_RECURRING_TYPE) + ->whereHas('document.recurring', function (Builder $query) { + $query->whereNull('deleted_at'); + }); + } + + public function scopeBill(Builder $query) + { + return $query->where($this->qualifyColumn('type'), '=', Document::BILL_TYPE); + } + + public function scopeBillRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', Document::BILL_RECURRING_TYPE) + ->whereHas('document.recurring', function (Builder $query) { + $query->whereNull('deleted_at'); + }); + } + + public function scopeStatus(Builder $query, string $status): Builder + { + return $query->where($this->qualifyColumn('status'), '=', $status); + } +} diff --git a/app/Models/Document/DocumentItem.php b/app/Models/Document/DocumentItem.php new file mode 100644 index 00000000..90a4d737 --- /dev/null +++ b/app/Models/Document/DocumentItem.php @@ -0,0 +1,157 @@ + 'double', + 'total' => 'double', + 'tax' => 'double', + 'discount_rate' => 'double', + 'deleted_at' => 'datetime', + ]; + + /** + * @var array + */ + public $cloneable_relations = ['taxes']; + + public static function boot() + { + parent::boot(); + + static::saving(fn ($model) => $model->offsetUnset('tax_ids')); + } + + public function document() + { + return $this->belongsTo('App\Models\Document\Document')->withoutGlobalScope('App\Scopes\Document'); + } + + public function item() + { + return $this->belongsTo('App\Models\Common\Item')->withDefault(['name' => trans('general.na')]); + } + + public function category() + { + return $this->belongsTo('App\Models\Setting\Category'); + } + + public function taxes() + { + return $this->hasMany('App\Models\Document\DocumentItemTax', 'document_item_id', 'id'); + } + + public function scopeType(Builder $query, string $type) + { + return $query->where($this->qualifyColumn('type'), '=', $type); + } + + public function scopeInvoice(Builder $query) + { + return $query->where($this->qualifyColumn('type'), '=', Document::INVOICE_TYPE); + } + + public function scopeInvoiceRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', Document::INVOICE_RECURRING_TYPE) + ->whereHas('document.recurring', function (Builder $query) { + $query->whereNull('deleted_at'); + }); + } + + public function scopeBill(Builder $query) + { + return $query->where($this->qualifyColumn('type'), '=', Document::BILL_TYPE); + } + + public function scopeBillRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', Document::BILL_RECURRING_TYPE) + ->whereHas('document.recurring', function (Builder $query) { + $query->whereNull('deleted_at'); + }); + } + + public function getDiscountAttribute(): string + { + if (setting('localisation.percent_position', 'after') === 'after') { + $text = ($this->discount_type === 'normal') ? $this->discount_rate . '%' : $this->discount_rate; + } else { + $text = ($this->discount_type === 'normal') ? '%' . $this->discount_rate : $this->discount_rate; + } + + return $text; + } + + public function getDiscountRateAttribute($value = 0) + { + $discount_rate = 0; + + switch (setting('localisation.discount_location', 'total')) { + case 'no': + case 'total': + $discount_rate = 0; + break; + case 'both': + case 'item': + $discount_rate = $value; + break; + } + + return $discount_rate; + } + + public function getTaxIdsAttribute(): array + { + return $this->taxes->pluck('tax_id')->all(); + } + + public function onCloning($src, $child = null) + { + unset($this->tax_ids); + } +} diff --git a/app/Models/Document/DocumentItemTax.php b/app/Models/Document/DocumentItemTax.php new file mode 100644 index 00000000..0b1ecd74 --- /dev/null +++ b/app/Models/Document/DocumentItemTax.php @@ -0,0 +1,73 @@ +belongsTo('App\Models\Document\Document')->withoutGlobalScope('App\Scopes\Document'); + } + + public function item() + { + return $this->belongsToThrough('App\Models\Common\Item', 'App\Models\Document\DocumentItem', 'document_item_id')->withDefault(['name' => trans('general.na')]); + } + + public function tax() + { + return $this->belongsTo('App\Models\Setting\Tax')->withDefault(['name' => trans('general.na'), 'rate' => 0]); + } + + public function scopeType(Builder $query, string $type) + { + return $query->where($this->qualifyColumn('type'), '=', $type); + } + + public function scopeInvoice(Builder $query) + { + return $query->where($this->qualifyColumn('type'), '=', Document::INVOICE_TYPE); + } + + public function scopeInvoiceRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', Document::INVOICE_RECURRING_TYPE) + ->whereHas('document.recurring', function (Builder $query) { + $query->whereNull('deleted_at'); + }); + } + + public function scopeBill(Builder $query) + { + return $query->where($this->qualifyColumn('type'), '=', Document::BILL_TYPE); + } + + public function scopeBillRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', Document::BILL_RECURRING_TYPE) + ->whereHas('document.recurring', function (Builder $query) { + $query->whereNull('deleted_at'); + }); + } + + public function onCloned($src) + { + $document_item = DocumentItem::find($this->document_item_id); + + $this->update(['document_id' => $document_item->document_id]); + } +} diff --git a/app/Models/Document/DocumentTotal.php b/app/Models/Document/DocumentTotal.php new file mode 100644 index 00000000..09592acd --- /dev/null +++ b/app/Models/Document/DocumentTotal.php @@ -0,0 +1,100 @@ +belongsTo('App\Models\Document\Document')->withoutGlobalScope('App\Scopes\Document'); + } + + public function scopeType(Builder $query, string $type) + { + return $query->where($this->qualifyColumn('type'), '=', $type); + } + + public function scopeInvoice(Builder $query) + { + return $query->where($this->qualifyColumn('type'), '=', Document::INVOICE_TYPE); + } + + public function scopeInvoiceRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', Document::INVOICE_RECURRING_TYPE) + ->whereHas('document.recurring', function (Builder $query) { + $query->whereNull('deleted_at'); + }); + } + + public function scopeBill(Builder $query) + { + return $query->where($this->qualifyColumn('type'), '=', Document::BILL_TYPE); + } + + public function scopeBillRecurring(Builder $query): Builder + { + return $query->where($this->qualifyColumn('type'), '=', Document::BILL_RECURRING_TYPE) + ->whereHas('document.recurring', function (Builder $query) { + $query->whereNull('deleted_at'); + }); + } + + public function scopeCode($query, $code) + { + return $query->where('code', '=', $code); + } + + public function getTitleAttribute() + { + $title = $this->name; + + $percent = 0; + + $tax = null; + + switch ($this->code) { + case 'discount': + $title = trans($title); + $percent = ($this->document->discount_rate && $this->document->discount_type == 'percentage') ? $this->document->discount_rate : 0; + + break; + case 'tax': + $tax = Tax::where('name', $title)->first(); + + if (! empty($tax->rate)) { + $percent = $tax->rate; + } + + break; + } + + if (! empty($percent)) { + $title .= ' ('; + + if (setting('localisation.percent_position', 'after') === 'after') { + $title .= ($this->code === 'discount') ? $percent . '%' : (($tax->type === 'fixed') ? $percent : $percent . '%'); + } else { + $title .= ($this->code === 'discount') ? '%' . $percent : (($tax->type === 'fixed') ? $percent : '%' . $percent); + } + + $title .= ')'; + } + + return $title; + } +} diff --git a/app/Models/Module/Module.php b/app/Models/Module/Module.php new file mode 100644 index 00000000..17ff14b7 --- /dev/null +++ b/app/Models/Module/Module.php @@ -0,0 +1,29 @@ +where('alias', $alias); + } +} diff --git a/app/Models/Module/ModuleHistory.php b/app/Models/Module/ModuleHistory.php new file mode 100644 index 00000000..9cefc835 --- /dev/null +++ b/app/Models/Module/ModuleHistory.php @@ -0,0 +1,22 @@ +belongsTo('App\Models\Module\Module'); + } +} diff --git a/app/Models/Setting/Category.php b/app/Models/Setting/Category.php new file mode 100644 index 00000000..c0640ebe --- /dev/null +++ b/app/Models/Setting/Category.php @@ -0,0 +1,452 @@ +resolveRouteBindingQuery($this, $value, $field) + ->withoutGlobalScope(Scope::class) + ->getWithoutChildren() + ->first(); + } + + public function categories() + { + return $this->hasMany(Category::class, 'parent_id')->withSubCategory(); + } + + public function sub_categories() + { + return $this->hasMany(Category::class, 'parent_id')->withSubCategory()->with('categories')->orderBy('name'); + } + + public function documents() + { + return $this->hasMany('App\Models\Document\Document'); + } + + public function bills() + { + return $this->documents()->where('documents.type', Document::BILL_TYPE); + } + + public function expense_transactions() + { + return $this->transactions()->whereIn('transactions.type', (array) $this->getExpenseTypes()); + } + + public function income_transactions() + { + return $this->transactions()->whereIn('transactions.type', (array) $this->getIncomeTypes()); + } + + public function invoices() + { + return $this->documents()->where('documents.type', Document::INVOICE_TYPE); + } + + public function items() + { + return $this->hasMany('App\Models\Common\Item'); + } + + public function transactions() + { + return $this->hasMany('App\Models\Banking\Transaction'); + } + + /** + * Scope code. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param $code + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeCode($query, $code) + { + return $query->where('code', $code); + } + + /** + * Scope to only include categories of a given type. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param mixed $types + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeType($query, $types) + { + if (empty($types)) { + return $query; + } + + return $query->whereIn($this->qualifyColumn('type'), (array) $types); + } + + /** + * Scope to include only income. + * Uses Categories trait to support multiple income types (e.g. from modules). + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeIncome($query) + { + return $query->whereIn($this->qualifyColumn('type'), $this->getIncomeCategoryTypes()); + } + + /** + * Scope to include only expense. + * Uses Categories trait to support multiple expense types (e.g. from modules). + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeExpense($query) + { + return $query->whereIn($this->qualifyColumn('type'), $this->getExpenseCategoryTypes()); + } + + /** + * Scope to include only direct cost. + * Uses Categories trait to support multiple direct cost types (e.g. from modules). + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeDirectCost($query) + { + return $query->whereIn($this->qualifyColumn('type'), $this->getDirectCostCategoryTypes()); + } + + /** + * Scope to include only item. + * Uses Categories trait to support multiple item types (e.g. from modules). + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeItem($query) + { + return $query->whereIn($this->qualifyColumn('type'), $this->getItemCategoryTypes()); + } + + /** + * Scope to include only other. + * Uses Categories trait to support multiple other types (e.g. from modules). + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeOther($query) + { + return $query->whereIn($this->qualifyColumn('type'), $this->getOtherCategoryTypes()); + } + + public function scopeName($query, $name) + { + return $query->where('name', '=', $name); + } + + /** + * Scope gets only parent categories. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeWithSubCategory($query) + { + return $query->withoutGlobalScope(new Scope); + } + + /** + * Scope gets only parent categories. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeIsNotSubCategory($query) + { + return $query->whereNull('parent_id'); + } + + /** + * Scope to export the rows of the current page filtered and sorted. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param $ids + * @param $sort + * @param $id_field + * + * @return \Illuminate\Support\LazyCollection + */ + public function scopeCollectForExport($query, $ids = [], $sort = 'name', $id_field = 'id') + { + $request = request(); + + if (!empty($ids)) { + $query->whereIn($id_field, (array) $ids); + } + + $search = $request->get('search'); + + $query->withSubCategory(); + + $query->usingSearchString($search)->sortable($sort); + + $page = (int) $request->get('page'); + $limit = (int) $request->get('limit', setting('default.list_limit', '25')); + $offset = $page ? ($page - 1) * $limit : 0; + + if (! $this instanceof WithParentSheet && count((array) $ids) < $limit) { + $query->offset($offset)->limit($limit); + } + + return $query->cursor(); + } + + /** + * Get the hex code of the color. + */ + public function getColorHexCodeAttribute(): string + { + $color = $this->color ?? 'green-500'; + + return $this->getHexCodeOfTailwindClass($color); + } + + /** + * Get the display name of the category. + */ + public function getDisplayNameAttribute(): string + { + $hideCode = $this->type ? $this->hideCodeCategoryType($this->type) : true; + $typeNames = $this->getCategoryTypes(); + + $typeName = $this->type ? ($typeNames[$this->type] ?? ucfirst($this->type)) : ''; + + $prefix = (!$hideCode && $this->code) ? $this->code . ' - ' : ''; + + return $prefix . $this->name . ' (' . $typeName . ')'; + } + + /** + * Get the balance of a category. + * + * @return double + */ + public function getBalanceAttribute() + { + // If view composer has set the balance, return it directly + if (isset($this->de_balance)) { + return $this->de_balance; + } + + $financial_year = $this->getFinancialYear(); + + $start_date = $financial_year->getStartDate(); + $end_date = $financial_year->getEndDate(); + + $this->transactions->whereBetween('paid_at', [$start_date, $end_date]) + ->each(function ($transaction) use (&$incomes, &$expenses) { + if (($transaction->isNotIncome() && $transaction->isNotExpense()) || $transaction->isTransferTransaction()) { + return; + } + + if ($transaction->isIncome()) { + $incomes += $transaction->getAmountConvertedToDefault(); + } else { + $expenses += $transaction->getAmountConvertedToDefault(); + } + }); + + $balance = $incomes - $expenses; + + $this->sub_categories() + ->each(function ($sub_category) use (&$balance) { + $balance += $sub_category->balance; + }); + + return $balance; + } + + /** + * Get the balance of a category without considering sub categories. + * + * @return double + */ + public function getBalanceWithoutSubcategoriesAttribute() + { + // If view composer has set the balance, return it directly + if (isset($this->without_subcategory_de_balance)) { + return $this->without_subcategory_de_balance; + } + + $financial_year = $this->getFinancialYear(); + + $start_date = $financial_year->getStartDate(); + $end_date = $financial_year->getEndDate(); + + $this->transactions->whereBetween('paid_at', [$start_date, $end_date]) + ->each(function ($transaction) use (&$incomes, &$expenses) { + if (($transaction->isNotIncome() && $transaction->isNotExpense()) || $transaction->isTransferTransaction()) { + return; + } + + if ($transaction->isIncome()) { + $incomes += $transaction->getAmountConvertedToDefault(); + } else { + $expenses += $transaction->getAmountConvertedToDefault(); + } + }); + + $balance = $incomes - $expenses; + + return $balance; + } + + /** + * Get the line actions. + * + * @return array + */ + public function getLineActionsAttribute() + { + $actions = []; + + $actions[] = [ + 'title' => trans('general.edit'), + 'icon' => 'create', + 'url' => route('categories.edit', $this->id), + 'permission' => 'update-settings-categories', + 'attributes' => [ + 'id' => 'index-line-actions-edit-category-' . $this->id, + ], + ]; + + if ($this->isTransferCategory()) { + return $actions; + } + + $actions[] = [ + 'type' => 'delete', + 'icon' => 'delete', + 'route' => 'categories.destroy', + 'permission' => 'delete-settings-categories', + 'attributes' => [ + 'id' => 'index-line-actions-delete-category-' . $this->id, + ], + 'model' => $this, + ]; + + return $actions; + } + + /** + * A no-op callback that gets fired when a model is cloning but before it gets + * committed to the database + * + * @param Illuminate\Database\Eloquent\Model $src + * @param boolean $child + * @return void + */ + public function onCloning($src, $child = null) + { + $this->code = $this->getNextCategoryCode(); + } + + /** + * Create a new factory instance for the model. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory + */ + protected static function newFactory() + { + return \Database\Factories\Category::new(); + } +} diff --git a/app/Models/Setting/Currency.php b/app/Models/Setting/Currency.php new file mode 100644 index 00000000..04fb33f7 --- /dev/null +++ b/app/Models/Setting/Currency.php @@ -0,0 +1,239 @@ + 'double', + 'enabled' => 'boolean', + 'deleted_at' => 'datetime', + ]; + + /** + * Sortable columns. + * + * @var array + */ + public $sortable = ['name', 'code', 'rate', 'enabled']; + + public function accounts() + { + return $this->hasMany('App\Models\Banking\Account', 'currency_code', 'code'); + } + + public function documents() + { + return $this->hasMany('App\Models\Document\Document', 'currency_code', 'code'); + } + + public function bills() + { + return $this->documents()->where('documents.type', Document::BILL_TYPE); + } + + public function contacts() + { + return $this->hasMany('App\Models\Common\Contact', 'currency_code', 'code'); + } + + public function customers() + { + return $this->contacts()->whereIn('contacts.type', (array) $this->getCustomerTypes()); + } + + public function expense_transactions() + { + return $this->transactions()->whereIn('transactions.type', (array) $this->getExpenseTypes()); + } + + public function income_transactions() + { + return $this->transactions()->whereIn('transactions.type', (array) $this->getIncomeTypes()); + } + + public function invoices() + { + return $this->documents()->where('documents.type', Document::INVOICE_TYPE); + } + + public function transactions() + { + return $this->hasMany('App\Models\Banking\Transaction', 'currency_code', 'code'); + } + + public function vendors() + { + return $this->contacts()->whereIn('contacts.type', (array) $this->getVendorTypes()); + } + + /** + * Get the default currency. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function default() + { + return $this->code(default_currency())->first(); + } + + /** + * Scope currency by code. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param mixed $code + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeCode($query, $code) + { + return $query->where($this->qualifyColumn('code'), $code); + } + + /** + * Get the current precision. + * + * @return string + */ + public function getPrecisionAttribute($value) + { + if (is_null($value)) { + return currency($this->code)->getPrecision(); + } + + return (int) $value; + } + + /** + * Get the current symbol. + * + * @return string + */ + public function getSymbolAttribute($value) + { + if (is_null($value)) { + return currency($this->code)->getSymbol(); + } + + return $value; + } + + /** + * Get the current symbol_first. + * + * @return string + */ + public function getSymbolFirstAttribute($value) + { + if (is_null($value)) { + return currency($this->code)->isSymbolFirst(); + } + + return $value; + } + + /** + * Get the current decimal_mark. + * + * @return string + */ + public function getDecimalMarkAttribute($value) + { + if (is_null($value)) { + return currency($this->code)->getDecimalMark(); + } + + return $value; + } + + /** + * Get the current thousands_separator. + * + * @return string + */ + public function getThousandsSeparatorAttribute($value) + { + if (is_null($value)) { + return currency($this->code)->getThousandsSeparator(); + } + + return $value; + } + + /** + * Get the line actions. + * + * @return array + */ + public function getLineActionsAttribute() + { + $actions = []; + + $actions[] = [ + 'title' => trans('general.edit'), + 'icon' => 'edit', + 'url' => route('currencies.edit', $this->id), + 'permission' => 'update-settings-currencies', + 'attributes' => [ + 'id' => 'index-line-actions-edit-currency-' . $this->id, + ], + ]; + + $actions[] = [ + 'type' => 'delete', + 'icon' => 'delete', + 'route' => 'currencies.destroy', + 'permission' => 'delete-settings-currencies', + 'attributes' => [ + 'id' => 'index-line-actions-delete-currency-' . $this->id, + ], + 'model' => $this, + ]; + + return $actions; + } + + /** + * Create a new factory instance for the model. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory + */ + protected static function newFactory() + { + return \Database\Factories\Currency::new(); + } +} diff --git a/app/Models/Setting/EmailTemplate.php b/app/Models/Setting/EmailTemplate.php new file mode 100644 index 00000000..cdaa7d24 --- /dev/null +++ b/app/Models/Setting/EmailTemplate.php @@ -0,0 +1,114 @@ +attributes['body'] = static::sanitizeBody($value); + } + + public static function sanitizeBody($value): string + { + $body = (string) ($value ?? ''); + + if ($body === '') { + return ''; + } + + try { + return trim(static::getBodyPurifier()->purify($body)); + } catch (\Throwable $e) { + return trim(strip_tags($body, '