Compare commits

...

198 Commits

Author SHA1 Message Date
Cüneyt Şentürk 9d6cfba94f update composer.json and composer.lock files.. 2026-03-14 11:31:24 +00:00
Cüneyt Şentürk cabef574d7 update app.css file 2026-03-14 10:52:29 +00:00
Cüneyt Şentürk b81befc343 update package-lock.json file.. 2026-03-14 10:18:19 +00:00
Cüneyt Şentürk 729834a0ef
Merge pull request #3348 from CihanSenturk/add-category-tabs
Improve category tabs
2026-03-14 09:33:42 +00:00
Cüneyt Şentürk a776c7b5a9 fixed bill test.. 2026-03-13 21:57:28 +00:00
Cihan Şentürk ce25e05b64 updated code visibility and streamline category type handling 2026-03-14 00:56:18 +03:00
Cihan Şentürk 31df25305d fix typo 2026-03-14 00:54:31 +03:00
Cihan Şentürk 09b4e1641e updated contact type determination in method 2026-03-14 00:54:16 +03:00
Cihan Şentürk 3ad75c4d6b updated category report listeners 2026-03-14 00:52:08 +03:00
Cihan Şentürk 5772eca363 wip 2026-03-14 00:50:44 +03:00
Cihan Şentürk c82b4883a7 added category types trait 2026-03-14 00:50:16 +03:00
Cüneyt Şentürk aba76b12c8 Added contact update for document status.. 2026-03-12 02:53:44 +00:00
Cüneyt Şentürk 311054bad7 Fixed document, transaction and category list page pinned tab search issue.. 2026-03-12 01:47:58 +00:00
Cüneyt Şentürk 9ddab69242
Merge pull request #3347 from CihanSenturk/add-category-tabs
Added Category Tabs
2026-03-12 01:35:55 +00:00
Cihan Şentürk 38677e65c2 wip 2026-03-12 00:31:30 +03:00
Cihan Şentürk 91f1956dc9 updated add new category modal 2026-03-12 00:30:50 +03:00
Cihan Şentürk 1789662440 updated category code field dynamic show 2026-03-11 23:04:45 +03:00
Cihan Şentürk 6f50a4e685 refactored category validation and removed unused module trait 2026-03-10 22:15:51 +03:00
Cihan Şentürk e6c67ef504 added balance component 2026-03-09 22:57:29 +03:00
Cihan Şentürk f9c57bd402 added early return for searchable name in getSearchStringValue method 2026-03-09 22:46:17 +03:00
Cihan Şentürk 3c033af0d6 added styling for select group title in app.css 2026-03-09 22:46:05 +03:00
Cihan Şentürk 3a94319805 added code and description fields to category import and export models 2026-03-09 22:45:01 +03:00
Cihan Şentürk 09574e0335 added code and description fields to category resource 2026-03-09 22:44:35 +03:00
Cihan Şentürk 804cb8e568 added dynamic validation for category code based on module status 2026-03-09 22:44:15 +03:00
Cihan Şentürk 6df965c930 Improved category types 2026-03-09 22:43:05 +03:00
Cihan Şentürk 2c4d52b15e added category new fields(code, description) 2026-03-09 22:35:54 +03:00
Cihan Şentürk 2c12e7524c added category tabs 2026-03-09 22:33:28 +03:00
Cüneyt Şentürk d85c1b51bb revert recurring Transactions.. 2026-03-03 03:04:10 +00:00
Cüneyt Şentürk 952626f52a recurring paid date format.. 2026-03-03 02:49:49 +00:00
Cüneyt Şentürk 939d2b726a Fixed recurring update issue.. 2026-03-02 21:21:52 +00:00
Cüneyt Şentürk 8359383ba9
Merge pull request #3337 from e1um/fix-transaction-contact-sync
fix: sync transaction contact_id when document contact changes
2026-02-28 10:16:21 +00:00
Cüneyt Şentürk 9aed0937ab Enhancement unauthenticated user url intended 2026-02-26 17:49:00 +00:00
Akaunting afb52a52f8
sentry logs 2026-02-16 21:46:03 +00:00
Akaunting 9da40446e6
widget data 2026-02-07 18:55:11 +00:00
Cihan Şentürk a44d9d156b
Merge pull request #3342 from CihanSenturk/fix-date-filter-financial-start-date-issue
Fixed date filter financial start date issue
2026-02-05 20:47:46 +03:00
Cihan Şentürk 2b098e5e51
fixed date filter financial start date issue 2026-02-05 20:32:32 +03:00
Cüneyt Şentürk 072e070d09 Datetime financial year support custom date range.. 2026-02-02 22:40:27 +00:00
Cüneyt Şentürk 752e670a06 Merge branch 'master' of github.com:akaunting/akaunting 2026-01-22 02:41:20 +00:00
Cüneyt Şentürk 24f0dbb961 added setting new form type.. 2026-01-22 02:41:09 +00:00
Cüneyt Şentürk b5d096c615 update package-lock.json file.. 2026-01-21 22:28:01 +00:00
Cüneyt Şentürk 12ec20f8ac update composer.lock file.. 2026-01-21 22:26:50 +00:00
e1um e44fd37bad
fix: sync transaction contact_id when document contact changes
When a document (invoice/bill) is updated to change the contact (vendor/customer),
any existing transactions linked to that document now have their contact_id updated
to match. This fixes an issue where reports filtering by contact would not include
transactions from documents that were reassigned to a different contact.

