From 1adf627c0db06176740af93ea3ffcb0b6f6beafe Mon Sep 17 00:00:00 2001 From: 15061959991 Date: Tue, 21 Jul 2026 18:43:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B4=A2=E5=8A=A1=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E4=BA=8C=E6=AC=A1=E5=BC=80=E5=8F=91=20-=20=E8=90=A5=E9=94=80?= =?UTF-8?q?=E8=B4=A6=E6=88=B7=E8=B4=A6=E5=8D=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 基础环境:Docker 部署配置,源码挂载实时同步 - 修复:super_admin 权限补全、登录重定向循环 - 开发:店铺营销账户账单 - 抖音账户(15字段汇总账单) - 开发:店铺营销账户账单 - 淘系支付宝账户(8字段流水账单) - 开发:店铺营销账户账单 - 拼多多账户(7字段流水账单) - 业务页面导航集成:config/business_navigation.php + Business\Pages 控制器 - 各平台独立表结构和视图,互不影响 --- .dockerignore | 16 + .editorconfig | 18 + .gitattributes | 27 + .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 | 106 + .htaccess | 49 + AGENTS.md | 72 + LICENSE.txt | 78 + LOCAL_DEPLOY.md | 119 + 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 + .../Banking/MarketingAccountBills.php | 313 + .../Banking/MarketingPinduoduoBills.php | 226 + .../Banking/MarketingTaobaoAlipayBills.php | 232 + .../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 | 165 + 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 + .../Requests/Banking/MarketingAccountBill.php | 35 + .../Banking/MarketingPinduoduoBill.php | 26 + .../Banking/MarketingTaobaoAlipayBill.php | 27 + 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/MarketingAccountBill.php | 56 + app/Models/Banking/MarketingPinduoduoBill.php | 30 + .../Banking/MarketingTaobaoAlipayBill.php | 32 + 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 + ...0_create_marketing_account_bills_table.php | 45 + ...te_marketing_taobao_alipay_bills_table.php | 37 + ...create_marketing_pinduoduo_bills_table.php | 35 + 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/Dockerfile.local | 100 + deploy/apache-vhost.conf | 16 + deploy/entrypoint.sh | 30 + deploy/host-nginx.conf | 19 + deploy/local-init.sh | 60 + deploy/php.ini | 10 + docker-compose.local.yml | 76 + docs/ecommerce-finance-development.md | 320 + docs/team-collaboration.md | 77 + index.php | 38 + manifest.json | 125 + modules/.gitkeep | 0 ...000000_create_ecommerce_finance_tables.php | 231 + .../Http/Controllers/Api/Channels.php | 50 + .../Http/Controllers/Api/ImportBatches.php | 58 + .../Http/Controllers/Api/ImportFiles.php | 30 + .../Api/ReconciliationExceptions.php | 19 + .../Http/Controllers/Api/Stores.php | 54 + .../Http/Requests/Channel.php | 31 + .../Http/Requests/ImportBatch.php | 24 + .../Http/Requests/ImportFile.php | 20 + .../EcommerceFinance/Http/Requests/Store.php | 35 + .../Jobs/MatchSettlementLines.php | 73 + .../Jobs/ProcessImportBatch.php | 295 + modules/EcommerceFinance/Models/Channel.php | 26 + .../EcommerceFinance/Models/ImportBatch.php | 40 + modules/EcommerceFinance/Models/Order.php | 55 + .../EcommerceFinance/Models/OrderEvent.php | 32 + modules/EcommerceFinance/Models/OrderItem.php | 38 + .../Models/ReconciliationMatch.php | 38 + .../EcommerceFinance/Models/Settlement.php | 54 + .../Models/SettlementLine.php | 40 + .../EcommerceFinance/Models/SkuMapping.php | 33 + modules/EcommerceFinance/Models/Store.php | 51 + modules/EcommerceFinance/Providers/Event.php | 18 + modules/EcommerceFinance/Providers/Main.php | 25 + .../Resources/lang/en-GB/general.php | 6 + .../Resources/lang/zh-CN/general.php | 6 + .../Resources/views/index.blade.php | 11 + modules/EcommerceFinance/Routes/admin.php | 7 + modules/EcommerceFinance/Routes/api.php | 11 + .../Tests/Feature/ChannelsTest.php | 140 + modules/EcommerceFinance/module.json | 18 + .../.github/workflows/tests.yml | 50 + .../.github/workflows/translations.yml | 37 + modules/OfflinePayments/.gitignore | 7 + .../Http/Controllers/Payment.php | 89 + .../Http/Controllers/Settings.php | 120 + .../OfflinePayments/Http/Requests/Setting.php | 30 + .../Http/Requests/SettingDelete.php | 30 + .../Http/Requests/SettingGet.php | 30 + .../OfflinePayments/Http/Requests/Show.php | 30 + .../Jobs/CreatePaymentMethod.php | 58 + .../Jobs/DeletePaymentMethod.php | 85 + .../Jobs/UpdatePaymentMethod.php | 59 + .../Listeners/AddLandingPage.php | 19 + .../Listeners/FinishInstallation.php | 36 + .../Listeners/ShowAsPaymentMethod.php | 27 + .../Listeners/ShowInSettingsMenu.php | 31 + .../Listeners/Update/Version300.php | 34 + modules/OfflinePayments/Providers/Event.php | 30 + modules/OfflinePayments/Providers/Main.php | 81 + modules/OfflinePayments/README.md | 11 + .../Resources/assets/js/offline-payments.js | 120 + .../assets/js/offline-payments.min.js | 72 + .../Resources/lang/ar-SA/general.php | 21 + .../Resources/lang/ca-ES/general.php | 21 + .../Resources/lang/da-DK/general.php | 21 + .../Resources/lang/de-DE/general.php | 21 + .../Resources/lang/en-GB/general.php | 22 + .../Resources/lang/es-ES/general.php | 21 + .../Resources/lang/fa-IR/general.php | 21 + .../Resources/lang/fr-FR/general.php | 21 + .../Resources/lang/id-ID/general.php | 21 + .../Resources/lang/it-IT/general.php | 21 + .../Resources/lang/ja-JP/general.php | 21 + .../Resources/lang/nb-NO/general.php | 21 + .../Resources/lang/pt-BR/general.php | 21 + .../Resources/lang/sq-AL/general.php | 21 + .../Resources/lang/uk-UA/general.php | 21 + .../Resources/views/confirm.blade.php | 7 + .../Resources/views/edit.blade.php | 124 + .../Resources/views/show.blade.php | 36 + modules/OfflinePayments/Routes/admin.php | 18 + modules/OfflinePayments/Routes/portal.php | 14 + modules/OfflinePayments/Routes/signed.php | 14 + .../Tests/Feature/PaymentsTest.php | 33 + .../Tests/Feature/SettingsTest.php | 102 + modules/OfflinePayments/composer.json | 21 + modules/OfflinePayments/crowdin.yml | 14 + modules/OfflinePayments/mix-manifest.json | 3 + modules/OfflinePayments/module.json | 20 + modules/OfflinePayments/package-lock.json | 15954 +++++ modules/OfflinePayments/package.json | 21 + modules/OfflinePayments/webpack.mix.js | 22 + .../.github/workflows/releases.yml | 292 + .../.github/workflows/tests.yml | 55 + .../.github/workflows/translations.yml | 37 + modules/PaypalStandard/.gitignore | 7 + .../Http/Controllers/Payment.php | 172 + .../Listeners/AddLandingPage.php | 19 + .../Listeners/ShowAsPaymentMethod.php | 23 + modules/PaypalStandard/Providers/Event.php | 30 + modules/PaypalStandard/Providers/Main.php | 81 + modules/PaypalStandard/README.md | 11 + .../Resources/lang/ar-SA/general.php | 20 + .../Resources/lang/ca-ES/general.php | 20 + .../Resources/lang/da-DK/general.php | 20 + .../Resources/lang/de-DE/general.php | 20 + .../Resources/lang/en-GB/general.php | 26 + .../Resources/lang/fa-IR/general.php | 20 + .../Resources/lang/fr-FR/general.php | 25 + .../Resources/lang/id-ID/general.php | 20 + .../Resources/lang/it-IT/general.php | 20 + .../Resources/lang/ja-JP/general.php | 20 + .../Resources/lang/nb-NO/general.php | 20 + .../Resources/lang/pt-BR/general.php | 20 + .../Resources/lang/sq-AL/general.php | 20 + .../Resources/lang/uk-UA/general.php | 20 + .../Resources/views/show.blade.php | 53 + modules/PaypalStandard/Routes/guest.php | 17 + modules/PaypalStandard/Routes/portal.php | 14 + modules/PaypalStandard/Routes/signed.php | 17 + .../Tests/Feature/SettingsTest.php | 39 + modules/PaypalStandard/composer.json | 21 + modules/PaypalStandard/crowdin.yml | 14 + modules/PaypalStandard/module.json | 108 + 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 + .../lang/zh-CN/marketing_account_bills.php | 56 + .../lang/zh-CN/marketing_pinduoduo_bills.php | 31 + .../zh-CN/marketing_taobao_alipay_bills.php | 36 + 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 + .../marketing_account_bills/form.blade.php | 188 + .../marketing_account_bills/index.blade.php | 186 + .../partials/embedded-table.blade.php | 161 + .../store-index.blade.php | 50 + .../marketing_pinduoduo_bills/form.blade.php | 113 + .../marketing_pinduoduo_bills/index.blade.php | 9 + .../partials/embedded-table.blade.php | 143 + .../form.blade.php | 126 + .../index.blade.php | 9 + .../partials/embedded-table.blade.php | 153 + .../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 | 116 + .../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 | 353 + 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 + test-data/README.md | 18 + test-data/database.sqlite | Bin 0 -> 765952 bytes test-data/imports/jd-orders-20260718.csv | 2 + test-data/imports/jd-settlement-20260718.csv | 2 + 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 + 4350 files changed, 418868 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 LOCAL_DEPLOY.md 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/MarketingAccountBills.php create mode 100644 app/Http/Controllers/Banking/MarketingPinduoduoBills.php create mode 100644 app/Http/Controllers/Banking/MarketingTaobaoAlipayBills.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/MarketingAccountBill.php create mode 100644 app/Http/Requests/Banking/MarketingPinduoduoBill.php create mode 100644 app/Http/Requests/Banking/MarketingTaobaoAlipayBill.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/MarketingAccountBill.php create mode 100644 app/Models/Banking/MarketingPinduoduoBill.php create mode 100644 app/Models/Banking/MarketingTaobaoAlipayBill.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/migrations/2026_07_21_000000_create_marketing_account_bills_table.php create mode 100644 database/migrations/2026_07_21_100000_create_marketing_taobao_alipay_bills_table.php create mode 100644 database/migrations/2026_07_21_110000_create_marketing_pinduoduo_bills_table.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/Dockerfile.local create mode 100644 deploy/apache-vhost.conf create mode 100644 deploy/entrypoint.sh create mode 100644 deploy/host-nginx.conf create mode 100644 deploy/local-init.sh create mode 100644 deploy/php.ini create mode 100644 docker-compose.local.yml 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 modules/EcommerceFinance/Database/Migrations/2026_07_18_000000_create_ecommerce_finance_tables.php create mode 100644 modules/EcommerceFinance/Http/Controllers/Api/Channels.php create mode 100644 modules/EcommerceFinance/Http/Controllers/Api/ImportBatches.php create mode 100644 modules/EcommerceFinance/Http/Controllers/Api/ImportFiles.php create mode 100644 modules/EcommerceFinance/Http/Controllers/Api/ReconciliationExceptions.php create mode 100644 modules/EcommerceFinance/Http/Controllers/Api/Stores.php create mode 100644 modules/EcommerceFinance/Http/Requests/Channel.php create mode 100644 modules/EcommerceFinance/Http/Requests/ImportBatch.php create mode 100644 modules/EcommerceFinance/Http/Requests/ImportFile.php create mode 100644 modules/EcommerceFinance/Http/Requests/Store.php create mode 100644 modules/EcommerceFinance/Jobs/MatchSettlementLines.php create mode 100644 modules/EcommerceFinance/Jobs/ProcessImportBatch.php create mode 100644 modules/EcommerceFinance/Models/Channel.php create mode 100644 modules/EcommerceFinance/Models/ImportBatch.php create mode 100644 modules/EcommerceFinance/Models/Order.php create mode 100644 modules/EcommerceFinance/Models/OrderEvent.php create mode 100644 modules/EcommerceFinance/Models/OrderItem.php create mode 100644 modules/EcommerceFinance/Models/ReconciliationMatch.php create mode 100644 modules/EcommerceFinance/Models/Settlement.php create mode 100644 modules/EcommerceFinance/Models/SettlementLine.php create mode 100644 modules/EcommerceFinance/Models/SkuMapping.php create mode 100644 modules/EcommerceFinance/Models/Store.php create mode 100644 modules/EcommerceFinance/Providers/Event.php create mode 100644 modules/EcommerceFinance/Providers/Main.php create mode 100644 modules/EcommerceFinance/Resources/lang/en-GB/general.php create mode 100644 modules/EcommerceFinance/Resources/lang/zh-CN/general.php create mode 100644 modules/EcommerceFinance/Resources/views/index.blade.php create mode 100644 modules/EcommerceFinance/Routes/admin.php create mode 100644 modules/EcommerceFinance/Routes/api.php create mode 100644 modules/EcommerceFinance/Tests/Feature/ChannelsTest.php create mode 100644 modules/EcommerceFinance/module.json create mode 100644 modules/OfflinePayments/.github/workflows/tests.yml create mode 100644 modules/OfflinePayments/.github/workflows/translations.yml create mode 100644 modules/OfflinePayments/.gitignore create mode 100644 modules/OfflinePayments/Http/Controllers/Payment.php create mode 100644 modules/OfflinePayments/Http/Controllers/Settings.php create mode 100644 modules/OfflinePayments/Http/Requests/Setting.php create mode 100644 modules/OfflinePayments/Http/Requests/SettingDelete.php create mode 100644 modules/OfflinePayments/Http/Requests/SettingGet.php create mode 100644 modules/OfflinePayments/Http/Requests/Show.php create mode 100644 modules/OfflinePayments/Jobs/CreatePaymentMethod.php create mode 100644 modules/OfflinePayments/Jobs/DeletePaymentMethod.php create mode 100644 modules/OfflinePayments/Jobs/UpdatePaymentMethod.php create mode 100644 modules/OfflinePayments/Listeners/AddLandingPage.php create mode 100644 modules/OfflinePayments/Listeners/FinishInstallation.php create mode 100644 modules/OfflinePayments/Listeners/ShowAsPaymentMethod.php create mode 100644 modules/OfflinePayments/Listeners/ShowInSettingsMenu.php create mode 100644 modules/OfflinePayments/Listeners/Update/Version300.php create mode 100644 modules/OfflinePayments/Providers/Event.php create mode 100644 modules/OfflinePayments/Providers/Main.php create mode 100644 modules/OfflinePayments/README.md create mode 100644 modules/OfflinePayments/Resources/assets/js/offline-payments.js create mode 100644 modules/OfflinePayments/Resources/assets/js/offline-payments.min.js create mode 100644 modules/OfflinePayments/Resources/lang/ar-SA/general.php create mode 100644 modules/OfflinePayments/Resources/lang/ca-ES/general.php create mode 100644 modules/OfflinePayments/Resources/lang/da-DK/general.php create mode 100644 modules/OfflinePayments/Resources/lang/de-DE/general.php create mode 100644 modules/OfflinePayments/Resources/lang/en-GB/general.php create mode 100644 modules/OfflinePayments/Resources/lang/es-ES/general.php create mode 100644 modules/OfflinePayments/Resources/lang/fa-IR/general.php create mode 100644 modules/OfflinePayments/Resources/lang/fr-FR/general.php create mode 100644 modules/OfflinePayments/Resources/lang/id-ID/general.php create mode 100644 modules/OfflinePayments/Resources/lang/it-IT/general.php create mode 100644 modules/OfflinePayments/Resources/lang/ja-JP/general.php create mode 100644 modules/OfflinePayments/Resources/lang/nb-NO/general.php create mode 100644 modules/OfflinePayments/Resources/lang/pt-BR/general.php create mode 100644 modules/OfflinePayments/Resources/lang/sq-AL/general.php create mode 100644 modules/OfflinePayments/Resources/lang/uk-UA/general.php create mode 100644 modules/OfflinePayments/Resources/views/confirm.blade.php create mode 100644 modules/OfflinePayments/Resources/views/edit.blade.php create mode 100644 modules/OfflinePayments/Resources/views/show.blade.php create mode 100644 modules/OfflinePayments/Routes/admin.php create mode 100644 modules/OfflinePayments/Routes/portal.php create mode 100644 modules/OfflinePayments/Routes/signed.php create mode 100644 modules/OfflinePayments/Tests/Feature/PaymentsTest.php create mode 100644 modules/OfflinePayments/Tests/Feature/SettingsTest.php create mode 100644 modules/OfflinePayments/composer.json create mode 100644 modules/OfflinePayments/crowdin.yml create mode 100644 modules/OfflinePayments/mix-manifest.json create mode 100644 modules/OfflinePayments/module.json create mode 100644 modules/OfflinePayments/package-lock.json create mode 100644 modules/OfflinePayments/package.json create mode 100644 modules/OfflinePayments/webpack.mix.js create mode 100644 modules/PaypalStandard/.github/workflows/releases.yml create mode 100644 modules/PaypalStandard/.github/workflows/tests.yml create mode 100644 modules/PaypalStandard/.github/workflows/translations.yml create mode 100644 modules/PaypalStandard/.gitignore create mode 100644 modules/PaypalStandard/Http/Controllers/Payment.php create mode 100644 modules/PaypalStandard/Listeners/AddLandingPage.php create mode 100644 modules/PaypalStandard/Listeners/ShowAsPaymentMethod.php create mode 100644 modules/PaypalStandard/Providers/Event.php create mode 100644 modules/PaypalStandard/Providers/Main.php create mode 100644 modules/PaypalStandard/README.md create mode 100644 modules/PaypalStandard/Resources/lang/ar-SA/general.php create mode 100644 modules/PaypalStandard/Resources/lang/ca-ES/general.php create mode 100644 modules/PaypalStandard/Resources/lang/da-DK/general.php create mode 100644 modules/PaypalStandard/Resources/lang/de-DE/general.php create mode 100644 modules/PaypalStandard/Resources/lang/en-GB/general.php create mode 100644 modules/PaypalStandard/Resources/lang/fa-IR/general.php create mode 100644 modules/PaypalStandard/Resources/lang/fr-FR/general.php create mode 100644 modules/PaypalStandard/Resources/lang/id-ID/general.php create mode 100644 modules/PaypalStandard/Resources/lang/it-IT/general.php create mode 100644 modules/PaypalStandard/Resources/lang/ja-JP/general.php create mode 100644 modules/PaypalStandard/Resources/lang/nb-NO/general.php create mode 100644 modules/PaypalStandard/Resources/lang/pt-BR/general.php create mode 100644 modules/PaypalStandard/Resources/lang/sq-AL/general.php create mode 100644 modules/PaypalStandard/Resources/lang/uk-UA/general.php create mode 100644 modules/PaypalStandard/Resources/views/show.blade.php create mode 100644 modules/PaypalStandard/Routes/guest.php create mode 100644 modules/PaypalStandard/Routes/portal.php create mode 100644 modules/PaypalStandard/Routes/signed.php create mode 100644 modules/PaypalStandard/Tests/Feature/SettingsTest.php create mode 100644 modules/PaypalStandard/composer.json create mode 100644 modules/PaypalStandard/crowdin.yml create mode 100644 modules/PaypalStandard/module.json 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/marketing_account_bills.php create mode 100644 resources/lang/zh-CN/marketing_pinduoduo_bills.php create mode 100644 resources/lang/zh-CN/marketing_taobao_alipay_bills.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/marketing_account_bills/form.blade.php create mode 100644 resources/views/banking/marketing_account_bills/index.blade.php create mode 100644 resources/views/banking/marketing_account_bills/partials/embedded-table.blade.php create mode 100644 resources/views/banking/marketing_account_bills/store-index.blade.php create mode 100644 resources/views/banking/marketing_pinduoduo_bills/form.blade.php create mode 100644 resources/views/banking/marketing_pinduoduo_bills/index.blade.php create mode 100644 resources/views/banking/marketing_pinduoduo_bills/partials/embedded-table.blade.php create mode 100644 resources/views/banking/marketing_taobao_alipay_bills/form.blade.php create mode 100644 resources/views/banking/marketing_taobao_alipay_bills/index.blade.php create mode 100644 resources/views/banking/marketing_taobao_alipay_bills/partials/embedded-table.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 test-data/README.md create mode 100644 test-data/database.sqlite create mode 100644 test-data/imports/jd-orders-20260718.csv create mode 100644 test-data/imports/jd-settlement-20260718.csv 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..440136f5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,27 @@ +# 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 +*.sqlite 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..b2e2fde5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,106 @@ +### 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 +!modules/EcommerceFinance/ +!modules/EcommerceFinance/** +!modules/OfflinePayments/ +!modules/OfflinePayments/** +!modules/PaypalStandard/ +!modules/PaypalStandard/** +.laravelstatsrc +.DS_Store +.workbuddy/ +runtime/.env 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/LOCAL_DEPLOY.md b/LOCAL_DEPLOY.md new file mode 100644 index 00000000..29abd260 --- /dev/null +++ b/LOCAL_DEPLOY.md @@ -0,0 +1,119 @@ +# CaiWuXiTong 本地部署说明 + +## 项目概况 +- 基于 [Akaunting](https://akaunting.com) 二次开发的"江陵"版财务系统 +- 技术栈:PHP 8.3 + Laravel + Vue + Apache + MySQL 8.0 +- 访问端口:**18088** + +## 文件清单(本地部署新增) +| 文件 | 作用 | +|------|------| +| `docker-compose.local.yml` | 本地 compose:app + mysql,bridge 网络 | +| `deploy/Dockerfile.local` | 本地 Dockerfile:基于原 Dockerfile,增加 composer/npm 依赖安装 | +| `deploy/local-init.sh` | 一键构建+启动+初始化脚本 | +| `runtime/.env` | 应用环境配置(挂载到容器 `/var/www/html/.env`) | +| `LOCAL_DEPLOY.md` | 本说明 | + +> 原 `compose.jiangling.yml` / `deploy/Dockerfile` 未改动,保留团队主线一致性。 + +## 一键部署 +```bash +cd CaiWuXiTong_src +bash deploy/local-init.sh +``` + +## 手动步骤 +```bash +# 1. 启动 +docker compose -f docker-compose.local.yml up -d --build + +# 2. 初始化数据库与管理员 +docker compose -f docker-compose.local.yml exec app \ + php artisan install --no-interaction \ + --db-host=mysql --db-name=akaunting \ + --db-username=akaunting --db-password=akaunting123 \ + --admin-email=admin@company.com --admin-password=admin123 \ + --locale=zh-CN + +# 3. 访问 +open http://127.0.0.1:18088 +``` + +## 登录信息 +- **地址**:http://127.0.0.1:18088 +- **邮箱**:`admin@company.com` +- **密码**:`admin123` + +## 常用维护命令 +```bash +# 查看日志 +docker compose -f docker-compose.local.yml logs -f app + +# 重启 +docker compose -f docker-compose.local.yml restart app + +# 进入容器 +docker compose -f docker-compose.local.yml exec app bash + +# 进入 MySQL +docker compose -f docker-compose.local.yml exec mysql mysql -uakaunting -pakaunting123 akaunting + +# 清缓存 +docker compose -f docker-compose.local.yml exec app php artisan optimize:clear + +# 重新迁移 +docker compose -f docker-compose.local.yml exec app php artisan migrate --force + +# 停止 / 启动 / 销毁 +docker compose -f docker-compose.local.yml stop +docker compose -f docker-compose.local.yml start +docker compose -f docker-compose.local.yml down # 仅停容器 +docker compose -f docker-compose.local.yml down -v # 同时删除数据卷(会清空数据库!) +``` + +## 端口映射 +| 服务 | 宿主机端口 | 容器端口 | +|------|-----------|----------| +| Akaunting (Apache) | 18088 | 18088 | +| MySQL | 13306 | 3306 | + +## 数据卷 +- `jiangling_storage` —— 应用上传文件、日志、缓存 +- `jiangling_mysql` —— MySQL 数据文件 + +## 注意事项 +- 修改 `.env` 后需重启 app 容器:`docker compose -f docker-compose.local.yml restart app` +- 修改源码后需重新构建:`docker compose -f docker-compose.local.yml up -d --build app` +- 此本地配置仅供开发/测试,**不要用于生产**(密码弱、APP_DEBUG=true) + +## 重要:DB_PREFIX 必须用短前缀 +项目某个模块迁移创建了较长的唯一索引 +(`ec_stores_company_id_channel_id_external_id_deleted_at_unique`,60 字符), +MySQL 标识符上限 64 字符,所以 `DB_PREFIX` 必须 ≤ 4 字符。本部署用 `a_`。 + +⚠️ **不要**让 `php artisan install` 自动生成前缀(它默认用 `str_random(3) . '_'` 是 4 字符, +刚好不超),但**首次 install 必须显式传 `--db-prefix=a_`** 或其他 ≤4 字符值, +否则可能随机到 5+ 字符前缀触发 1059 错误。 + +## 重置数据(清空重来) +```bash +docker compose -f docker-compose.local.yml exec mysql \ + mysql -uroot -prootpass -e "DROP DATABASE IF EXISTS akaunting; CREATE DATABASE akaunting CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" + +docker compose -f docker-compose.local.yml exec app php artisan install --no-interaction \ + --db-host=mysql --db-name=akaunting \ + --db-username=akaunting --db-password=akaunting123 \ + --db-prefix=a_ \ + --company-name="本地测试公司" --company-email=admin@company.com \ + --admin-email=admin@company.com --admin-password=admin123 \ + --locale=zh-CN +``` + +## 部署中遇到并已解决的问题(备忘) +1. **Docker Hub 国内访问**:daemon.json 配置 `https://docker.1ms.run` 镜像源 +2. **containerd snapshotter 导致 binary 全 0 字节**:在 Docker Desktop 设置里关闭 `UseContainerdSnapshotter`,改用 overlay2 +3. **apt 交互卡住**:Dockerfile 加 `DEBIAN_FRONTEND=noninteractive` + `/etc/apt/apt.conf.d/99force-conf` +4. **deb.debian.org 不稳**:Dockerfile 切到清华 Debian 镜像 +5. **node-sass 需要 python/make/g++**:Dockerfile 补装 `python3 make g++` +6. **npm run prod OOM**:改用 `npm run dev` + `NODE_OPTIONS=--max-old-space-size=2048` +7. **ec_stores 索引名超长**:用 `--db-prefix=a_`(见上) 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/MarketingAccountBills.php b/app/Http/Controllers/Banking/MarketingAccountBills.php new file mode 100644 index 00000000..0fe39409 --- /dev/null +++ b/app/Http/Controllers/Banking/MarketingAccountBills.php @@ -0,0 +1,313 @@ + 'platform_douyin', + 'taobao-alipay' => 'platform_taobao-alipay', + 'taobao-wechat' => 'platform_taobao-wechat', + 'pinduoduo' => 'platform_pinduoduo', + 'jd-wallet' => 'platform_jd-wallet', + 'dewu' => 'platform_dewu', + 'vipshop' => 'platform_vipshop', + ]; + + public function __construct() + { + $this->middleware('permission:read-banking-accounts')->only('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 storeIndex() + { + return view('banking.marketing_account_bills.store-index', [ + 'platforms' => self::PLATFORMS, + ]); + } + + /** + * 单平台账单列表 + */ + public function platformIndex(string $platform) + { + return $this->index($platform); + } + + /** + * 创建表单 + */ + public function create(string $platform) + { + $platform = $this->resolvePlatform($platform); + + return view('banking.marketing_account_bills.form', [ + 'platform' => $platform, + 'account' => new MarketingAccountBill([ + 'company_id' => company_id(), + 'bill_date' => now()->toDateString(), + ]), + ]); + } + + /** + * 下载导入模板 + */ + public function template(string $platform) + { + $this->resolvePlatform($platform); + + return response()->streamDownload(function () { + $csv = fopen('php://output', 'w'); + fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF)); // BOM for Excel + + fputcsv($csv, [ + '店铺名称', '账单日期', '总存入金额', '总转入金额', '总转出金额', + '非赠款消耗金额', '赠款消耗金额', '消返红包消耗金额', '立减红包消耗金额', + '共享钱包消耗金额', '共享赠款消耗金额', '当日资金总金额', '期末总余额', + '赠款余额', '非赠款余额', '备注', + ]); + + fclose($csv); + }, "marketing_account_bills_{$platform}_template.csv", [ + 'Content-Type' => 'text/csv; charset=UTF-8', + ]); + } + + /** + * 批量导出 + */ + public function export(HttpRequest $request, string $platform) + { + $this->resolvePlatform($platform); + + $ids = $request->validate([ + 'ids' => ['nullable', 'array', 'max:5000'], + 'ids.*' => ['integer'], + ])['ids'] ?? []; + + $query = MarketingAccountBill::platform($platform)->oldest('id'); + + if (!empty($ids)) { + $query->whereKey($ids); + } + + $bills = $query->get(); + + return response()->streamDownload(function () use ($bills) { + $csv = fopen('php://output', 'w'); + fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF)); + + fputcsv($csv, [ + '店铺名称', '账单日期', '总存入金额', '总转入金额', '总转出金额', + '非赠款消耗金额', '赠款消耗金额', '消返红包消耗金额', '立减红包消耗金额', + '共享钱包消耗金额', '共享赠款消耗金额', '当日资金总金额', '期末总余额', + '赠款余额', '非赠款余额', '备注', + ]); + + foreach ($bills as $bill) { + fputcsv($csv, [ + $bill->store_name, + $bill->bill_date->toDateString(), + $bill->total_deposit, + $bill->total_transfer_in, + $bill->total_transfer_out, + $bill->non_grant_consumption, + $bill->grant_consumption, + $bill->rebate_redpack_consumption, + $bill->discount_redpack_consumption, + $bill->shared_wallet_consumption, + $bill->shared_grant_consumption, + $bill->daily_total_amount, + $bill->period_end_balance, + $bill->grant_balance, + $bill->non_grant_balance, + $bill->notes, + ]); + } + + fclose($csv); + }, 'marketing_account_bills_export.csv', [ + 'Content-Type' => 'text/csv; charset=UTF-8', + ]); + } + + /** + * 批量导入 + */ + public function import(HttpRequest $request, string $platform): RedirectResponse + { + $this->resolvePlatform($platform); + + $validated = $request->validate([ + 'import' => ['required', 'file', 'mimes:xlsx,xls,csv', 'max:5120'], + ], [ + 'import.required' => trans('marketing_account_bills.import_required'), + 'import.mimes' => trans('marketing_account_bills.import_file_type'), + 'import.max' => trans('marketing_account_bills.import_file_size'), + ]); + + $file = $validated['import']; + $handle = fopen($file->getRealPath(), 'r'); + + // Skip BOM + $bom = fread($handle, 3); + if ($bom !== chr(0xEF) . chr(0xBB) . chr(0xBF)) { + rewind($handle); + } + + // Skip header + fgetcsv($handle); + + $count = 0; + $companyId = company_id(); + $userId = user()->id; + + while (($row = fgetcsv($handle)) !== false) { + if (empty($row[0]) && empty($row[1])) { + continue; + } + + MarketingAccountBill::create([ + 'company_id' => $companyId, + 'platform' => $platform, + 'store_name' => $row[0] ?? '', + 'bill_date' => $row[1] ?? now()->toDateString(), + 'total_deposit' => (float) ($row[2] ?? 0), + 'total_transfer_in' => (float) ($row[3] ?? 0), + 'total_transfer_out' => (float) ($row[4] ?? 0), + 'non_grant_consumption' => (float) ($row[5] ?? 0), + 'grant_consumption' => (float) ($row[6] ?? 0), + 'rebate_redpack_consumption' => (float) ($row[7] ?? 0), + 'discount_redpack_consumption' => (float) ($row[8] ?? 0), + 'shared_wallet_consumption' => (float) ($row[9] ?? 0), + 'shared_grant_consumption' => (float) ($row[10] ?? 0), + 'daily_total_amount' => (float) ($row[11] ?? 0), + 'period_end_balance' => (float) ($row[12] ?? 0), + 'grant_balance' => (float) ($row[13] ?? 0), + 'non_grant_balance' => (float) ($row[14] ?? 0), + 'notes' => $row[15] ?? null, + 'created_from' => 'core::ui', + 'created_by' => $userId, + ]); + + $count++; + } + + fclose($handle); + + flash(trans('marketing_account_bills.import_success', ['count' => $count]))->success(); + + return redirect()->route('marketing-account-bills.platform.index', $platform); + } + + /** + * 存储新账单 + */ + public function store(Request $request, string $platform): RedirectResponse + { + $this->resolvePlatform($platform); + $data = $request->validated(); + + MarketingAccountBill::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->platformTitle($platform)]))->success(); + + return redirect()->route('marketing-account-bills.platform.index', $platform); + } + + /** + * 编辑表单 + */ + public function edit(string $platform, MarketingAccountBill $marketingAccountBill) + { + abort_unless($marketingAccountBill->platform === $this->resolvePlatform($platform), 404); + + return view('banking.marketing_account_bills.form', [ + 'platform' => $marketingAccountBill->platform, + 'account' => $marketingAccountBill, + ]); + } + + /** + * 更新账单 + */ + public function update(Request $request, string $platform, MarketingAccountBill $marketingAccountBill): RedirectResponse + { + abort_unless($marketingAccountBill->platform === $this->resolvePlatform($platform), 404); + + $marketingAccountBill->update($request->validated()); + + flash(trans('messages.success.updated', ['type' => $this->platformTitle($platform)]))->success(); + + return redirect()->route('marketing-account-bills.platform.index', $platform); + } + + /** + * 删除账单 + */ + public function destroy(string $platform, MarketingAccountBill $marketingAccountBill): RedirectResponse + { + abort_unless($marketingAccountBill->platform === $this->resolvePlatform($platform), 404); + + $marketingAccountBill->delete(); + + flash(trans('messages.success.deleted', ['type' => $this->platformTitle($platform)]))->success(); + + return redirect()->route('marketing-account-bills.platform.index', $platform); + } + + /** + * 列表页 + */ + private function index(string $platform) + { + $platform = $this->resolvePlatform($platform); + + return view('banking.marketing_account_bills.index', [ + 'platform' => $platform, + 'accounts' => MarketingAccountBill::platform($platform) + ->orderBy('bill_date', 'desc') + ->orderBy('id', 'desc') + ->paginate(setting('default.list_limit', 25)), + ]); + } + + /** + * 校验并返回有效平台标识 + */ + private function resolvePlatform(string $platform): string + { + abort_unless(array_key_exists($platform, self::PLATFORMS), 404); + + return $platform; + } + + /** + * 获取平台中文标题 + */ + private function platformTitle(string $platform): string + { + return trans('marketing_account_bills.' . $platform); + } +} diff --git a/app/Http/Controllers/Banking/MarketingPinduoduoBills.php b/app/Http/Controllers/Banking/MarketingPinduoduoBills.php new file mode 100644 index 00000000..1aa1c8e8 --- /dev/null +++ b/app/Http/Controllers/Banking/MarketingPinduoduoBills.php @@ -0,0 +1,226 @@ +middleware('permission:read-banking-accounts')->only('index'); + $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 index() + { + return view('banking.marketing_pinduoduo_bills.index', [ + 'bills' => MarketingPinduoduoBill::orderBy('transaction_time', 'desc') + ->orderBy('id', 'desc') + ->paginate(setting('default.list_limit', 25)), + ]); + } + + /** + * 创建表单 + */ + public function create() + { + return view('banking.marketing_pinduoduo_bills.form', [ + 'bill' => new MarketingPinduoduoBill([ + 'company_id' => company_id(), + 'transaction_time' => now(), + ]), + ]); + } + + /** + * 下载导入模板 + */ + public function template() + { + return response()->streamDownload(function () { + $csv = fopen('php://output', 'w'); + fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF)); + + fputcsv($csv, [ + '店铺名称', '时间', '资金类型', '流水类型', + '交易金额', '余额', '交易摘要', + ]); + + fclose($csv); + }, 'marketing_pinduoduo_bills_template.csv', [ + 'Content-Type' => 'text/csv; charset=UTF-8', + ]); + } + + /** + * 批量导出 + */ + public function export(HttpRequest $request) + { + $ids = $request->validate([ + 'ids' => ['nullable', 'array', 'max:5000'], + 'ids.*' => ['integer'], + ])['ids'] ?? []; + + $query = MarketingPinduoduoBill::oldest('id'); + + if (!empty($ids)) { + $query->whereKey($ids); + } + + $bills = $query->get(); + + return response()->streamDownload(function () use ($bills) { + $csv = fopen('php://output', 'w'); + fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF)); + + fputcsv($csv, [ + '店铺名称', '时间', '资金类型', '流水类型', + '交易金额', '余额', '交易摘要', + ]); + + foreach ($bills as $bill) { + fputcsv($csv, [ + $bill->store_name, + $bill->transaction_time?->format('Y-m-d H:i:s'), + $bill->fund_type, + $bill->flow_type, + $bill->transaction_amount, + $bill->balance, + $bill->transaction_summary, + ]); + } + + fclose($csv); + }, 'marketing_pinduoduo_bills_export.csv', [ + 'Content-Type' => 'text/csv; charset=UTF-8', + ]); + } + + /** + * 批量导入 + */ + public function import(HttpRequest $request): RedirectResponse + { + $validated = $request->validate([ + 'import' => ['required', 'file', 'mimes:xlsx,xls,csv', 'max:5120'], + ], [ + 'import.required' => trans('marketing_pinduoduo_bills.import_required'), + 'import.mimes' => trans('marketing_pinduoduo_bills.import_file_type'), + 'import.max' => trans('marketing_pinduoduo_bills.import_file_size'), + ]); + + $file = $validated['import']; + $handle = fopen($file->getRealPath(), 'r'); + + $bom = fread($handle, 3); + if ($bom !== chr(0xEF) . chr(0xBB) . chr(0xBF)) { + rewind($handle); + } + + fgetcsv($handle); + + $count = 0; + $companyId = company_id(); + $userId = user()->id; + + while (($row = fgetcsv($handle)) !== false) { + if (empty($row[0]) && empty($row[1])) { + continue; + } + + $transactionTime = null; + if (!empty($row[1])) { + try { + $transactionTime = \Carbon\Carbon::parse($row[1]); + } catch (\Exception $e) { + $transactionTime = null; + } + } + + MarketingPinduoduoBill::create([ + 'company_id' => $companyId, + 'store_name' => $row[0] ?? '', + 'transaction_time' => $transactionTime, + 'fund_type' => $row[2] ?? '', + 'flow_type' => $row[3] ?? null, + 'transaction_amount' => (float) ($row[4] ?? 0), + 'balance' => (float) ($row[5] ?? 0), + 'transaction_summary' => $row[6] ?? null, + 'created_from' => 'core::ui', + 'created_by' => $userId, + ]); + + $count++; + } + + fclose($handle); + + flash(trans('marketing_pinduoduo_bills.import_success', ['count' => $count]))->success(); + + return redirect()->route('marketing-pinduoduo-bills.index'); + } + + /** + * 存储新账单 + */ + public function store(Request $request): RedirectResponse + { + $data = $request->validated(); + + MarketingPinduoduoBill::create(array_merge($data, [ + 'company_id' => company_id(), + 'created_from' => 'core::ui', + 'created_by' => user()->id, + ])); + + flash(trans('messages.success.created', ['type' => trans('marketing_pinduoduo_bills.title')]))->success(); + + return redirect()->route('marketing-pinduoduo-bills.index'); + } + + /** + * 编辑表单 + */ + public function edit(MarketingPinduoduoBill $marketingPinduoduoBill) + { + return view('banking.marketing_pinduoduo_bills.form', [ + 'bill' => $marketingPinduoduoBill, + ]); + } + + /** + * 更新账单 + */ + public function update(Request $request, MarketingPinduoduoBill $marketingPinduoduoBill): RedirectResponse + { + $marketingPinduoduoBill->update($request->validated()); + + flash(trans('messages.success.updated', ['type' => trans('marketing_pinduoduo_bills.title')]))->success(); + + return redirect()->route('marketing-pinduoduo-bills.index'); + } + + /** + * 删除账单 + */ + public function destroy(MarketingPinduoduoBill $marketingPinduoduoBill): RedirectResponse + { + $marketingPinduoduoBill->delete(); + + flash(trans('messages.success.deleted', ['type' => trans('marketing_pinduoduo_bills.title')]))->success(); + + return redirect()->route('marketing-pinduoduo-bills.index'); + } +} diff --git a/app/Http/Controllers/Banking/MarketingTaobaoAlipayBills.php b/app/Http/Controllers/Banking/MarketingTaobaoAlipayBills.php new file mode 100644 index 00000000..d0ee4ec2 --- /dev/null +++ b/app/Http/Controllers/Banking/MarketingTaobaoAlipayBills.php @@ -0,0 +1,232 @@ +middleware('permission:read-banking-accounts')->only('index'); + $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 index() + { + return view('banking.marketing_taobao_alipay_bills.index', [ + 'bills' => MarketingTaobaoAlipayBill::orderBy('transaction_date', 'desc') + ->orderBy('id', 'desc') + ->paginate(setting('default.list_limit', 25)), + ]); + } + + /** + * 创建表单 + */ + public function create() + { + return view('banking.marketing_taobao_alipay_bills.form', [ + 'bill' => new MarketingTaobaoAlipayBill([ + 'company_id' => company_id(), + 'transaction_date' => now()->toDateString(), + 'bookkeeping_time' => now(), + ]), + ]); + } + + /** + * 下载导入模板 + */ + public function template() + { + return response()->streamDownload(function () { + $csv = fopen('php://output', 'w'); + fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF)); // BOM for Excel + + fputcsv($csv, [ + '店铺名称', '交易日期', '记账时间', '收支类型', '交易类型', + '操作金额(元)', '操作后余额(元)', '备注', + ]); + + fclose($csv); + }, 'marketing_taobao_alipay_bills_template.csv', [ + 'Content-Type' => 'text/csv; charset=UTF-8', + ]); + } + + /** + * 批量导出 + */ + public function export(HttpRequest $request) + { + $ids = $request->validate([ + 'ids' => ['nullable', 'array', 'max:5000'], + 'ids.*' => ['integer'], + ])['ids'] ?? []; + + $query = MarketingTaobaoAlipayBill::oldest('id'); + + if (!empty($ids)) { + $query->whereKey($ids); + } + + $bills = $query->get(); + + return response()->streamDownload(function () use ($bills) { + $csv = fopen('php://output', 'w'); + fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF)); + + fputcsv($csv, [ + '店铺名称', '交易日期', '记账时间', '收支类型', '交易类型', + '操作金额(元)', '操作后余额(元)', '备注', + ]); + + foreach ($bills as $bill) { + fputcsv($csv, [ + $bill->store_name, + $bill->transaction_date->toDateString(), + $bill->bookkeeping_time?->format('Y-m-d H:i:s'), + $bill->income_expense_type, + $bill->transaction_type, + $bill->operation_amount, + $bill->balance_after, + $bill->notes, + ]); + } + + fclose($csv); + }, 'marketing_taobao_alipay_bills_export.csv', [ + 'Content-Type' => 'text/csv; charset=UTF-8', + ]); + } + + /** + * 批量导入 + */ + public function import(HttpRequest $request): RedirectResponse + { + $validated = $request->validate([ + 'import' => ['required', 'file', 'mimes:xlsx,xls,csv', 'max:5120'], + ], [ + 'import.required' => trans('marketing_taobao_alipay_bills.import_required'), + 'import.mimes' => trans('marketing_taobao_alipay_bills.import_file_type'), + 'import.max' => trans('marketing_taobao_alipay_bills.import_file_size'), + ]); + + $file = $validated['import']; + $handle = fopen($file->getRealPath(), 'r'); + + // Skip BOM + $bom = fread($handle, 3); + if ($bom !== chr(0xEF) . chr(0xBB) . chr(0xBF)) { + rewind($handle); + } + + // Skip header + fgetcsv($handle); + + $count = 0; + $companyId = company_id(); + $userId = user()->id; + + while (($row = fgetcsv($handle)) !== false) { + if (empty($row[0]) && empty($row[1])) { + continue; + } + + // Parse bookkeeping time - handle various formats + $bookkeepingTime = null; + if (!empty($row[2])) { + try { + $bookkeepingTime = \Carbon\Carbon::parse($row[2]); + } catch (\Exception $e) { + $bookkeepingTime = null; + } + } + + MarketingTaobaoAlipayBill::create([ + 'company_id' => $companyId, + 'store_name' => $row[0] ?? '', + 'transaction_date' => $row[1] ?? now()->toDateString(), + 'bookkeeping_time' => $bookkeepingTime, + 'income_expense_type' => $row[3] ?? '支出', + 'transaction_type' => $row[4] ?? null, + 'operation_amount' => (float) ($row[5] ?? 0), + 'balance_after' => (float) ($row[6] ?? 0), + 'notes' => $row[7] ?? null, + 'created_from' => 'core::ui', + 'created_by' => $userId, + ]); + + $count++; + } + + fclose($handle); + + flash(trans('marketing_taobao_alipay_bills.import_success', ['count' => $count]))->success(); + + return redirect()->route('marketing-taobao-alipay-bills.index'); + } + + /** + * 存储新账单 + */ + public function store(Request $request): RedirectResponse + { + $data = $request->validated(); + + MarketingTaobaoAlipayBill::create(array_merge($data, [ + 'company_id' => company_id(), + 'created_from' => 'core::ui', + 'created_by' => user()->id, + ])); + + flash(trans('messages.success.created', ['type' => trans('marketing_taobao_alipay_bills.title')]))->success(); + + return redirect()->route('marketing-taobao-alipay-bills.index'); + } + + /** + * 编辑表单 + */ + public function edit(MarketingTaobaoAlipayBill $marketingTaobaoAlipayBill) + { + return view('banking.marketing_taobao_alipay_bills.form', [ + 'bill' => $marketingTaobaoAlipayBill, + ]); + } + + /** + * 更新账单 + */ + public function update(Request $request, MarketingTaobaoAlipayBill $marketingTaobaoAlipayBill): RedirectResponse + { + $marketingTaobaoAlipayBill->update($request->validated()); + + flash(trans('messages.success.updated', ['type' => trans('marketing_taobao_alipay_bills.title')]))->success(); + + return redirect()->route('marketing-taobao-alipay-bills.index'); + } + + /** + * 删除账单 + */ + public function destroy(MarketingTaobaoAlipayBill $marketingTaobaoAlipayBill): RedirectResponse + { + $marketingTaobaoAlipayBill->delete(); + + flash(trans('messages.success.deleted', ['type' => trans('marketing_taobao_alipay_bills.title')]))->success(); + + return redirect()->route('marketing-taobao-alipay-bills.index'); + } +} 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..69296f65 --- /dev/null +++ b/app/Http/Controllers/Business/Pages.php @@ -0,0 +1,165 @@ +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); + + $content = $this->resolveContent($page, $tab); + + return view('business.pages.show', [ + 'section' => $sectionConfig, + 'page' => $pageConfig, + 'tabs' => $tabs, + 'activeTab' => $activeTab, + 'content' => $content, + ]); + } + + /** + * 根据页面和标签加载对应的业务内容数据 + */ + protected function resolveContent(string $page, string $tab): ?array + { + return match ($page) { + 'marketing-account-bills' => $this->contentForMarketingAccountBills($tab), + default => null, + }; + } + + /** + * 营销账户账单 - 加载指定平台的数据 + * 淘系支付宝、拼多多使用独立的流水账单表,其他平台使用通用汇总账单表 + */ + protected function contentForMarketingAccountBills(string $tab): ?array + { + if (!in_array($tab, self::MARKETING_PLATFORMS, true)) { + return null; + } + + // 淘系支付宝 - 使用独立的流水账单表(8字段) + if ($tab === 'taobao-alipay') { + return [ + 'view' => 'banking.marketing_taobao_alipay_bills.partials.embedded-table', + 'bills' => MarketingTaobaoAlipayBill::orderBy('transaction_date', 'desc') + ->orderBy('id', 'desc') + ->paginate(setting('default.list_limit', 25)), + ]; + } + + // 拼多多 - 使用独立的流水账单表(7字段) + if ($tab === 'pinduoduo') { + return [ + 'view' => 'banking.marketing_pinduoduo_bills.partials.embedded-table', + 'bills' => MarketingPinduoduoBill::orderBy('transaction_time', 'desc') + ->orderBy('id', 'desc') + ->paginate(setting('default.list_limit', 25)), + ]; + } + + // 其他平台 - 使用通用汇总账单表(15字段) + return [ + 'view' => 'banking.marketing_account_bills.partials.embedded-table', + 'platform' => $tab, + 'accounts' => MarketingAccountBill::platform($tab) + ->orderBy('bill_date', 'desc') + ->orderBy('id', 'desc') + ->paginate(setting('default.list_limit', 25)), + ]; + } + + 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/MarketingAccountBill.php b/app/Http/Requests/Banking/MarketingAccountBill.php new file mode 100644 index 00000000..b9e2cf1d --- /dev/null +++ b/app/Http/Requests/Banking/MarketingAccountBill.php @@ -0,0 +1,35 @@ + ['required', 'string', 'max:191'], + 'bill_date' => ['required', 'date'], + 'total_deposit' => ['nullable', 'numeric', 'min:0'], + 'total_transfer_in' => ['nullable', 'numeric', 'min:0'], + 'total_transfer_out' => ['nullable', 'numeric', 'min:0'], + 'non_grant_consumption' => ['nullable', 'numeric', 'min:0'], + 'grant_consumption' => ['nullable', 'numeric', 'min:0'], + 'rebate_redpack_consumption' => ['nullable', 'numeric', 'min:0'], + 'discount_redpack_consumption' => ['nullable', 'numeric', 'min:0'], + 'shared_wallet_consumption' => ['nullable', 'numeric', 'min:0'], + 'shared_grant_consumption' => ['nullable', 'numeric', 'min:0'], + 'daily_total_amount' => ['nullable', 'numeric', 'min:0'], + 'period_end_balance' => ['nullable', 'numeric', 'min:0'], + 'grant_balance' => ['nullable', 'numeric', 'min:0'], + 'non_grant_balance' => ['nullable', 'numeric', 'min:0'], + 'notes' => ['nullable', 'string', 'max:500'], + ]; + } +} diff --git a/app/Http/Requests/Banking/MarketingPinduoduoBill.php b/app/Http/Requests/Banking/MarketingPinduoduoBill.php new file mode 100644 index 00000000..509d6bce --- /dev/null +++ b/app/Http/Requests/Banking/MarketingPinduoduoBill.php @@ -0,0 +1,26 @@ + ['required', 'string', 'max:191'], + 'transaction_time' => ['required', 'date'], + 'fund_type' => ['required', 'string', 'max:50'], + 'flow_type' => ['nullable', 'string', 'max:100'], + 'transaction_amount' => ['required', 'numeric'], + 'balance' => ['nullable', 'numeric'], + 'transaction_summary' => ['nullable', 'string', 'max:1000'], + ]; + } +} diff --git a/app/Http/Requests/Banking/MarketingTaobaoAlipayBill.php b/app/Http/Requests/Banking/MarketingTaobaoAlipayBill.php new file mode 100644 index 00000000..2abaee2e --- /dev/null +++ b/app/Http/Requests/Banking/MarketingTaobaoAlipayBill.php @@ -0,0 +1,27 @@ + ['required', 'string', 'max:191'], + 'transaction_date' => ['required', 'date'], + 'bookkeeping_time' => ['nullable', 'date'], + 'income_expense_type' => ['required', 'string', 'max:50'], + 'transaction_type' => ['nullable', 'string', 'max:100'], + 'operation_amount' => ['required', 'numeric'], + 'balance_after' => ['nullable', 'numeric'], + 'notes' => ['nullable', 'string', 'max:500'], + ]; + } +} 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/MarketingAccountBill.php b/app/Models/Banking/MarketingAccountBill.php new file mode 100644 index 00000000..5ed86a93 --- /dev/null +++ b/app/Models/Banking/MarketingAccountBill.php @@ -0,0 +1,56 @@ + 'date', + 'total_deposit' => 'double', + 'total_transfer_in' => 'double', + 'total_transfer_out' => 'double', + 'non_grant_consumption' => 'double', + 'grant_consumption' => 'double', + 'rebate_redpack_consumption' => 'double', + 'discount_redpack_consumption' => 'double', + 'shared_wallet_consumption' => 'double', + 'shared_grant_consumption' => 'double', + 'daily_total_amount' => 'double', + 'period_end_balance' => 'double', + 'grant_balance' => 'double', + 'non_grant_balance' => 'double', + 'deleted_at' => 'datetime', + ]; + + public function scopePlatform($query, string $platform) + { + return $query->where('platform', $platform); + } +} diff --git a/app/Models/Banking/MarketingPinduoduoBill.php b/app/Models/Banking/MarketingPinduoduoBill.php new file mode 100644 index 00000000..7a9d3c37 --- /dev/null +++ b/app/Models/Banking/MarketingPinduoduoBill.php @@ -0,0 +1,30 @@ + 'datetime', + 'transaction_amount' => 'double', + 'balance' => 'double', + 'deleted_at' => 'datetime', + ]; +} diff --git a/app/Models/Banking/MarketingTaobaoAlipayBill.php b/app/Models/Banking/MarketingTaobaoAlipayBill.php new file mode 100644 index 00000000..cfe55a53 --- /dev/null +++ b/app/Models/Banking/MarketingTaobaoAlipayBill.php @@ -0,0 +1,32 @@ + 'date', + 'bookkeeping_time' => 'datetime', + 'operation_amount' => 'double', + 'balance_after' => 'double', + 'deleted_at' => 'datetime', + ]; +} 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, '