Fixes #3336
2026-01-21 12:44:51 -05:00
Cüneyt Şentürk 4829c7728d Contact show page layz load issue fixed.. 2026-01-18 01:04:46 +00:00
Cihan Şentürk fb9f7eaaa3
improved search string filter logic 2026-01-16 21:14:56 +03:00
Cüneyt Şentürk 7c130a4ece update package-lock.json file.. 2026-01-11 00:56:02 +00:00
Cüneyt Şentürk 7f1fc69330 update composer.lock file.. 2026-01-11 00:36:11 +00:00
Cüneyt Şentürk 695f8cf6ae Fixed document currency conversation issue.. 2026-01-02 02:37:12 +00:00
Cüneyt Şentürk 4a0419774b
Merge pull request #3333 from cuneytsenturk/master
Fixed category, tax and contact form group search and add new issues..
2026-01-01 20:51:18 +00:00
Cüneyt Şentürk c5d61d8f4e Fixed category, tax and contact form group search and add new issues.. 2026-01-01 16:41:32 +00:00
Cüneyt Şentürk d4f2b5b4da Fixed document transaction updating upload file.. 2025-12-25 13:53:32 +00:00
Cihan Şentürk 6c67e99a1b
version update 3.1.20 to 3.1.21 2025-12-13 22:11:04 +03:00
Cihan Şentürk ef56e7e91e
update composer.lock file.. 2025-12-13 22:09:57 +03:00
Cihan Şentürk a7917f42b3
fixed cache module version issue 2025-12-13 22:08:24 +03:00
Cihan Şentürk 24fc846ce3
version update 3.1.19 to 3.1.20 2025-12-12 00:15:27 +03:00
Cihan Şentürk 11459b5bb3
update package-lock.json file.. 2025-12-11 21:59:31 +03:00
Cihan Şentürk 1f8d3f3cbf
update composer.lock file.. 2025-12-11 21:56:12 +03:00
Cihan Şentürk 421cc1091a
add new middleware for module limitation 2025-12-11 21:46:49 +03:00
Cihan Şentürk aa393c58fc
Merge pull request #3302 from akaunting/translations
New Crowdin translations
2025-12-07 19:34:50 +03:00
Cihan Şentürk b12cebd08a
Merge pull request #3328 from CihanSenturk/fix-import-transaction-type-issue
Fixed import transaction type issue
2025-12-07 19:32:27 +03:00
Cihan Şentürk 4d59bdf754
fixed import transaction type issue 2025-12-07 19:31:45 +03:00
Cihan Şentürk df83efb84e
version check method update 2025-12-07 19:30:31 +03:00
Cihan Şentürk e523979ad6
Merge pull request #3322 from imhayatunnabi/fix/mathematical-calculations
Mathematical Calculation Bugs
2025-12-07 18:36:15 +03:00
Cihan Şentürk d6c9083b80
Merge pull request #3327 from CihanSenturk/fix-update-attachment-remove-issue
Fixed api update attachment issue
2025-12-07 18:34:27 +03:00
Cihan Şentürk c78fb4ce34
fixed api update attachment issue 2025-12-07 18:31:41 +03:00
Crowdin Bot e8556ee306 new crowdin translations 2025-12-07 00:28:57 +00:00
Cihan Şentürk 86b987eb9b
Merge pull request #3326 from CihanSenturk/update-import-validation-message
Update import validation message
2025-11-30 21:43:30 +03:00
Cihan Şentürk 6cfd9410d6
update-import-validation-message 2025-11-27 15:19:48 +03:00
Cüneyt Şentürk 0c7fa0cf0d update composer.lock file.. 2025-11-15 10:54:14 +00:00
Cüneyt Şentürk 7e5a64262c Added transactions relation for n+1 issues.. 2025-11-15 10:44:46 +00:00
Cüneyt Şentürk c6716e7763
Merge pull request #3321 from imhayatunnabi/fix/hp-n1-problems
Fix Critical N+1 Query Performance Issues
2025-11-15 10:40:09 +00:00
Hayatunnabi Nabil b56a3bda56 fix: error handling for arithmetic operations and improve calculations in Document and TaxSummary classes. Added checks for division and modulo by zero, and adjusted percentage calculation logic to handle zero sub-total cases. 2025-11-13 13:41:12 +06:00
Hayatunnabi Nabil 89b600dec9 fix: eager loading for db transactions across multiple components to prevent N+1 query issues. 2025-11-13 13:12:22 +06:00
Cüneyt Şentürk b5d8499ac7 Contact n+1 issue solved.. 2025-11-09 15:25:46 +00:00
Cüneyt Şentürk 1f68dd4b4d Fixed n+1 issue for document and transactions taxes.. 2025-11-09 14:13:50 +00:00
Cüneyt Şentürk 1a0bf56c63 Fixed empty page stack issue.. 2025-11-09 13:28:05 +00:00
Cüneyt Şentürk c7b70dd224 update composer.lock file.. 2025-11-09 13:07:01 +00:00
Cüneyt Şentürk e76c84e46c Merge branch 'master' of github.com:akaunting/akaunting 2025-11-09 12:54:48 +00:00
Cüneyt Şentürk dee95e412e update package-lock.json file.. 2025-11-09 12:54:36 +00:00
Cüneyt Şentürk ac58f4d0ae
Merge pull request #3316 from imhayatunnabi/fix/recurring-invoices-empty-request
Recurring Invoices Break When Creating or Updating
2025-11-08 08:29:48 +00:00
Cüneyt Şentürk c1dd91238d
Merge pull request #3317 from imhayatunnabi/fix/missing-non-recurring-helpers
Fix missing non-recurring helpers in shared traits
2025-11-08 08:28:31 +00:00
Hayatunnabi Nabil 416f879353 fix: document and transaction traits to improve method naming consistency 2025-11-06 02:44:55 +06:00
Hayatunnabi Nabil f720e22117 fix: Refactor store and update methods in RecurringInvoices controller to merge 'issued_at' directly into the request before dispatching document creation and update actions. 2025-11-06 02:40:53 +06:00
Cihan Şentürk 8fb34dc8d2
Merge pull request #3313 from CihanSenturk/add-new-stack-document-ıtem-show
Added new stack document item show
2025-09-08 21:01:26 +03:00
Cihan Şentürk dc13c8fabb
added new stack document item show 2025-09-08 20:27:22 +03:00
Cüneyt Şentürk 1bd3c5a153 fixed performans.. 2025-09-01 22:25:57 +01:00
Cüneyt Şentürk 70a5e5acdd
Merge pull request #3309 from CihanSenturk/fix-global-discount-delete-issue
Fix global discount delete issue
2025-08-30 08:43:42 +01:00
Cüneyt Şentürk 47dd774286 update composer.lock file.. 2025-08-30 01:14:52 +01:00
Cüneyt Şentürk 355cee0cf2 Transaction edit via document information.. 2025-08-30 01:06:12 +01:00
Cüneyt Şentürk 774f64a051 close #3310 #3311 issue fixed transactions api not allow document.. 2025-08-30 00:54:12 +01:00
Cüneyt Şentürk 48ae7a5a8a Added new stack for company custom fields.. 2025-08-25 13:33:00 +01:00
Cihan Şentürk 6c485567ca
fixed global discount delete issue 2025-08-09 14:20:45 +03:00
Cihan Şentürk 502c7b0d31 fixed tax calculate issue 2025-08-04 15:26:09 +03:00
Cihan Şentürk 51d2097cd6
Merge pull request #3308 from CihanSenturk/add-laravel-cloud-compability
Added laravel cloud compability
2025-08-01 15:52:26 +03:00
Cihan Şentürk b7492d3ab0
added laravel cloud compability 2025-08-01 15:50:23 +03:00
Cihan Şentürk 7317d1b512
Merge pull request #3307 from CihanSenturk/fix-document-tax-calculate-issue
Fixed document tax calculate issue
2025-08-01 11:59:40 +03:00
Cihan Şentürk f9b3d9ebfa
fixed document tax calculate issue 2025-08-01 11:57:50 +03:00
Cihan Şentürk 2beba59bb1
Merge pull request #3306 from CihanSenturk/add-payment-attachment
Add attachment feature to document payments
2025-08-01 11:55:24 +03:00
Cihan Şentürk c1f73ae6dc
add attachment feature to document payments 2025-08-01 11:53:18 +03:00
Cihan Şentürk bbf83478ea
Merge pull request #3305 from CihanSenturk/fix-undefined-currency-code-issue
Fixed undefined currency code issue
2025-08-01 11:45:47 +03:00
Cihan Şentürk 795baf9585
fixed undefined currency code issue 2025-08-01 11:44:54 +03:00
Cihan Şentürk 65705deafb
Merge pull request #3304 from CihanSenturk/add-empty-page-button-stack
Added empty page button stack
2025-07-30 13:35:37 +03:00
Cihan Şentürk a4f48d2342
added empty page button stack 2025-07-30 13:18:32 +03:00
Cihan Şentürk 7b1eaf30fc
version update 3.1.18 to 3.1.19 2025-07-22 10:29:13 +03:00
Cihan Şentürk 6dd89a1b0f
update composer.lock file 2025-07-22 10:27:10 +03:00
Cüneyt Şentürk 0d8c1eece4
Merge pull request #3288 from akaunting/translations
New Crowdin translations
2025-07-22 01:23:31 +03:00
Crowdin Bot 3a151bdc7f new crowdin translations 2025-07-21 12:41:01 +00:00
Cihan Şentürk 71b5ebcd32
update version check exception message 2025-07-21 15:26:25 +03:00
Cihan Şentürk 9f66fed1ec
Add try-catch to version check 2025-07-21 15:20:05 +03:00
Cihan Şentürk c059ad261c
Merge pull request #3301 from CihanSenturk/fix-resend-pasword-issue
Fixed resend password email issue
2025-07-21 15:14:45 +03:00
Cihan Şentürk 41d0e4b3cc
fixed resend password email issue 2025-07-21 15:13:10 +03:00
Cihan Şentürk 139be97e11
refactor n+1 query optimization 2025-07-21 15:03:52 +03:00
Cihan Şentürk 355c34920c
update package-lock.json file.. 2025-07-21 14:56:00 +03:00
Cihan Şentürk 65407e04b2
Merge pull request #3300 from CihanSenturk/update-invoice-logo-size
Update invoice logo size
2025-07-21 14:54:52 +03:00
Cihan Şentürk 42ddac24d5
updated invoice logo size 2025-07-21 14:51:16 +03:00
Cihan Şentürk 180c8bc100
Merge pull request #3299 from CihanSenturk/fix-tax-summary-report-tax-name-issue
Fixed tax summary report tax name issue #86c3nzq4r
2025-07-17 22:17:07 +03:00
Cihan Şentürk ffe2442d18
fixed tax summary report tax name issue 2025-07-17 22:15:52 +03:00
Cihan Şentürk dfb165f379
Merge pull request #3294 from mavrickdeveloper/feature/fix-n1-queries-performance
perf: Fix critical N+1 queries for 85% performance improvement
2025-07-17 17:27:15 +03:00
Cihan Şentürk c1eb27034f
fixed report sytle issue 2025-07-17 17:25:10 +03:00
Cihan Şentürk cd446685a7
Merge pull request #3297 from CihanSenturk/fix-extra-module-update
Fixed extra module update issue
2025-06-30 16:18:10 +03:00
Cihan Şentürk 037cf7421c
fixed extra module update issue 2025-06-30 16:17:14 +03:00
mavrickdeveloper 21cc0b7c62 perf: Fix critical N+1 queries for 85% performance improvement 2025-05-29 22:34:24 +01:00
Cihan Şentürk 8850f8ef84
wip 2025-05-15 13:36:27 +03:00
Cihan Şentürk d252b8bfd3
Merge pull request #3291 from CihanSenturk/fix-transaction-print-and-download-type-issue
Fixed print transaction type issue
2025-05-15 13:27:43 +03:00
Cihan Şentürk 10a2198141
Merge pull request #3290 from CihanSenturk/add-discount-summary-report
Add discount summary report
2025-05-15 13:27:11 +03:00
Cihan Şentürk 1e3b2ffc91
fixed print transaction type issue 2025-05-15 13:25:42 +03:00
Cihan Şentürk 155e02d0fd
update permission discount summary 2025-05-14 16:43:00 +03:00
Cihan Şentürk 8dc32979ae
added discount summary report 2025-05-14 16:21:59 +03:00
Cihan Şentürk 56ac86ad3c
Merge pull request #3289 from CihanSenturk/add-report-data-to-array
Added report data to array
2025-05-01 14:12:53 +03:00
Cihan Şentürk 79f7911e1a
added report data to array 2025-05-01 13:21:37 +03:00
Cihan Şentürk bd0fe0e0be
added new scopes to report model 2025-04-29 20:50:09 +03:00
Cihan Şentürk 62315c98b5
version update 3.1.17 to 3.1.18 2025-04-29 12:14:50 +03:00
Cihan Şentürk 45899f1992
update composer.lock file 2025-04-28 22:00:55 +03:00
Cihan Şentürk 6f706fce1b
Merge pull request #3284 from akaunting/translations
New Crowdin translations
2025-04-28 21:50:30 +03:00
Cihan Şentürk b07c5dc62d
Merge pull request #3287 from CihanSenturk/fix-tax-summary-report-issue
Fix tax summary report issue
2025-04-28 20:59:40 +03:00
Cihan Şentürk dd4d09305f
added warning messages for connected transactions with taxes 2025-04-28 20:28:45 +03:00
Cihan Şentürk 635fee12ff
added transaction taxes in the tax report 2025-04-28 20:26:36 +03:00
Crowdin Bot 6f43604832 new crowdin translations 2025-04-28 00:24:40 +00:00
Cihan Şentürk 0fceb96078
Added moduleIsEnabled scope to Model.php 2025-04-27 16:24:29 +03:00
Cihan Şentürk 4c25459dd7
Merge pull request #3286 from CihanSenturk/update-recurring-relations
Update recurring relations
2025-04-22 22:32:38 +03:00
Cihan Şentürk eaa1476c26
Merge pull request #3285 from CihanSenturk/update-table-more-actions-delete-button
Updated more actions view component delete button
2025-04-22 20:38:29 +03:00
Cihan Şentürk b614a22b3e
updated more actions view component delete button 2025-04-22 20:36:26 +03:00
Cihan Şentürk 84ee8b123a
fixed recurring cloneable relations type 2025-04-22 19:55:33 +03:00
Cihan Şentürk 09e32ad262
include 'taxes' in cloneable_relations for recurring transactions 2025-04-22 19:52:52 +03:00
Cihan Şentürk 07579e61bb
version update 3.1.16 to 3.1.17 2025-04-03 14:48:46 +03:00
Cihan Şentürk b9949d1140
update composer.lock file 2025-04-03 14:46:48 +03:00
Cihan Şentürk 76b9e18c26
removed required decimal mark in currency request 2025-04-03 14:37:04 +03:00
Cihan Şentürk 6d22c72f69
Merge pull request #3283 from CihanSenturk/fix-wizard-create-currency-issue
Fixed wizard create currency issue
2025-04-02 15:44:19 +03:00
Cihan Şentürk 729488713a
fixed wizard create currency issue 2025-04-02 15:38:54 +03:00
Cüneyt Şentürk 9e8be67be8
Merge pull request #3125 from robertalexa/patch-1
Introduce csv as allowed file type in mediable config
2025-03-29 03:14:30 +03:00
Cüneyt Şentürk febb4ede9c
Merge pull request #3259 from kaspernowak/patch-1
feat: add OpenLiteSpeed rewrite rule for protected files
2025-03-29 03:09:40 +03:00
Cüneyt Şentürk 723dfa7f04
Merge pull request #3252 from robertsilen/patch-1
add MariaDB to README.md
2025-03-29 03:03:29 +03:00
Cihan Şentürk 5ab50131d3
version update 3.1.15 to 3.1.16 2025-03-28 14:29:29 +03:00
Cihan Şentürk 85352bc768
update composer.lock file 2025-03-28 13:23:30 +03:00
Cihan Şentürk 6ccf4a7277
updated currency request reqex 2025-03-28 12:24:33 +03:00
Cihan Şentürk 16b8147733
Merge pull request #3280 from CihanSenturk/fix-reconcilation-calculate-issue
Fixed reconcilation calculate issue fixed
2025-03-28 11:17:19 +03:00
Cihan Şentürk b9d2c61d2f
fixed reconcilation calculate issue fixed 2025-03-28 11:16:41 +03:00
Cihan Şentürk 89ae0f4fdb
Merge pull request #3279 from CihanSenturk/fix-bulk-action-page-and-limit-control
Fixed bulk action page and limit control
2025-03-28 11:14:09 +03:00
Cihan Şentürk 9f30bc7174
fixed bulk action page and limit control 2025-03-28 11:13:02 +03:00
Cihan Şentürk 19adb56f52
Merge pull request #3278 from CihanSenturk/fix-create-new-currency-decimal-and-thousands-field-control
Create new currency decimal and thousands field control
2025-03-28 11:09:54 +03:00
Cihan Şentürk 2d29a83e1e
create new currency decimal and thousands field control 2025-03-28 11:08:25 +03:00
Cihan Şentürk a1062f9c74
Merge pull request #3277 from CihanSenturk/fix-report-search-invalid-date-filter-isue
Fixed report search invalid date filter issue
2025-03-27 20:36:59 +03:00
Cihan Şentürk 5d0f76dfd1
fixed search end date invalid issue 2025-03-27 20:36:13 +03:00
Cihan Şentürk 776e8c8599
fixed report search invalid date format issue 2025-03-27 20:29:48 +03:00
Cihan Şentürk 543d180060
fixed search end date invalid issue 2025-03-27 18:26:45 +03:00
Cihan Şentürk de4a32ec00
Merge pull request #3276 from CihanSenturk/fix-import-recurring-sample-files
Fixed import recurring sample files
2025-03-27 13:52:58 +03:00
Cihan Şentürk e0ed546c56 added recurring new import sample files 2025-03-27 13:49:23 +03:00
Cihan Şentürk cab78dde3c removed recurring old import sample files 2025-03-27 13:48:02 +03:00
Cihan Şentürk df374afbb5 added new sample import files 2025-03-27 12:42:47 +03:00
Cihan Şentürk aede38ff1b removed old sample import files 2025-03-27 12:41:16 +03:00
Cihan Şentürk bf024c5ae2
Merge pull request #3275 from CihanSenturk/fix-bulk-action-download
Fixed bulk action download
2025-03-27 11:09:47 +03:00
Cihan Şentürk cfa2a64e1b
fixed bulk action download 2025-03-27 11:06:07 +03:00
Cihan Şentürk c466a1d153
Merge pull request #3274 from CihanSenturk/update-import-currency-thousands-separator
Updated import currency thousands separator
2025-03-27 01:01:18 +03:00
Cihan Şentürk 49fe71d671
updated import currency thousands separator 2025-03-27 01:00:25 +03:00
Cihan Şentürk b93ef9bf13
Merge pull request #3273 from CihanSenturk/update-reconcilation-list-page
Updated reconcilation list page
2025-03-26 16:20:48 +03:00
Cihan Şentürk 023db3e020
updated reconcilation list page 2025-03-26 16:20:16 +03:00
Cihan Şentürk 5d1ab331d4
Merge pull request #3272 from CihanSenturk/add-export-document-item-discount
Added export document item discount fields
2025-03-26 13:05:01 +03:00
Cihan Şentürk 3a3f3ce86f
added export document item discount fields 2025-03-26 11:41:53 +03:00
Cihan Şentürk 49937eb721
Merge pull request #3270 from CihanSenturk/fix-import-date-format
Fixed import date format issue
2025-03-10 11:43:57 +03:00
Cihan Şentürk c6e41da70b
fixed import date format issue 2025-03-07 18:17:59 +03:00
Cihan Şentürk 59ede8853d
updated actions version 2025-03-03 16:24:43 +03:00
Cihan Şentürk 9b54043cf1
added document real type check 2025-03-03 12:04:44 +03:00
Cihan Şentürk e06a26d62e
Merge pull request #3269 from CihanSenturk/add-recurring-transaction-tax
Added recurring transaction tax
2025-03-03 12:01:42 +03:00
Cihan Şentürk 763bc57eac
added recurring transaction tax 2025-03-03 12:00:48 +03:00
Cihan Şentürk cfee94711c
Merge pull request #3268 from CihanSenturk/update-vue-styling
Updated Vue file styles.
2025-02-27 13:36:19 +03:00
Cihan Şentürk f6897049f6
Merge pull request #3267 from CihanSenturk/update-category-component
Update category component
2025-02-27 13:34:05 +03:00
Cihan Şentürk 15f4da3d3f
Merge pull request #3266 from CihanSenturk/fix-line-discount-calculation-issue
Fixed document line discount calculation issue
2025-02-27 13:33:46 +03:00
Cihan Şentürk 1ba84a040a
added report.js class name control 2025-02-27 13:24:20 +03:00
Cihan Şentürk 7ba3313add
updated vue styling 2025-02-27 13:23:35 +03:00
Cihan Şentürk ec3659ef5e
added option field view components 2025-02-27 13:17:27 +03:00
Cihan Şentürk 811076afcf
updated category check control 2025-02-27 13:12:50 +03:00
Cihan Şentürk 805dbb8a71
fixed document line discount calculation issue 2025-02-27 13:00:24 +03:00
Cihan Şentürk a8adeff326
Merge pull request #3265 from CihanSenturk/fix-report-show-print-issue
Fixed report show print issue
2025-02-26 12:53:47 +03:00
Cihan Şentürk 003453e9e0
fixed report show print issue 2025-02-26 12:53:00 +03:00
Cihan Şentürk 00989224b9
Merge pull request #3263 from mervekaraman/master
Safelist update
2025-02-20 20:18:24 +03:00
merve karaman e8920c7f9f
Safelist update 2025-02-20 19:43:13 +03:00
Kasper Nowak 8ca80ba745
eat: add OpenLiteSpeed rewrite rule for protected files
- Added a rewrite rule to block direct access to sensitive files (.env, .log, artisan)
  for OpenLiteSpeed environments.
- Retained the existing <FilesMatch> block for Apache compatibility.
- Ensures that both Apache and OpenLiteSpeed users have proper protection for protected files.
2025-02-05 18:40:26 +01:00
Robert Silén 1a5d5bcfed
add MariaDB to README.md 2025-01-10 14:20:57 +02:00
Robert Alexa dffc1370f3
Introduce csv as allowed file type in mediable config 2024-01-09 22:01:14 +00:00
224 changed files with 14961 additions and 11743 deletions

View File

@ -20,10 +20,10 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Cache Composer
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ~/.composer/cache/files
key: php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}

View File

@ -14,7 +14,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Sync with Crowdin
uses: crowdin/github-action@master

View File

@ -24,6 +24,10 @@
</IfModule>
</FilesMatch>
# 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]

View File

@ -17,7 +17,7 @@ Online accounting software designed for small businesses and freelancers. Akaunt
## Requirements
* PHP 8.1 or higher
* Database (e.g.: MySQL, PostgreSQL, SQLite)
* Database (e.g.: MariaDB, MySQL, PostgreSQL, SQLite)
* Web Server (eg: Apache, Nginx, IIS)
* [Other libraries](https://akaunting.com/hc/docs/on-premise/requirements/)

View File

@ -304,7 +304,7 @@ abstract class BulkAction
$batch[] = new CreateMediableForDownload(user(), $file_name, $translation);
Bus::chain($batch)->onQueue('default')->dispatch();
Bus::chain($batch)->onQueue('jobs')->dispatch();
$message = trans('messages.success.download_queued', ['type' => $translation]);
@ -314,7 +314,7 @@ abstract class BulkAction
} else {
$this->dispatch(new CreateZipForDownload($selected, $class, $file_name));
$folder_path = 'app/temp/' . company_id() . '/bulk_actions/';
$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);
}

View File

@ -16,6 +16,9 @@ use Illuminate\Pagination\Paginator;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Routing\Controller as BaseController;
/**
* @property string $type
*/
abstract class Controller extends BaseController
{
use AuthorizesRequests, Jobs, Permissions, Relationships, SearchString, ValidatesRequests;
@ -114,6 +117,19 @@ abstract class Controller extends BaseController
}
}
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;
}
@ -152,5 +168,105 @@ abstract class Controller extends BaseController
}
}
}
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');
}
}
}

View File

@ -70,8 +70,11 @@ abstract class Import implements HasLocalePreference, ShouldQueue, SkipsEmptyRow
}
try {
$row[$date_field] = Date::parse(ExcelDate::excelToDateTimeObject($row[$date_field]))
->format('Y-m-d H:i:s');
$row[$date_field] = is_numeric($row[$date_field])
? Date::parse(ExcelDate::excelToDateTimeObject($row[$date_field]))
->format('Y-m-d H:i:s')
: Date::parse($row[$date_field])
->format('Y-m-d H:i:s');
} catch (InvalidFormatException | \Exception $e) {
Log::info($e->getMessage());
}
@ -119,7 +122,18 @@ abstract class Import implements HasLocalePreference, ShouldQueue, SkipsEmptyRow
} catch (ValidationException $e) {
foreach ($e->validator->failed() as $attribute => $value) {
foreach ($value as $rule => $params) {
$validator->addFailure($row . '.' . $attribute, $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);
}
}
}

View File

@ -5,13 +5,14 @@ namespace App\Abstracts\Listeners;
use App\Models\Banking\Account;
use App\Models\Common\Contact;
use App\Models\Setting\Category;
use App\Traits\Categories;
use App\Traits\Contacts;
use App\Traits\DateTime;
use App\Traits\SearchString;
abstract class Report
{
use Contacts, DateTime, SearchString;
use Categories, Contacts, DateTime, SearchString;
protected $classes = [];
@ -90,22 +91,24 @@ abstract class Report
public function getItemCategories($limit = false)
{
return $this->getCategories('item', $limit);
return $this->getCategories($this->getItemCategoryTypes(), $limit);
}
public function getIncomeCategories($limit = false)
{
return $this->getCategories('income', $limit);
return $this->getCategories($this->getIncomeCategoryTypes(), $limit);
}
public function getExpenseCategories($limit = false)
{
return $this->getCategories('expense', $limit);
return $this->getCategories($this->getExpenseCategoryTypes(), $limit);
}
public function getIncomeExpenseCategories($limit = false)
{
return $this->getCategories(['income', 'expense'], $limit);
$types = array_merge($this->getIncomeCategoryTypes(), $this->getExpenseCategoryTypes());
return $this->getCategories($types, $limit);
}
public function getCategories($types, $limit = false)
@ -158,11 +161,42 @@ abstract class Report
];
}
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)
{
$input = request('search', '');

View File

@ -246,6 +246,15 @@ abstract class Model extends Eloquent implements Ownable
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()) {

View File

@ -40,6 +40,8 @@ abstract class Report
public $has_money = true;
public $group;
public $groups = [];
public $year;
@ -251,7 +253,7 @@ abstract class Report
public function show()
{
return view($this->views['show'])->with('class', $this);
return view($this->views['show'], ['print' => false])->with('class', $this);
}
public function print()
@ -259,6 +261,41 @@ abstract class Report
return view($this->views['print'], ['print' => true])->with('class', $this);
}
public function array(): array
{
$data = [];
$group = Str::plural($this->group ?? $this->getSetting('group'));
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();
@ -509,11 +546,19 @@ abstract class Report
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;
}
@ -551,7 +596,6 @@ abstract class Report
$url .= $parameters;
}
return $url;
}
@ -570,6 +614,11 @@ abstract class Report
return $this->getSearchStringValue('period', $this->getSetting('period'));
}
public function getDiscount()
{
return $this->getSearchStringValue('discount');
}
public function getFields()
{
return [

View File

@ -30,6 +30,10 @@ abstract class Index extends Component
public $page;
public $permissionCreate;
public $permissionUpdate;
public $permissionDelete;
/* -- Main End -- */
/* -- Buttons Start -- */

View File

@ -483,6 +483,7 @@ abstract class Index extends Component
'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',
];
}
@ -494,6 +495,7 @@ abstract class Index extends Component
'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',
];
}

View File

@ -93,6 +93,12 @@ abstract class Form extends Component
/** @var string */
public $inputGroupClass = '';
/** @var string */
public $icon = '';
/** @var string */
public $trailing = '';
/** @var array */
public $custom_attributes = [];
@ -112,7 +118,7 @@ abstract class Form extends Component
$options = [], $option = [], string $optionKey = 'id', string $optionValue = 'name', $fullOptions = [], $checked = null, $checkedKey = null, $selected = null, $selectedKey = null, $rows = '3',
$remote = false, $multiple = false, $addNew = false, $group = false,
bool $searchable = false, bool $disabled = false, bool $readonly = false, bool $required = true, bool $notRequired = false,
string $formGroupClass = '', string $inputGroupClass = '',
string $formGroupClass = '', string $inputGroupClass = '', $icon = '', $trailing = '',
$dynamicAttributes = '',
bool $hideCurrency = false
) {
@ -146,6 +152,9 @@ abstract class Form extends Component
$this->formGroupClass = $this->getFromGroupClass($formGroupClass);
$this->inputGroupClass = $this->getInputGroupClass($inputGroupClass);
$this->icon = $icon;
$this->trailing = $trailing;
$this->custom_attributes = $this->getCustomAttributes();
$this->setDynamicAttributes($dynamicAttributes);

View File

@ -304,6 +304,9 @@ abstract class Show extends Component
/** @var bool */
public $hideRecurringMessage;
/** @var bool */
public $hideConnectMessage;
/** @var bool */
public $hideCreated;
@ -335,7 +338,7 @@ abstract class Show extends Component
string $routeDocumentShow = '', string $routeTransactionShow = '', string $textButtonAddNew = '',
bool $hideSchedule = false, bool $hideChildren = false, bool $hideConnect = false, bool $hideTransfer = false, bool $hideAttachment = false, $attachment = [],
array $connectTranslations = [], string $textRecurringType = '', bool $hideRecurringMessage = false, bool $hideCreated = false
array $connectTranslations = [], string $textRecurringType = '', bool $hideRecurringMessage = false, $hideConnectMessage = false, bool $hideCreated = false
) {
$this->type = $type;
$this->transaction = $transaction;
@ -472,6 +475,7 @@ abstract class Show extends Component
// Connect translations
$this->connectTranslations = $this->getTranslationsForConnect($type);
$this->hideConnectMessage = $hideConnectMessage;
$this->textRecurringType = $this->getTextRecurringType($type, $textRecurringType);
$this->hideRecurringMessage = $hideRecurringMessage;

View File

@ -28,6 +28,8 @@ abstract class Widget
'header' => 'components.widgets.header',
];
public array $data = [];
public function __construct($model = null)
{
$this->model = $model;

View File

@ -53,6 +53,7 @@ class Categories extends BulkAction
public function edit($request)
{
$selected = $this->getSelectedInput($request);
$types = $this->getCategoryTypes();
return $this->response('bulk-actions.settings.categories.edit', compact('selected', 'types'));

View File

@ -218,12 +218,14 @@ class RecurringCheck extends Command
$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 = [];
$template->cloneable_relations = ['taxes'];
$model = $template->duplicate();
@ -233,6 +235,8 @@ class RecurringCheck extends Command
$model->created_from = 'core::recurring';
$model->save();
$this->updateRelationTypes($model, $template->cloneable_relations);
return $model;
}
@ -266,4 +270,15 @@ class RecurringCheck extends Command
{
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]);
}
}
}

View File

@ -116,9 +116,9 @@ class Handler extends ExceptionHandler
*/
protected function unauthenticated($request, AuthenticationException $exception)
{
// Store the current url in the session
if ($request->url() !== config('app.url')) {
session(['url.intended' => $request->url()]);
// 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()

View File

@ -37,6 +37,8 @@ class BillItems extends Export implements WithParentSheet
'item_description',
'item_type',
'quantity',
'discount_type',
'discount_rate',
'price',
'total',
'tax',

View File

@ -37,6 +37,8 @@ class RecurringBillItems extends Export implements WithParentSheet
'item_description',
'item_type',
'quantity',
'discount_type',
'discount_rate',
'price',
'total',
'tax',

View File

@ -37,6 +37,8 @@ class InvoiceItems extends Export implements WithParentSheet
'item_description',
'item_type',
'quantity',
'discount_type',
'discount_rate',
'price',
'total',
'tax',

View File

@ -37,6 +37,8 @@ class RecurringInvoiceItems extends Export implements WithParentSheet
'item_description',
'item_type',
'quantity',
'discount_type',
'discount_rate',
'price',
'total',
'tax',

View File

@ -25,9 +25,11 @@ class Categories extends Export
public function fields(): array
{
return [
'code',
'name',
'type',
'color',
'description',
'parent_name',
'enabled',
];

View File

@ -43,6 +43,10 @@ class Transactions extends ApiController
*/
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));
@ -57,6 +61,10 @@ class Transactions extends ApiController
*/
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());
@ -70,6 +78,10 @@ class Transactions extends ApiController
*/
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));

View File

@ -6,6 +6,7 @@ use App\Abstracts\Http\ApiController;
use App\Http\Requests\Banking\Transaction as Request;
use App\Http\Resources\Banking\Transaction as Resource;
use App\Jobs\Banking\CreateBankingDocumentTransaction;
use App\Jobs\Banking\UpdateBankingDocumentTransaction;
use App\Jobs\Banking\DeleteTransaction;
use App\Models\Banking\Transaction;
use App\Models\Document\Document;
@ -32,7 +33,7 @@ class DocumentTransactions extends ApiController
*/
public function index($document_id)
{
$transactions = Transaction::documentId($document_id)->get();
$transactions = Transaction::with(['document', 'taxes'])->documentId($document_id)->get();
return Resource::collection($transactions);
}
@ -46,7 +47,7 @@ class DocumentTransactions extends ApiController
*/
public function show($document_id, $id)
{
$transaction = Transaction::documentId($document_id)->find($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);
@ -71,6 +72,25 @@ class DocumentTransactions extends ApiController
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.
*

View File

@ -19,7 +19,7 @@ class Documents extends ApiController
*/
public function index()
{
$documents = Document::with('contact', 'histories', 'items', 'transactions')->collect(['issued_at'=> 'desc']);
$documents = Document::with('contact', 'histories', 'items', 'item_taxes', 'totals', 'transactions')->collect(['issued_at'=> 'desc']);
return Resource::collection($documents);
}
@ -34,9 +34,33 @@ class Documents extends ApiController
{
// Check if we're querying by id or number
if (is_numeric($id)) {
$document = Document::find($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::where('document_number', $id)->first();
$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) {

View File

@ -13,9 +13,11 @@ use App\Models\Banking\Account;
use App\Models\Banking\Transaction;
use App\Models\Common\Recurring;
use App\Models\Setting\Currency;
use App\Models\Setting\Tax;
use App\Traits\Currencies;
use App\Traits\DateTime;
use App\Traits\Transactions as TransactionsTrait;
use App\Utilities\Date;
class RecurringTransactions extends Controller
{
@ -76,13 +78,16 @@ class RecurringTransactions extends Controller
$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'
'currency',
'taxes'
));
}
@ -95,7 +100,9 @@ class RecurringTransactions extends Controller
*/
public function store(Request $request)
{
$response = $this->ajaxDispatch(new CreateTransaction($request->merge(['paid_at' => $request->get('recurring_started_at')])));
$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);
@ -173,6 +180,8 @@ class RecurringTransactions extends Controller
$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(
@ -182,6 +191,7 @@ class RecurringTransactions extends Controller
'contact_type',
'recurring_transaction',
'currency',
'taxes',
'date_format'
));
}
@ -196,7 +206,9 @@ class RecurringTransactions extends Controller
*/
public function update(Transaction $recurring_transaction, Request $request)
{
$response = $this->ajaxDispatch(new UpdateTransaction($recurring_transaction, $request->merge(['paid_at' => $request->get('recurring_started_at')])));
$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);

View File

@ -39,7 +39,7 @@ class Transactions extends Controller
{
$this->setActiveTabForTransactions();
$transactions = Transaction::with('account', 'category', 'contact')->collect(['paid_at'=> 'desc']);
$transactions = Transaction::with('account', 'category', 'contact', 'taxes')->collect(['paid_at'=> 'desc']);
$total_transactions = Transaction::count();
@ -239,6 +239,19 @@ class Transactions extends Controller
*/
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']) {
@ -333,7 +346,9 @@ class Transactions extends Controller
{
event(new TransactionPrinting($transaction));
$view = view('banking.transactions.print_default', compact('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');
}
@ -351,7 +366,9 @@ class Transactions extends Controller
$currency_style = true;
$view = view('banking.transactions.print_default', compact('transaction', 'currency_style'))->render();
$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');
@ -395,7 +412,7 @@ class Transactions extends Controller
$translations = collect($this->getTranslationsForConnect($transaction->type));
$data = [
'transaction' => $transaction->load(['account', 'category'])->toJson(),
'transaction' => $transaction->load(['account', 'category', 'taxes'])->toJson(),
'currency' => $transaction->currency->toJson(),
'documents' => $documents,
'translations' => $translations->toJson(),

View File

@ -37,7 +37,8 @@ class Dashboards extends Controller
*/
public function index()
{
$dashboards = user()->dashboards()->collect();
// 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'));
}

View File

@ -245,11 +245,12 @@ class Items extends Controller
$currency_code = default_currency();
}
$autocomplete = Item::autocomplete([
$autocomplete = Item::with('taxes')->autocomplete([
'name' => $query
]);
$items = $autocomplete->get();
// Eager load taxes and tax relationships to prevent N+1 queries
$items = $autocomplete->with(['taxes.tax'])->get();
if ($items) {
foreach ($items as $item) {
@ -260,6 +261,7 @@ class Items extends Controller
$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) {

View File

@ -4,6 +4,7 @@ namespace App\Http\Controllers\Common;
use App\Abstracts\Http\Controller;
use App\Http\Requests\Common\Report as Request;
use App\Http\Requests\Common\ReportShow as ShowRequest;
use App\Jobs\Common\CreateReport;
use App\Jobs\Common\DeleteReport;
use App\Jobs\Common\UpdateReport;
@ -66,9 +67,10 @@ class Reports extends Controller
* Show the form for viewing the specified resource.
*
* @param Report $report
* @param ShowRequest $request
* @return Response
*/
public function show(Report $report)
public function show(Report $report, ShowRequest $request)
{
if (Utility::cannotShow($report->class)) {
abort(403);

View File

@ -6,10 +6,14 @@ use App\Abstracts\Http\Controller;
use App\Http\Requests\Setting\Category as Request;
use App\Jobs\Setting\CreateCategory;
use App\Models\Setting\Category;
use App\Traits\Categories as Helper;
use App\Traits\Modules;
use Illuminate\Http\Request as IRequest;
class Categories extends Controller
{
use Helper, Modules;
/**
* Instantiate a new controller instance.
*/
@ -29,19 +33,29 @@ class Categories extends Controller
*/
public function create(IRequest $request)
{
$type = $request->get('type', 'item');
$type = $request->get('type', Category::ITEM_TYPE);
$category_types = $this->getTypeCategoryTypes($type);
$hide_code_types = $this->hideCodeCategoryTypes($category_types);
$categories = collect();
Category::type($type)->enabled()->orderBy('name')->get()->each(function ($category) use (&$categories) {
$categories->push([
'id' => $category->id,
'title' => $category->name,
'level' => $category->level,
]);
});
Category::type($category_types)
->enabled()
->orderBy('name')
->get()
->each(function ($category) use (&$categories) {
$categories->push([
'id' => $category->id,
'title' => $category->name,
'level' => $category->level,
]);
});
$html = view('modals.categories.create', compact('type', 'categories'))->render();
$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,
@ -61,7 +75,7 @@ class Categories extends Controller
public function store(Request $request)
{
$request['enabled'] = 1;
$request['type'] = $request->get('type', 'income');
$request['type'] = $request->get('type', Category::ITEM_TYPE);
$request['color'] = $request->get('color', '#' . dechex(rand(0x000000, 0xFFFFFF)));
$response = $this->ajaxDispatch(new CreateCategory($request));

View File

@ -42,6 +42,8 @@ class DocumentTransactions extends Controller
*/
public function create(Document $document)
{
$document->load(['totals', 'transactions']);
$currency = Currency::where('code', $document->currency_code)->first();
$paid = $document->paid;
@ -149,6 +151,8 @@ class DocumentTransactions extends Controller
*/
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

View File

@ -44,6 +44,20 @@ class Invoices extends Controller
*/
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));

View File

@ -69,7 +69,10 @@ class Payments extends Controller
event(new TransactionPrinting($payment));
$transaction = $payment;
$view = view('banking.transactions.print_default', compact('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');
}
@ -88,7 +91,10 @@ class Payments extends Controller
$currency_style = true;
$transaction = $payment;
$view = view('banking.transactions.print_default', compact('transaction', 'currency_style'))->render();
$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');

View File

@ -45,6 +45,20 @@ class Bills extends Controller
*/
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'));
}

View File

@ -13,6 +13,7 @@ use App\Jobs\Document\UpdateDocument;
use App\Models\Common\Recurring;
use App\Models\Document\Document;
use App\Traits\Documents;
use App\Utilities\Date;
class RecurringBills extends Controller
{
@ -80,7 +81,11 @@ class RecurringBills extends Controller
*/
public function store(Request $request)
{
$response = $this->ajaxDispatch(new CreateDocument($request->merge(['issued_at' => $request->get('recurring_started_at')])));
$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);
@ -163,7 +168,11 @@ class RecurringBills extends Controller
*/
public function update(Document $recurring_bill, Request $request)
{
$response = $this->ajaxDispatch(new UpdateDocument($recurring_bill, $request->merge(['issued_at' => $request->get('recurring_started_at')])));
$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);

View File

@ -30,7 +30,20 @@ class Vendors extends Controller
*/
public function index()
{
$vendors = Contact::with('media', 'bills.histories', 'bills.totals', 'bills.transactions', 'bills.media')->vendor()->collect();
$vendors = Contact::with([
'media',
'bills.histories',
'bills.totals',
'bills.transactions',
'bills.media'
])
->withCount([
'contact_persons as contact_persons_with_email_count' => function ($query) {
$query->whereNotNull('email');
}
])
->vendor()
->collect();
return $this->response('purchases.vendors.index', compact('vendors'));
}

View File

@ -30,7 +30,20 @@ class Customers extends Controller
*/
public function index()
{
$customers = Contact::customer()->with('media', 'invoices.histories', 'invoices.totals', 'invoices.transactions', 'invoices.media')->collect();
$customers = Contact::customer()
->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'));
}

View File

@ -31,7 +31,7 @@ class Invoices extends Controller
{
$this->setActiveTabForDocuments();
$invoices = Document::invoice()->with('contact', 'items', 'item_taxes', 'last_history', 'transactions', 'totals', 'histories', 'media')->collect(['document_number'=> 'desc']);
$invoices = Document::invoice()->with('contact', 'items', 'items.taxes', 'item_taxes', 'last_history', 'transactions', 'totals', 'histories', 'media')->collect(['document_number'=> 'desc']);
$total_invoices = Document::invoice()->count();
@ -47,6 +47,20 @@ class Invoices extends Controller
*/
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'));
}

View File

@ -13,6 +13,7 @@ use App\Jobs\Document\UpdateDocument;
use App\Models\Common\Recurring;
use App\Models\Document\Document;
use App\Traits\Documents;
use App\Utilities\Date;
class RecurringInvoices extends Controller
{
@ -80,7 +81,11 @@ class RecurringInvoices extends Controller
*/
public function store(Request $request)
{
$response = $this->ajaxDispatch(new CreateDocument($request->merge(['issued_at' => $request->get('recurring_started_at')])));
$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);
@ -163,7 +168,11 @@ class RecurringInvoices extends Controller
*/
public function update(Document $recurring_invoice, Request $request)
{
$response = $this->ajaxDispatch(new UpdateDocument($recurring_invoice, $request->merge(['issued_at' => $request->get('recurring_started_at')])));
$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);

View File

@ -24,17 +24,44 @@ class Categories extends Controller
*/
public function index()
{
$this->setActiveTabForCategories();
$query = Category::with('sub_categories');
if (request()->has('search')) {
$query->withSubcategory();
if (search_string_value('searchable')) {
$query->withSubCategory();
}
$types = $this->getCategoryTypes();
$categories = $query->type(array_keys($types))->collect();
if (request()->get('list_records') == 'all') {
$query->type(array_keys($types));
}
return $this->response('settings.categories.index', compact('categories', 'types'));
$categories = $query->collect();
$tabs = $this->getCategoryTabs();
$tab = request()->get('list_records');
$tab_active = ! empty($tab) ? 'categories-' . $tab : 'categories-all';
$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 (! 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', 'tabs', 'tab_active', 'hide_code_column'));
}
/**
@ -54,14 +81,17 @@ class Categories extends Controller
*/
public function create()
{
$types = $this->getCategoryTypes();
$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,
@ -70,7 +100,7 @@ class Categories extends Controller
];
});
return view('settings.categories.create', compact('types', 'categories'));
return view('settings.categories.create', compact('types', 'categories', 'type_group', 'hide_code_types'));
}
/**
@ -134,8 +164,6 @@ class Categories extends Controller
*/
public function edit(Category $category)
{
$types = $this->getCategoryTypes();
$type_disabled = (Category::where('type', $category->type)->count() == 1) ?: false;
$edited_category_id = $category->id;
@ -146,6 +174,11 @@ class Categories extends Controller
$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;
@ -175,7 +208,7 @@ class Categories extends Controller
$parent_categories = $categories[$category->type] ?? [];
return view('settings.categories.edit', compact('category', 'types', 'type_disabled', 'categories', 'parent_categories'));
return view('settings.categories.edit', compact('category', 'types', 'type_disabled', 'categories', 'parent_categories', 'type_group', 'hide_code_types'));
}
/**

View File

@ -6,9 +6,12 @@ use App\Abstracts\Http\SettingController;
use App\Models\Banking\Account;
use App\Models\Setting\Category;
use App\Models\Setting\Tax;
use App\Traits\Categories;
class Defaults extends SettingController
{
use Categories;
public function edit()
{
$accounts = Account::enabled()->orderBy('name')->get()->pluck('title', 'id');
@ -39,11 +42,16 @@ class Defaults extends SettingController
$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',
));
}
}

View File

@ -84,6 +84,7 @@ class Kernel extends HttpKernel
'menu.admin',
'permission:read-admin-panel',
'plan.limits',
'module.subscription',
],
'wizard' => [
@ -175,6 +176,7 @@ class Kernel extends HttpKernel
'dropzone' => \App\Http\Middleware\Dropzone::class,
'header.x' => \App\Http\Middleware\AddXHeader::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,

View File

@ -0,0 +1,40 @@
<?php
namespace App\Http\Middleware;
use App\Traits\Modules;
use App\Utilities\Versions;
use Closure;
class RedirectIfHitModuleSubscription
{
use Modules;
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if (! $request->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);
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Http\Requests\Common;
use App\Abstracts\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
class ReportShow extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'start_date' => '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;
}
}
}

View File

@ -15,8 +15,14 @@ class Category extends FormRequest
{
$types = collect(config('type.category'))->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',
];

View File

@ -29,8 +29,9 @@ class Currency extends FormRequest
'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' => 'different:decimal_mark',
'thousands_separator' => 'nullable|different:decimal_mark|regex:/^[A-Za-z.,_\s-]+$/',
];
}
}

View File

@ -17,9 +17,11 @@ class Category extends JsonResource
return [
'id' => $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_from' => $this->created_from,

View File

@ -32,6 +32,10 @@ class Transactions extends Import
{
$row = parent::map($row);
if (!isset($row['type'])) {
return [];
}
$real_type = $this->getRealTypeTransaction($row['type']);
$contact_type = config('type.transaction.' . $real_type . '.contact_type', $real_type == 'income' ? 'customer' : 'vendor');
@ -49,6 +53,7 @@ class Transactions extends Import
public function prepareRules($rules): array
{
$rules['number'] = 'required|string';
$rules['type'] = 'required|string';
//$rules['currency_rate'] = 'required|gt:0';
return $rules;

View File

@ -5,6 +5,7 @@ namespace App\Imports\Purchases\Bills\Sheets;
use App\Abstracts\Import;
use App\Http\Requests\Banking\Transaction as Request;
use App\Models\Banking\Transaction as Model;
use App\Models\Setting\Category;
class BillTransactions extends Import
{
@ -36,9 +37,9 @@ class BillTransactions extends Import
$row = parent::map($row);
$row['type'] = 'expense';
$row['type'] = Model::EXPENSE_TYPE;
$row['account_id'] = $this->getAccountId($row);
$row['category_id'] = $this->getCategoryId($row, 'expense');
$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);

View File

@ -5,6 +5,7 @@ namespace App\Imports\Sales\Invoices\Sheets;
use App\Abstracts\Import;
use App\Http\Requests\Banking\Transaction as Request;
use App\Models\Banking\Transaction as Model;
use App\Models\Setting\Category;
class InvoiceTransactions extends Import
{
@ -37,10 +38,10 @@ class InvoiceTransactions extends Import
$row = parent::map($row);
$row['type'] = 'income';
$row['type'] = Model::INCOME_TYPE;
$row['currency_code'] = $this->getCurrencyCode($row);
$row['account_id'] = $this->getAccountId($row);
$row['category_id'] = $this->getCategoryId($row, 'income');
$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'];

View File

@ -15,6 +15,8 @@ class Categories extends Import
public $columns = [
'name',
'type',
'code',
'description',
];
public function model(array $row)

View File

@ -34,7 +34,9 @@ class UpdateUser extends Job implements ShouldUpdate
$media = $this->getMedia($this->request->file('picture'), 'users');
$this->model->attachMedia($media, 'picture');
} elseif (! $this->request->file('picture') && $this->model->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);
}

View File

@ -38,13 +38,6 @@ class CreateBankingDocumentTransaction extends Job implements ShouldCreate
\DB::transaction(function () {
$this->transaction = $this->dispatch(new CreateTransaction($this->request));
// Upload attachment
if ($this->request->file('attachment')) {
$media = $this->getMedia($this->request->file('attachment'), 'transactions');
$this->transaction->attachMedia($media, 'attachment');
}
$this->model->save();
$this->createHistory();

View File

@ -18,6 +18,8 @@ class UpdateBankingDocumentTransaction extends Job implements ShouldUpdate
{
use Currencies;
protected Transaction $transaction;
public function __construct(Document $model, Transaction $transaction, $request)
{
$this->model = $model;
@ -37,13 +39,6 @@ class UpdateBankingDocumentTransaction extends Job implements ShouldUpdate
\DB::transaction(function () {
$this->transaction = $this->dispatch(new UpdateTransaction($this->transaction, $this->request));
// Upload attachment
if ($this->request->file('attachment')) {
$media = $this->getMedia($this->request->file('attachment'), 'transactions');
$this->transaction->attachMedia($media, 'attachment');
}
$this->model->save();
$this->createHistory();

View File

@ -35,7 +35,9 @@ class UpdateTransaction extends Job implements ShouldUpdate
$this->model->attachMedia($media, 'attachment');
}
} elseif (! $this->request->file('attachment') && $this->model->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);
}

View File

@ -26,7 +26,9 @@ class UpdateTransfer extends Job implements ShouldUpdate
$this->model->attachMedia($media, 'attachment');
}
} elseif (! $this->request->file('attachment') && $this->model->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);
}

View File

@ -60,7 +60,7 @@ class CreateMediableForDownload extends JobShouldQueue
public function getQueuedMedia()
{
return config('excel.temporary_files.remote_disk') !== null
return config('dompdf.disk') !== null
? $this->getRemoteQueuedMedia()
: $this->getLocalQueuedMedia();
}
@ -88,12 +88,15 @@ class CreateMediableForDownload extends JobShouldQueue
public function getRemoteQueuedMedia()
{
$disk = config('excel.temporary_files.remote_disk');
$prefix = config('excel.temporary_files.remote_prefix');
$disk = config('dompdf.disk');
$content = Storage::disk($disk)->get($this->file_name);
$folder_path = 'app/temp/' . company_id() . '/bulk_actions/';
$file_name = str_replace([$prefix, '.xlsx', '.xls'], '', $this->file_name);
$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');
@ -106,7 +109,7 @@ class CreateMediableForDownload extends JobShouldQueue
->toDirectory($destination)
->upload();
Storage::disk($disk)->delete($this->file_name);
Storage::disk($disk)->delete($source);
return $media;
}

View File

@ -4,6 +4,7 @@ namespace App\Jobs\Common;
use App\Abstracts\JobShouldQueue;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use ZipArchive;
class CreateZipForDownload extends JobShouldQueue
@ -34,27 +35,76 @@ class CreateZipForDownload extends JobShouldQueue
{
$zip_archive = new ZipArchive();
$folder_path = 'app/temp/' . company_id() . '/bulk_actions/';
$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 = get_storage_path($folder_path . $this->file_name . '.zip');
$zip_path = storage_path($folder_path . $this->file_name . '.zip');
$zip_archive->open($zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE);
$total = count($this->selected);
$current = 0;
foreach ($this->selected as $selected) {
$current++;
if ($current === $total) {
$this->dispatch(new $this->class($selected, $folder_path, $zip_archive, true));
} else {
$this->dispatch(new $this->class($selected, $folder_path, $zip_archive));
}
$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;
}
}

View File

@ -28,10 +28,14 @@ class UpdateContact extends Job implements ShouldUpdate
// 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->file('logo') && $this->model->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);
}

View File

@ -20,9 +20,15 @@ class UpdateItem extends Job implements ShouldUpdate
// 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']);

View File

@ -47,10 +47,12 @@ class CreateDocumentItem extends Job implements HasOwner, HasSource, ShouldCreat
// Apply total discount to amount
if (! empty($this->request['global_discount'])) {
if ($this->request['global_discount_type'] === 'percentage') {
$item_discounted_amount -= $item_discounted_amount * ($this->request['global_discount'] / 100);
$global_discount = $item_discounted_amount * ($this->request['global_discount'] / 100);
} else {
$item_discounted_amount -= $this->request['global_discount'];
$global_discount = $this->request['global_discount'];
}
$item_discounted_amount -= $global_discount;
}
$tax_amount = 0;
@ -153,6 +155,12 @@ class CreateDocumentItem extends Job implements HasOwner, HasSource, ShouldCreat
}
}
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;

View File

@ -70,7 +70,8 @@ class CreateDocumentItemsAndTotals extends Job implements HasOwner, HasSource, S
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 - $discount_amount_total) * ($this->request['discount'] / 100);
$discount_total = $sub_total * ($this->request['discount'] / 100);
} else {
$discount_total = $this->request['discount'];
}
@ -173,17 +174,16 @@ class CreateDocumentItemsAndTotals extends Job implements HasOwner, HasSource, S
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 )
// 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'] = '';
$item['global_discount_type'] = $this->request['discount_type'];
} else {
$item['global_discount'] = $this->request['discount'];
$item['global_discount_type'] = $this->request['discount_type'];
}
}
*/
$item['created_from'] = $this->request['created_from'];
$item['created_by'] = $this->request['created_by'];
@ -211,7 +211,9 @@ class CreateDocumentItemsAndTotals extends Job implements HasOwner, HasSource, S
$document_item = $this->dispatch(new CreateDocumentItem($this->document, $item));
$item_amount = (double) $item['price'] * (double) $item['quantity'];
# This line changed for discount calcualter issue
//$item_amount = (double) $item['price'] * (double) $item['quantity'];
$item_amount = $document_item->total;
$discount_amount = 0;
@ -236,11 +238,11 @@ class CreateDocumentItemsAndTotals extends Job implements HasOwner, HasSource, S
// 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'] += $item_tax['amount'];
$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' => $item_tax['amount'],
'amount' => round((float) $item_tax['amount'], $this->document->currency->precision),
];
}
}

View File

@ -49,28 +49,20 @@ class DownloadDocument extends Job
switch ($this->method) {
case 'download':
return $pdf->download($file_name);
$response = $pdf->download($file_name);
break;
default:
if (empty($this->zip_archive)) {
return;
}
$pdf_path = get_storage_path($this->folder_path . $file_name);
// Save the PDF file into temp folder
$pdf->save($pdf_path);
$this->zip_archive->addFile($pdf_path, $file_name);
if ($this->close_zip) {
$this->zip_archive->close();
}
return;
$response = $pdf_path;
break;
}
return $response;
}
}

View File

@ -18,18 +18,21 @@ class UpdateDocument extends Job implements ShouldUpdate
public function handle(): Document
{
$this->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'];
}
$this->request['discount_rate'] = $this->request['discount'] ?? null;
event(new DocumentUpdating($this->model, $this->request));
\DB::transaction(function () {
// 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);
@ -39,7 +42,9 @@ class UpdateDocument extends Job implements ShouldUpdate
$this->model->attachMedia($media, 'attachment');
}
} elseif (! $this->request->file('attachment') && $this->model->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);
}
@ -66,6 +71,13 @@ class UpdateDocument extends Job implements ShouldUpdate
$this->model->update($this->request->all());
// Sync transaction contact_id if document contact changed
if (isset($this->request['contact_id']) && $originalContactId != $this->request['contact_id']) {
$this->model->transactions()->update([
'contact_id' => $this->request['contact_id'],
]);
}
$this->model->updateRecurring($this->request->all());
});
@ -73,4 +85,23 @@ class UpdateDocument extends Job implements ShouldUpdate
return $this->model;
}
/**
* Determine if this action is applicable.
*/
public function authorize(): void
{
$lockedStatuses = ['sent', 'received', 'viewed', 'partial', 'paid', 'overdue', 'unpaid', 'cancelled'];
if (
isset($this->request['contact_id']) &&
(int) $this->request['contact_id'] !== (int) $this->model->contact_id &&
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);
}
}
}

View File

@ -13,6 +13,7 @@ class AddCustomers extends Listener
protected $classes = [
'App\Reports\IncomeSummary',
'App\Reports\IncomeExpenseSummary',
'App\Reports\DiscountSummary',
];
/**

View File

@ -14,6 +14,7 @@ class AddDate extends Listener
'App\Reports\IncomeExpenseSummary',
'App\Reports\ProfitLoss',
'App\Reports\TaxSummary',
'App\Reports\DiscountSummary',
];
/**

View File

@ -0,0 +1,52 @@
<?php
namespace App\Listeners\Report;
use App\Abstracts\Listeners\Report as Listener;
use App\Events\Report\FilterApplying;
use App\Events\Report\FilterShowing;
class AddDiscount extends Listener
{
protected $classes = [
'App\Reports\DiscountSummary',
];
/**
* Handle filter showing event.
*
* @param $event
* @return void
*/
public function handleFilterShowing(FilterShowing $event)
{
if ($this->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);
}
}

View File

@ -26,8 +26,8 @@ class AddExpenseCategories extends Listener
}
// send true for add limit on search and filter..
$event->class->filters['categories'] = $this->getExpenseCategories(true);
$event->class->filters['routes']['categories'] = ['categories.index', 'search=type:expense enabled:1'];
$event->class->filters['categories'] = $this->getExpenseCategories();
$event->class->filters['routes']['categories'] = ['categories.index', 'search=type:' . $this->getExpenseCategoryTypes('string') . ' enabled:1'];
$event->class->filters['multiple']['categories'] = true;
}

View File

@ -26,8 +26,8 @@ class AddIncomeCategories extends Listener
}
// send true for add limit on search and filter..
$event->class->filters['categories'] = $this->getIncomeCategories(true);
$event->class->filters['routes']['categories'] = ['categories.index', 'search=type:income enabled:1'];
$event->class->filters['categories'] = $this->getIncomeCategories();
$event->class->filters['routes']['categories'] = ['categories.index', 'search=type:' . $this->getIncomeCategoryTypes('string') . ' enabled:1'];
$event->class->filters['multiple']['categories'] = true;
}

View File

@ -20,14 +20,17 @@ class AddIncomeExpenseCategories extends Listener
{
$classes = [
'App\Reports\IncomeExpenseSummary',
'App\Reports\DiscountSummary',
];
if (empty($event->class) || !in_array(get_class($event->class), $classes)) {
return;
}
$event->class->filters['categories'] = $this->getIncomeExpenseCategories(true);
$event->class->filters['routes']['categories'] = ['categories.index', 'search=type:income,expense enabled:1'];
$types = array_merge($this->getIncomeCategoryTypes(), $this->getExpenseCategoryTypes());
$event->class->filters['categories'] = $this->getIncomeExpenseCategories();
$event->class->filters['routes']['categories'] = ['categories.index', 'search=type:' . implode(',', $types) . ' enabled:1'];
$event->class->filters['multiple']['categories'] = true;
}
@ -42,6 +45,7 @@ class AddIncomeExpenseCategories extends Listener
$classes = [
'App\Reports\IncomeExpenseSummary',
'App\Reports\ProfitLoss',
'App\Reports\DiscountSummary',
];
if (empty($event->class) || !in_array(get_class($event->class), $classes)) {
@ -67,7 +71,7 @@ class AddIncomeExpenseCategories extends Listener
return;
}
$categories = Category::type(['income', 'expense'])->orderBy('name')->get();
$categories = Category::type(array_merge($this->getIncomeCategoryTypes(), $this->getExpenseCategoryTypes()))->orderBy('name')->get();
$rows = $categories->pluck('name', 'id')->toArray();
$this->setRowNamesAndValuesForCategories($event, $rows, $categories);
@ -81,10 +85,12 @@ class AddIncomeExpenseCategories extends Listener
{
foreach ($event->class->dates as $date) {
foreach ($event->class->tables as $table_key => $table_name) {
$table_keys = $table_key == Category::INCOME_TYPE ? $this->getIncomeCategoryTypes() : $this->getExpenseCategoryTypes();
foreach ($rows as $id => $name) {
$category = $categories->where('id', $id)->first();
if ($category->type != $table_key) {
if (!in_array($category->type, $table_keys)) {
continue;
}
@ -98,10 +104,12 @@ class AddIncomeExpenseCategories extends Listener
public function setTreeNodesForCategories($event, $nodes, $categories)
{
foreach ($event->class->tables as $table_key => $table_name) {
$table_keys = $table_key == Category::INCOME_TYPE ? $this->getIncomeCategoryTypes() : $this->getExpenseCategoryTypes();
foreach ($nodes as $id => $node) {
$category = $categories->where('id', $id)->first();
if ($category->type != $table_key) {
if (!in_array($category->type, $table_keys)) {
continue;
}

View File

@ -13,6 +13,7 @@ class AddPeriod extends Listener
'App\Reports\IncomeExpenseSummary',
'App\Reports\ProfitLoss',
'App\Reports\TaxSummary',
'App\Reports\DiscountSummary',
];
/**

View File

@ -13,6 +13,7 @@ class AddSearchString extends Listener
'App\Reports\IncomeExpenseSummary',
'App\Reports\ProfitLoss',
'App\Reports\TaxSummary',
'App\Reports\DiscountSummary',
];
/**
@ -27,8 +28,8 @@ class AddSearchString extends Listener
return;
}
$old = old();
$request = request()->all();
$old = old() ?? [];
$request = request()->all() ?? [];
if ($old || $request) {
$input = request('search');

View File

@ -13,6 +13,7 @@ class AddVendors extends Listener
protected $classes = [
'App\Reports\ExpenseSummary',
'App\Reports\IncomeExpenseSummary',
'App\Reports\DiscountSummary',
];
/**

View File

@ -0,0 +1,58 @@
<?php
namespace App\Listeners\Update\V31;
use App\Abstracts\Listeners\Update as Listener;
use App\Events\Install\UpdateFinished as Event;
use App\Traits\Permissions;
use Illuminate\Support\Facades\Log;
class Version3119 extends Listener
{
use Permissions;
const ALIAS = 'core';
const VERSION = '3.1.19';
/**
* Handle the event.
*
* @param $event
* @return void
*/
public function handle(Event $event)
{
if ($this->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);
}
}

View File

@ -5,12 +5,13 @@ namespace App\Models\Banking;
use App\Abstracts\Model;
use App\Models\Banking\Transaction;
use App\Traits\Currencies;
use App\Traits\Transactions;
use Bkwld\Cloner\Cloneable;
use Illuminate\Database\Eloquent\Builder;
class TransactionTax extends Model
{
use Cloneable, Currencies;
use Cloneable, Currencies, Transactions;
protected $table = 'transaction_taxes';
@ -31,34 +32,92 @@ class TransactionTax extends Model
return $this->belongsTo('App\Models\Banking\Transaction')->withDefault(['name' => trans('general.na')]);
}
public function scopeType(Builder $query, string $type)
public function scopeType(Builder $query, $types): Builder
{
return $query->where($this->qualifyColumn('type'), '=', $type);
if (empty($types)) {
return $query;
}
return $query->whereIn($this->qualifyColumn('type'), (array) $types);
}
public function scopeIncome(Builder $query)
{
return $query->where($this->qualifyColumn('type'), '=', Transaction::INCOME_TYPE);
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('document.recurring', function (Builder $query) {
$query->whereNull('deleted_at');
});
->whereHas('transaction.recurring', function (Builder $query) {
$query->whereNull('deleted_at');
});
}
public function scopeExpense(Builder $query)
{
return $query->where($this->qualifyColumn('type'), '=', Transaction::EXPENSE_TYPE);
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('document.recurring', function (Builder $query) {
$query->whereNull('deleted_at');
});
->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');
});
}
}

View File

@ -616,9 +616,10 @@ class Company extends Eloquent implements Ownable
setting()->forgetAll();
setting()->load(true);
// Override settings and currencies
// Override settings, currencies, and category types
Overrider::load('settings');
Overrider::load('currencies');
Overrider::load('categoryTypes');
event(new CompanyMadeCurrent($this));

View File

@ -303,7 +303,11 @@ class Contact extends Model
{
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;

View File

@ -5,6 +5,7 @@ namespace App\Models\Common;
use App\Abstracts\Model;
use Bkwld\Cloner\Cloneable;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Builder;
class Report extends Model
{
@ -43,6 +44,43 @@ class Report extends Model
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.
*

View File

@ -132,7 +132,7 @@ class Document extends Model
public function items()
{
return $this->hasMany('App\Models\Document\DocumentItem', 'document_id');
return $this->hasMany('App\Models\Document\DocumentItem', 'document_id')->with('taxes');
}
public function item_taxes()
@ -274,14 +274,22 @@ class Document extends Model
public function getSentAtAttribute(string $value = null)
{
$sent = $this->histories()->where('document_histories.status', 'sent')->first();
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)
{
$received = $this->histories()->where('document_histories.status', 'received')->first();
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;
}
@ -347,7 +355,11 @@ class Document extends Model
if ($discount) {
$sub_total = $this->totals->where('code', 'sub_total')->makeHidden('title')->pluck('amount')->first();
$percent = number_format((($discount * 100) / $sub_total), 0);
if ($sub_total && $sub_total > 0) {
$percent = number_format((($discount * 100) / $sub_total), 0);
} else {
$percent = 0;
}
}
return $percent;
@ -374,6 +386,11 @@ class Document extends Model
$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;
@ -406,6 +423,11 @@ class Document extends Model
$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;
@ -415,7 +437,7 @@ class Document extends Model
}
if ($transaction->reconciled) {
$reconciled_amount = +$amount;
$reconciled_amount += $amount;
}
}
}

View File

@ -50,6 +50,7 @@ class DocumentItem extends Model
'price' => 'double',
'total' => 'double',
'tax' => 'double',
'discount_rate' => 'double',
'deleted_at' => 'datetime',
];
@ -132,7 +133,7 @@ class DocumentItem extends Model
return $text;
}
public function getDiscountRateAttribute(int $value = 0)
public function getDiscountRateAttribute($value = 0)
{
$discount_rate = 0;

View File

@ -4,11 +4,13 @@ namespace App\Models\Setting;
use App\Abstracts\Model;
use App\Builders\Category as Builder;
use App\Models\Banking\Transaction;
use App\Models\Document\Document;
use App\Interfaces\Export\WithParentSheet;
use App\Relations\HasMany\Category as HasMany;
use App\Scopes\Category as Scope;
use App\Traits\Categories;
use App\Traits\DateTime;
use App\Traits\Tailwind;
use App\Traits\Transactions;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
@ -17,7 +19,7 @@ use Illuminate\Database\Eloquent\Model as EloquentModel;
class Category extends Model
{
use Categories, HasFactory, Tailwind, Transactions;
use Categories, HasFactory, Tailwind, Transactions, DateTime;
public const INCOME_TYPE = 'income';
public const EXPENSE_TYPE = 'expense';
@ -33,14 +35,14 @@ class Category extends Model
*
* @var array
*/
protected $fillable = ['company_id', 'name', 'type', 'color', 'enabled', 'created_from', 'created_by', 'parent_id'];
protected $fillable = ['company_id', 'code', 'name', 'type', 'color', 'description', 'enabled', 'created_from', 'created_by', 'parent_id'];
/**
* Sortable columns.
*
* @var array
*/
public $sortable = ['name', 'type', 'enabled'];
public $sortable = ['code', 'name', 'type', 'enabled'];
/**
* The "booted" method of the model.
@ -137,6 +139,18 @@ class Category extends Model
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.
*
@ -155,46 +169,50 @@ class Category extends Model
/**
* 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->where($this->qualifyColumn('type'), '=', 'income');
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->where($this->qualifyColumn('type'), '=', 'expense');
return $query->whereIn($this->qualifyColumn('type'), $this->getExpenseCategoryTypes());
}
/**
* 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->where($this->qualifyColumn('type'), '=', 'item');
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->where($this->qualifyColumn('type'), '=', 'other');
return $query->whereIn($this->qualifyColumn('type'), $this->getOtherCategoryTypes());
}
public function scopeName($query, $name)
@ -213,6 +231,17 @@ class Category extends Model
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.
*
@ -233,7 +262,7 @@ class Category extends Model
$search = $request->get('search');
$query->withSubcategory();
$query->withSubCategory();
$query->usingSearchString($search)->sortable($sort);
@ -261,9 +290,91 @@ class Category extends Model
/**
* Get the display name of the category.
*/
public function getDisplayNameAttribute()
public function getDisplayNameAttribute(): string
{
return $this->name . ' (' . ucfirst($this->type) . ')';
$hideCode = $this->hideCodeCategoryType($this->type);
$typeNames = $this->getCategoryTypes();
$typeName = $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;
}
/**
@ -303,6 +414,19 @@ class Category extends Model
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.
*

View File

@ -102,6 +102,16 @@ class Currency extends Model
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.
*

View File

@ -42,9 +42,10 @@ class Invitation extends Notification
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
public function toMail($notifiable): MailMessage
{
return (new MailMessage)
->from(config('mail.from.address'), config('mail.from.name'))
->line(trans('auth.invitation.message_1'))
->action(trans('auth.invitation.button'), route('register', $this->invitation->token))
->line(trans('auth.invitation.message_2'));

View File

@ -14,10 +14,18 @@ class Reset extends Notification
*/
public $token;
/**
* The email address.
*
* @var string
*/
public $email;
/**
* Create a notification instance.
*
* @param string $token
* @param string $email
*/
public function __construct($token, $email)
{
@ -42,9 +50,10 @@ class Reset extends Notification
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
public function toMail($notifiable): MailMessage
{
return (new MailMessage)
->from(config('mail.from.address'), config('mail.from.name'))
->line(trans('auth.notification.message_1'))
->action(trans('auth.notification.button'), route('reset', ['token' => $this->token, 'email' => $this->email]))
->line(trans('auth.notification.message_2'));

View File

@ -49,9 +49,10 @@ class DownloadCompleted extends Notification implements ShouldQueue
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
public function toMail($notifiable): MailMessage
{
return (new MailMessage)
->from(config('mail.from.address'), config('mail.from.name'))
->subject(trans('notifications.download.completed.title'))
->line(new HtmlString('<br><br>'))
->line(trans('notifications.download.completed.description'))

View File

@ -48,9 +48,10 @@ class DownloadFailed extends Notification implements ShouldQueue
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
public function toMail($notifiable): MailMessage
{
return (new MailMessage)
->from(config('mail.from.address'), config('mail.from.name'))
->subject(trans('notifications.download.failed.title'))
->line(new HtmlString('<br><br>'))
->line(trans('notifications.download.failed.description'))

View File

@ -49,9 +49,10 @@ class ExportCompleted extends Notification implements ShouldQueue
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
public function toMail($notifiable): MailMessage
{
return (new MailMessage)
->from(config('mail.from.address'), config('mail.from.name'))
->subject(trans('notifications.export.completed.title'))
->line(new HtmlString('<br><br>'))
->line(trans('notifications.export.completed.description'))

View File

@ -48,9 +48,10 @@ class ExportFailed extends Notification implements ShouldQueue
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
public function toMail($notifiable): MailMessage
{
return (new MailMessage)
->from(config('mail.from.address'), config('mail.from.name'))
->subject(trans('notifications.export.failed.title'))
->line(new HtmlString('<br><br>'))
->line(trans('notifications.export.failed.description'))

View File

@ -44,11 +44,12 @@ class ImportCompleted extends Notification implements ShouldQueue
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
public function toMail($notifiable): MailMessage
{
$dashboard_url = route('dashboard', ['company_id' => company_id()]);
return (new MailMessage)
->from(config('mail.from.address'), config('mail.from.name'))
->subject(trans('notifications.import.completed.title'))
->line(new HtmlString('<br><br>'))
->line(trans('notifications.import.completed.description'))

View File

@ -48,9 +48,10 @@ class ImportFailed extends Notification implements ShouldQueue
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
public function toMail($notifiable): MailMessage
{
$message = (new MailMessage)
->from(config('mail.from.address'), config('mail.from.name'))
->subject(trans('notifications.import.failed.title'))
->line(new HtmlString('<br><br>'))
->line(trans('notifications.import.failed.description'));

View File

@ -49,11 +49,12 @@ class InvalidEmail extends Notification implements ShouldQueue
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
public function toMail($notifiable): MailMessage
{
$dashboard_url = route('dashboard', ['company_id' => company_id()]);
return (new MailMessage)
->from(config('mail.from.address'), config('mail.from.name'))
->subject(trans('notifications.email.invalid.title', ['type' => $this->type]))
->line(new HtmlString('<br><br>'))
->line(trans('notifications.email.invalid.description', ['email' => $this->email]))

View File

@ -32,6 +32,7 @@ class Event extends Provider
'App\Listeners\Update\V31\Version318',
'App\Listeners\Update\V31\Version3112',
'App\Listeners\Update\V31\Version3115',
'App\Listeners\Update\V31\Version3119',
],
'Illuminate\Routing\Events\PreparingResponse' => [
'App\Listeners\Common\PreparingResponse',
@ -149,5 +150,6 @@ class Event extends Provider
'App\Listeners\Report\AddBasis',
'App\Listeners\Report\AddPeriod',
'App\Listeners\Report\AddDate',
'App\Listeners\Report\AddDiscount',
];
}

View File

@ -0,0 +1,125 @@
<?php
namespace App\Reports;
use App\Abstracts\Report;
use App\Models\Document\Document;
use App\Utilities\Recurring;
use App\Utilities\Date;
use App\Traits\Currencies;
use App\Events\Report\TotalCalculating;
use App\Events\Report\TotalCalculated;
class DiscountSummary extends Report
{
use Currencies;
public $default_name = 'reports.discount_summary';
public $icon = 'sell';
public $type = 'summary';
public $chart = [
'income' => [
'bar' => [
'colors' => [
'#8bb475',
],
],
'donut' => [
//
],
],
'expense' => [
'bar' => [
'colors' => [
'#fb7185',
],
],
'donut' => [
//
],
],
];
public function setTables()
{
$this->tables = [
'income' => trans_choice('general.incomes', 1),
'expense' => trans_choice('general.expenses', 2),
];
}
public function setData()
{
$invoices = $this->applyFilters(Document::invoice()->with('totals')->accrued(), ['date_field' => 'issued_at'])->get();
Recurring::reflect($invoices, 'issued_at');
$this->setTotals($invoices, 'issued_at', false, 'income');
// Bills
$bills = $this->applyFilters(Document::bill()->with('totals')->accrued(), ['date_field' => 'issued_at'])->get();
Recurring::reflect($bills, 'issued_at');
$this->setTotals($bills, 'issued_at', false, 'expense');
}
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->getSetting('group') . '_id';
foreach ($items as $item) {
// Make groups extensible
$item = $this->applyGroups($item);
$date = $this->getFormattedDate(Date::parse($item->$date_field));
if (!isset($item->$group_field)) {
continue;
}
$group = $item->$group_field;
$totals = $item->totals;
foreach ($totals as $total) {
if (! in_array($total->code, ['item_discount', 'discount'])) {
continue;
}
if (
!isset($this->row_values[$table][$group])
|| !isset($this->row_values[$table][$group][$date])
|| !isset($this->footer_totals[$table][$date])
) {
continue;
}
$amount = $this->convertToDefault($total->amount, $item->currency_code, $item->currency_rate);
$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 getFields()
{
return [
$this->getGroupField(),
$this->getPeriodField(),
];
}
}

View File

@ -97,4 +97,19 @@ class ProfitLoss extends Report
}
}
}
public function array(): array
{
$data = parent::array();
$net_profit = $this->net_profit;
if ($this->has_money) {
$net_profit = array_map(fn($value) => money($value)->format(), $net_profit);
}
$data['net_profit'] = $net_profit;
return $data;
}
}

View File

@ -4,11 +4,13 @@ namespace App\Reports;
use App\Abstracts\Report;
use App\Models\Banking\Transaction;
use App\Models\Banking\TransactionTax;
use App\Models\Document\Document;
use App\Models\Setting\Tax;
use App\Traits\Currencies;
use App\Utilities\Recurring;
use App\Utilities\Date;
use Illuminate\Support\Str;
class TaxSummary extends Report
{
@ -18,6 +20,8 @@ class TaxSummary extends Report
public $category = 'general.accounting';
public $group = 'tax';
public $icon = 'percent';
public $type = 'detail';
@ -68,6 +72,14 @@ class TaxSummary extends Report
break;
}
// Incomes
$incomes = $this->applyFilters(Transaction::with('taxes')->income()->isNotDocument()->isNotTransfer(), ['date_field' => 'paid_at'])->get();
$this->setTotals($incomes, 'paid_at');
// Expenses
$expenses = $this->applyFilters(Transaction::with('taxes')->expense()->isNotDocument()->isNotTransfer(), ['date_field' => 'paid_at'])->get();
$this->setTotals($expenses, 'paid_at');
}
public function setTotals($items, $date_field, $check_type = false, $table = 'default', $with_tax = true)
@ -78,6 +90,11 @@ class TaxSummary extends Report
$type = ($item->type === Document::INVOICE_TYPE || $item->type === 'income') ? 'income' : 'expense';
if ($item instanceof Transaction && empty($item->document_id)) {
$this->setTransactionTaxTotal($item, $type, $date_field);
continue;
}
$date = $this->getFormattedDate(Date::parse($item->$date_field));
if ($date_field == 'paid_at') {
@ -93,16 +110,28 @@ class TaxSummary extends Report
continue;
}
$item_total_name = $item_total->name;
if (
!isset($this->row_values[$item_total->name][$type][$date])
|| !isset($this->footer_totals[$item_total->name][$date])
!isset($this->row_values[$item_total_name][$type][$date])
|| !isset($this->footer_totals[$item_total_name][$date])
) {
continue;
$item_total_name = Str::lcfirst($item_total_name); // #Clickup@86c3nzq4r
if (!isset($this->row_values[$item_total_name][$type][$date])
|| !isset($this->footer_totals[$item_total_name][$date])
) {
continue;
}
}
if ($date_field == 'paid_at') {
$rate = ($item->amount * 100) / $type_item->amount;
$item_amount = ($item_total->amount / 100) * $rate;
if ($type_item->amount != 0) {
$rate = ($item->amount * 100) / $type_item->amount;
$item_amount = ($item_total->amount / 100) * $rate;
} else {
$item_amount = $item_total->amount;
}
} else {
$item_amount = $item_total->amount;
}
@ -110,18 +139,48 @@ class TaxSummary extends Report
$amount = $this->convertToDefault($item_amount, $item->currency_code, $item->currency_rate);
if ($type == 'income') {
$this->row_values[$item_total->name][$type][$date] += $amount;
$this->row_values[$item_total_name][$type][$date] += $amount;
$this->footer_totals[$item_total->name][$date] += $amount;
$this->footer_totals[$item_total_name][$date] += $amount;
} else {
$this->row_values[$item_total->name][$type][$date] -= $amount;
$this->row_values[$item_total_name][$type][$date] -= $amount;
$this->footer_totals[$item_total->name][$date] -= $amount;
$this->footer_totals[$item_total_name][$date] -= $amount;
}
}
}
}
public function setTransactionTaxTotal($item, $type, $date_field)
{
if (empty($item->taxes)) {
return;
}
$date = $this->getFormattedDate(Date::parse($item->$date_field));
foreach ($item->taxes as $tax) {
if (
!isset($this->row_values[$tax->name][$type][$date])
|| !isset($this->footer_totals[$tax->name][$date])
) {
continue;
}
$amount = $this->convertToDefault($tax->amount, $item->currency_code, $item->currency_rate);
if ($type == 'income') {
$this->row_values[$tax->name][$type][$date] += $amount;
$this->footer_totals[$tax->name][$date] += $amount;
} else {
$this->row_values[$tax->name][$type][$date] -= $amount;
$this->footer_totals[$tax->name][$date] -= $amount;
}
}
}
public function getFields()
{
return [

View File

@ -8,10 +8,158 @@ use Illuminate\Support\Str;
trait Categories
{
public function getCategoryTypes(bool $translate = true): array
public function isIncomeCategory(): bool
{
$types = [];
$configs = config('type.category');
$type = $this->type ?? $this->category->type ?? $this->model->type ?? Category::INCOME_TYPE;
return in_array($type, $this->getIncomeCategoryTypes());
}
public function isExpenseCategory(): bool
{
$type = $this->type ?? $this->category->type ?? $this->model->type ?? Category::EXPENSE_TYPE;
return in_array($type, $this->getExpenseCategoryTypes());
}
public function isItemCategory(): bool
{
$type = $this->type ?? $this->category->type ?? $this->model->type ?? Category::ITEM_TYPE;
return in_array($type, $this->getItemCategoryTypes());
}
public function isOtherCategory(): bool
{
$type = $this->type ?? $this->category->type ?? $this->model->type ?? Category::OTHER_TYPE;
return in_array($type, $this->getOtherCategoryTypes());
}
public function getTypeCategoryTypes(string $type, string $return = 'array'): string|array
{
switch ($type) {
case Category::INCOME_TYPE:
$types = $this->getIncomeCategoryTypes($return);
break;
case Category::EXPENSE_TYPE:
$types = $this->getExpenseCategoryTypes($return);
break;
case Category::ITEM_TYPE:
$types = $this->getItemCategoryTypes($return);
break;
case Category::OTHER_TYPE:
$types = $this->getOtherCategoryTypes($return);
break;
default:
$types = ($return == 'array') ? [$type] : $type;
}
return $types;
}
public function getIncomeCategoryTypes(string $return = 'array'): string|array
{
return $this->getCategoryTypesByIndex(Category::INCOME_TYPE, $return);
}
public function getExpenseCategoryTypes(string $return = 'array'): string|array
{
return $this->getCategoryTypesByIndex(Category::EXPENSE_TYPE, $return);
}
public function getItemCategoryTypes(string $return = 'array'): string|array
{
return $this->getCategoryTypesByIndex(Category::ITEM_TYPE, $return);
}
public function getOtherCategoryTypes(string $return = 'array'): string|array
{
return $this->getCategoryTypesByIndex(Category::OTHER_TYPE, $return);
}
public function getCategoryTypesByIndex(string $index, string $return = 'array'): string|array
{
$types = (string) setting('category.type.' . $index);
return ($return == 'array') ? explode(',', $types) : $types;
}
public function addIncomeCategoryType(string $new_type): void
{
$this->addCategoryType($new_type, Category::INCOME_TYPE);
}
public function addExpenseCategoryType(string $new_type): void
{
$this->addCategoryType($new_type, Category::EXPENSE_TYPE);
}
public function addItemCategoryType(string $new_type): void
{
$this->addCategoryType($new_type, Category::ITEM_TYPE);
}
public function addOtherCategoryType(string $new_type): void
{
$this->addCategoryType($new_type, Category::OTHER_TYPE);
}
public function addCategoryType(string $new_type, string $index): void
{
$types = !empty(setting('category.type.' . $index)) ? explode(',', setting('category.type.' . $index)) : [];
if (in_array($new_type, $types)) {
return;
}
$types[] = $new_type;
setting([
'category.type.' . $index => implode(',', $types),
])->save();
}
public function isGroupCategoryType(): bool
{
$setting_category_types = setting('category.type');
foreach ($setting_category_types as $type => $category) {
$categories = explode(',', $category);
if (count($categories) > 1) {
return true;
}
}
return false;
}
public function hideCodeCategoryType(string $type, bool $default = true): bool
{
return $this->hideCodeCategoryTypes($type)[$type] ?? $default;
}
public function hideCodeCategoryTypes(string|array $types): array
{
$types = is_string($types) ? explode(',', $types) : $types;
$type_codes = [];
foreach ($types as $type) {
$config_type = config('type.category.' . $type, []);
$type_codes[$type] = ! empty($config_type['hide']) && in_array('code', $config_type['hide']) ? true : false;
}
return $type_codes;
}
public function getCategoryTypes(bool $translate = true, bool $group = false, array $types = []): array
{
$category_types = [];
$configs = empty($types) ? config('type.category') : array_intersect_key(config('type.category'), array_flip($types));
foreach ($configs as $type => $attr) {
$plural_type = Str::plural($type);
@ -22,10 +170,47 @@ trait Categories
$name = $attr['alias'] . '::' . $name;
}
$types[$type] = $translate ? trans_choice($name, 1) : $name;
if ($group) {
$group_key = $attr['group'] ?? $type;
$category_types[$group_key][$type] = $translate ? trans_choice($name, 1) : $name;
} else {
$category_types[$type] = $translate ? trans_choice($name, 1) : $name;
}
}
return $types;
return $category_types;
}
public function getCategoryTabs(): array
{
$tabs = [];
$configs = config('type.category');
foreach ($configs as $type => $attr) {
$tab_key = 'categories-' . ($attr['group'] ?? $type);
if (isset($tabs[$tab_key])) {
$tabs[$tab_key]['key'] .= ',' . $type;
continue;
}
$plural_type = Str::plural($attr['group'] ?? $type);
$name = $attr['translation']['prefix'] . '.' . $plural_type;
if (!empty($attr['alias'])) {
$name = $attr['alias'] . '::' . $name;
}
$tabs[$tab_key] = [
'key' => $type,
'name' => trans_choice($name, 2),
'show_code' => $attr['show_code'] ?? false,
];
}
return $tabs;
}
public function getCategoryWithoutChildren(int $id): mixed
@ -36,7 +221,7 @@ trait Categories
public function getTransferCategoryId(): mixed
{
// 1 hour set cache for same query
return Cache::remember('transferCategoryId', 60, function () {
return Cache::remember('transferCategoryId.' . company_id(), 60, function () {
return Category::other()->pluck('id')->first();
});
}
@ -62,4 +247,16 @@ trait Categories
return $ids;
}
/**
* Finds existing maximum code and increase it
*
* @return mixed
*/
public function getNextCategoryCode()
{
return Category::isNotSubCategory()->get(['code'])->reject(function ($category) {
return !preg_match('/^[0-9]*$/', $category->code);
})->max('code') + 1;
}
}

View File

@ -53,7 +53,7 @@ trait DateTime
return [$start, $end];
}
public function getFinancialStart($year = null): Date
public function getFinancialStart($year = null, $date = null): Date
{
$start_of_year = Date::now()->startOfYear();
@ -64,9 +64,8 @@ trait DateTime
$year = $year ?? $start_of_year->year;
$financial_start = Date::create($year, $month, $day);
if ((setting('localisation.financial_denote') == 'ends') && ($financial_start->dayOfYear != 1) ||
$financial_start->greaterThan(Date::now())
if ((setting('localisation.financial_denote') == 'ends') && ($financial_start->dayOfYear != 1) ||
$financial_start->greaterThan($date ?? Date::now())
) {
$financial_start->subYear();
}
@ -74,10 +73,10 @@ trait DateTime
return $financial_start;
}
public function getFinancialWeek($year = null): CarbonPeriod
public function getFinancialWeek($year = null, $date = null): CarbonPeriod
{
$today = Date::today();
$financial_weeks = $this->getFinancialWeeks($year);
$financial_weeks = $this->getFinancialWeeks($year, $date);
foreach ($financial_weeks as $week) {
if ($today->lessThan($week->getStartDate()) || $today->greaterThan($week->getEndDate())) {
@ -96,10 +95,10 @@ trait DateTime
return $this_week;
}
public function getFinancialMonth($year = null): CarbonPeriod
public function getFinancialMonth($year = null, $date = null): CarbonPeriod
{
$today = Date::today();
$financial_months = $this->getFinancialMonths($year);
$financial_months = $this->getFinancialMonths($year, $date);
foreach ($financial_months as $month) {
if ($today->lessThan($month->getStartDate()) || $today->greaterThan($month->getEndDate())) {
@ -118,10 +117,10 @@ trait DateTime
return $this_month;
}
public function getFinancialQuarter($year = null): CarbonPeriod
public function getFinancialQuarter($year = null, $date = null): CarbonPeriod
{
$today = Date::today();
$financial_quarters = $this->getFinancialQuarters($year);
$financial_quarters = $this->getFinancialQuarters($year, $date);
foreach ($financial_quarters as $quarter) {
if ($today->lessThan($quarter->getStartDate()) || $today->greaterThan($quarter->getEndDate())) {
@ -140,23 +139,23 @@ trait DateTime
return $this_quarter;
}
public function getFinancialYear($year = null): CarbonPeriod
public function getFinancialYear($year = null, $date = null): CarbonPeriod
{
$financial_start = $this->getFinancialStart($year);
$financial_start = $this->getFinancialStart($year, $date);
return CarbonPeriod::create($financial_start, $financial_start->copy()->addYear()->subDay()->endOfDay());
}
public function getFinancialWeeks($year = null): array
public function getFinancialWeeks($year = null, $date = null): array
{
$weeks = [];
$start = $this->getFinancialStart($year);
$start = $this->getFinancialStart($year, $date);
$w = 52;
if (request()->filled('start_date') && request()->filled('end_date')) {
$w = Date::parse(request('start_date'))->diffInWeeks(Date::parse(request('end_date'))) + 1;
$w = Date::parse($start->copy())->diffInWeeks(Date::parse(request('end_date'))) + 1;
}
for ($i = 0; $i < $w; $i++) {
@ -166,16 +165,16 @@ trait DateTime
return $weeks;
}
public function getFinancialMonths($year = null): array
public function getFinancialMonths($year = null, $date = null): array
{
$months = [];
$start = $this->getFinancialStart($year);
$start = $this->getFinancialStart($year, $date);
$m = 12;
if (request()->filled('start_date') && request()->filled('end_date')) {
$m = Date::parse(request('start_date'))->diffInMonths(Date::parse(request('end_date'))) + 1;
$m = Date::parse($start->copy())->diffInMonths(Date::parse(request('end_date'))) + 1;
}
for ($i = 0; $i < $m; $i++) {
@ -185,16 +184,21 @@ trait DateTime
return $months;
}
public function getFinancialQuarters($year = null): array
public function getFinancialQuarters($year = null, $date = null): array
{
$quarters = [];
$start = $this->getFinancialStart($year);
$start = $this->getFinancialStart($year, $date);
$q = 4;
/*
Previously, diffInQuarters was calculated from start_date, which caused errors
when the financial start value differed from the default value.
Therefore, this change has been made. Ticket: #8106
*/
if (request()->filled('start_date') && request()->filled('end_date')) {
$q = Date::parse(request('start_date'))->diffInQuarters(Date::parse(request('end_date'))) + 1;
$q = Date::parse($start->copy())->diffInQuarters(Date::parse(request('end_date'))) + 1;
}
for ($i = 0; $i < $q; $i++) {
@ -285,7 +289,7 @@ trait DateTime
switch ($period) {
case 'yearly':
$financial_year = $this->getFinancialYear($year);
$financial_year = $this->getFinancialYear($year, $date);
if ($date->greaterThanOrEqualTo($financial_year->getStartDate()) && $date->lessThanOrEqualTo($financial_year->getEndDate())) {
if (setting('localisation.financial_denote') == 'begins') {
@ -297,7 +301,7 @@ trait DateTime
break;
case 'quarterly':
$quarters = $this->getFinancialQuarters($year);
$quarters = $this->getFinancialQuarters($year, $date);
foreach ($quarters as $quarter) {
if ($date->lessThan($quarter->getStartDate()) || $date->greaterThan($quarter->getEndDate())) {
@ -314,7 +318,7 @@ trait DateTime
break;
case 'weekly':
$weeks = $this->getFinancialWeeks($year);
$weeks = $this->getFinancialWeeks($year, $date);
foreach ($weeks as $week) {
if ($date->lessThan($week->getStartDate()) || $date->greaterThan($week->getEndDate())) {
@ -366,4 +370,4 @@ trait DateTime
{
return $this->scopeDateFilter($query, $field);
}
}
}

View File

@ -28,7 +28,7 @@ trait Documents
public function isNotRecurringDocument(): bool
{
return ! $this->isRecurring();
return ! $this->isRecurringDocument();
}
public function getRecurringDocumentTypes() : array
@ -206,7 +206,8 @@ trait Documents
$today = Date::today()->toDateString();
$documents = $documents ?: Document::type($type)->with('transactions')->future();
// Eager load transactions with currency to prevent N+1 queries when calling getAmountConvertedToDefault()
$documents = $documents ?: Document::type($type)->with(['transactions', 'transactions.currency'])->future();
$documents->each(function ($document) use (&$totals, $today) {
if (! in_array($document->status, $this->getDocumentStatusesForFuture())) {
@ -267,6 +268,13 @@ trait Documents
return true;
}
public function getRealTypeOfDocument(string $type): string
{
$type = $this->getRealTypeOfRecurringDocument($type);
return $type;
}
public function getRealTypeOfRecurringDocument(string $recurring_type): string
{
return Str::replace('-recurring', '', $recurring_type);

Some files were not shown because too many files have changed in this diff Show More