Compare commits

...
Sign in to create a new pull request.

295 commits

Author SHA1 Message Date
David Bomba
60fc35bfc8
Merge pull request #6199 from hitjethva/patch-2
Update install.rst
2021-07-04 08:05:38 +10:00
Hitesh Jethva
b80f15ce96
Update install.rst
Correct Debian 10 and Nginx link
2021-07-03 13:47:32 +05:30
David Bomba
2bc5f1e573
Merge pull request #6193 from hitjethva/patch-1
Update install.rst
2021-07-03 09:20:09 +10:00
Hitesh Jethva
b9681a7af4
Update install.rst
Add invoice ninja installation tutorial link for Debian 10
2021-07-02 21:08:39 +05:30
David Bomba
fecd35fc3e
Merge pull request #6029 from turbo124/develop
Update license
2021-06-15 21:43:55 +10:00
David Bomba
5eb380c1f9 Update license 2021-06-15 21:42:34 +10:00
David Bomba
f8edb57f5c
Merge pull request #6017 from turbo124/develop
Update license
2021-06-14 19:15:23 +10:00
David Bomba
452bd9a1b2 Update license 2021-06-14 19:14:35 +10:00
David Bomba
ca7592b4e3
Merge pull request #5770 from dinandmentink/#2537-allow-configuring-mailgun-endpoint
Allow configuring mailgun endpoint by setting MAILGUN_ENDPOINT
2021-05-21 19:03:31 +10:00
Dinand Mentink
5bb0229a3a
Allow configuring mailgun endpoint by setting MAILGUN_ENDPOINT
Similar to https://laravel.com/docs/8.x/mail#mailgun-driver but keeping default the same as current US endpoint.
2021-05-21 10:53:12 +02:00
David Bomba
a87b2235b2
Merge pull request #5589 from turbo124/develop
Code of conduct
2021-05-03 21:25:37 +10:00
David Bomba
84abe28c78 Code of Conduct 2021-05-03 21:24:45 +10:00
David Bomba
3c395ab933 updated texts 2021-04-28 09:57:10 +10:00
David Bomba
c2511272ee
Merge pull request #5543 from turbo124/develop
Updated Texts
2021-04-28 09:44:40 +10:00
David Bomba
c95abe1936 New Texts 2021-04-28 09:44:01 +10:00
David Bomba
61be22f2d7 texts 2021-04-14 07:29:59 +10:00
David Bomba
3733b2e882
Merge pull request #5426 from turbo124/develop
Update texts
2021-04-14 07:24:48 +10:00
David Bomba
b753c3192c
Merge branch 'develop' into develop 2021-04-14 07:24:41 +10:00
David Bomba
50db50c305 update texts 2021-04-14 07:23:23 +10:00
Hillel Coren
1909878195 Update lang files 2021-04-12 20:07:21 +03:00
Hillel Coren
4e1eb7529a Update version 2021-04-02 11:22:56 +03:00
Hillel Coren
706888945a Update version 2021-04-01 12:10:41 +03:00
Benjamin Beganović
b88041540b
Merge pull request #5219 from beganovich/v4-2203-translations-sync-develop
Sync translations (develop)
2021-03-22 11:46:36 +01:00
Benjamin Beganović
b3f70e4767 sync translations 2021-03-22 11:45:59 +01:00
David Bomba
9396e11036 Update texts 2021-03-20 13:49:06 +11:00
Hillel Coren
130d8cb795 Update version 2021-03-17 13:07:36 +02:00
Hillel Coren
948a101ad8 Update languages 2021-03-17 08:41:52 +02:00
David Bomba
659c81953c Fixes for missing translations 2021-03-16 09:02:02 +11:00
David Bomba
942cfa9b94
Merge pull request #5062 from turbo124/develop
Add russian language
2021-03-09 08:13:14 +11:00
David Bomba
196fad46f6 Add russian language 2021-03-09 08:07:07 +11:00
David Bomba
7ed344a0d2
Merge pull request #5038 from turbo124/develop
Missing translations
2021-03-05 21:30:25 +11:00
David Bomba
a104c3ece8
Merge branch 'develop' into develop 2021-03-05 20:30:02 +11:00
David Bomba
38b07bbfd2 Translations 2021-03-05 20:28:10 +11:00
Hillel Coren
0c5d2cdc4e Update version 2021-02-28 08:00:54 +02:00
Hillel Coren
a7901412a0 Update PHP min version to 7.1 2021-02-19 10:22:16 +02:00
Hillel Coren
e61466cfd6 Update docs 2021-02-17 13:29:51 +02:00
Hillel Coren
7c412b16e2 Update version 2021-02-17 12:48:02 +02:00
Hillel Coren
71aa4c9bcd Update lang files 2021-02-17 12:47:03 +02:00
Benjamin Beganović
6fd119bfd1
Update texts.php 2021-02-17 11:42:40 +01:00
Hillel Coren
4629631dd0 Update lang files 2021-02-16 18:40:38 +02:00
Hillel Coren
e80672bb4c Update lang file 2021-02-16 18:34:43 +02:00
Benjamin Beganović
9b81f3250a
Update texts.php 2021-02-16 13:34:05 +01:00
David Bomba
ad77c7a38a
Merge pull request #4902 from turbo124/develop
Update texts
2021-02-15 07:53:43 +11:00
David Bomba
136e64c054 Update texts 2021-02-15 07:53:23 +11:00
Hillel Coren
e658dcd202 Update languages 2021-02-03 23:31:20 +02:00
Hillel Coren
7bb7a8b7d5 Update version 2021-02-03 23:27:12 +02:00
David Bomba
1fa5044a0d
Merge pull request #4810 from turbo124/develop
Fixes for lang file
2021-02-01 20:43:36 +11:00
David Bomba
148e4dd398 Fixes for lang file 2021-02-01 20:42:59 +11:00
=
459f95bde3 Fixes for lang file 2021-02-01 18:36:37 +11:00
David Bomba
5d20757ee5
Merge pull request #4805 from turbo124/develop
Update lang files
2021-02-01 09:03:02 +11:00
David Bomba
7d8c86684c Update lang files 2021-02-01 09:01:31 +11:00
Hillel Coren
196f2d6a17 Update lang file 2021-01-31 08:42:11 +02:00
Hillel Coren
37495b7f2f Correct typo 2021-01-21 07:59:44 +02:00
Hillel Coren
199ed4c777 Update lang file 2021-01-20 20:49:44 +02:00
Hillel Coren
e0e04c27bf Update version 2021-01-20 10:55:09 +02:00
Hillel Coren
2a873ea586 Update version 2021-01-18 09:16:44 +02:00
Hillel Coren
268bf8b059 Update version 2021-01-14 09:05:22 +02:00
Hillel Coren
fc6344c24f Update version 2021-01-10 12:34:07 +02:00
Hillel Coren
e0754666cd Update lang files 2021-01-06 08:36:59 +02:00
Hillel Coren
1052f03398 Update version 2021-01-06 08:32:24 +02:00
Hillel Coren
ea5d2393b0 Update version 2021-01-01 08:24:09 +02:00
Hillel Coren
36b41c205c Update version 2020-12-29 08:07:40 +02:00
Hillel Coren
16f9cc41de Update version 2020-12-17 08:13:08 +02:00
Hillel Coren
55706ffbb8 Update lang files 2020-12-10 21:33:59 +02:00
Hillel Coren
9f9c61435d Update version 2020-11-28 20:31:16 +02:00
Hillel Coren
e85a802380 Update version 2020-11-25 12:32:00 +02:00
Hillel Coren
34adf53e5b Update lang file 2020-11-12 20:26:30 +02:00
Hillel Coren
2419859d23 Update version 2020-11-05 10:51:26 +02:00
Hillel Coren
8f3deee107 Update language files 2020-11-01 17:50:10 +02:00
Hillel Coren
c0e7e7f3b2 Update version 2020-11-01 17:44:38 +02:00
Hillel Coren
8fcbd66bc2 Minor fixes 2020-11-01 17:35:30 +02:00
Hillel Coren
4c37e44cf3 Remove Skrill to fix #4199 2020-11-01 16:03:35 +02:00
Hillel Coren
54deb818f4 Update lang file 2020-11-01 11:58:26 +02:00
Hillel Coren
9acfb40d42 Update lang file 2020-10-15 22:32:00 +03:00
David Bomba
cf189e548d
Merge pull request #4115 from Nikdro/develop
Bugfix: Use HTMLUtils also on DB-Settings
2020-09-29 21:14:41 +10:00
Niklas Droste
f0507809cd
Bugfix: Use HTMLUtils also on DB-Settings 2020-09-28 23:17:51 +02:00
David Bomba
03138e8598
Merge pull request #3301 from sakalauskas/patch-2
[PHP 7.4] implode parameter order
2020-09-12 19:56:31 +10:00
Laurynas Sakalauskas
540afd4d5a switch implode parameter order 2020-09-12 12:10:13 +03:00
David Bomba
7edc1fb195
Merge pull request #4048 from oliverpool/clone_fix_rate
Preserve tax rates on cloning
2020-09-09 07:21:03 +10:00
oliverpool
037655e0bb Preserve tax rates on cloning
Closes #4040
2020-09-08 16:38:26 +02:00
Hillel Coren
da564aa8ec Update lang file 2020-08-30 07:04:06 +03:00
David Bomba
e66e899346
Merge pull request #3918 from woodspire/fix/missing-subscriptions
Fix missing subscriptions events.
2020-07-21 19:20:19 +10:00
Felix Labrecque
d2491323a0 Fix missing subscriptions events. 2020-07-21 04:07:24 -04:00
Hillel Coren
c20a8e5609 Update languages 2020-06-28 14:23:33 +03:00
Hillel Coren
f28b90f42a Update version 2020-06-28 14:10:26 +03:00
Hillel Coren
eb56acf03a Update lang file 2020-06-25 18:50:47 +03:00
Hillel Coren
694d3ad0f0 Update lang file 2020-06-25 18:49:22 +03:00
Leon Aves
9d2c0ea03b
Import correct Document class into HasLogo trait. (#3717)
If you aren't using the local driver for logo storage, the attempts to use Document::getDirectFileUrl in this file will fail as it tries to autoload from Traits, Document needs to be explicitly imported.
2020-05-19 21:28:36 +10:00
erichester76
cd2cbadd40
fixed bug with contact and authed user clash (#3685)
If a contact key is set and a user is also logged in. this was causing the account id to be set to that of the contact, not the authed user. broke multi-account setups.
2020-05-19 09:53:07 +10:00
David Fiel
a8c8392f8d
Fix missing word in Digital Ocean documentation (#3623)
Added the word "suggest" to the FAQ to complete a sentence
2020-04-13 11:48:46 +10:00
Hillel Coren
b3784125c2
Merge pull request #3599 from DanAtIntegrateIT/develop
Update GoCardlessV2RedirectPaymentDriver.php
2020-04-08 16:36:38 +03:00
Dan Holliday
2ef88fb859 Update GoCardlessV2RedirectPaymentDriver.php
Fixed an issue where payments from gocardless were being logged as ACH payments incorrectly.  This then stops the auto billing working for gocardless.
2020-04-07 15:47:49 +01:00
Hillel Coren
b6e925381c Update lang files 2020-04-02 08:51:16 +03:00
Hillel Coren
7f43913a3c Update version 2020-03-31 19:57:40 +03:00
Hillel Coren
762b06667c Merge branch 'develop' of github.com:invoiceninja/invoiceninja into develop 2020-03-31 13:01:02 +03:00
Hillel Coren
eb676a3b9d Update languages 2020-03-31 13:00:20 +03:00
Kristian Feldsam
ecc5a00600
Reports - Added "Total" of all currencies (#3518)
* Reports -  Invoices - Added TAX to totals

Signed-off-by: Kristián Feldsam <feldsam@gmail.com>

* Reports - Add totals calculated from all currencies

Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2020-03-24 19:51:37 +11:00
Kristian Feldsam
5f5a1a6740
Fix ExportReportResults (#3506)
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2020-03-24 19:51:11 +11:00
Hillel Coren
bfd13719c9 Merge branch 'develop' of github.com:invoiceninja/invoiceninja into develop 2020-03-15 10:12:06 +02:00
Hillel Coren
84e344b61b Fix typo in texts 2020-03-15 10:11:59 +02:00
Braunson Yager
86750892b5
Fixed Postmark API env variable (#3444)
As per #3443
2020-03-07 09:06:37 +11:00
Hillel Coren
1ed3c9df1f Update lang file 2020-03-01 09:57:50 +02:00
Hillel Coren
91e67eef75 Update lang file 2020-02-23 12:39:23 +02:00
Hillel Coren
6974bab120 Merge branch 'develop' of github.com:invoiceninja/invoiceninja into develop 2020-02-23 12:32:16 +02:00
Hillel Coren
fc3074ad51 Fix for localizaition 2020-02-23 12:32:04 +02:00
David Bomba
a98ad5ba02 Additional fixes for Checkout.com 2020-02-17 06:52:43 +11:00
David Bomba
aadda80988
fixes for checkout.com (#3337) 2020-02-16 17:41:23 +11:00
David Bomba
139e0a4b0e
Update credit_card.blade.php 2020-02-12 07:54:36 +11:00
David Bomba
d45ae2c50b
Revert "Revert "Export settings with company array (#3264)" (#3265)" (#3266)
This reverts commit ddf376090a.
2020-01-30 12:25:44 +11:00
David Bomba
ddf376090a
Revert "Export settings with company array (#3264)" (#3265)
This reverts commit ab4a44ff18.
2020-01-30 12:24:55 +11:00
Benjamin Beganović
ab4a44ff18
Export settings with company array (#3264) 2020-01-30 12:24:38 +11:00
Hillel Coren
58a09087db Fix for postmark logs 2020-01-28 19:54:24 +02:00
Benjamin Beganović
161161f628 (Patched) Export contacts with client array #3251 (#3253) 2020-01-28 07:55:35 +11:00
David Bomba
0ffa9a971d Remove last_login from migration array of clients 2020-01-24 13:16:02 +11:00
David Bomba
536c9c4a82 Fixes for export attributes 2020-01-23 09:47:50 +11:00
Benjamin Beganović
869967ba6a (v1): Export data for migration (#3238)
* Scaffold migration controller & steps

* - Basic controller scaffold
- Intro step
- Downloading data

* Import step for the v2

* Export file using account_key property

* Fix button displaying

* Client export mappings

* Company export mappings

* Add withTrashed() for clients

* Export users mappings

* Export invoices mappings

* Export company properties as array

* add withTrashed() for invoices

* Downloading the json file

* Fix 'account_id' for company export
- Code cleanup & leftovers

* Creating zip for logo & storing json as file (wip)

* Zipping & sending migration files (wip)

* Exporting & mappings for quotes

* Re-enable headers for downloading

* Mappings for tax rates

* Mappings for the products

* Fix formattings & php doc messages

* Bring back headers for downloading

* Export 'company_id', 'user_id' with clients

* Export 'id' with users resource

* Remove unused parameter

* Export mappings for payments

* Export 'line_items' with invoices

* Export credits & export client id

* Export 'credits' within main JSON

* Mappings for line_items

* Fix 'is_amount_discount' for invoice export

* Exporting migration file as .zip archive

* Improve settings card for migration

* Removed duplicated keys in texts.php for en

* Fix formatting in en/texts.php

* Integrate the migration part with rest of UI

* Export company as object, not array

* Clean up

* Fixes for V1

* Apply formatting

* Remove comments
- Added 'ext-zip' in composer.json
- Fixed $credits transform

* Remove unused params

Co-authored-by: David Bomba <turbo124@gmail.com>
2020-01-23 08:09:22 +11:00
Rob Peck
1d1c650648 Allow tasks to be associated with products, and pass that data through to the invoice. (#3205) 2020-01-22 08:28:45 +11:00
trevDev()
7c8d9f5c24 allow scrolling in time-tracker task form (#3215) 2020-01-16 20:59:47 +10:00
Hillel Coren
af7510a194 Update prices 2019-12-22 08:54:39 +02:00
Hillel Coren
cb62e92af0 Update version 2019-12-03 11:54:03 +02:00
Hillel Coren
8aa90e1bb7 Minor fixes 2019-12-03 11:28:19 +02:00
Hillel Coren
6bbcad249d Minor fixes 2019-12-03 11:27:50 +02:00
David Bomba
9284d76481
Fixes for Ticket Mailer (#3084)
* Fixes for tickets

* Fixes for markInvitationSent
2019-11-20 18:19:27 +11:00
David Bomba
3c82ba1343
Fixes for tickets (#3083) 2019-11-20 18:12:11 +11:00
Hillel Coren
419d9ea5eb Merge branch 'develop' of github.com:invoiceninja/invoiceninja into develop 2019-11-20 08:43:40 +02:00
Hillel Coren
19080af049 Import currency fix 2019-11-20 08:43:30 +02:00
David Bomba
14ed415051
Fix for regression
Fixes an issue where client statements were not generating. using dispatch_now fixes this as the returning object is not a boolean, but the required payload.
2019-11-14 22:09:43 +11:00
Hillel Coren
b71f32a3af Update lang files 2019-11-04 11:00:57 +02:00
Hillel Coren
5255997eba Fix for mobile translations file 2019-11-04 11:00:43 +02:00
Hillel Coren
3e4bd48ed3 Update lang files 2019-10-27 11:53:55 +02:00
Hillel Coren
ddf2502df3 Minor fixes 2019-10-27 11:41:55 +02:00
Hillel Coren
76153812dc
Merge pull request #3018 from maty-arch/clone-tasks
clone tasks
2019-10-25 04:13:15 +03:00
maty-arch
dc62abd766 clone tasks 2019-10-24 21:31:35 +02:00
Christopher Di Carlo
0fccf69e62 Allow module trans helper to replace values (#3006) 2019-10-22 13:29:22 +11:00
Christopher Di Carlo
429f079cf5 Update fire method reference to handle due to Laravel 5.5 update (#3003) 2019-10-19 13:05:04 +11:00
Christopher Di Carlo
c1f8f93c7f Fix JS when default value not specified for SimpleSelectComponent (#2974) 2019-10-09 17:48:20 +11:00
Christopher Di Carlo
bdd73414c9 Fixed typo (#2972)
Fixed missing character for stub variable
2019-10-09 17:46:25 +11:00
Christopher Di Carlo
f3c6e1148c Fixed incorrect module name expansion (#2971) 2019-10-08 08:19:24 +11:00
Hillel Coren
e106eda7e2 Update lang files 2019-10-06 12:26:26 +03:00
Hillel Coren
21c4721942 Bug fixes 2019-09-29 12:29:51 +03:00
Hillel Coren
ae672a25b1 Duplicate payments 2019-09-29 12:02:51 +03:00
Hillel Coren
be39bf93d2 Merge branch 'develop' of github.com:invoiceninja/invoiceninja into develop 2019-09-29 10:27:44 +03:00
Hillel Coren
a6198ea6ec Update GrapesJS 2019-09-29 10:27:39 +03:00
Hillel Coren
96d53f49c2
Merge pull request #2953 from joshuadwire/develop
Upgrade to Laravel 5.5 and PHP 7.3
2019-09-15 12:23:51 +03:00
Joshua Dwire
ac1e48455f Upgrade modules and debugbar dependencies 2019-09-14 14:35:11 -04:00
Joshua Dwire
7f2e8bfcd6 Merge branch 'develop' of https://github.com/invoiceninja/invoiceninja into develop 2019-09-14 14:18:53 -04:00
David Bomba
75d3ab3c3d
Update Setup.SQL (#2954)
* Allow artisan to be executable

* Update setup SQL file
2019-09-14 10:12:47 +10:00
Joshua Dwire
0ce77608a4 Upgrade to Laravel 5.5 and PHP 7.3 2019-09-12 19:40:13 -04:00
Hillel Coren
d78e609081 Updated readme 2019-09-05 12:14:46 +03:00
Hillel Coren
5206bd4629 Merge branch 'develop' of github.com:invoiceninja/invoiceninja into develop 2019-09-05 07:15:43 +03:00
Hillel Coren
b0aba3359b Update docs 2019-09-05 07:15:07 +03:00
Hillel Coren
d9afbe0c4b
Merge pull request #2943 from joshuadwire/develop
Fix support for currencies with varying precisions
2019-08-30 10:56:29 +03:00
Joshua Dwire
b98a569888 Fix support for currencies with varying precisions 2019-08-29 17:11:59 -04:00
Hillel Coren
1ce3a023cd Update lang files 2019-08-29 13:17:41 +03:00
Hillel Coren
a996cd6bcf IAP 2019-08-29 13:12:43 +03:00
Hillel Coren
9357405f14 Fix for ninja Stripe payments 2019-08-29 13:10:28 +03:00
Hillel Coren
ded1acd66c In app purchase 2019-08-28 10:14:24 +03:00
Hillel Coren
ac6284a94a Update lang file 2019-08-20 22:04:49 +03:00
Hillel Coren
3e3ac4989c Fix for tests 2019-08-12 12:16:33 +03:00
Joshua Dwire
74710e9985 Update PHP version number in Digital Ocean docs (#2928) 2019-08-11 12:10:50 +10:00
Hillel Coren
c69f9e652c
Merge pull request #2925 from joshuadwire/develop
Add Digital Ocean image docs (develop)
2019-08-10 20:52:44 +03:00
Joshua Dwire
45e65b1b52 Add Digital Ocean image docs 2019-08-10 10:48:45 -04:00
Hillel Coren
f8c7ae1a73
Merge pull request #2924 from aleksanderd/ninja-fixes-develop
Ninja fixes #2917
2019-08-08 09:17:13 +03:00
Aleksander V. Dyomin
67cb2b3215 TaxRateReport: avoid additional queries in the result items loop
(cherry picked from commit d00d1e926628e5e0e83eb7570716eb3f332d6b27)
2019-08-07 17:35:50 +03:00
Aleksander V. Dyomin
de6919d37c Utils::getFromCache speedup via static variable cache
(cherry picked from commit 7d65f8b1b726075626476501a4a58540e3aeb71f)
2019-08-07 17:35:48 +03:00
Aleksander V. Dyomin
4476c9eea2 InvoiceReport: avoid additional queries in the result items loop
(cherry picked from commit 9fd6ac0d7108a7e19faa5b3eae360974c1093d73)
2019-08-07 17:35:40 +03:00
hjone72
a469870b2a Fix for subscription fails on getDisplayName() (#2912)
As per #2908 - Dev branch
2019-07-25 11:53:39 +10:00
Hillel Coren
2d70fa7ad8 Update version 2019-07-21 11:03:29 +03:00
Hillel Coren
fd39f98f74 Bug fix 2019-07-21 10:59:16 +03:00
Hillel Coren
068cda7392 Update lang files 2019-07-21 09:47:50 +03:00
Hillel Coren
1f8c4b922e Minor fixes 2019-07-21 09:28:24 +03:00
Hillel Coren
7052919731 Make 'email history' easier to find 2019-07-21 09:19:51 +03:00
Hillel Coren
acc73d53dc API fixes 2019-07-21 09:12:42 +03:00
Francisco Ferreira
f639ebae94 Fixes recurring invoices end date parsing (#2905)
* Invoice edit view: fix date parsing

View gets the date already formatted and Carbon isn't always able to parse that format automatically, so convert it back to SQL format

* RecurringInvoiceDatatable: fix status label
2019-07-18 16:26:44 +10:00
David Bomba
6263e839d8 fixes for file upload 2019-07-14 12:20:42 +10:00
David Bomba
dc0f47cd9e fixes for file upload 2019-07-14 12:17:16 +10:00
Hillel Coren
fe7035c6a6 Fix for composer install 2019-07-13 23:45:27 +03:00
Joshua Dwire
50f2c80190 Stripe 3D secure support (#2897)
* Support Stripe 3D secure

* Use the old Charge API for bank account payments
2019-07-12 14:35:35 +10:00
cheey2003
e7fe3c938f Update POSTMARK_API_TICKET_TOKEN parameter (#2901)
The parameter change is essential to ensure that outbound ticket notifications are working
2019-07-12 14:35:08 +10:00
Christopher Di Carlo
3335f78e52 Display client task rate in projects datatable (#2879)
* Display client task rate in projects datatable

Displays the client task rate in the projects datatable if a project rate has not been set.  Also adds an icon indicating what type of rate is being used.  Fixes #2527

* Added account task rate display to project datatable
2019-07-05 09:54:23 +10:00
Christopher Di Carlo
2124417c31 Added payment amount to dashboard activity. (#2889) 2019-07-05 08:21:27 +10:00
Christopher Di Carlo
0c6a8cee73 Added custom fields to recurring expense (#2878) 2019-06-27 22:07:40 +10:00
Christopher Di Carlo
048ee7181e Added client usually pays in average days (#2876)
* Added client usually pays in average days

* Added translation for days label

* Code cleanup

* Added client usually pays in average days

* Added translation for days label

* Code cleanup

* Limit calculation to most recent 20 paid invoices, eager load fix
2019-06-27 22:01:46 +10:00
Christopher Di Carlo
8b4976cacf Added default Valid Until setting (#2874)
Added field to settings and calculation of due date
2019-06-21 07:25:23 +10:00
Hillel Coren
1dada2035c
Merge pull request #2863 from OXINARF/pr/stripe-improvements
Stripe improvements
2019-06-05 09:50:13 +03:00
Francisco Ferreira
cee4652b7c
Stripe: fill payment card details 2019-06-04 13:15:52 +01:00
Francisco Ferreira
8e8414ab49
View credit card: send more billing data to Stripe when available 2019-06-04 13:15:52 +01:00
Hillel Coren
8ed210d618
Merge pull request #2847 from RnBConsulting/develop
Also check archived/trashed invoice_id associated with a task
2019-05-27 18:08:56 +03:00
Andreas Böhler
51dd22d7b5 Also check archived/trashed invoiced associated with a task 2019-05-27 14:22:57 +02:00
Hillel Coren
f134ee1b51 Fix for tests 2019-05-26 21:58:09 +03:00
Hillel Coren
b97b47f7cc Update version 2019-05-26 21:31:01 +03:00
Hillel Coren
f4a4ccc3b1 Update lang files 2019-05-26 21:22:50 +03:00
Hillel Coren
3a13d99dff Update docs 2019-05-26 19:31:48 +03:00
Hillel Coren
23bf67b69b Fix overlapping text 2019-05-26 18:40:22 +03:00
Hillel Coren
d2fab1c747 Fix recurring invoice status label 2019-05-26 18:31:45 +03:00
Hillel Coren
800db04f1e Fix for Stripe import issue 2019-05-26 18:24:15 +03:00
Hillel Coren
b1c64bae6c Fix imported payment date 2019-05-26 15:25:22 +03:00
Hillel Coren
54df8e5055 Added client address to invoice/quote reports 2019-05-26 11:23:18 +03:00
Hillel Coren
1119701683 Update docs 2019-05-26 10:50:40 +03:00
Hillel Coren
4736d2bdab Set WorldPay to offsite 2019-05-26 10:50:23 +03:00
Hillel Coren
3bea02e22e Adjustments to check data 2019-05-23 21:33:22 +03:00
Hillel Coren
4db7ef0fba Typo in texts 2019-05-23 21:26:31 +03:00
Hillel Coren
f09f10753f Fix duplicate confirmation emails 2019-05-23 21:18:00 +03:00
Hillel Coren
b1689ca6de Add currencies 2019-05-22 14:41:40 +03:00
Hillel Coren
fcf846d530
Merge pull request #2841 from cab2753/2839-timezone-fix
Respect user's timezone in Utils::dateToString().
2019-05-21 11:43:10 +03:00
cab2753
904cff732c Respect user's timezone in Utils::dateToString(). 2019-05-20 18:35:30 -07:00
Hillel Coren
9a608bcc65
Merge pull request #2830 from dicarlosystems/in2816-display-contact-in-activity
Display contact in activity dashboard and client activity
2019-05-19 17:04:52 +03:00
Christopher Di Carlo
955307bdfe Added ability to set default value (#2835)
* Added ability to set default value

* Fixed bug for case when user is the entity

* Removed debugging code

* Removed debugging code
2019-05-18 15:10:04 +10:00
Christopher Di Carlo
6eb2717d13 Fixed query bug related to inclusion of contact in activity message 2019-05-17 08:27:17 -04:00
Christopher Di Carlo
97019f22e9 Updated activity messages to include contact and client 2019-05-17 08:26:04 -04:00
Christopher Di Carlo
aa97b0a9ac Fixed sorting issue if activities created with exact same timestamp 2019-05-17 08:25:40 -04:00
Christopher Di Carlo
b854115389 Display contact in activity dashboard and client activity 2019-05-09 07:07:15 -04:00
David Bomba
14c140c520
Allow artisan to be executable (#2828) 2019-05-09 08:54:10 +10:00
Kristian Feldsam
ab739ae358 Fix quote reminders - skip converted quotes (#2819)
Signed-off-by: Kristián Feldsam <feldsam@gmail.com>
2019-05-06 08:51:01 +10:00
Hillel Coren
2e3d79a700
Merge pull request #2818 from FELDSAM-INC/fix/invoice-item-vars-year-inc
invoice items - processVariables - fix year increments in JS
2019-05-04 21:58:53 +03:00
Kristián Feldsam
9df361a378 invoice items - processVariables - fix year increments in JS
Signed-off-by: Kristián Feldsam <feldsam@gmail.com>
2019-05-04 13:07:49 +02:00
Hillel Coren
f9021b4102
Merge pull request #2814 from FELDSAM-INC/feature/invoice-vars-support-yearoverlaps
Utils::processVariables count with year overlaps
2019-05-01 16:38:42 +03:00
Kristián Feldsam
925bcca617 Utils::processVariables count with year overlaps
Works fine with increments and decrements like this ":MONTH+1 :YEAR", ":QUARTER+1 :YEAR", ":MONTH-1 :YEAR" and ":QUARTER-1 :YEAR"

Known issue: Does not work with multiple increments like this one ":MONTH+6 :YEAR, :MONTH+7 :YEAR, :MONTH+8 :YEAR"

Signed-off-by: Kristián Feldsam <feldsam@gmail.com>
2019-05-01 13:21:52 +02:00
Hillel Coren
694de222a7
Merge pull request #2810 from FELDSAM-INC/feature/option-to-disable-relatime-preview
Option to disable PDF realtime preview
2019-05-01 11:44:47 +03:00
Hillel Coren
69a563cac4
Merge pull request #2813 from titan-fail/patch-1
Updated shell script URL
2019-05-01 11:43:45 +03:00
Mike Skaggs
af13bde5f4
Updated shell script URL
Updated the URL for the automatic update script to point to a git repository instead of pastebin. This should alleviate issues with DOS-style line breaks when users don't run the downloaded file through dos2unix and end up with "invalid option" errors when trying to run the script.
2019-05-01 04:40:01 -04:00
Hillel Coren
9d8291c0fe
Merge pull request #2811 from FELDSAM-INC/fix/invoice-authorizations
Fixed invoice authorizations
2019-05-01 06:16:22 +03:00
Hillel Coren
6f2c6fd905
Merge pull request #2809 from FELDSAM-INC/feature/reccuring-on-quote-clone
Enable reccuring also on quote clone
2019-05-01 06:08:12 +03:00
Hillel Coren
6a9dae79c3
Merge pull request #2808 from FELDSAM-INC/feature/recurring-invoices-dropdown-in-lists
Recurring quotes list and Invoices list - show recurring invoices dro…
2019-05-01 06:07:34 +03:00
Kristián Feldsam
d9724c3c50 Fixed invoice authorizations
Signed-off-by: Kristián Feldsam <feldsam@gmail.com>
2019-04-30 12:53:52 +02:00
Kristián Feldsam
def674a2e4 Option to disable PDF realtime preview
Signed-off-by: Kristián Feldsam <feldsam@gmail.com>
2019-04-30 08:55:05 +02:00
Kristián Feldsam
d515126a5b Recurring quotes list and Invoices list - show recurring invoices dropdown
Signed-off-by: Kristián Feldsam <feldsam@gmail.com>
2019-04-29 20:50:23 +02:00
Kristián Feldsam
097764f866 Enable reccuring also on quote clone
Signed-off-by: Kristián Feldsam <feldsam@gmail.com>
2019-04-29 20:49:21 +02:00
Hillel Coren
e14e27cac6
Merge pull request #2807 from FELDSAM-INC/feature/require-quote-approve-option
Feature/require quote approve option
2019-04-29 19:46:19 +03:00
Hillel Coren
02890eee2a
Merge pull request #2806 from FELDSAM-INC/fix/invoice-signatures
Invoices - fix requesting signature if already signed
2019-04-29 19:46:10 +03:00
Hillel Coren
17b72a8d95 Merge branch 'develop' of github.com:invoiceninja/invoiceninja into develop 2019-04-29 15:34:41 +03:00
Hillel Coren
b238e18352 Update readme 2019-04-29 15:34:31 +03:00
Kristián Feldsam
98ab5599e5 Quote workflow settings enhancements
Signed-off-by: Kristián Feldsam <feldsam@gmail.com>
2019-04-29 13:58:27 +02:00
Kristián Feldsam
7f304028e6 View Quote - Approve not required - Show link to Invoice
Signed-off-by: Kristián Feldsam <feldsam@gmail.com>
2019-04-29 12:51:28 +02:00
Kristián Feldsam
90d132ad20 BasePaymentDriver - Convert Quote - Fix Invitation
Signed-off-by: Kristián Feldsam <feldsam@gmail.com>
2019-04-29 12:07:11 +02:00
Kristián Feldsam
d7368851f5 PaymentDriver - Convert quote to invoice on payment
Signed-off-by: Kristián Feldsam <feldsam@gmail.com>
2019-04-29 11:06:19 +02:00
Kristián Feldsam
f119a6f74c Add option to disable requiring quote approving
Signed-off-by: Kristián Feldsam <feldsam@gmail.com>
2019-04-29 11:06:14 +02:00
Kristián Feldsam
e8233f6cbd Invoices - fix requesting signature if already signed
Signed-off-by: Kristián Feldsam <feldsam@gmail.com>
2019-04-29 11:04:28 +02:00
Hillel Coren
e6cb3dc5e7
Merge pull request #2804 from FELDSAM-INC/feature/quote-reminders
Quote Reminders
2019-04-29 09:45:35 +03:00
Hillel Coren
46f0cd500f
Merge pull request #2803 from FELDSAM-INC/fix/dashboard-totals-currency-chart
Dashboard totals - fix currency in chart
2019-04-29 09:45:27 +03:00
Hillel Coren
503178693f
Merge pull request #2802 from FELDSAM-INC/feature/schd-reports-ranges
Scheduled Reports - Add ranges with quarters
2019-04-29 09:45:17 +03:00
Hillel Coren
0c3c72aa8b
Merge pull request #2801 from FELDSAM-INC/feature/quarters-in-daterange
DateRangePicker added Quarters
2019-04-29 09:45:09 +03:00
Kristián Feldsam
19f3eb43eb Quote Reminders
Signed-off-by: Kristián Feldsam <feldsam@gmail.com>
2019-04-29 02:04:34 +02:00
Kristián Feldsam
31abf8b8b6 Dashboard totals - fix currency in chart
Signed-off-by: Kristián Feldsam <feldsam@gmail.com>
2019-04-28 22:59:35 +02:00
Kristián Feldsam
ec9f77b53d Scheduled Reports - Add ranges with quarters
Signed-off-by: Kristián Feldsam <feldsam@gmail.com>
2019-04-28 17:37:25 +02:00
Kristián Feldsam
8d706b62d9 DateRangePicker added Quarters
Signed-off-by: Kristián Feldsam <feldsam@gmail.com>
2019-04-28 17:35:09 +02:00
Hillel Coren
258b5c6453 Update docs 2019-04-25 15:54:06 +03:00
Hillel Coren
00700765d9 Update readme 2019-04-25 15:51:44 +03:00
Hillel Coren
733b597240 quote to invoice private notes #2742 2019-04-17 18:37:20 +03:00
Hillel Coren
a5ae3aa6bc XSS fixes 2019-04-17 18:29:20 +03:00
Hillel Coren
c666a867ab Fix endless reminder translation 2019-04-17 18:18:30 +03:00
Hillel Coren
7164c52d4b Update docs 2019-03-26 08:38:17 +02:00
Hillel Coren
cdb0c80acc Merge branch 'develop' of github.com:invoiceninja/invoiceninja into develop 2019-03-26 08:32:29 +02:00
Hillel Coren
3508cce983 Fix for task tranformer 2019-03-26 08:32:18 +02:00
Hillel Coren
96d9b162c9
Merge pull request #2745 from RnBConsulting/develop
Add user id and displayname in task status (API) v2
2019-03-22 09:00:18 +02:00
Andreas Böhler
fde4e553df Change username -> user 2019-03-22 07:55:29 +01:00
Andreas Böhler
afbd8df072 Fixes for Code Review 2019-03-21 16:46:46 +01:00
Andreas Böhler
feb2f9ae82 Add user id and displayname in task status (API) 2019-03-21 16:42:41 +01:00
Hillel Coren
a0e29467a3 Change Payment Express back to onsite gateway 2019-03-12 08:45:02 +02:00
Hillel Coren
52ac8e83d6 Fix for Payment Express 2019-03-10 09:46:56 +02:00
Hillel Coren
724381a75a Fix placholder translation 2019-02-28 15:08:21 +02:00
Hillel Coren
af51e715a5
Merge pull request #2701 from tjmv/2699-translate-selected-vendor-currency
Use translated name for the selected currency on vendor page
2019-02-28 15:06:31 +02:00
Hillel Coren
2d03b28a72 Update version 2019-02-28 09:48:20 +02:00
Hillel Coren
994899e17a Update language files 2019-02-28 09:45:55 +02:00
Hillel Coren
0d515da62e Add currencies 2019-02-28 09:39:24 +02:00
Hillel Coren
87bbe4ceea Fix for invoice transformer 2019-02-28 09:27:33 +02:00
Hillel Coren
cf24e11f14 Hide export PDF option 2019-02-28 09:26:33 +02:00
tjmv
cd4322c0c2 Use translated name for the selected currency on the create/edit vendors page. 2019-02-26 17:01:09 +01:00
Hillel Coren
bee27dbb69 Fix for tests 2019-02-25 15:26:17 +02:00
Hillel Coren
a01f879023 Merge branch 'develop' of github.com:invoiceninja/invoiceninja into develop 2019-02-25 12:39:01 +02:00
Hillel Coren
153e223010 Fix for duplicate invitations 2019-02-25 12:38:52 +02:00
Hillel Coren
f1c562be12
Merge pull request #2693 from tjmv/fix-undefined-trackevent-function
Fix a small issue with a trackEvent function call.
2019-02-25 12:27:35 +02:00
tjmv
c765200599 Fix an issue where the trackEvent function is being called before it has been defined. 2019-02-23 01:03:15 +01:00
Hillel Coren
572e307362 Added lang strings 2019-02-22 09:03:54 +02:00
Hillel Coren
d443d3ac89 Fix for GoCardless 2019-02-21 15:53:23 +02:00
Hillel Coren
c0bad70c0d Update version 2019-02-19 18:02:13 +02:00
Hillel Coren
b249bf78c0 Update language files 2019-02-19 18:01:10 +02:00
Hillel Coren
d7966689cf Fix for Google OAuth 2019-02-19 13:18:13 +02:00
Hillel Coren
64b8c599a2 Merge bug fixes 2019-02-19 13:07:29 +02:00
Hillel Coren
4418451b71 Added BOB currency 2019-02-19 12:15:50 +02:00
Hillel Coren
006bb1c6e4 Fix for Payment Express 2019-02-19 12:06:35 +02:00
Hillel Coren
bd8414f0d8 Add currencies 2019-02-19 11:47:47 +02:00
Hillel Coren
c991cde22d Bug fixes 2019-02-18 16:10:19 +02:00
Hillel Coren
46d342fa63 Bug fixes 2019-02-18 16:08:32 +02:00
Hillel Coren
aabb32b6ed Bug fixes 2019-02-18 13:05:15 +02:00
Hillel Coren
fe81900402 Fix for tests 2019-02-17 14:22:27 +02:00
Hillel Coren
cabb98a976 Update .gitignore 2019-02-17 14:02:34 +02:00
Hillel Coren
2174ce15d9 Fixes for mobile app 2019-02-17 13:57:10 +02:00
Hillel Coren
1da78d8f29
Merge pull request #2660 from OXINARF/#2114-Add_more_labels
Account: allow to set more custom Invoice labels
2019-02-10 13:41:44 +02:00
Francisco Ferreira
d71874a20a
Account: allow to set more custom Invoice labels 2019-02-10 00:50:33 +00:00
David Bomba
8eaa382604
Fixes for Google Maps (#2652) 2019-02-03 11:51:49 +11:00
David Bomba
febb465c71
Fixes for travis (#2646)
* Fixes for travis

* fixes for travis
2019-01-31 20:51:12 +11:00
David Bomba
0b501181ac
Dev branch from Hillel (#2640)
* file and dir permissions

* Dev from Hillel

* file and dir permissions
2019-01-31 08:16:04 +11:00
David Bomba
fce09b036a
file and dir permissions (#2636) 2019-01-30 22:25:07 +11:00
David Bomba
ade15ec390 Rebase Dev 2019-01-30 22:00:26 +11:00
669 changed files with 104331 additions and 59523 deletions

5
.gitignore vendored
View file

@ -9,7 +9,8 @@
/public/packages /public/packages
/public/vendor /public/vendor
/resources/assets/bower /resources/assets/bower
/storage /storage/*.key
/storage/documents
/bootstrap/compiled.php /bootstrap/compiled.php
/bootstrap/environment.php /bootstrap/environment.php
/vendor /vendor
@ -39,3 +40,5 @@ tests/_support/_generated/
/c3.php /c3.php
_ide_helper.php _ide_helper.php
storage/version.txt
storage/framework/.DS_Store

View file

@ -8,6 +8,9 @@ group: deprecated-2017Q4
php: php:
- 7.2 - 7.2
services:
- mysql
addons: addons:
hosts: hosts:
- www.ninja.test - www.ninja.test

4
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,4 @@
# Invoice Ninja Code of Conduct
The development team has invested a tremendous amount of time and energy into this project. While we appreciate that bugs can be frustrating we ask that our community refrain from insults and snide remarks. We're happy to provide support to both our hosted and selfhosted communities but ask that feedback is always polite.

88
LICENSE
View file

@ -1,47 +1,47 @@
Copyright (c) 2018 by Hillel Coren Elastic License 2.0 (ELv2)
Invoice Ninja * https://www.invoiceninja.com Elastic License
"CREATE. SEND. GET PAID"
All Rights Reserved Acceptance
ATTRIBUTION ASSURANCE LICENSE (adapted from the original BSD license) By using the software, you agree to all of the terms and conditions below.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the conditions below are met.
These conditions require a modest attribution to InvoiceNinja.com (the
"Author"), who hopes that its promotional value may help justify the
thousands of dollars in otherwise billable time invested in writing
this and other freely available, open-source software.
1. Redistributions of source code, in whole or part and with or without Copyright License
modification (the "Code"), must prominently display this GPG-signed The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below
text in verifiable form.
2. Redistributions of the Code in binary form must be accompanied by
this GPG-signed text in any documentation and, each time the resulting
executable program or a program dependent thereon is launched, a
prominent display (e.g., splash screen or banner text) of the Author's
attribution information, which includes:
(a) Name ("Hillel Coren"),
(b) Professional identification ("Invoice Ninja"), and
(c) URL ("https://www.invoiceninja.com").
3. Neither the name nor any trademark of the Author may be used to
endorse or promote products derived from this software without specific
prior written permission.
4. Users are entirely responsible, to the exclusion of the Author and
any other persons, for compliance with (1) regulations set by owners or
administrators of employed equipment, (2) licensing terms of any other
software, and (3) local regulations regarding use, including those
regarding import, export, and use of encryption software.
THIS FREE SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND Limitations
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT You may not provide the software to third parties as a hosted or managed service, where the service provides users with access to any substantial set of the features or functionality of the software.
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO You may not move, change, disable, or circumvent the license key functionality in the software, and you may not remove or obscure any functionality in the software that is protected by the license key.
EVENT SHALL THE AUTHOR OR ANY CONTRIBUTOR BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensors trademarks is subject to applicable law.
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
EFFECTS OF UNAUTHORIZED OR MALICIOUS NETWORK ACCESS; Patents
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT Notices
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms.
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. If you modify the software, you must include in any modified copies of the software prominent notices stating that you have modified the software.
No Other Rights
These terms do not imply any licenses other than those expressly granted in these terms.
Termination
If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your licenses will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently.
No Liability
As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.
Definitions
The licensor is the entity offering these terms, and the software is the software the licensor makes available under these terms, including any portion of it.
you refers to the individual or entity agreeing to these terms.
your company is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. control means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
your licenses are all the licenses granted to you for the software under these terms.
use means anything you do with the software requiring one of your licenses.
trademark means trademarks, service marks, and similar rights.
For more information regarding the interpretation of this license please see here: https://invoiceninja.github.io/docs/legal/license/

View file

@ -4,14 +4,18 @@
# Invoice Ninja # Invoice Ninja
[![Build Status](https://travis-ci.org/invoiceninja/invoiceninja.svg?branch=master)](https://travis-ci.org/invoiceninja/invoiceninja) [![Build Status](https://travis-ci.org/invoiceninja/invoiceninja.svg?branch=develop)](https://travis-ci.org/invoiceninja/invoiceninja)
[![Docs](https://readthedocs.org/projects/invoice-ninja/badge/?version=latest)](https://invoice-ninja.readthedocs.io/en/latest/?badge=latest) [![Docs](https://readthedocs.org/projects/invoice-ninja/badge/?version=latest)](https://invoice-ninja.readthedocs.io/en/latest/?badge=latest)
## [Hosted](https://www.invoiceninja.com) | [Self-Hosted](https://www.invoiceninja.org) ## [Hosted](https://www.invoiceninja.com) | [Self-Hosted](https://www.invoiceninja.org)
### We're on Slack, join us at [slack.invoiceninja.com](http://slack.invoiceninja.com) ### We're on Slack, join us at [slack.invoiceninja.com](http://slack.invoiceninja.com) or if you like [StackOverflow](https://stackoverflow.com/tags/invoice-ninja/)
All Pro and Enterprise features from the hosted app are included in the open-source code. We offer a $20 per year white-label license to remove our branding for personal use. Just make sure to add the `invoice-ninja` tag to your question.
#### Note: v5 is now in beta. To upgrade from v4 you need to [install v5](https://invoiceninja.github.io/docs/self-host-installation/) as a separate app and then use the migration tool in the latest version of v4 on Settings > Account Management.
All Pro and Enterprise features from the hosted app are included in the open-source code. We offer a $30 per year white-label license to remove the Invoice Ninja branding from client facing parts of the app.
The self-host zip includes all third party libraries whereas downloading the code from GitHub requires using Composer to install the dependencies. The self-host zip includes all third party libraries whereas downloading the code from GitHub requires using Composer to install the dependencies.
@ -19,22 +23,14 @@ The self-host zip includes all third party libraries whereas downloading the cod
* [Videos](https://www.youtube.com/channel/UCXAHcBvhW05PDtWYIq7WDFA/videos) * [Videos](https://www.youtube.com/channel/UCXAHcBvhW05PDtWYIq7WDFA/videos)
* [User Guide](https://invoice-ninja.readthedocs.io/en/latest/) * [User Guide](https://invoice-ninja.readthedocs.io/en/latest/)
* [Support Forum](https://www.invoiceninja.com/forums/forum/support/) * [Support Forum](https://www.invoiceninja.com/forums/forum/support/)
* [Roadmap](https://trello.com/b/63BbiVVe/)
## Affiliates Programs ## Referral Program
* Referral Program (we pay you) * Earn 50% of Pro & Enterprise Plans up to 4 years - [Learn more](https://www.invoiceninja.com/referral-program/)
* $100 per sign up paid over 3 years - [Learn more](https://www.invoiceninja.com/referral-program/)
* White-Label Reseller (you pay us)
* Hosted: $500 annually and either 10% of revenue or $1/user/month
* Self-Hosted: Contact us for volume license pricing
## Mobile Apps ## Mobile App
* Current: [github.com/invoiceninja/flutter-mobile](https://github.com/invoiceninja/flutter-mobile) * [iPhone](https://itunes.apple.com/us/app/invoice-ninja/id1435514417?ls=1&mt=8)
* [iPhone](https://itunes.apple.com/us/app/invoice-ninja/id1435514417?ls=1&mt=8) * [Android](https://play.google.com/store/apps/details?id=com.invoiceninja.flutter)
* [Android](https://play.google.com/store/apps/details?id=com.invoiceninja.flutter) * [Source Code](https://github.com/invoiceninja/flutter-mobile)
* Legacy
* [iPhone](https://itunes.apple.com/us/app/invoice-ninja/id1220337560?ls=1&mt=8)
* [Android](https://play.google.com/store/apps/details?id=com.invoiceninja.invoiceninja)
## Installation Options ## Installation Options
* [Ansible](https://github.com/invoiceninja/ansible-installer) * [Ansible](https://github.com/invoiceninja/ansible-installer)
@ -56,6 +52,8 @@ The self-host zip includes all third party libraries whereas downloading the cod
## Third Party Modules ## Third Party Modules
* [Event Scheduler](https://github.com/cytech/Scheduler-InvoiceNinja) * [Event Scheduler](https://github.com/cytech/Scheduler-InvoiceNinja)
* [Manufacturer Module](https://github.com/dicarlosystems/manufacturer-invoiceninja) * [Manufacturer Module](https://github.com/dicarlosystems/manufacturer-invoiceninja)
* [Point of Sale](https://github.com/dicarlosystems/pointofsale-invoiceninja)
* [Invoice Design Import/Export](https://github.com/feyst/invoicedesignexport)
> Feel free to email us for help if you're working on a module, we're happy to provide developer support. > Feel free to email us for help if you're working on a module, we're happy to provide developer support.
@ -65,10 +63,10 @@ The self-host zip includes all third party libraries whereas downloading the cod
* [Shopping Cart](https://github.com/Scifabric/invoiceninjashoppingcart) * [Shopping Cart](https://github.com/Scifabric/invoiceninjashoppingcart)
## Third Party Developers ## Third Party Developers
* [Some Techie](https://www.sometechie.com/customize-invoice-ninja/) * [Bold Compass](https://boldcompass.com/customize-invoice-ninja/)
## Contributing ## Contributing
All contributors are welcome! All contributors are welcome!
For information on how contribute to Invoice Ninja, please see our [contributing guide](CONTRIBUTING.md). For information on how contribute to Invoice Ninja, please see our [contributing guide](CONTRIBUTING.md).
## Credits ## Credits
@ -80,13 +78,16 @@ For information on how contribute to Invoice Ninja, please see our [contributing
* [Troels Liebe Bentsen](https://github.com/tlbdk) * [Troels Liebe Bentsen](https://github.com/tlbdk)
* [Jeramy Simpson](https://github.com/JeramyMywork) - [MyWork](https://www.mywork.com.au) * [Jeramy Simpson](https://github.com/JeramyMywork) - [MyWork](https://www.mywork.com.au)
* [Sigitas Limontas](https://lt.linkedin.com/in/sigitaslimontas) * [Sigitas Limontas](https://lt.linkedin.com/in/sigitaslimontas)
* [Joshua Dwire](https://github.com/joshuadwire) - [Some Techie](https://www.sometechie.com) * [Joshua Dwire](https://github.com/joshuadwire) - [Bold Compass](https://boldcompass.com/)
* [Holger Lösken](https://github.com/codedge) - [codedge](http://codedge.de) * [Holger Lösken](https://github.com/codedge) - [codedge](http://codedge.de)
* [Samuel Laulhau](https://github.com/lalop) - [Lalop](http://lalop.co/) * [Samuel Laulhau](https://github.com/lalop) - [Lalop](http://lalop.co/)
* [Alexander Zamponi](https://github.com/alexz707) * [Alexander Zamponi](https://github.com/alexz707)
* [Matthieu Calie](https://github.com/Matth--) * [Matthieu Calie](https://github.com/Matth--)
* [Suhas Sunil Gaikwad](https://github.com/Suhas-Gaikwad) - (Security) * [Christopher Di Carlo](https://github.com/dicarlosystems) - [Di Carlo Systems Inc.](https://www.dicarlosystems.ca)
* [Kristian Feldsam](https://github.com/feldsam) - [FeldHost™](https://www.feldhost.net)
* [Suhas Sunil Gaikwad](https://github.com/Suhas-Gaikwad)
* [Mike Skaggs](https://github.com/titan-fail)
## License ## License
Invoice Ninja is released under the Attribution Assurance License. Invoice Ninja is released under the Attribution Assurance License.
See [LICENSE](LICENSE) for details. See [LICENSE](LICENSE) for details.

View file

@ -57,7 +57,7 @@ class ChargeRenewalInvoices extends Command
$this->paymentService = $paymentService; $this->paymentService = $paymentService;
} }
public function fire() public function handle()
{ {
$this->info(date('r').' ChargeRenewalInvoices...'); $this->info(date('r').' ChargeRenewalInvoices...');

View file

@ -67,7 +67,7 @@ class CheckData extends Command
protected $log = ''; protected $log = '';
protected $isValid = true; protected $isValid = true;
public function fire() public function handle()
{ {
$this->logMessage(date('Y-m-d h:i:s') . ' Running CheckData...'); $this->logMessage(date('Y-m-d h:i:s') . ' Running CheckData...');
@ -91,7 +91,7 @@ class CheckData extends Command
if (! $this->option('client_id')) { if (! $this->option('client_id')) {
$this->checkOAuth(); $this->checkOAuth();
$this->checkInvitations(); //$this->checkInvitations();
$this->checkAccountData(); $this->checkAccountData();
$this->checkLookupData(); $this->checkLookupData();
$this->checkFailedJobs(); $this->checkFailedJobs();
@ -427,7 +427,7 @@ class CheckData extends Command
$queueDB = config('queue.connections.database.connection'); $queueDB = config('queue.connections.database.connection');
$count = DB::connection($queueDB)->table('failed_jobs')->count(); $count = DB::connection($queueDB)->table('failed_jobs')->count();
if ($count > 0) { if ($count > 25) {
$this->isValid = false; $this->isValid = false;
} }
@ -676,6 +676,8 @@ class CheckData extends Command
foreach ($clients as $client) { foreach ($clients as $client) {
$this->logMessage("=== Company: {$client->company_id} Account:{$client->account_id} Client:{$client->id} Balance:{$client->balance} Actual Balance:{$client->actual_balance} ==="); $this->logMessage("=== Company: {$client->company_id} Account:{$client->account_id} Client:{$client->id} Balance:{$client->balance} Actual Balance:{$client->actual_balance} ===");
/*
$foundProblem = false; $foundProblem = false;
$lastBalance = 0; $lastBalance = 0;
$lastAdjustment = 0; $lastAdjustment = 0;
@ -838,6 +840,7 @@ class CheckData extends Command
->where('id', $client->id) ->where('id', $client->id)
->update($data); ->update($data);
} }
*/
} }
} }

View file

@ -37,7 +37,7 @@ class CreateLuisData extends Command
/** /**
* @return bool * @return bool
*/ */
public function fire() public function handle()
{ {
$this->fakerField = $this->argument('faker_field'); $this->fakerField = $this->argument('faker_field');

View file

@ -2,11 +2,16 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Ticket;
use App\Models\TicketCategory;
use App\Models\TicketComment;
use App\Models\TicketTemplate;
use App\Ninja\Repositories\AccountRepository; use App\Ninja\Repositories\AccountRepository;
use App\Ninja\Repositories\ClientRepository; use App\Ninja\Repositories\ClientRepository;
use App\Ninja\Repositories\ExpenseRepository; use App\Ninja\Repositories\ExpenseRepository;
use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\InvoiceRepository;
use App\Ninja\Repositories\PaymentRepository; use App\Ninja\Repositories\PaymentRepository;
use App\Ninja\Repositories\TicketRepository;
use App\Ninja\Repositories\VendorRepository; use App\Ninja\Repositories\VendorRepository;
use App\Ninja\Repositories\TaskRepository; use App\Ninja\Repositories\TaskRepository;
use App\Ninja\Repositories\ProjectRepository; use App\Ninja\Repositories\ProjectRepository;
@ -15,6 +20,7 @@ use App\Models\TaxRate;
use App\Models\Project; use App\Models\Project;
use App\Models\ExpenseCategory; use App\Models\ExpenseCategory;
use Auth; use Auth;
use Carbon\Carbon;
use Faker\Factory; use Faker\Factory;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Utils; use Utils;
@ -48,8 +54,12 @@ class CreateTestData extends Command
* @param ExpenseRepository $expenseRepo * @param ExpenseRepository $expenseRepo
* @param TaskRepository $taskRepo * @param TaskRepository $taskRepo
* @param AccountRepository $accountRepo * @param AccountRepository $accountRepo
* @param TicketRepository $ticketRepo
* @param ProjectRepository $projectRepo
*/ */
public function __construct( public function __construct(
TicketRepository $ticketRepo,
ClientRepository $clientRepo, ClientRepository $clientRepo,
InvoiceRepository $invoiceRepo, InvoiceRepository $invoiceRepo,
PaymentRepository $paymentRepo, PaymentRepository $paymentRepo,
@ -71,12 +81,13 @@ class CreateTestData extends Command
$this->taskRepo = $taskRepo; $this->taskRepo = $taskRepo;
$this->projectRepo = $projectRepo; $this->projectRepo = $projectRepo;
$this->accountRepo = $accountRepo; $this->accountRepo = $accountRepo;
$this->ticketRepo = $ticketRepo;
} }
/** /**
* @return bool * @return bool
*/ */
public function fire() public function handle()
{ {
if (Utils::isNinjaProd()) { if (Utils::isNinjaProd()) {
$this->info('Unable to run in production'); $this->info('Unable to run in production');
@ -103,6 +114,8 @@ class CreateTestData extends Command
Auth::loginUsingId(1); Auth::loginUsingId(1);
} }
//$this->createTicketStubs();
//$this->createTicketTemplates();
$this->createClients(); $this->createClients();
$this->createVendors(); $this->createVendors();
$this->createOtherObjects(); $this->createOtherObjects();
@ -134,6 +147,84 @@ class CreateTestData extends Command
$this->createInvoices($client); $this->createInvoices($client);
$this->createInvoices($client, true); $this->createInvoices($client, true);
$this->createTasks($client); $this->createTasks($client);
$this->createTickets($client);
}
}
private function createTicketTemplates()
{
$ticketTemplate = TicketTemplate::createNew();
$ticketTemplate->name = 'Default response';
$ticketTemplate->description = $this->faker->realText(50);
$ticketTemplate->save();
$ticketTemplate = TicketTemplate::createNew();
$ticketTemplate->name = 'Updated ticket';
$ticketTemplate->description = $this->faker->realText(50);
$ticketTemplate->save();
$ticketTemplate = TicketTemplate::createNew();
$ticketTemplate->name = 'Ticket closed';
$ticketTemplate->description = $this->faker->realText(50);
$ticketTemplate->save();
$ticketTemplate = TicketTemplate::createNew();
$ticketTemplate->name = 'Generic response';
$ticketTemplate->description = $this->faker->realText(50);
$ticketTemplate->save();
}
/**
* @param $client
*/
private function createTickets($client)
{
$this->info('creating tickets');
for ($i = 0; $i < $this->count; $i++)
{
$maxTicketNumber = Ticket::getNextTicketNumber(Auth::user()->account->id);
$this->info('next ticket number = '.$maxTicketNumber);
$data = [
'priority_id'=> TICKET_PRIORITY_LOW,
'category_id'=> 1,
'client_id' => $client->id,
'is_deleted'=> 0,
'is_internal'=> (bool)random_int(0, 1),
'status_id'=> random_int(1,3),
'category_id'=> 1,
'subject'=> $this->faker->realText(10),
'description'=> $this->faker->realText(50),
'tags'=> json_encode($this->faker->words($nb = 5, $asText = false)),
'private_notes'=> $this->faker->realText(50),
'ccs'=> json_encode([]),
'contact_key'=> $client->getPrimaryContact()->contact_key,
'due_date'=> Carbon::now(),
'ticket_number' => $maxTicketNumber ? $maxTicketNumber : 1,
'action' => TICKET_SAVE_ONLY,
];
$ticket = $this->ticketRepo->save($data);
$ticketComment = TicketComment::createNew($ticket);
$ticketComment->description = $this->faker->realText(70);
$ticketComment->contact_key = $client->getPrimaryContact()->contact_key;
$ticket->comments()->save($ticketComment);
$ticketComment = TicketComment::createNew($ticket);
$ticketComment->description = $this->faker->realText(40);
$ticketComment->user_id = 1;
$ticket->comments()->save($ticketComment);
$this->info("Ticket: - {$ticket->ticket_number} - {$client->account->account_ticket_settings->ticket_number_start} - {$maxTicketNumber}");
} }
} }

View file

@ -362,6 +362,7 @@ class InitLookup extends Command
DB::statement('truncate lookup_invitations'); DB::statement('truncate lookup_invitations');
DB::statement('truncate lookup_proposal_invitations'); DB::statement('truncate lookup_proposal_invitations');
DB::statement('truncate lookup_account_tokens'); DB::statement('truncate lookup_account_tokens');
DB::statement('truncate lookup_ticket_invitations');
DB::statement('SET FOREIGN_KEY_CHECKS = 1'); DB::statement('SET FOREIGN_KEY_CHECKS = 1');
} }

View file

@ -0,0 +1,113 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Nwidart\Modules\Commands\GeneratorCommand;
use Nwidart\Modules\Support\Stub;
use Nwidart\Modules\Traits\ModuleCommandTrait;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
class MakeModuleSettings extends GeneratorCommand
{
use ModuleCommandTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
// protected $signature = 'ninja:make-module-settings {name : Module name} {--route : Add routes }';
protected $name = 'ninja:make-module-settings';
protected $argumentName = 'module';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create module settings';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
public function getTemplateContents()
{
$module = $this->laravel['modules']->findOrFail($this->getModuleName());
$path = str_replace('/', '\\', config('modules.paths.generator.module-settings-view'));
return (new Stub('/module-settings-view.stub', [
'MODULE_NAME' => $module->getName(),
'LOWER_NAME' => $module->getLowerName(),
'SHOW_ROUTES' => $this->option('route') ? true : false
]))->render();
}
public function handle() {
$this->info('Creating settings view template for ' . $this->getModuleName());
$module = $this->laravel['modules']->findOrFail($this->getModuleName());
parent::handle();
// add default routes if option specified
$route = $this->option('route');
if ($route) {
file_put_contents(
$this->getModuleRoutesFilePath(),
(new Stub('/module-settings-routes.stub', [
'MODULE_NAME' => $module->getName(),
'LOWER_NAME' => $module->getLowerName(),
]))->render(),
FILE_APPEND
);
$this->info('Added routes to module routes.php.');
}
}
protected function getModuleRoutesFilePath() {
$path = $this->laravel['modules']->getModulePath($this->getModuleName());
$seederPath = $this->laravel['modules']->config('paths.generator.module-settings-routes');
return $path . $seederPath . '/routes.php';
}
public function getDestinationFilePath()
{
$path = $this->laravel['modules']->getModulePath($this->getModuleName());
$seederPath = $this->laravel['modules']->config('paths.generator.module-settings-view');
return $path . $seederPath . '/' . $this->getFileName();
}
protected function getArguments()
{
return [
['module', InputArgument::REQUIRED, 'The name of the module.']
];
}
protected function getOptions()
{
return [
['route', null, InputOption::VALUE_NONE, 'Add default routes.', null]
];
}
/**
* @return string
*/
protected function getFileName()
{
return 'settings.blade.php';
}
}

View file

@ -0,0 +1,121 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\DbServer;
use App\Models\User;
use App\Models\Company;
use App\Libraries\CurlUtils;
class MobileLocalization extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:mobile-localization {--type=}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate mobile localization resources';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$type = strtolower($this->option('type'));
switch ($type) {
case 'laravel':
$this->laravelResources();
break;
default:
$this->flutterResources();
break;
}
}
private function laravelResources()
{
$resources = $this->getResources();
foreach ($resources as $key => $val) {
$transKey = "texts.{$key}";
if (trans($transKey) == $transKey) {
echo "'$key' => '$val',\n";
}
}
}
private function flutterResources()
{
$languages = cache('languages');
$resources = $this->getResources();
foreach ($languages as $language) {
if ($language->locale == 'en') {
continue;
}
echo "'{$language->locale}': {\n";
foreach ($resources as $key => $val) {
$text = trim(addslashes(trans("texts.{$key}", [], $language->locale)));
if (substr($text, 0, 6) == 'texts.') {
$text = $resources->$key;
}
$text = str_replace(array('<b>', '</b>'), '', $text);
$text = str_replace(array('<i>', '</i>'), '', $text);
$text = str_replace(array('<strong>', '</strong>'), '', $text);
echo "'$key': '$text',\n";
}
echo "},\n";
}
}
private function getResources()
{
$url = 'https://raw.githubusercontent.com/invoiceninja/flutter-client/develop/lib/utils/i18n.dart';
$data = CurlUtils::get($url);
$start = strpos($data, 'do not remove comment') + 25;
$end = strpos($data, '},', $start);
$data = substr($data, $start, $end - $start - 5);
$data = str_replace("\n", "", $data);
$data = str_replace("\"", "\'", $data);
$data = str_replace("'", "\"", $data);
return json_decode('{' . rtrim($data, ',') . '}');
}
protected function getOptions()
{
return [
['type', null, InputOption::VALUE_OPTIONAL, 'Type', null],
];
}
}

View file

@ -21,7 +21,7 @@ class PruneData extends Command
*/ */
protected $description = 'Delete inactive accounts'; protected $description = 'Delete inactive accounts';
public function fire() public function handle()
{ {
$this->info(date('r').' Running PruneData...'); $this->info(date('r').' Running PruneData...');

View file

@ -21,7 +21,7 @@ class RemoveOrphanedDocuments extends Command
*/ */
protected $description = 'Removes old documents not associated with an expense or invoice'; protected $description = 'Removes old documents not associated with an expense or invoice';
public function fire() public function handle()
{ {
$this->info(date('r').' Running RemoveOrphanedDocuments...'); $this->info(date('r').' Running RemoveOrphanedDocuments...');

View file

@ -21,7 +21,7 @@ class ResetData extends Command
*/ */
protected $description = 'Reset data'; protected $description = 'Reset data';
public function fire() public function handle()
{ {
$this->info(date('r') . ' Running ResetData...'); $this->info(date('r') . ' Running ResetData...');

View file

@ -0,0 +1,79 @@
<?php
namespace App\Console\Commands;
use App\Jobs\SendOverdueTicketNotification;
use App\Models\Ticket;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
/**
* Class SendOverdueTickets.
*/
class SendOverdueTickets extends Command
{
/**
* @var string
*/
protected $name = 'ninja:send-overdue-tickets';
/**
* @var string
*/
protected $description = 'Send overdue tickets';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$this->info(date('r') . ' Running SendOverdueTickets...');
if ($database = $this->option('database'))
config(['database.default' => $database]);
$this->sendReminders();
$this->info(date('r') . ' Done');
}
private function sendReminders()
{
$tickets = Ticket::with('account', 'account.account_ticket_settings')
->where('due_date', '<', Carbon::now())
->whereIn('status_id', [1,2])
->where('overdue_notification_sent', '=', 0)
->whereHas('account.account_ticket_settings', function ($query) {
$query->where('alert_ticket_overdue_agent_id', '>', '0');
})->get();
foreach($tickets as $ticket)
dispatch(new SendOverdueTicketNotification($ticket));
}
/**
* @return array
*/
protected function getArguments()
{
return [];
}
/**
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
];
}
}

View file

@ -48,7 +48,7 @@ class SendRecurringInvoices extends Command
$this->recurringExpenseRepo = $recurringExpenseRepo; $this->recurringExpenseRepo = $recurringExpenseRepo;
} }
public function fire() public function handle()
{ {
$this->info(date('r') . ' Running SendRecurringInvoices...'); $this->info(date('r') . ' Running SendRecurringInvoices...');
@ -81,8 +81,8 @@ class SendRecurringInvoices extends Command
$invoices = Invoice::with('account.timezone', 'invoice_items', 'client', 'user') $invoices = Invoice::with('account.timezone', 'invoice_items', 'client', 'user')
->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND is_recurring IS TRUE AND is_public IS TRUE AND frequency_id > 0 AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', [$today, $today]) ->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND is_recurring IS TRUE AND is_public IS TRUE AND frequency_id > 0 AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', [$today, $today])
->orderBy('id', 'asc') ->orderBy('id', 'asc')
->get(); ->cursor();
$this->info(date('r ') . $invoices->count() . ' recurring invoice(s) found'); $this->info(date('r ') . ' Recurring invoice(s) found');
foreach ($invoices as $recurInvoice) { foreach ($invoices as $recurInvoice) {
$shouldSendToday = $recurInvoice->shouldSendToday(); $shouldSendToday = $recurInvoice->shouldSendToday();

View file

@ -70,7 +70,7 @@ class SendReminders extends Command
$this->userMailer = $userMailer; $this->userMailer = $userMailer;
} }
public function fire() public function handle()
{ {
$this->info(date('r') . ' Running SendReminders...'); $this->info(date('r') . ' Running SendReminders...');
@ -141,8 +141,14 @@ class SendReminders extends Command
$account->loadLocalizationSettings($invoice->client); // support trans to add fee line item $account->loadLocalizationSettings($invoice->client); // support trans to add fee line item
$number = preg_replace('/[^0-9]/', '', $reminder); $number = preg_replace('/[^0-9]/', '', $reminder);
$amount = $account->account_email_settings->{"late_fee{$number}_amount"}; if ($invoice->isQuote()) {
$percent = $account->account_email_settings->{"late_fee{$number}_percent"}; $amount = $account->account_email_settings->{"late_fee_quote{$number}_amount"};
$percent = $account->account_email_settings->{"late_fee_quote{$number}_percent"};
} else {
$amount = $account->account_email_settings->{"late_fee{$number}_amount"};
$percent = $account->account_email_settings->{"late_fee{$number}_percent"};
}
$this->invoiceRepo->setLateFee($invoice, $amount, $percent); $this->invoiceRepo->setLateFee($invoice, $amount, $percent);
} }
} }
@ -184,6 +190,18 @@ class SendReminders extends Command
$this->info(date('r') . ' Send email: ' . $invoice->id); $this->info(date('r') . ' Send email: ' . $invoice->id);
dispatch(new SendInvoiceEmail($invoice, $invoice->user_id, 'reminder4')); dispatch(new SendInvoiceEmail($invoice, $invoice->user_id, 'reminder4'));
} }
// endless quote reminders
$invoices = $this->invoiceRepo->findNeedingEndlessReminding($account, true);
$this->info(date('r ') . $account->name . ': ' . $invoices->count() . ' endless quotes found');
foreach ($invoices as $invoice) {
if ($invoice->last_sent_date == date('Y-m-d')) {
continue;
}
$this->info(date('r') . ' Send email: ' . $invoice->id);
dispatch(new SendInvoiceEmail($invoice, $invoice->user_id, 'quote_reminder4'));
}
} }
} }
@ -211,8 +229,8 @@ class SendReminders extends Command
// send email as user // send email as user
auth()->onceUsingId($user->id); auth()->onceUsingId($user->id);
$report = dispatch(new RunReport($scheduledReport->user, $reportType, $config, true)); $report = dispatch_now(new RunReport($scheduledReport->user, $reportType, $config, $account, true));
$file = dispatch(new ExportReportResults($scheduledReport->user, $config['export_format'], $reportType, $report->exportParams)); $file = dispatch_now(new ExportReportResults($scheduledReport->user, $config['export_format'], $reportType, $report->exportParams));
if ($file) { if ($file) {
try { try {
@ -240,13 +258,24 @@ class SendReminders extends Command
if (config('ninja.exchange_rates_enabled')) { if (config('ninja.exchange_rates_enabled')) {
$this->info(date('r') . ' Loading latest exchange rates...'); $this->info(date('r') . ' Loading latest exchange rates...');
$response = CurlUtils::get(config('ninja.exchange_rates_url')); $url = config('ninja.exchange_rates_url');
$apiKey = config('ninja.exchange_rates_api_key');
$url = str_replace('{apiKey}', $apiKey, $url);
$response = CurlUtils::get($url);
$data = json_decode($response); $data = json_decode($response);
if ($data && property_exists($data, 'rates')) { if ($data && property_exists($data, 'rates') && property_exists($data, 'base')) {
Currency::whereCode(config('ninja.exchange_rates_base'))->update(['exchange_rate' => 1]); $base = config('ninja.exchange_rates_base');
// should calculate to different base
$recalculate = ($data->base != $base);
foreach ($data->rates as $code => $rate) { foreach ($data->rates as $code => $rate) {
if($recalculate) {
$rate = 1 / $data->rates->{$base} * $rate;
}
Currency::whereCode($code)->update(['exchange_rate' => $rate]); Currency::whereCode($code)->update(['exchange_rate' => $rate]);
} }
} else { } else {

View file

@ -48,7 +48,7 @@ class SendRenewalInvoices extends Command
$this->accountRepo = $repo; $this->accountRepo = $repo;
} }
public function fire() public function handle()
{ {
$this->info(date('r').' Running SendRenewalInvoices...'); $this->info(date('r').' Running SendRenewalInvoices...');

View file

@ -37,7 +37,7 @@ class TestOFX extends Command
$this->bankAccountService = $bankAccountService; $this->bankAccountService = $bankAccountService;
} }
public function fire() public function handle()
{ {
$this->info(date('r').' Running TestOFX...'); $this->info(date('r').' Running TestOFX...');
} }

View file

@ -27,7 +27,7 @@ class UpdateKey extends Command
*/ */
protected $description = 'Update application key'; protected $description = 'Update application key';
public function fire() public function handle()
{ {
$this->info(date('r') . ' Running UpdateKey...'); $this->info(date('r') . ' Running UpdateKey...');

2
app/Console/Commands/stubs/command.stub Executable file → Normal file
View file

@ -37,7 +37,7 @@ class $CLASS$ extends Command
* *
* @return mixed * @return mixed
*/ */
public function fire() public function handle()
{ {
// //
} }

0
app/Console/Commands/stubs/composer.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/controller-plain.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/controller.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/event.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/job.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/json.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/listener.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/mail.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/middleware.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/migration/add.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/migration/create.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/migration/delete.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/migration/drop.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/migration/plain.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/model.stub Executable file → Normal file
View file

View file

@ -0,0 +1,8 @@
Route::group(['middleware' => ['web', 'lookup:user', 'auth:user'], 'namespace' => 'Modules\$MODULE_NAME$\Http\Controllers'], function()
{
Route::get('settings/$LOWER_NAME$', function() {
return view('$LOWER_NAME$::settings');
});
Route::post('settings/$LOWER_NAME$', '$MODULE_NAME$Controller@saveSettings');
});

View file

@ -0,0 +1,31 @@
@extends('header')
@section('content')
@parent
@include('accounts.nav', ['selected' => '$MODULE_NAME$'])
<div class="row">
<div class="col-md-12">
{!! Former::open('settings/$MODULE_NAME$') !!}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">$MODULE_NAME$ Settings</h3>
</div>
<div class="panel-group">
<div class="form-group"></div>
<div class="form-group">
<label class="control-label col-lg-4 col-sm-4"></label>
<div class="col-lg-8 col-sm-8">
{!! Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) !!}
</div>
</div>
</div>
</div>
{!! Former::close() !!}
</div>
</div>
@stop

0
app/Console/Commands/stubs/notification.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/provider.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/request.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/route-provider.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/routes.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/scaffold/config.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/scaffold/provider.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/seeder.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/start.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/views.stub Executable file → Normal file
View file

0
app/Console/Commands/stubs/views/master.stub Executable file → Normal file
View file

View file

@ -30,6 +30,9 @@ class Kernel extends ConsoleKernel
'App\Console\Commands\InitLookup', 'App\Console\Commands\InitLookup',
'App\Console\Commands\CalculatePayouts', 'App\Console\Commands\CalculatePayouts',
'App\Console\Commands\UpdateKey', 'App\Console\Commands\UpdateKey',
'App\Console\Commands\MobileLocalization',
'App\Console\Commands\SendOverdueTickets',
'App\Console\Commands\MakeModuleSettings',
]; ];
/** /**
@ -44,13 +47,13 @@ class Kernel extends ConsoleKernel
$logFile = storage_path() . '/logs/cron.log'; $logFile = storage_path() . '/logs/cron.log';
$schedule $schedule
->command('ninja:send-invoices --force') ->command('ninja:send-invoices')
->sendOutputTo($logFile) ->sendOutputTo($logFile)
->withoutOverlapping() ->withoutOverlapping()
->hourly(); ->hourly();
$schedule $schedule
->command('ninja:send-reminders --force') ->command('ninja:send-reminders')
->sendOutputTo($logFile) ->sendOutputTo($logFile)
->daily(); ->daily();
} }

View file

@ -19,6 +19,7 @@ if (! defined('APP_NAME')) {
define('ENTITY_INVOICE_ITEM', 'invoice_item'); define('ENTITY_INVOICE_ITEM', 'invoice_item');
define('ENTITY_INVITATION', 'invitation'); define('ENTITY_INVITATION', 'invitation');
define('ENTITY_RECURRING_INVOICE', 'recurring_invoice'); define('ENTITY_RECURRING_INVOICE', 'recurring_invoice');
define('ENTITY_RECURRING_QUOTE', 'recurring_quote');
define('ENTITY_PAYMENT', 'payment'); define('ENTITY_PAYMENT', 'payment');
define('ENTITY_CREDIT', 'credit'); define('ENTITY_CREDIT', 'credit');
define('ENTITY_QUOTE', 'quote'); define('ENTITY_QUOTE', 'quote');
@ -47,6 +48,13 @@ if (! defined('APP_NAME')) {
define('ENTITY_PROPOSAL_SNIPPET', 'proposal_snippet'); define('ENTITY_PROPOSAL_SNIPPET', 'proposal_snippet');
define('ENTITY_PROPOSAL_CATEGORY', 'proposal_category'); define('ENTITY_PROPOSAL_CATEGORY', 'proposal_category');
define('ENTITY_PROPOSAL_INVITATION', 'proposal_invitation'); define('ENTITY_PROPOSAL_INVITATION', 'proposal_invitation');
define('ENTITY_TICKET', 'ticket');
define('ENTITY_TICKET_COMMENT', 'ticket_comment');
define('ENTITY_TICKET_STATUS', 'ticket_status');
define('ENTITY_TICKET_CATEGORY', 'ticket_category');
define('ENTITY_TICKET_RELATION', 'ticket_relation');
define('ENTITY_TICKET_TEMPLATE', 'ticket_template');
define('ENTITY_TICKET_INVITATION', 'ticket_invitation');
$permissionEntities = [ $permissionEntities = [
ENTITY_CLIENT, ENTITY_CLIENT,
@ -59,10 +67,11 @@ if (! defined('APP_NAME')) {
ENTITY_PROJECT, ENTITY_PROJECT,
ENTITY_PROPOSAL, ENTITY_PROPOSAL,
ENTITY_QUOTE, ENTITY_QUOTE,
'dashboard',
'reports', 'reports',
ENTITY_TICKET,
ENTITY_TASK, ENTITY_TASK,
ENTITY_VENDOR, ENTITY_VENDOR,
ENTITY_RECURRING_INVOICE,
]; ];
define('PERMISSION_ENTITIES', json_encode($permissionEntities)); define('PERMISSION_ENTITIES', json_encode($permissionEntities));
@ -95,6 +104,7 @@ if (! defined('APP_NAME')) {
define('ACCOUNT_MAP', 'import_map'); define('ACCOUNT_MAP', 'import_map');
define('ACCOUNT_EXPORT', 'export'); define('ACCOUNT_EXPORT', 'export');
define('ACCOUNT_TAX_RATES', 'tax_rates'); define('ACCOUNT_TAX_RATES', 'tax_rates');
define('ACCOUNT_TICKETS', 'tickets');
define('ACCOUNT_PRODUCTS', 'products'); define('ACCOUNT_PRODUCTS', 'products');
define('ACCOUNT_ADVANCED_SETTINGS', 'advanced_settings'); define('ACCOUNT_ADVANCED_SETTINGS', 'advanced_settings');
define('ACCOUNT_INVOICE_SETTINGS', 'invoice_settings'); define('ACCOUNT_INVOICE_SETTINGS', 'invoice_settings');
@ -162,6 +172,16 @@ if (! defined('APP_NAME')) {
define('ACTIVITY_TYPE_DELETE_TASK', 45); define('ACTIVITY_TYPE_DELETE_TASK', 45);
define('ACTIVITY_TYPE_RESTORE_TASK', 46); define('ACTIVITY_TYPE_RESTORE_TASK', 46);
define('ACTIVITY_TYPE_UPDATE_EXPENSE', 47); define('ACTIVITY_TYPE_UPDATE_EXPENSE', 47);
define('ACTIVITY_TYPE_USER_UPDATE_TICKET', 48);
define('ACTIVITY_TYPE_USER_CLOSE_TICKET', 49);
define('ACTIVITY_TYPE_USER_MERGE_TICKET', 50);
define('ACTIVITY_TYPE_USER_SPLIT_TICKET', 51);
define('ACTIVITY_TYPE_CONTACT_OPEN_TICKET', 52);
define('ACTIVITY_TYPE_CONTACT_REOPEN_TICKET', 53);
define('ACTIVITY_TYPE_USER_REOPEN_TICKET', 54);
define('ACTIVITY_TYPE_CONTACT_REPLY_TICKET', 55);
define('ACTIVITY_TYPE_USER_VIEW_TICKET', 56);
define('DEFAULT_INVOICE_NUMBER', '0001'); define('DEFAULT_INVOICE_NUMBER', '0001');
define('RECENTLY_VIEWED_LIMIT', 20); define('RECENTLY_VIEWED_LIMIT', 20);
@ -202,9 +222,9 @@ if (! defined('APP_NAME')) {
define('IMPORT_PANCAKE', 'Pancake'); define('IMPORT_PANCAKE', 'Pancake');
define('MAX_NUM_CLIENTS', 100); define('MAX_NUM_CLIENTS', 100);
define('MAX_NUM_CLIENTS_PRO', 20000); define('MAX_NUM_CLIENTS_PRO', 40000);
define('MAX_NUM_CLIENTS_LEGACY', 500); define('MAX_NUM_CLIENTS_LEGACY', 500);
define('MAX_INVOICE_AMOUNT', 1000000000); define('MAX_INVOICE_AMOUNT', 10000000000);
define('LEGACY_CUTOFF', 57800); define('LEGACY_CUTOFF', 57800);
define('ERROR_DELAY', 3); define('ERROR_DELAY', 3);
@ -361,7 +381,7 @@ if (! defined('APP_NAME')) {
define('NINJA_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com')); define('NINJA_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com'));
define('NINJA_DOCS_URL', env('NINJA_DOCS_URL', 'https://invoice-ninja.readthedocs.io/en/latest')); define('NINJA_DOCS_URL', env('NINJA_DOCS_URL', 'https://invoice-ninja.readthedocs.io/en/latest'));
define('NINJA_DATE', '2000-01-01'); define('NINJA_DATE', '2000-01-01');
define('NINJA_VERSION', '4.5.5' . env('NINJA_VERSION_SUFFIX')); define('NINJA_VERSION', '4.5.37' . env('NINJA_VERSION_SUFFIX'));
define('NINJA_TERMS_VERSION', '1.0.1'); define('NINJA_TERMS_VERSION', '1.0.1');
define('SOCIAL_LINK_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja')); define('SOCIAL_LINK_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja'));
@ -371,9 +391,9 @@ if (! defined('APP_NAME')) {
define('NINJA_FORUM_URL', env('NINJA_FORUM_URL', 'https://www.invoiceninja.com/forums/forum/support/')); define('NINJA_FORUM_URL', env('NINJA_FORUM_URL', 'https://www.invoiceninja.com/forums/forum/support/'));
define('NINJA_CONTACT_URL', env('NINJA_CONTACT_URL', 'https://www.invoiceninja.com/contact/')); define('NINJA_CONTACT_URL', env('NINJA_CONTACT_URL', 'https://www.invoiceninja.com/contact/'));
define('NINJA_FROM_EMAIL', env('NINJA_FROM_EMAIL', 'maildelivery@invoiceninja.com')); define('NINJA_FROM_EMAIL', env('NINJA_FROM_EMAIL', 'maildelivery@invoiceninja.com'));
define('NINJA_IOS_APP_URL', 'https://itunes.apple.com/WebObjects/MZStore.woa/wa/viewSoftware?id=1220337560&mt=8'); define('NINJA_IOS_APP_URL', 'https://itunes.apple.com/us/app/invoice-ninja/id1435514417?ls=1&mt=8');
define('NINJA_ANDROID_APP_URL', 'https://play.google.com/store/apps/details?id=com.invoiceninja.invoiceninja'); define('NINJA_ANDROID_APP_URL', 'https://play.google.com/store/apps/details?id=com.invoiceninja.flutter');
define('RELEASES_URL', env('RELEASES_URL', 'https://trello.com/b/63BbiVVe/invoice-ninja')); define('RELEASES_URL', env('RELEASES_URL', 'https://github.com/invoiceninja/invoiceninja/releases'));
define('ZAPIER_URL', env('ZAPIER_URL', 'https://zapier.com/zapbook/invoice-ninja')); define('ZAPIER_URL', env('ZAPIER_URL', 'https://zapier.com/zapbook/invoice-ninja'));
define('OUTDATE_BROWSER_URL', env('OUTDATE_BROWSER_URL', 'http://browsehappy.com/')); define('OUTDATE_BROWSER_URL', env('OUTDATE_BROWSER_URL', 'http://browsehappy.com/'));
define('PDFMAKE_DOCS', env('PDFMAKE_DOCS', 'http://pdfmake.org/playground.html')); define('PDFMAKE_DOCS', env('PDFMAKE_DOCS', 'http://pdfmake.org/playground.html'));
@ -414,12 +434,12 @@ if (! defined('APP_NAME')) {
define('INVOICE_DESIGNS_AFFILIATE_KEY', 'T3RS74'); define('INVOICE_DESIGNS_AFFILIATE_KEY', 'T3RS74');
define('SELF_HOST_AFFILIATE_KEY', '8S69AD'); define('SELF_HOST_AFFILIATE_KEY', '8S69AD');
define('PLAN_PRICE_PRO_MONTHLY', env('PLAN_PRICE_PRO_MONTHLY', 8)); define('PLAN_PRICE_PRO_MONTHLY', env('PLAN_PRICE_PRO_MONTHLY', 10));
define('PLAN_PRICE_ENTERPRISE_MONTHLY_2', env('PLAN_PRICE_ENTERPRISE_MONTHLY_2', 12)); define('PLAN_PRICE_ENTERPRISE_MONTHLY_2', env('PLAN_PRICE_ENTERPRISE_MONTHLY_2', 14));
define('PLAN_PRICE_ENTERPRISE_MONTHLY_5', env('PLAN_PRICE_ENTERPRISE_MONTHLY_5', 18)); define('PLAN_PRICE_ENTERPRISE_MONTHLY_5', env('PLAN_PRICE_ENTERPRISE_MONTHLY_5', 26));
define('PLAN_PRICE_ENTERPRISE_MONTHLY_10', env('PLAN_PRICE_ENTERPRISE_MONTHLY_10', 24)); define('PLAN_PRICE_ENTERPRISE_MONTHLY_10', env('PLAN_PRICE_ENTERPRISE_MONTHLY_10', 36));
define('PLAN_PRICE_ENTERPRISE_MONTHLY_20', env('PLAN_PRICE_ENTERPRISE_MONTHLY_20', 36)); define('PLAN_PRICE_ENTERPRISE_MONTHLY_20', env('PLAN_PRICE_ENTERPRISE_MONTHLY_20', 44));
define('WHITE_LABEL_PRICE', env('WHITE_LABEL_PRICE', 20)); define('WHITE_LABEL_PRICE', env('WHITE_LABEL_PRICE', 30));
define('INVOICE_DESIGNS_PRICE', env('INVOICE_DESIGNS_PRICE', 10)); define('INVOICE_DESIGNS_PRICE', env('INVOICE_DESIGNS_PRICE', 10));
define('USER_TYPE_SELF_HOST', 'SELF_HOST'); define('USER_TYPE_SELF_HOST', 'SELF_HOST');
@ -493,6 +513,10 @@ if (! defined('APP_NAME')) {
define('TEMPLATE_REMINDER2', 'reminder2'); define('TEMPLATE_REMINDER2', 'reminder2');
define('TEMPLATE_REMINDER3', 'reminder3'); define('TEMPLATE_REMINDER3', 'reminder3');
define('TEMPLATE_REMINDER4', 'reminder4'); define('TEMPLATE_REMINDER4', 'reminder4');
define('TEMPLATE_QUOTE_REMINDER1', 'quote_reminder1');
define('TEMPLATE_QUOTE_REMINDER2', 'quote_reminder2');
define('TEMPLATE_QUOTE_REMINDER3', 'quote_reminder3');
define('TEMPLATE_QUOTE_REMINDER4', 'quote_reminder4');
define('CUSTOM_MESSAGE_DASHBOARD', 'dashboard'); define('CUSTOM_MESSAGE_DASHBOARD', 'dashboard');
define('CUSTOM_MESSAGE_UNPAID_INVOICE', 'unpaid_invoice'); define('CUSTOM_MESSAGE_UNPAID_INVOICE', 'unpaid_invoice');
@ -574,6 +598,7 @@ if (! defined('APP_NAME')) {
define('FEATURE_MORE_INVOICE_DESIGNS', 'more_invoice_designs'); define('FEATURE_MORE_INVOICE_DESIGNS', 'more_invoice_designs');
define('FEATURE_QUOTES', 'quotes'); define('FEATURE_QUOTES', 'quotes');
define('FEATURE_TASKS', 'tasks'); define('FEATURE_TASKS', 'tasks');
define('FEATURE_TICKETS', 'tickets');
define('FEATURE_EXPENSES', 'expenses'); define('FEATURE_EXPENSES', 'expenses');
define('FEATURE_REPORTS', 'reports'); define('FEATURE_REPORTS', 'reports');
define('FEATURE_BUY_NOW_BUTTONS', 'buy_now_buttons'); define('FEATURE_BUY_NOW_BUTTONS', 'buy_now_buttons');
@ -663,6 +688,45 @@ if (! defined('APP_NAME')) {
// Fix for mPDF: https://github.com/kartik-v/yii2-mpdf/issues/9 // Fix for mPDF: https://github.com/kartik-v/yii2-mpdf/issues/9
define('_MPDF_TTFONTDATAPATH', storage_path('framework/cache/')); define('_MPDF_TTFONTDATAPATH', storage_path('framework/cache/'));
/** STD constatns */
if(!defined('STDIN')) define('STDIN', fopen('php://stdin', 'r'));
if(!defined('STDOUT')) define('STDOUT', fopen('php://stdout', 'w'));
if(!defined('STDERR')) define('STDERR', fopen('php://stderr', 'w'));
/** Tickets constants */
define('TICKET_PRIORITY_LOW', 10);
define('TICKET_PRIORITY_MEDIUM', 20);
define('TICKET_PRIORITY_HIGH', 30);
define('TICKET_STATUS_NEW', 1);
define('TICKET_STATUS_OPEN',2);
define('TICKET_STATUS_CLOSED',3);
define('TICKET_STATUS_MERGED',4);
define('TICKET_CLIENT_NEW', 'ticket_client_new');
define('TICKET_CLIENT_UPDATE', 'ticket_client_update');
define('TICKET_INBOUND_NEW', 'ticket_inbound_new');
define('TICKET_INBOUND_NEW_INTERNAL', 'ticket_inbound_new_internal');
define('TICKET_INBOUND_REPLY', 'ticket_inbound_reply');
define('TICKET_INBOUND_CONTACT_REPLY', 'ticket_inbound_contact_reply');
define('TICKET_INBOUND_AGENT_REPLY', 'ticket_inbound_agent_reply');
define('TICKET_INBOUND_ADMIN_REPLY', 'ticket_inbound_admin_reply');
define('TICKET_AGENT_UPDATE', 'ticket_agent_update');
define('TICKET_AGENT_NEW', 'ticket_agent_new');
define('TICKET_MERGE', 'ticket_merge');
define('TICKET_ASSIGNED', 'ticket_assigned');
define('TICKET_OVERDUE', 'ticket_overdue');
define('TICKET_AGENT_CLOSED', 'ticket_agent_closed');
define('TICKET_SAVE_ONLY', 'ticket_save_only');
/* Default ticket statuses - Category - support*/
$supportTicketStatuses = [
trans('texts.new'),
trans('texts.open'),
trans('texts.closed'),
trans('texts.merged')
];
function uctrans($text, $data = []) function uctrans($text, $data = [])
{ {
$locale = Session::get(SESSION_LOCALE); $locale = Session::get(SESSION_LOCALE);
@ -695,7 +759,7 @@ if (! defined('APP_NAME')) {
} }
// include modules in translations // include modules in translations
function mtrans($entityType, $text = false) function mtrans($entityType, $text = false, $replace = [])
{ {
if (! $text) { if (! $text) {
$text = $entityType; $text = $entityType;
@ -704,7 +768,7 @@ if (! defined('APP_NAME')) {
// check if this has been translated in a module language file // check if this has been translated in a module language file
if (! Utils::isNinjaProd() && $module = Module::find($entityType)) { if (! Utils::isNinjaProd() && $module = Module::find($entityType)) {
$key = "{$module->getLowerName()}::texts.{$text}"; $key = "{$module->getLowerName()}::texts.{$text}";
$value = trans($key); $value = trans($key, $replace);
if ($key != $value) { if ($key != $value) {
return $value; return $value;
} }

View file

@ -30,4 +30,26 @@ class Domain
{ {
return 'maildelivery@' . static::getDomainFromId($id); return 'maildelivery@' . static::getDomainFromId($id);
} }
public static function getPostmarkTokenFromId($id)
{
switch($id)
{
case static::INVOICENINJA_COM:
return config('services.postmark_token');
case static::INVOICE_SERVICES:
return config('services.postmark_token_2');
}
}
public static function getSupportDomainFromId($id)
{
switch($id)
{
case static::INVOICENINJA_COM:
return config('ninja.tickets.ticket_support_domain');
case static::INVOICE_SERVICES:
return config('ninja.tickets.ticket_support_domain_2');
}
}
} }

View file

@ -0,0 +1,29 @@
<?php
namespace App\Events;
use App\Models\Ticket;
use Illuminate\Queue\SerializesModels;
/**
* Class TicketUserViewed.
*/
class TicketUserViewed extends Event
{
use SerializesModels;
/**
* @var Ticket
*/
public $ticket;
/**
* Create a new event instance.
*
* @param Ticket $ticket
*/
public function __construct(Ticket $ticket)
{
$this->ticket = $ticket;
}
}

View file

@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Events\UserSignedUp; use App\Events\UserSignedUp;
use App\Http\Requests\RegisterRequest; use App\Http\Requests\RegisterRequest;
use App\Http\Requests\UpdateAccountRequest; use App\Http\Requests\UpdateAccountRequest;
use App\Models\Company;
use App\Models\Account; use App\Models\Account;
use App\Models\User; use App\Models\User;
use App\Ninja\OAuth\OAuth; use App\Ninja\OAuth\OAuth;
@ -14,6 +15,7 @@ use App\Ninja\Transformers\UserAccountTransformer;
use App\Services\AuthService; use App\Services\AuthService;
use Auth; use Auth;
use Cache; use Cache;
use Carbon;
use Exception; use Exception;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@ -105,9 +107,9 @@ class AccountApiController extends BaseAPIController
$user = Auth::user(); $user = Auth::user();
$account = $user->account; $account = $user->account;
if ($createToken) { if ($createToken)
$this->accountRepo->createTokens($user, $request->token_name); $this->accountRepo->createTokens($user, $request->token_name);
}
$users = $this->accountRepo->findUsers($user, 'account.account_tokens'); $users = $this->accountRepo->findUsers($user, 'account.account_tokens');
$transformer = new UserAccountTransformer($account, $request->serializer, $request->token_name); $transformer = new UserAccountTransformer($account, $request->serializer, $request->token_name);
@ -261,6 +263,17 @@ class AccountApiController extends BaseAPIController
$oAuth = new OAuth(); $oAuth = new OAuth();
$user = $oAuth->getProvider($provider)->getTokenResponse($token); $user = $oAuth->getProvider($provider)->getTokenResponse($token);
/*
if ($user->google_2fa_secret && strpos($request->token_name, 'invoice-ninja-') !== false) {
$secret = \Crypt::decrypt($user->google_2fa_secret);
if (! $request->one_time_password) {
return $this->errorResponse(['message' => 'OTP_REQUIRED'], 401);
} elseif (! \Google2FA::verifyKey($secret, $request->one_time_password)) {
return $this->errorResponse(['message' => 'Invalid one time password'], 401);
}
}
*/
if ($user) { if ($user) {
Auth::login($user); Auth::login($user);
return $this->processLogin($request); return $this->processLogin($request);
@ -276,4 +289,49 @@ class AccountApiController extends BaseAPIController
} }
public function upgrade(Request $request)
{
$user = Auth::user();
$account = $user->account;
$company = $account->company;
$orderId = $request->order_id;
$timestamp = $request->timestamp;
$productId = $request->product_id;
if ($company->app_store_order_id) {
return '{"message":"error"}';
}
if ($productId == 'v1_pro_yearly') {
$company->plan = PLAN_PRO;
$company->num_users = 1;
$company->plan_price = PLAN_PRICE_PRO_MONTHLY * 10;
} else if ($productId == 'v1_enterprise_2_yearly') {
$company->plan = PLAN_ENTERPRISE;
$company->num_users = 2;
$company->plan_price = PLAN_PRICE_ENTERPRISE_MONTHLY_2 * 10;
} else if ($productId == 'v1_enterprise_5_yearly') {
$company->plan = PLAN_ENTERPRISE;
$company->num_users = 5;
$company->plan_price = PLAN_PRICE_ENTERPRISE_MONTHLY_5 * 10;
} else if ($productId == 'v1_enterprise_10_yearly') {
$company->plan = PLAN_ENTERPRISE;
$company->num_users = 10;
$company->plan_price = PLAN_PRICE_ENTERPRISE_MONTHLY_10 * 10;
} else if ($productId == 'v1_enterprise_20_yearly') {
$company->plan = PLAN_ENTERPRISE;
$company->num_users = 20;
$company->plan_price = PLAN_PRICE_ENTERPRISE_MONTHLY_20 * 10;
}
$company->app_store_order_id = $orderId;
$company->plan_term = PLAN_TERM_YEARLY;
$company->plan_started = $company->plan_started ?: date('Y-m-d');
$company->plan_paid = date('Y-m-d');
$company->plan_expires = Carbon::now()->addYear()->format('Y-m-d');
$company->trial_plan = null;
$company->save();
return '{"message":"success"}';
}
} }

View file

@ -8,9 +8,11 @@ use App\Events\UserSettingsChanged;
use App\Events\UserSignedUp; use App\Events\UserSignedUp;
use App\Http\Requests\SaveClientPortalSettings; use App\Http\Requests\SaveClientPortalSettings;
use App\Http\Requests\SaveEmailSettings; use App\Http\Requests\SaveEmailSettings;
use App\Http\Requests\SaveTicketSettings;
use App\Http\Requests\UpdateAccountRequest; use App\Http\Requests\UpdateAccountRequest;
use App\Models\Account; use App\Models\Account;
use App\Models\AccountGateway; use App\Models\AccountGateway;
use App\Models\AccountTicketSettings;
use App\Models\Affiliate; use App\Models\Affiliate;
use App\Models\Document; use App\Models\Document;
use App\Models\Gateway; use App\Models\Gateway;
@ -21,6 +23,7 @@ use App\Models\License;
use App\Models\PaymentTerm; use App\Models\PaymentTerm;
use App\Models\Product; use App\Models\Product;
use App\Models\TaxRate; use App\Models\TaxRate;
use App\Models\TicketTemplate;
use App\Models\User; use App\Models\User;
use App\Models\AccountEmailSettings; use App\Models\AccountEmailSettings;
use App\Ninja\Mailers\ContactMailer; use App\Ninja\Mailers\ContactMailer;
@ -30,6 +33,7 @@ use App\Ninja\Repositories\ReferralRepository;
use App\Services\AuthService; use App\Services\AuthService;
use App\Services\PaymentService; use App\Services\PaymentService;
use App\Services\TemplateService; use App\Services\TemplateService;
use Monolog\Handler\Curl\Util;
use Nwidart\Modules\Facades\Module; use Nwidart\Modules\Facades\Module;
use Auth; use Auth;
use Cache; use Cache;
@ -289,7 +293,9 @@ class AccountController extends BaseController
} elseif ($section == ACCOUNT_INVOICE_SETTINGS) { } elseif ($section == ACCOUNT_INVOICE_SETTINGS) {
return self::showInvoiceSettings(); return self::showInvoiceSettings();
} elseif ($section == ACCOUNT_IMPORT_EXPORT) { } elseif ($section == ACCOUNT_IMPORT_EXPORT) {
return View::make('accounts.import_export', ['title' => trans('texts.import_export')]); return View::make('accounts.import_export', [
'title' => trans('texts.import_export'),
]);
} elseif ($section == ACCOUNT_MANAGEMENT) { } elseif ($section == ACCOUNT_MANAGEMENT) {
return self::showAccountManagement(); return self::showAccountManagement();
} elseif ($section == ACCOUNT_INVOICE_DESIGN || $section == ACCOUNT_CUSTOMIZE_DESIGN) { } elseif ($section == ACCOUNT_INVOICE_DESIGN || $section == ACCOUNT_CUSTOMIZE_DESIGN) {
@ -302,6 +308,8 @@ class AccountController extends BaseController
return self::showProducts(); return self::showProducts();
} elseif ($section === ACCOUNT_TAX_RATES) { } elseif ($section === ACCOUNT_TAX_RATES) {
return self::showTaxRates(); return self::showTaxRates();
} elseif ($section === ACCOUNT_TICKETS) {
return self::showTickets();
} elseif ($section === ACCOUNT_PAYMENT_TERMS) { } elseif ($section === ACCOUNT_PAYMENT_TERMS) {
return self::showPaymentTerms(); return self::showPaymentTerms();
} elseif ($section === ACCOUNT_SYSTEM_SETTINGS) { } elseif ($section === ACCOUNT_SYSTEM_SETTINGS) {
@ -510,11 +518,29 @@ class AccountController extends BaseController
$data = [ $data = [
'account' => Auth::user()->account, 'account' => Auth::user()->account,
'title' => trans('texts.product_library'), 'title' => trans('texts.product_library'),
]; ];
return View::make('accounts.products', $data); return View::make('accounts.products', $data);
} }
/**
* @return mixed
*/
private function showTickets()
{
$data = [
'account' => Auth::user()->account,
'account_ticket_settings' => Auth::user()->account->account_ticket_settings,
'templates' => TicketTemplate::scope()->get(),
'title' => trans('texts.ticket_settings'),
'section' => ACCOUNT_TICKETS,
];
return View::make('accounts.tickets', $data);
}
/** /**
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Contracts\View\View
*/ */
@ -633,6 +659,7 @@ class AccountController extends BaseController
$data['invoiceFonts'] = Cache::get('fonts'); $data['invoiceFonts'] = Cache::get('fonts');
$data['section'] = $section; $data['section'] = $section;
$data['pageSizes'] = array_combine(InvoiceDesign::$pageSizes, InvoiceDesign::$pageSizes); $data['pageSizes'] = array_combine(InvoiceDesign::$pageSizes, InvoiceDesign::$pageSizes);
$data['showModuleSettings'] = Utils::hasModuleSettings();
$design = false; $design = false;
foreach ($data['invoiceDesigns'] as $item) { foreach ($data['invoiceDesigns'] as $item) {
@ -694,8 +721,14 @@ class AccountController extends BaseController
'account' => $account, 'account' => $account,
'products' => Product::scope()->orderBy('product_key')->get(), 'products' => Product::scope()->orderBy('product_key')->get(),
'gateway_types' => $options, 'gateway_types' => $options,
]; ];
if (Utils::isSelfHost()) {
$js = $account->client_view_js ? $account->client_view_js : '';
$data['client_view_js'] = $js;
}
return View::make('accounts.client_portal', $data); return View::make('accounts.client_portal', $data);
} }
@ -719,6 +752,7 @@ class AccountController extends BaseController
]; ];
} }
$data['title'] = trans('texts.email_templates'); $data['title'] = trans('texts.email_templates');
$data['showModuleSettings'] = Utils::hasModuleSettings();
return View::make('accounts.templates_and_reminders', $data); return View::make('accounts.templates_and_reminders', $data);
} }
@ -786,6 +820,7 @@ class AccountController extends BaseController
$user->save(); $user->save();
$account->live_preview = Input::get('live_preview') ? true : false; $account->live_preview = Input::get('live_preview') ? true : false;
$account->realtime_preview = Input::get('realtime_preview') ? true : false;
// Automatically disable live preview when using a large font // Automatically disable live preview when using a large font
$fonts = Cache::get('fonts')->filter(function ($font) use ($account) { $fonts = Cache::get('fonts')->filter(function ($font) use ($account) {
@ -856,6 +891,7 @@ class AccountController extends BaseController
$account->fill($request->all()); $account->fill($request->all());
$account->client_view_css = $request->client_view_css; $account->client_view_css = $request->client_view_css;
$account->client_view_js = $request->client_view_js;
$account->subdomain = $request->subdomain; $account->subdomain = $request->subdomain;
$account->iframe_url = $request->iframe_url; $account->iframe_url = $request->iframe_url;
$account->is_custom_domain = $request->is_custom_domain; $account->is_custom_domain = $request->is_custom_domain;
@ -905,21 +941,29 @@ class AccountController extends BaseController
$account->account_email_settings->$bodyField = ($body == $account->getDefaultEmailTemplate($type) ? null : $body); $account->account_email_settings->$bodyField = ($body == $account->getDefaultEmailTemplate($type) ? null : $body);
} }
foreach ([TEMPLATE_REMINDER1, TEMPLATE_REMINDER2, TEMPLATE_REMINDER3] as $type) { foreach ([TEMPLATE_REMINDER1, TEMPLATE_REMINDER2, TEMPLATE_REMINDER3, TEMPLATE_QUOTE_REMINDER1, TEMPLATE_QUOTE_REMINDER2, TEMPLATE_QUOTE_REMINDER3] as $type) {
$enableField = "enable_{$type}"; $enableField = "enable_{$type}";
$account->$enableField = Input::get($enableField) ? true : false; $account->account_email_settings->$enableField = Input::get($enableField) ? true : false;
$account->{"num_days_{$type}"} = Input::get("num_days_{$type}"); $account->account_email_settings->{"num_days_{$type}"} = Input::get("num_days_{$type}");
$account->{"field_{$type}"} = Input::get("field_{$type}"); $account->account_email_settings->{"field_{$type}"} = Input::get("field_{$type}");
$account->{"direction_{$type}"} = Input::get("field_{$type}") == REMINDER_FIELD_INVOICE_DATE ? REMINDER_DIRECTION_AFTER : Input::get("direction_{$type}"); $account->account_email_settings->{"direction_{$type}"} = Input::get("field_{$type}") == REMINDER_FIELD_INVOICE_DATE ? REMINDER_DIRECTION_AFTER : Input::get("direction_{$type}");
$number = preg_replace('/[^0-9]/', '', $type); $number = preg_replace('/[^0-9]/', '', $type);
$account->account_email_settings->{"late_fee{$number}_amount"} = Input::get("late_fee{$number}_amount"); if (strpos($type, 'quote') !== false) {
$account->account_email_settings->{"late_fee{$number}_percent"} = Input::get("late_fee{$number}_percent"); $account->account_email_settings->{"late_fee_quote{$number}_amount"} = Input::get("late_fee_quote{$number}_amount");
$account->account_email_settings->{"late_fee_quote{$number}_percent"} = Input::get("late_fee_quote{$number}_percent");
} else {
$account->account_email_settings->{"late_fee{$number}_amount"} = Input::get("late_fee{$number}_amount");
$account->account_email_settings->{"late_fee{$number}_percent"} = Input::get("late_fee{$number}_percent");
}
} }
$account->enable_reminder4 = Input::get('enable_reminder4') ? true : false; $account->account_email_settings->enable_reminder4 = Input::get('enable_reminder4') ? true : false;
$account->account_email_settings->frequency_id_reminder4 = Input::get('frequency_id_reminder4'); $account->account_email_settings->frequency_id_reminder4 = Input::get('frequency_id_reminder4');
$account->account_email_settings->enable_quote_reminder4 = Input::get('enable_quote_reminder4') ? true : false;
$account->account_email_settings->frequency_id_quote_reminder4 = Input::get('frequency_id_quote_reminder4');
$account->save(); $account->save();
$account->account_email_settings->save(); $account->account_email_settings->save();
@ -929,6 +973,29 @@ class AccountController extends BaseController
return Redirect::to('settings/'.ACCOUNT_TEMPLATES_AND_REMINDERS); return Redirect::to('settings/'.ACCOUNT_TEMPLATES_AND_REMINDERS);
} }
/**
* @return \Illuminate\Http\RedirectResponse
*/
public function saveTickets(SaveTicketSettings $request)
{
$account_ticket_settings = Auth::user()->account->account_ticket_settings;
$account_ticket_settings->fill($request->all());
$account_ticket_settings->save();
Session::flash('message', trans('texts.updated_settings'));
return Redirect::to('settings/'.ACCOUNT_TICKETS);
}
public function checkUniqueLocalPart()
{
if(AccountTicketSettings::checkUniqueLocalPart(Input::get('support_email_local_part'), Auth::user()->account))
return RESULT_SUCCESS;
else
return RESULT_FAILURE;
}
/** /**
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
*/ */
@ -950,6 +1017,7 @@ class AccountController extends BaseController
{ {
$account = Auth::user()->account; $account = Auth::user()->account;
$account->show_product_notes = Input::get('show_product_notes') ? true : false;
$account->fill_products = Input::get('fill_products') ? true : false; $account->fill_products = Input::get('fill_products') ? true : false;
$account->update_products = Input::get('update_products') ? true : false; $account->update_products = Input::get('update_products') ? true : false;
$account->convert_products = Input::get('convert_products') ? true : false; $account->convert_products = Input::get('convert_products') ? true : false;
@ -998,6 +1066,8 @@ class AccountController extends BaseController
$account->quote_terms = Input::get('quote_terms'); $account->quote_terms = Input::get('quote_terms');
$account->auto_convert_quote = Input::get('auto_convert_quote'); $account->auto_convert_quote = Input::get('auto_convert_quote');
$account->auto_archive_quote = Input::get('auto_archive_quote'); $account->auto_archive_quote = Input::get('auto_archive_quote');
$account->require_approve_quote = Input::get('require_approve_quote');
$account->allow_approve_expired_quote = Input::get('allow_approve_expired_quote');
$account->auto_archive_invoice = Input::get('auto_archive_invoice'); $account->auto_archive_invoice = Input::get('auto_archive_invoice');
$account->auto_email_invoice = Input::get('auto_email_invoice'); $account->auto_email_invoice = Input::get('auto_email_invoice');
$account->recurring_invoice_number_prefix = Input::get('recurring_invoice_number_prefix'); $account->recurring_invoice_number_prefix = Input::get('recurring_invoice_number_prefix');
@ -1010,6 +1080,7 @@ class AccountController extends BaseController
$account->credit_number_pattern = trim(Input::get('credit_number_pattern')); $account->credit_number_pattern = trim(Input::get('credit_number_pattern'));
$account->reset_counter_frequency_id = Input::get('reset_counter_frequency_id'); $account->reset_counter_frequency_id = Input::get('reset_counter_frequency_id');
$account->reset_counter_date = $account->reset_counter_frequency_id ? Utils::toSqlDate(Input::get('reset_counter_date')) : null; $account->reset_counter_date = $account->reset_counter_frequency_id ? Utils::toSqlDate(Input::get('reset_counter_date')) : null;
$account->custom_fields_options = request()->custom_fields_options;
if (Input::has('recurring_hour')) { if (Input::has('recurring_hour')) {
$account->recurring_hour = Input::get('recurring_hour'); $account->recurring_hour = Input::get('recurring_hour');
@ -1201,6 +1272,7 @@ class AccountController extends BaseController
*/ */
public function saveUserDetails() public function saveUserDetails()
{ {
/** @var \App\Models\User $user */ /** @var \App\Models\User $user */
$user = Auth::user(); $user = Auth::user();
$email = trim(strtolower(Input::get('email'))); $email = trim(strtolower(Input::get('email')));
@ -1230,6 +1302,7 @@ class AccountController extends BaseController
$user->email = $email; $user->email = $email;
$user->phone = trim(Input::get('phone')); $user->phone = trim(Input::get('phone'));
$user->dark_mode = Input::get('dark_mode'); $user->dark_mode = Input::get('dark_mode');
$user->signature = Input::get('signature');
if (! Auth::user()->is_admin) { if (! Auth::user()->is_admin) {
$user->notify_sent = Input::get('notify_sent'); $user->notify_sent = Input::get('notify_sent');
@ -1249,6 +1322,8 @@ class AccountController extends BaseController
} }
} }
$this->saveUserAvatar(Input::file('avatar'), $user);
$user->save(); $user->save();
event(new UserSettingsChanged()); event(new UserSettingsChanged());
@ -1258,6 +1333,89 @@ class AccountController extends BaseController
} }
} }
/**
* @param $avatar
* @param $user
*/
private function saveUserAvatar($avatar, $user)
{
/* Logo image file */
if ($uploaded = $avatar) {
$path = $avatar->getRealPath();
$disk = $user->getAvatarDisk();
$extension = strtolower($uploaded->getClientOriginalExtension());
if (empty(Document::$types[$extension]) && ! empty(Document::$extraExtensions[$extension])) {
$documentType = Document::$extraExtensions[$extension];
} else {
$documentType = $extension;
}
if (! in_array($documentType, ['jpeg', 'png', 'gif'])) {
Session::flash('warning', 'Unsupported file type');
} else {
$documentTypeData = Document::$types[$documentType];
$filePath = $uploaded->path();
$size = filesize($filePath);
if ($size / 1000 > MAX_DOCUMENT_SIZE) {
Session::flash('error', trans('texts.logo_warning_too_large'));
} else {
if ($documentType != 'gif') {
$user->avatar = str_random(21).'.'.$documentType;
try {
$imageSize = getimagesize($filePath);
$user->avatar_width = $imageSize[0];
$user->avatar_height = $imageSize[1];
$user->avatar_size = $size;
// make sure image isn't interlaced
if (extension_loaded('fileinfo')) {
$image = Image::make($path);
$image->interlace(false);
$imageStr = (string) $image->encode($documentType);
$disk->put($user->avatar, $imageStr);
$user->avatar_size = strlen($imageStr);
} else {
if (Utils::isInterlaced($filePath)) {
$user->clearAvatar();
Session::flash('error', trans('texts.logo_warning_invalid'));
} else {
$stream = fopen($filePath, 'r');
$disk->getDriver()->putStream($user->avatar, $stream, ['mimetype' => $documentTypeData['mime']]);
fclose($stream);
}
}
} catch (Exception $exception) {
$user->clearAvatar();
Session::flash('error', trans('texts.logo_warning_invalid'));
}
} else {
if (extension_loaded('fileinfo')) {
$user->avatar = str_random(32).'.png';
$image = Image::make($path);
$image = Image::canvas($image->width(), $image->height(), '#FFFFFF')->insert($image);
$imageStr = (string) $image->encode('png');
$disk->put($user->avatar, $imageStr);
$user->avatar_size = strlen($imageStr);
$user->avatar_width = $image->width();
$user->avatar_height = $image->height();
} else {
Session::flash('error', trans('texts.logo_warning_fileinfo'));
}
}
}
}
$user->save();
}
}
/** /**
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
*/ */
@ -1326,6 +1484,28 @@ class AccountController extends BaseController
return Redirect::to('settings/'.ACCOUNT_COMPANY_DETAILS); return Redirect::to('settings/'.ACCOUNT_COMPANY_DETAILS);
} }
/**
* @return mixed
*/
public function removeAvatar()
{
$user = Auth::user();
if (! Utils::isNinjaProd() && $user->hasAvatar()) {
$user->getAvatarDisk()->delete($user->avatar);
}
$user->avatar = null;
$user->avatar_size = null;
$user->avatar_width = null;
$user->avatar_height = null;
$user->save();
Session::flash('message', trans('texts.removed_logo'));
return Redirect::to('settings/'.ACCOUNT_USER_DETAILS);
}
/** /**
* @return string * @return string
*/ */

View file

@ -44,6 +44,10 @@ class AppController extends BaseController
return Redirect::to('/'); return Redirect::to('/');
} }
if (file_exists(base_path() . '/.env')) {
exit('Error: app is already configured, backup then delete the .env file to re-run the setup');
}
return View::make('setup'); return View::make('setup');
} }
@ -131,7 +135,6 @@ class AppController extends BaseController
Cache::flush(); Cache::flush();
Artisan::call('db:seed', ['--force' => true, '--class' => 'UpdateSeeder']); Artisan::call('db:seed', ['--force' => true, '--class' => 'UpdateSeeder']);
Artisan::call('optimize', ['--force' => true]);
if (! Account::count()) { if (! Account::count()) {
$firstName = trim(Input::get('first_name')); $firstName = trim(Input::get('first_name'));
@ -269,7 +272,6 @@ class AppController extends BaseController
if (Industry::count() == 0) { if (Industry::count() == 0) {
Artisan::call('db:seed', ['--force' => true]); Artisan::call('db:seed', ['--force' => true]);
} }
Artisan::call('optimize', ['--force' => true]);
} catch (Exception $e) { } catch (Exception $e) {
Utils::logError($e); Utils::logError($e);
@ -307,7 +309,6 @@ class AppController extends BaseController
Artisan::call('route:clear'); Artisan::call('route:clear');
Artisan::call('view:clear'); Artisan::call('view:clear');
Artisan::call('config:clear'); Artisan::call('config:clear');
Artisan::call('optimize', ['--force' => true]);
Auth::logout(); Auth::logout();
Cache::flush(); Cache::flush();
Session::flush(); Session::flush();

View file

@ -42,7 +42,7 @@ class AuthController extends Controller
*/ */
public function oauthLogin($provider, Request $request) public function oauthLogin($provider, Request $request)
{ {
return $this->authService->execute($provider, $request->has('code')); return $this->authService->execute($provider, $request->filled('code'));
} }
/** /**

View file

@ -25,7 +25,7 @@ class CalendarController extends BaseController
public function loadEvents() public function loadEvents()
{ {
if (auth()->user()->account->hasFeature(FEATURE_REPORTS)) { if (auth()->user()->account->hasFeature(FEATURE_REPORTS)) {
$events = dispatch(new GenerateCalendarEvents()); $events = dispatch_now(new GenerateCalendarEvents());
} else { } else {
$events = []; $events = [];
} }

View file

@ -43,7 +43,7 @@ class ClientApiController extends BaseAPIController
public function index() public function index()
{ {
$clients = Client::scope() $clients = Client::scope()
->orderBy('created_at', 'desc') ->orderBy('updated_at', 'desc')
->withTrashed(); ->withTrashed();
if ($email = Input::get('email')) { if ($email = Input::get('email')) {

View file

@ -175,11 +175,9 @@ class LoginController extends Controller
*/ */
public function getLogoutWrapper(Request $request) public function getLogoutWrapper(Request $request)
{ {
$contactKey = session('contact_key');
self::logout($request); self::logout($request);
return redirect('/client/dashboard/' . $contactKey); return redirect('/client/login?account_key=' . $request->account_key);
} }
} }

View file

@ -106,6 +106,9 @@ class ClientController extends BaseController
if ($user->can('create', ENTITY_RECURRING_INVOICE)) { if ($user->can('create', ENTITY_RECURRING_INVOICE)) {
$actionLinks[] = ['label' => trans('texts.new_recurring_invoice'), 'url' => URL::to('/recurring_invoices/create/'.$client->public_id)]; $actionLinks[] = ['label' => trans('texts.new_recurring_invoice'), 'url' => URL::to('/recurring_invoices/create/'.$client->public_id)];
} }
if ($user->can('create', ENTITY_RECURRING_QUOTE)) {
$actionLinks[] = ['label' => trans('texts.new_recurring_quote'), 'url' => URL::to('/recurring_quotes/create/'.$client->public_id)];
}
if (! empty($actionLinks)) { if (! empty($actionLinks)) {
$actionLinks[] = \DropdownButton::DIVIDER; $actionLinks[] = \DropdownButton::DIVIDER;
@ -133,6 +136,7 @@ class ClientController extends BaseController
'credit' => $client->getTotalCredit(), 'credit' => $client->getTotalCredit(),
'title' => trans('texts.view_client'), 'title' => trans('texts.view_client'),
'hasRecurringInvoices' => $account->isModuleEnabled(ENTITY_RECURRING_INVOICE) && Invoice::scope()->recurring()->withArchived()->whereClientId($client->id)->count() > 0, 'hasRecurringInvoices' => $account->isModuleEnabled(ENTITY_RECURRING_INVOICE) && Invoice::scope()->recurring()->withArchived()->whereClientId($client->id)->count() > 0,
'hasRecurringQuotes' => $account->isModuleEnabled(ENTITY_RECURRING_INVOICE) && Invoice::scope()->recurringQuote()->withArchived()->whereClientId($client->id)->count() > 0,
'hasQuotes' => $account->isModuleEnabled(ENTITY_QUOTE) && Invoice::scope()->quotes()->withArchived()->whereClientId($client->id)->count() > 0, 'hasQuotes' => $account->isModuleEnabled(ENTITY_QUOTE) && Invoice::scope()->quotes()->withArchived()->whereClientId($client->id)->count() > 0,
'hasTasks' => $account->isModuleEnabled(ENTITY_TASK) && Task::scope()->withArchived()->whereClientId($client->id)->count() > 0, 'hasTasks' => $account->isModuleEnabled(ENTITY_TASK) && Task::scope()->withArchived()->whereClientId($client->id)->count() > 0,
'hasExpenses' => $account->isModuleEnabled(ENTITY_EXPENSE) && Expense::scope()->withArchived()->whereClientId($client->id)->count() > 0, 'hasExpenses' => $account->isModuleEnabled(ENTITY_EXPENSE) && Expense::scope()->withArchived()->whereClientId($client->id)->count() > 0,
@ -260,7 +264,7 @@ class ClientController extends BaseController
} }
if (request()->json) { if (request()->json) {
return dispatch(new GenerateStatementData($client, request()->all())); return dispatch_now(new GenerateStatementData($client, request()->all()));
} }
$data = [ $data = [
@ -276,14 +280,14 @@ class ClientController extends BaseController
public function getEmailHistory() public function getEmailHistory()
{ {
$history = dispatch(new LoadPostmarkHistory(request()->email)); $history = dispatch_now(new LoadPostmarkHistory(request()->email));
return response()->json($history); return response()->json($history);
} }
public function reactivateEmail() public function reactivateEmail()
{ {
$result = dispatch(new ReactivatePostmarkEmail(request()->bounce_id)); $result = dispatch_now(new ReactivatePostmarkEmail(request()->bounce_id));
return response()->json($result); return response()->json($result);
} }

View file

@ -136,21 +136,24 @@ class ClientPortalController extends BaseController
} }
} }
if ($wepayGateway = $account->getGatewayConfig(GATEWAY_WEPAY)) { if (! Input::has('phantomjs')) {
$data['enableWePayACH'] = $wepayGateway->getAchEnabled(); if ($wepayGateway = $account->getGatewayConfig(GATEWAY_WEPAY)) {
} $data['enableWePayACH'] = $wepayGateway->getAchEnabled();
if ($stripeGateway = $account->getGatewayConfig(GATEWAY_STRIPE)) { }
//$data['enableStripeSources'] = $stripeGateway->getAlipayEnabled(); if ($stripeGateway = $account->getGatewayConfig(GATEWAY_STRIPE)) {
$data['enableStripeSources'] = true; //$data['enableStripeSources'] = $stripeGateway->getAlipayEnabled();
$data['enableStripeSources'] = true;
}
} }
$showApprove = $invoice->quote_invoice_id ? false : true; $showApprove = ($invoice->isQuote() && $account->require_approve_quote) ? true: false;
if ($invoice->invoice_status_id >= INVOICE_STATUS_APPROVED) { if ($invoice->invoice_status_id >= INVOICE_STATUS_APPROVED) {
$showApprove = false; $showApprove = false;
} }
$data += [ $data += [
'account' => $account, 'account' => $account,
'approveRequired' => $account->require_approve_quote,
'showApprove' => $showApprove, 'showApprove' => $showApprove,
'showBreadcrumbs' => false, 'showBreadcrumbs' => false,
'invoice' => $invoice->hidePrivateFields(), 'invoice' => $invoice->hidePrivateFields(),
@ -321,7 +324,7 @@ class ClientPortalController extends BaseController
->make(); ->make();
} }
public function recurringInvoiceIndex() public function recurringInvoiceIndex($quotes = false)
{ {
if (! $contact = $this->getContact()) { if (! $contact = $this->getContact()) {
return $this->returnError(); return $this->returnError();
@ -341,12 +344,19 @@ class ClientPortalController extends BaseController
$columns[] = 'auto_bill'; $columns[] = 'auto_bill';
} }
$title = trans('texts.recurring_invoices');
$entityType = ENTITY_RECURRING_INVOICE;
if ($quotes) {
$title = trans('texts.recurring_quotes');
$entityType = ENTITY_RECURRING_QUOTE;
}
$data = [ $data = [
'color' => $color, 'color' => $color,
'account' => $account, 'account' => $account,
'client' => $client, 'client' => $client,
'title' => trans('texts.recurring_invoices'), 'title' => $title,
'entityType' => ENTITY_RECURRING_INVOICE, 'entityType' => $entityType,
'columns' => Utils::trans($columns), 'columns' => Utils::trans($columns),
'sortColumn' => 1, 'sortColumn' => 1,
]; ];
@ -354,6 +364,11 @@ class ClientPortalController extends BaseController
return response()->view('public_list', $data); return response()->view('public_list', $data);
} }
public function recurringQuoteIndex()
{
return self::recurringInvoiceIndex(true);
}
public function invoiceIndex() public function invoiceIndex()
{ {
if (! $contact = $this->getContact()) { if (! $contact = $this->getContact()) {
@ -399,6 +414,15 @@ class ClientPortalController extends BaseController
return $this->invoiceRepo->getClientRecurringDatatable($contact->id); return $this->invoiceRepo->getClientRecurringDatatable($contact->id);
} }
public function recurringQuoteDatatable()
{
if (! $contact = $this->getContact()) {
return '';
}
return $this->invoiceRepo->getClientRecurringDatatable($contact->id, ENTITY_RECURRING_QUOTE);
}
public function paymentIndex() public function paymentIndex()
{ {
if (! $contact = $this->getContact()) { if (! $contact = $this->getContact()) {
@ -500,6 +524,7 @@ class ClientPortalController extends BaseController
$data = [ $data = [
'color' => $color, 'color' => $color,
'account' => $account, 'account' => $account,
'client' => $contact->client,
'title' => trans('texts.quotes'), 'title' => trans('texts.quotes'),
'entityType' => ENTITY_QUOTE, 'entityType' => ENTITY_QUOTE,
'columns' => Utils::trans(['quote_number', 'quote_date', 'quote_total', 'due_date', 'status']), 'columns' => Utils::trans(['quote_number', 'quote_date', 'quote_total', 'due_date', 'status']),
@ -627,7 +652,7 @@ class ClientPortalController extends BaseController
return $this->documentRepo->getClientDatatable($contact->id, ENTITY_DOCUMENT, Input::get('sSearch')); return $this->documentRepo->getClientDatatable($contact->id, ENTITY_DOCUMENT, Input::get('sSearch'));
} }
private function returnError($error = false) public function returnError($error = false)
{ {
if (request()->phantomjs) { if (request()->phantomjs) {
abort(404); abort(404);
@ -640,7 +665,7 @@ class ClientPortalController extends BaseController
]); ]);
} }
private function getContact() public function getContact()
{ {
$contactKey = session('contact_key'); $contactKey = session('contact_key');
@ -1033,7 +1058,7 @@ class ClientPortalController extends BaseController
} }
if (request()->json) { if (request()->json) {
return dispatch(new GenerateStatementData($client, request()->all(), $contact)); return dispatch_now(new GenerateStatementData($client, request()->all(), $contact));
} }
$data = [ $data = [
@ -1047,4 +1072,6 @@ class ClientPortalController extends BaseController
return view('clients.statement', $data); return view('clients.statement', $data);
} }
} }

View file

@ -54,7 +54,7 @@ class ClientPortalProposalController extends BaseController
$proposal = $invitation->proposal; $proposal = $invitation->proposal;
$pdf = dispatch(new ConvertProposalToPdf($proposal)); $pdf = dispatch_now(new ConvertProposalToPdf($proposal));
$this->downloadResponse($proposal->getFilename(), $pdf); $this->downloadResponse($proposal->getFilename(), $pdf);
} }

View file

@ -0,0 +1,202 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\UpdateClientPortalTicketRequest;
use App\Libraries\Utils;
use App\Models\Ticket;
use App\Ninja\Repositories\TicketRepository;
use App\Services\TicketService;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Session;
class ClientPortalTicketController extends ClientPortalController
{
/**
* @var TicketRepository
*/
private $ticketRepo;
/**
* @var TicketService
*/
private $ticketService;
/**
* ClientPortalTicketController constructor.
* @param TicketRepository $ticketRepo
* @param TicketService $ticketService
*/
public function __construct(TicketRepository $ticketRepo, TicketService $ticketService)
{
$this->ticketRepo = $ticketRepo;
$this->ticketService = $ticketService;
}
/**
* @return \Illuminate\Http\Response
*/
public function index()
{
$contact = $this->getContact();
if ((!$contact || (!$contact->account->enable_client_portal)))
return $this->returnError();
$account = $contact->account;
$data = [
'color' => $account->primary_color ? $account->primary_color : '#0b4d78',
'account' => $account,
'title' => trans('texts.tickets'),
'entityType' => ENTITY_TICKET,
'columns' => Utils::trans(['ticket_number', 'subject', 'created_at', 'status']),
'sortColumn' => 0,
];
return response()->view('public_list', $data);
}
/**
* @return bool
*/
public function ticketDatatable()
{
if (! $contact = $this->getContact())
return false;
return $this->ticketService->getClientDatatable($contact->client->id);
}
/**
* @param $invitationKey
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function viewTicket($invitationKey)
{
if (! $invitation = $this->ticketRepo->findInvitationByKey($invitationKey))
return $this->returnError(trans('texts.ticket_not_found'));
$account = $invitation->account;
$ticket = $invitation->ticket;
$data = [
'ticket' => $ticket,
'account' => $account,
'ticketInvitation' => $invitation,
];
return view('invited.ticket', $data);
}
/**
* @param $ticketid
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function view($ticketId)
{
if (! $contact = $this->getContact())
$this->returnError();
$account = $contact->account;
$ticket = Ticket::scope($ticketId, $account->id)
->with('comments', 'documents')
->first();
$data['method'] = 'PUT';
$data['entityType'] = ENTITY_TICKET;
$data = array_merge($data, self::getViewModel($contact, $ticket));
return view('tickets.portal.ticket_view', $data);
}
/**
*
*/
public function update(UpdateClientPortalTicketRequest $request)
{
$contact = $this->getContact();
$data = $request->input();
$data['document_ids'] = $request->document_ids;
$data['contact_key'] = $contact->contact_key;
$data['method'] = 'PUT';
$data['entityType'] = ENTITY_TICKET;
$data['action'] = TICKET_INBOUND_CONTACT_REPLY;
$ticket = $this->ticketService->save($data, $request->entity());
$ticket->load('documents');
if(!$ticket)
$this->returnError();
$data = array_merge($data, self::getViewModel($contact, $ticket));
Session::flash('message', trans('texts.updated_ticket'));
return view('tickets.portal.ticket_view', $data);
}
public function create()
{
if (! $contact = $this->getContact())
$this->returnError();
$data['method'] = 'POST';
$data['entityType'] = ENTITY_TICKET;
$data = array_merge($data, self::getViewModel($contact));
return view('tickets.portal.ticket_view', $data);
}
public function store(UpdateClientPortalTicketRequest $request)
{
if (! $contact = $this->getContact())
$this->returnError();
$data = $request->input();
$data['document_ids'] = $request->document_ids;
$data['contact_key'] = $contact->contact_key;
$data['action'] = TICKET_CLIENT_NEW;
$data['is_internal'] = 0;
$ticket = $this->ticketService->save($data, $request->entity());
Session::flash('message', trans('texts.updated_ticket'));
return Redirect::to('/client/tickets/'.$ticket->public_id);
}
private static function getViewModel($contact, $ticket = false)
{
return [
'color' => $contact->account->primary_color ? $contact->account->primary_color : '#0b4d78',
'ticket' => $ticket,
'contact' => $contact,
'account' => $contact->account,
'title' => $ticket ? trans('texts.ticket')." ".$ticket->ticket_number : trans('texts.new_ticket'),
'comments' => $ticket ? $ticket->comments() :null,
'url' => $ticket ? 'client/tickets/' . $ticket->public_id : 'client/tickets/create',
//'timezone' => $ticket ? $ticket->account->timezone->name : DEFAULT_TIMEZONE,
'datetimeFormat' => $contact->account->getMomentDateTimeFormat(),
'account_ticket_settings' => $contact->account->account_ticket_settings
];
}
}

View file

@ -46,7 +46,7 @@ class CreditApiController extends BaseAPIController
$credits = Credit::scope() $credits = Credit::scope()
->withTrashed() ->withTrashed()
->with(['client']) ->with(['client'])
->orderBy('created_at', 'desc'); ->orderBy('updated_at', 'desc');
return $this->listResponse($credits); return $this->listResponse($credits);
} }

View file

@ -3,9 +3,11 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Client; use App\Models\Client;
use App\Models\Currency;
use App\Models\Expense; use App\Models\Expense;
use App\Ninja\Repositories\DashboardRepository; use App\Ninja\Repositories\DashboardRepository;
use Auth; use Auth;
use App\Libraries\MoneyUtils;
use Utils; use Utils;
use View; use View;
@ -34,7 +36,7 @@ class DashboardController extends BaseController
$metrics = $dashboardRepo->totals($accountId, $userId, $viewAll); $metrics = $dashboardRepo->totals($accountId, $userId, $viewAll);
$paidToDate = $dashboardRepo->paidToDate($account, $userId, $viewAll); $paidToDate = $dashboardRepo->paidToDate($account, $userId, $viewAll);
$averageInvoice = $dashboardRepo->averages($account, $userId, $viewAll); $averageInvoice = $dashboardRepo->averages($account, $userId, $viewAll);
$balances = $dashboardRepo->balances($accountId, $userId, $viewAll); $balances = $dashboardRepo->balances($account, $userId, $viewAll);
$activities = $dashboardRepo->activities($accountId, $userId, $viewAll); $activities = $dashboardRepo->activities($accountId, $userId, $viewAll);
$pastDue = $dashboardRepo->pastDue($accountId, $userId, $viewAll); $pastDue = $dashboardRepo->pastDue($accountId, $userId, $viewAll);
$upcoming = $dashboardRepo->upcoming($accountId, $userId, $viewAll); $upcoming = $dashboardRepo->upcoming($accountId, $userId, $viewAll);
@ -42,6 +44,60 @@ class DashboardController extends BaseController
$expenses = $dashboardRepo->expenses($account, $userId, $viewAll); $expenses = $dashboardRepo->expenses($account, $userId, $viewAll);
$tasks = $dashboardRepo->tasks($accountId, $userId, $viewAll); $tasks = $dashboardRepo->tasks($accountId, $userId, $viewAll);
// calculate paid to date totals
$paidToDateTotal = 0;
foreach($paidToDate as $item) {
$paidToDateTotal += ($item->value * $item->exchange_rate);
}
// calculate average invoice totals
$invoiceTotal = 0;
$invoiceTotalCount = 0;
foreach ($averageInvoice as $item) {
$invoiceTotalCount += $item->invoice_count;
if (! $item->exchange_rate) {
$invoiceTotal += $item->invoice_avg * $item->invoice_count;
continue;
}
$invoiceTotal += ($item->invoice_avg * $item->invoice_count / $item->exchange_rate);
}
$averageInvoiceTotal = $invoiceTotalCount ? ($invoiceTotal / $invoiceTotalCount) : 0;
// calculate balances totals
$balancesTotals = 0;
$currencies = [];
foreach ($balances as $item) {
if ($item->currency_id == $account->getCurrencyId()) {
$balancesTotals += $item->value;
continue;
}
if (! isset($currencies[$item->currency_id])) {
$currencies[$item->currency_id] = Currency::where('id', $item->currency_id)->firstOrFail();
}
try {
$balancesTotals += MoneyUtils::convert($item->value, $currencies[$item->currency_id]->code, $account->currency->code);
} catch (\Exception $e) {
Utils::logError($e);
$balancesTotals += $item->value;
}
}
// calculate expenses totals
$expensesTotals = 0;
foreach ($expenses as $item) {
if ($item->currency_id == $account->getCurrencyId()) {
$expensesTotals += $item->value;
continue;
}
$expensesTotals += ($item->value * $item->exchange_rate);
}
$showBlueVinePromo = false; $showBlueVinePromo = false;
if ($user->is_admin && env('BLUEVINE_PARTNER_UNIQUE_ID')) { if ($user->is_admin && env('BLUEVINE_PARTNER_UNIQUE_ID')) {
$showBlueVinePromo = ! $account->company->bluevine_status $showBlueVinePromo = ! $account->company->bluevine_status
@ -67,10 +123,14 @@ class DashboardController extends BaseController
'account' => $user->account, 'account' => $user->account,
'user' => $user, 'user' => $user,
'paidToDate' => $paidToDate, 'paidToDate' => $paidToDate,
'paidToDateTotal' => $paidToDateTotal,
'balances' => $balances, 'balances' => $balances,
'balancesTotals' => $balancesTotals,
'averageInvoice' => $averageInvoice, 'averageInvoice' => $averageInvoice,
'averageInvoiceTotal' => $averageInvoiceTotal,
'invoicesSent' => $metrics ? $metrics->invoices_sent : 0, 'invoicesSent' => $metrics ? $metrics->invoices_sent : 0,
'activeClients' => $metrics ? $metrics->active_clients : 0, 'activeClients' => $metrics ? $metrics->active_clients : 0,
'invoiceExchangeRateMissing' => $account->getInvoiceExchangeRateCustomFieldIndex() ? false : true,
'activities' => $activities, 'activities' => $activities,
'pastDue' => $pastDue, 'pastDue' => $pastDue,
'upcoming' => $upcoming, 'upcoming' => $upcoming,
@ -80,6 +140,7 @@ class DashboardController extends BaseController
'showBreadcrumbs' => false, 'showBreadcrumbs' => false,
'currencies' => $this->getCurrencyCodes(), 'currencies' => $this->getCurrencyCodes(),
'expenses' => $expenses, 'expenses' => $expenses,
'expensesTotals' => $expensesTotals,
'tasks' => $tasks, 'tasks' => $tasks,
'showBlueVinePromo' => $showBlueVinePromo, 'showBlueVinePromo' => $showBlueVinePromo,
'showWhiteLabelExpired' => $showWhiteLabelExpired, 'showWhiteLabelExpired' => $showWhiteLabelExpired,

View file

@ -151,10 +151,10 @@ class DocumentAPIController extends BaseAPIController
* ) * )
*/ */
public function destroy(UpdateDocumentRequest $request) public function destroy(UpdateDocumentRequest $request)
{ {
$entity = $request->entity(); $entity = $request->entity();
$this->documentRepo->delete($entity); $entity->delete();
return $this->itemResponse($entity); return $this->itemResponse($entity);
} }

View file

@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Http\Requests\CreateDocumentRequest; use App\Http\Requests\CreateDocumentRequest;
use App\Http\Requests\DocumentRequest; use App\Http\Requests\DocumentRequest;
use App\Http\Requests\UpdateDocumentRequest; use App\Http\Requests\UpdateDocumentRequest;
use App\Models\Contact;
use App\Models\Document; use App\Models\Document;
use App\Ninja\Repositories\DocumentRepository; use App\Ninja\Repositories\DocumentRepository;
use Redirect; use Redirect;
@ -56,6 +57,7 @@ class DocumentController extends BaseController
public function getPreview(DocumentRequest $request) public function getPreview(DocumentRequest $request)
{ {
$document = $request->entity(); $document = $request->entity();
if (empty($document->preview)) { if (empty($document->preview)) {
@ -118,6 +120,7 @@ class DocumentController extends BaseController
'code' => 200, 'code' => 200,
]; ];
} }
return Response::json($response, 200); return Response::json($response, 200);
} }
} }

View file

@ -47,7 +47,7 @@ class ExpenseApiController extends BaseAPIController
$expenses = Expense::scope() $expenses = Expense::scope()
->withTrashed() ->withTrashed()
->with('client', 'invoice', 'vendor', 'expense_category') ->with('client', 'invoice', 'vendor', 'expense_category')
->orderBy('created_at', 'desc'); ->orderBy('updated_at', 'desc');
return $this->listResponse($expenses); return $this->listResponse($expenses);
} }

View file

@ -54,14 +54,14 @@ class InvoiceApiController extends BaseAPIController
* response="default", * response="default",
* description="an ""unexpected"" error" * description="an ""unexpected"" error"
* ) * )
* ) * );
*/ */
public function index() public function index()
{ {
$invoices = Invoice::scope() $invoices = Invoice::scope()
->withTrashed() ->withTrashed()
->with('invoice_items', 'client') ->with('invoice_items', 'client')
->orderBy('created_at', 'desc'); ->orderBy('updated_at', 'desc');
// Filter by invoice number // Filter by invoice number
if ($invoiceNumber = Input::get('invoice_number')) { if ($invoiceNumber = Input::get('invoice_number')) {
@ -73,11 +73,11 @@ class InvoiceApiController extends BaseAPIController
$invoices->where('invoice_status_id', '>=', $statusId); $invoices->where('invoice_status_id', '>=', $statusId);
} }
if (request()->has('is_recurring')) { if (request()->filled('is_recurring')) {
$invoices->where('is_recurring', '=', request()->is_recurring); $invoices->where('is_recurring', '=', request()->is_recurring);
} }
if (request()->has('invoice_type_id')) { if (request()->filled('invoice_type_id')) {
$invoices->where('invoice_type_id', '=', request()->invoice_type_id); $invoices->where('invoice_type_id', '=', request()->invoice_type_id);
} }
@ -104,7 +104,7 @@ class InvoiceApiController extends BaseAPIController
* response="default", * response="default",
* description="an ""unexpected"" error" * description="an ""unexpected"" error"
* ) * )
* ) * );
*/ */
public function show(InvoiceRequest $request) public function show(InvoiceRequest $request)
{ {
@ -130,7 +130,7 @@ class InvoiceApiController extends BaseAPIController
* response="default", * response="default",
* description="an ""unexpected"" error" * description="an ""unexpected"" error"
* ) * )
* ) * );
*/ */
public function store(CreateInvoiceAPIRequest $request) public function store(CreateInvoiceAPIRequest $request)
{ {
@ -455,7 +455,7 @@ class InvoiceApiController extends BaseAPIController
* response="default", * response="default",
* description="an ""unexpected"" error" * description="an ""unexpected"" error"
* ) * )
* ) * );
*/ */
public function destroy(UpdateInvoiceAPIRequest $request) public function destroy(UpdateInvoiceAPIRequest $request)
{ {

View file

@ -81,6 +81,14 @@ class InvoiceController extends BaseController
return $this->recurringInvoiceService->getDatatable($accountId, $clientPublicId, ENTITY_RECURRING_INVOICE, $search); return $this->recurringInvoiceService->getDatatable($accountId, $clientPublicId, ENTITY_RECURRING_INVOICE, $search);
} }
public function getRecurringQuotesDatatable($clientPublicId = null)
{
$accountId = Auth::user()->account_id;
$search = Input::get('sSearch');
return $this->recurringInvoiceService->getDatatable($accountId, $clientPublicId, ENTITY_RECURRING_QUOTE, $search);
}
public function edit(InvoiceRequest $request, $publicId, $clone = false) public function edit(InvoiceRequest $request, $publicId, $clone = false)
{ {
$account = Auth::user()->account; $account = Auth::user()->account;
@ -101,7 +109,6 @@ class InvoiceController extends BaseController
$entityType = $clone == INVOICE_TYPE_STANDARD ? ENTITY_INVOICE : ENTITY_QUOTE; $entityType = $clone == INVOICE_TYPE_STANDARD ? ENTITY_INVOICE : ENTITY_QUOTE;
$invoice->id = $invoice->public_id = null; $invoice->id = $invoice->public_id = null;
$invoice->is_public = false; $invoice->is_public = false;
$invoice->is_recurring = $invoice->is_recurring && $clone == INVOICE_TYPE_STANDARD;
$invoice->invoice_type_id = $clone; $invoice->invoice_type_id = $clone;
$invoice->invoice_number = $account->getNextNumber($invoice); $invoice->invoice_number = $account->getNextNumber($invoice);
$invoice->due_date = null; $invoice->due_date = null;
@ -138,7 +145,11 @@ class InvoiceController extends BaseController
'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS), 'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS),
]; ];
$lastSent = ($invoice->is_recurring && $invoice->last_sent_date) ? $invoice->recurring_invoices->last() : null; $lastSent = null;
if($invoice->is_recurring && $invoice->last_sent_date)
{
$lastSent = ($invoice->subEntityType() == ENTITY_RECURRING_INVOICE) ? $invoice->recurring_invoices->last() : $invoice->recurring_quotes->last();
}
if (! Auth::user()->hasPermission('view_client')) { if (! Auth::user()->hasPermission('view_client')) {
$clients = $clients->where('clients.user_id', '=', Auth::user()->id); $clients = $clients->where('clients.user_id', '=', Auth::user()->id);
@ -192,14 +203,17 @@ class InvoiceController extends BaseController
} }
} }
if (Auth::user()->registered && ! Auth::user()->confirmed) {
session()->flash('warning', trans('texts.confirmation_required', ['link' => link_to('/resend_confirmation', trans('texts.click_here'))]));
}
return View::make('invoices.edit', $data); return View::make('invoices.edit', $data);
} }
public function create(InvoiceRequest $request, $clientPublicId = 0, $isRecurring = false) public function create(InvoiceRequest $request, $clientPublicId = 0, $entityType = ENTITY_INVOICE)
{ {
$account = Auth::user()->account; $account = Auth::user()->account;
$entityType = $isRecurring ? ENTITY_RECURRING_INVOICE : ENTITY_INVOICE;
$clientId = null; $clientId = null;
if ($request->client_id) { if ($request->client_id) {
@ -230,7 +244,12 @@ class InvoiceController extends BaseController
public function createRecurring(InvoiceRequest $request, $clientPublicId = 0) public function createRecurring(InvoiceRequest $request, $clientPublicId = 0)
{ {
return self::create($request, $clientPublicId, true); return self::create($request, $clientPublicId, ENTITY_RECURRING_INVOICE);
}
public function createRecurringQuote(InvoiceRequest $request, $clientPublicId = 0)
{
return self::create($request, $clientPublicId, ENTITY_RECURRING_QUOTE);
} }
private static function getViewModel($invoice) private static function getViewModel($invoice)
@ -302,7 +321,7 @@ class InvoiceController extends BaseController
// Check for any taxes which have been deleted // Check for any taxes which have been deleted
$taxRateOptions = $account->present()->taxRateOptions; $taxRateOptions = $account->present()->taxRateOptions;
if ($invoice->exists) { if ($invoice->exists && !$invoice->deleted_at) {
foreach ($invoice->getTaxes() as $key => $rate) { foreach ($invoice->getTaxes() as $key => $rate) {
$key = '0 ' . $key; // mark it as a standard exclusive rate option $key = '0 ' . $key; // mark it as a standard exclusive rate option
if (isset($taxRateOptions[$key])) { if (isset($taxRateOptions[$key])) {
@ -510,6 +529,10 @@ class InvoiceController extends BaseController
$entityType = ENTITY_RECURRING_INVOICE; $entityType = ENTITY_RECURRING_INVOICE;
} }
if (strpos(\Request::server('HTTP_REFERER'), 'recurring_quotes')) {
$entityType = ENTITY_RECURRING_QUOTE;
}
return $this->returnBulk($entityType, $action, $ids); return $this->returnBulk($entityType, $action, $ids);
} }

View file

@ -0,0 +1,611 @@
<?php
namespace App\Http\Controllers\Migration;
use App\Models\Credit;
use App\Models\User;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Product;
use App\Models\TaxRate;
use App\Libraries\Utils;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use App\Http\Controllers\BaseController;
class StepsController extends BaseController
{
private $account;
/**
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function start()
{
return view('migration.start');
}
public function import()
{
return view('migration.import');
}
/**
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function download()
{
return view('migration.download');
}
/**
* Handle data downloading for the migration.
*
* @return \Illuminate\Http\JsonResponse
*/
public function handleDownload()
{
$this->account = Auth::user()->account;
$date = date('Y-m-d');
$accountKey = $this->account->account_key;
$output = fopen('php://output', 'w') or Utils::fatalError();
$fileName = "{$accountKey}-{$date}-invoiceninja";
$data = [
'company' => $this->getCompany(),
'users' => $this->getUsers(),
'tax_rates' => $this->getTaxRates(),
'clients' => $this->getClients(),
'products' => $this->getProducts(),
'invoices' => $this->getInvoices(),
'quotes' => $this->getQuotes(),
'payments' => array_merge($this->getPayments(), $this->getCredits()),
'credits' => $this->getCreditsNotes(),
];
$file = storage_path("{$fileName}.zip");
$zip = new \ZipArchive();
$zip->open($file, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
$zip->addFromString('migration.json', json_encode($data));
$zip->close();
header('Content-Type: application/zip');
header('Content-Length: ' . filesize($file));
header("Content-Disposition: attachment; filename={$fileName}.zip");
readfile($file);
unlink($file);
return response()->json($data);
}
/**
* Export company and map to the v2 fields.
*
* @return array
*/
protected function getCompany()
{
return [
'account_id' => $this->account->id,
'industry_id' => $this->account->industry_id,
'ip' => $this->account->ip,
'company_key' => $this->account->account_key,
'logo' => $this->account->logo,
'convert_products' => $this->account->convert_products,
'fill_products' => $this->account->fill_products,
'update_products' => $this->account->update_products,
'show_product_details' => $this->account->show_product_notes,
'custom_surcharge_taxes1' => $this->account->custom_invoice_taxes1,
'custom_surcharge_taxes2' => $this->account->custom_invoice_taxes2,
'enable_invoice_quantity' => !$this->account->hide_quantity,
'subdomain' => $this->account->subdomain,
'size_id' => $this->account->size_id,
'enable_modules' => $this->account->enabled_modules,
'custom_fields' => $this->account->custom_fields,
//'uses_inclusive_taxes' => $this->account->inclusive_taxes,
'created_at' => $this->account->created_at ? $this->account->created_at->toDateString() : null,
'updated_at' => $this->account->updated_at ? $this->account->updated_at->toDateString() : null,
'settings' => $this->getCompanySettings(),
];
}
public function getCompanySettings()
{
// In v1: custom_invoice_taxes1 & custom_invoice_taxes2, v2: 'invoice_taxes'. What do to with this?
// V1: invoice_number_prefix, v2: invoice_number_pattern.. same with quote_number, client_number,
return [
'timezone_id' => $this->account->timezone_id,
'date_format_id' => $this->account->date_format_id,
'currency_id' => $this->account->currency_id,
'name' => $this->account->name,
'address1' => $this->account->address1,
'address2' => $this->account->address2,
'city' => $this->account->city,
'state' => $this->account->state,
'postal_code' => $this->account->postal_code,
'country_id' => $this->account->country_id,
'invoice_terms' => $this->account->invoice_terms,
'enabled_item_tax_rates' => $this->account->invoice_item_taxes,
'invoice_design_id' => $this->account->invoice_design_id,
'phone' => $this->account->work_phone,
'email' => $this->account->work_email,
'language_id' => $this->account->language_id,
'custom_value1' => $this->account->custom_value1,
'custom_value2' => $this->account->custom_value2,
'hide_paid_to_date' => $this->account->hide_paid_to_date,
'vat_number' => $this->account->vat_number,
'shared_invoice_quote_counter' => $this->account->share_counter, // @verify,
'id_number' => $this->account->id_number,
'invoice_footer' => $this->account->invoice_footer,
'pdf_email_attachment' => $this->account->pdf_email_attachment,
'font_size' => $this->account->font_size,
'invoice_labels' => $this->account->invoice_labels,
'military_time' => $this->account->military_time,
'invoice_number_pattern' => $this->account->invoice_number_pattern,
'quote_number_pattern' => $this->account->quote_number_pattern,
'quote_terms' => $this->account->quote_terms,
'website' => $this->account->website,
'auto_convert_quote' => $this->account->auto_convert_quote,
'all_pages_footer' => $this->account->all_pages_footer,
'all_pages_header' => $this->account->all_pages_header,
'show_currency_code' => $this->account->show_currency_code,
'enable_client_portal_password' => $this->account->enable_portal_password,
'send_portal_password' => $this->account->send_portal_password,
'recurring_number_prefix' => $this->account->recurring_invoice_number_prefix, // @verify
'enable_client_portal' => $this->account->enable_client_portal,
'invoice_fields' => $this->account->invoice_fields,
'company_logo' => $this->account->logo,
'embed_documents' => $this->account->invoice_embed_documents,
'document_email_attachment' => $this->account->document_email_attachment,
'enable_client_portal_dashboard' => $this->account->enable_client_portal_dashboard,
'page_size' => $this->account->page_size,
'show_accept_invoice_terms' => $this->account->show_accept_invoice_terms,
'show_accept_quote_terms' => $this->account->show_accept_quote_terms,
'require_invoice_signature' => $this->account->require_invoice_signature,
'require_quote_signature' => $this->account->require_quote_signature,
'client_number_counter' => $this->account->client_number_counter,
'client_number_pattern' => $this->account->client_number_pattern,
'payment_terms' => $this->account->payment_terms,
'reset_counter_frequency_id' => $this->account->reset_counter_frequency_id,
'payment_type_id' => $this->account->payment_type_id,
'reset_counter_date' => $this->account->reset_counter_date,
'tax_name1' => $this->account->tax_name1,
'tax_rate1' => $this->account->tax_rate1,
'tax_name2' => $this->account->tax_name2,
'tax_rate2' => $this->account->tax_rate2,
'quote_design_id' => $this->account->quote_design_id,
'credit_number_counter' => $this->account->credit_number_counter,
'credit_number_pattern' => $this->account->credit_number_pattern,
'default_task_rate' => $this->account->task_rate,
'inclusive_taxes' => $this->account->inclusive_taxes,
'signature_on_pdf' => $this->account->signature_on_pdf,
'ubl_email_attachment' => $this->account->ubl_email_attachment,
'auto_archive_invoice' => $this->account->auto_archive_invoice,
'auto_archive_quote' => $this->account->auto_archive_quote,
'auto_email_invoice' => $this->account->auto_email_invoice,
];
}
/**
* @return array
*/
public function getTaxRates()
{
$rates = TaxRate::where('account_id', $this->account->id)
->withTrashed()
->get();
$transformed = [];
foreach ($rates as $rate) {
$transformed[] = [
'name' => $rate->name,
'rate' => $rate->rate,
'company_id' => $rate->account_id,
'user_id' => $rate->user_id,
'created_at' => $rate->created_at ? $rate->created_at->toDateString() : null,
'updated_at' => $rate->updated_at ? $rate->updated_at->toDateString() : null,
'deleted_at' => $rate->deleted_at ? $rate->deleted_at->toDateString() : null,
];
}
return $transformed;
}
/**
* @return array
*/
protected function getClients()
{
$clients = [];
foreach ($this->account->clients()->withTrashed()->get() as $client) {
$clients[] = [
'id' => $client->id,
'company_id' => $client->account_id,
'user_id' => $client->user_id,
'name' => $client->name,
'balance' => $client->balance,
'paid_to_date' => $client->paid_to_date,
'address1' => $client->address1,
'address2' => $client->address2,
'city' => $client->city,
'state' => $client->state,
'postal_code' => $client->postal_code,
'country_id' => $client->country_id,
'phone' => $client->work_phone,
'private_notes' => $client->private_notes,
'website' => $client->website,
'industry_id' => $client->industry_id,
'size_id' => $client->size_id,
'is_deleted' => $client->is_deleted,
'vat_number' => $client->vat_number,
'id_number' => $client->id_number,
'custom_value1' => $client->custom_value1,
'custom_value2' => $client->custom_value2,
'shipping_address1' => $client->shipping_address1,
'shipping_address2' => $client->shipping_address2,
'shipping_city' => $client->shipping_city,
'shipping_state' => $client->shipping_state,
'shipping_postal_code' => $client->shipping_postal_code,
'shipping_country_id' => $client->shipping_country_id,
'contacts' => $this->getClientContacts($client->contacts),
];
}
return $clients;
}
/**
* @param $contacts
* @return array
*/
protected function getClientContacts($contacts)
{
$transformed = [];
foreach($contacts as $contact) {
$transformed[] = [
'id' => $contact->id,
'company_id' => $contact->account_id,
'user_id' => $contact->user_id,
'client_id' => $contact->client_id,
'first_name' => $contact->first_name,
'last_name' => $contact->last_name,
'phone' => $contact->phone,
'custom_value1' => $contact->custom_value1,
'custom_value2' => $contact->custom_value2,
'email' => $contact->email,
'is_primary' => $contact->is_primary,
'send_invoice' => $contact->send_invoice,
'confirmed' => $contact->confirmation_token ? true : false,
'last_login' => $contact->last_login,
'password' => $contact->password,
'remember_token' => $contact->remember_token,
'contact_key' => $contact->contact_key,
];
}
return $transformed;
}
/**
* @return array
*/
protected function getProducts()
{
$products = Product::where('account_id', $this->account->id)
->withTrashed()
->get();
$transformed = [];
foreach ($products as $product) {
$transformed[] = [
'company_id' => $product->account_id,
'user_id' => $product->user_id,
'custom_value1' => $product->custom_value1,
'custom_value2' => $product->custom_value2,
'product_key' => $product->product_key,
'notes' => $product->notes,
'cost' => $product->cost,
'quantity' => $product->qty,
'tax_name1' => $product->tax_name1,
'tax_name2' => $product->tax_name2,
'tax_rate1' => $product->tax_rate1,
'tax_rate2' => $product->tax_rate2,
'created_at' => $product->created_at ? $product->created_at->toDateString() : null,
'updated_at' => $product->updated_at ? $product->updated_at->toDateString() : null,
'deleted_at' => $product->deleted_at ? $product->deleted_at->toDateString() : null,
];
}
return $transformed;
}
/**
* @return array
*/
public function getUsers()
{
$users = User::where('account_id', $this->account->id)
->withTrashed()
->get();
$transformed = [];
foreach ($users as $user) {
$transformed[] = [
'id' => $user->id,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'phone' => $user->phone,
'email' => $user->email,
'confirmation_code' => $user->confirmation_code,
'failed_logins' => $user->failed_logins,
'referral_code' => $user->referral_code,
'oauth_user_id' => $user->oauth_user_id,
'oauth_provider_id' => $user->oauth_provider_id,
'google_2fa_secret' => $user->google_2fa_secret,
'accepted_terms_version' => $user->accepted_terms_version,
'password' => $user->password,
'remember_token' => $user->remember_token,
'created_at' => $user->created_at ? $user->created_at->toDateString() : null,
'updated_at' => $user->updated_at ? $user->updated_at->toDateString() : null,
'deleted_at' => $user->deleted_at ? $user->deleted_at->toDateString() : null,
];
}
return $transformed;
}
private function getCreditsNotes()
{
$credits = [];
foreach ($this->account->invoices()->where('amount', '<', '0')->withTrashed()->get() as $credit) {
$credits[] = [
'id' => $credit->id,
'client_id' => $credit->client_id,
'user_id' => $credit->user_id,
'company_id' => $credit->account_id,
'status_id' => $credit->invoice_status_id,
'design_id' => $credit->invoice_design_id,
'number' => $credit->invoice_number,
'discount' => $credit->discount,
'is_amount_discount' => $credit->is_amount_discount ?: false,
'po_number' => $credit->po_number,
'date' => $credit->invoice_date,
'last_sent_date' => $credit->last_sent_date,
'due_date' => $credit->due_date,
'is_deleted' => $credit->is_deleted,
'footer' => $credit->invoice_footer,
'public_notes' => $credit->public_notes,
'private_notes' => $credit->private_notes,
'terms' => $credit->terms,
'tax_name1' => $credit->tax_name1,
'tax_name2' => $credit->tax_name2,
'tax_rate1' => $credit->tax_rate1,
'tax_rate2' => $credit->tax_rate2,
'custom_value1' => $credit->custom_value1,
'custom_value2' => $credit->custom_value2,
'next_send_date' => null,
'amount' => $credit->amount,
'balance' => $credit->balance,
'partial' => $credit->partial,
'partial_due_date' => $credit->partial_due_date,
'line_items' => $this->getInvoiceItems($credit->invoice_items),
'created_at' => $credit->created_at ? $credit->created_at->toDateString() : null,
'updated_at' => $credit->updated_at ? $credit->updated_at->toDateString() : null,
'deleted_at' => $credit->deleted_at ? $credit->deleted_at->toDateString() : null,
];
}
return $credits;
}
/**
* @return array
*/
protected function getInvoices()
{
$invoices = [];
foreach ($this->account->invoices()->where('amount', '>=', '0')->withTrashed()->get() as $invoice) {
$invoices[] = [
'id' => $invoice->id,
'client_id' => $invoice->client_id,
'user_id' => $invoice->user_id,
'company_id' => $invoice->account_id,
'status_id' => $invoice->invoice_status_id,
'design_id' => $invoice->invoice_design_id,
'number' => $invoice->invoice_number,
'discount' => $invoice->discount,
'is_amount_discount' => $invoice->is_amount_discount ?: false,
'po_number' => $invoice->po_number,
'date' => $invoice->invoice_date,
'last_sent_date' => $invoice->last_sent_date,
'due_date' => $invoice->due_date,
'is_deleted' => $invoice->is_deleted,
'footer' => $invoice->invoice_footer,
'public_notes' => $invoice->public_notes,
'private_notes' => $invoice->private_notes,
'uses_inclusive_taxes' => $this->account->inclusive_taxes,
'terms' => $invoice->terms,
'tax_name1' => $invoice->tax_name1,
'tax_name2' => $invoice->tax_name2,
'tax_rate1' => $invoice->tax_rate1,
'tax_rate2' => $invoice->tax_rate2,
'custom_value1' => $invoice->custom_value1,
'custom_value2' => $invoice->custom_value2,
'next_send_date' => null,
'amount' => $invoice->amount,
'balance' => $invoice->balance,
'partial' => $invoice->partial,
'partial_due_date' => $invoice->partial_due_date,
'line_items' => $this->getInvoiceItems($invoice->invoice_items),
'created_at' => $invoice->created_at ? $invoice->created_at->toDateString() : null,
'updated_at' => $invoice->updated_at ? $invoice->updated_at->toDateString() : null,
'deleted_at' => $invoice->deleted_at ? $invoice->deleted_at->toDateString() : null,
];
}
return $invoices;
}
/**
* @param $items
* @return array
*/
public function getInvoiceItems($items)
{
$transformed = [];
foreach ($items as $item) {
$transformed[] = [
'id' => $item->id,
'quantity' => $item->qty,
'cost' => $item->cost,
'product_key' => $item->product_key,
'notes' => $item->notes,
'discount' => $item->discount,
'tax_name1' => $item->tax_name1,
'tax_rate1' => $item->tax_rate1,
'date' => $item->created_at,
'custom_value1' => $item->custom_value1,
'custom_value2' => $item->custom_value2,
'line_item_type_id' => $item->invoice_item_type_id,
];
}
return $transformed;
}
/**
* @return array
*/
public function getQuotes()
{
$transformed = [];
$quotes = Invoice::where('account_id', $this->account->id)
->where('invoice_type_id', '=', INVOICE_TYPE_QUOTE)
->withTrashed()
->get();
foreach ($quotes as $quote) {
$transformed[] = [
'id' => $quote->id,
'client_id' => $quote->client_id,
'user_id' => $quote->user_id,
'company_id' => $quote->account_id,
'status_id' => $quote->invoice_status_id,
'design_id' => $quote->invoice_design_id,
'number' => $quote->invoice_number,
'discount' => $quote->discount,
'is_amount_discount' => $quote->is_amount_discount ?: false,
'po_number' => $quote->po_number,
'date' => $quote->invoice_date,
'last_sent_date' => $quote->last_sent_date,
'due_date' => $quote->due_date,
'is_deleted' => $quote->is_deleted,
'footer' => $quote->invoice_footer,
'public_notes' => $quote->public_notes,
'private_notes' => $quote->private_notes,
'terms' => $quote->terms,
'tax_name1' => $quote->tax_name1,
'tax_name2' => $quote->tax_name2,
'tax_rate1' => $quote->tax_rate1,
'tax_rate2' => $quote->tax_rate2,
'custom_value1' => $quote->custom_value1,
'custom_value2' => $quote->custom_value2,
'next_send_date' => null,
'amount' => $quote->amount,
'balance' => $quote->balance,
'partial' => $quote->partial,
'partial_due_date' => $quote->partial_due_date,
'created_at' => $quote->created_at ? $quote->created_at->toDateString() : null,
'updated_at' => $quote->updated_at ? $quote->updated_at->toDateString() : null,
'deleted_at' => $quote->deleted_at ? $quote->deleted_at->toDateString() : null,
];
}
return $transformed;
}
public function getPayments()
{
$transformed = [];
$payments = Payment::where('account_id', $this->account->id)
->withTrashed()
->get();
foreach ($payments as $payment) {
$transformed[] = [
'id' => $payment->id,
'invoices' => [
['invoice_id' => $payment->invoice_id, 'amount' => $payment->amount, 'refunded' => $payment->refunded],
],
'invoice_id' => $payment->invoice_id,
'company_id' => $payment->account_id,
'client_id' => $payment->client_id,
'user_id' => $payment->user_id,
'client_contact_id' => $payment->contact_id,
'invitation_id' => $payment->invitation_id,
'company_gateway_id' => $payment->account_gateway_id,
'type_id' => $payment->payment_type_id,
'status_id' => $payment->payment_status_id,
'amount' => $payment->amount,
'applied' => $payment->amount,
'refunded' => $payment->refunded,
'date' => $payment->payment_date,
'transaction_reference' => $payment->transaction_reference,
'payer_id' => $payment->payer_id,
'is_deleted' => $payment->is_deleted,
'updated_at' => $payment->updated_at ? $payment->updated_at->toDateString() : null,
'created_at' => $payment->created_at ? $payment->created_at->toDateString() : null,
'deleted_at' => $payment->deleted_at ? $payment->deleted_at->toDateString() : null,
];
}
return $transformed;
}
/**
* @return array
*/
private function getCredits()
{
$credits = Credit::where('account_id', $this->account->id)->where('balance', '>', '0')->whereIsDeleted(false)
->withTrashed()
->get();
$transformed = [];
foreach ($credits as $credit) {
$transformed[] = [
'client_id' => $credit->client_id,
'user_id' => $credit->user_id,
'company_id' => $credit->account_id,
'is_deleted' => $credit->is_deleted,
'amount' => $credit->balance,
'applied' => 0,
'refunded' => 0,
'date' => $credit->date,
'created_at' => $credit->created_at ? $credit->created_at->toDateString() : null,
'updated_at' => $credit->updated_at ? $credit->updated_at->toDateString() : null,
'deleted_at' => $credit->deleted_at ? $credit->deleted_at->toDateString() : null,
];
}
return $transformed;
}
}

View file

@ -11,6 +11,7 @@ use App\Models\Payment;
use App\Models\PaymentMethod; use App\Models\PaymentMethod;
use App\Models\Product; use App\Models\Product;
use App\Ninja\Mailers\UserMailer; use App\Ninja\Mailers\UserMailer;
use App\Ninja\PaymentDrivers\PaymentActionRequiredException;
use App\Ninja\Repositories\ClientRepository; use App\Ninja\Repositories\ClientRepository;
use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\InvoiceRepository;
use App\Services\InvoiceService; use App\Services\InvoiceService;
@ -124,11 +125,18 @@ class OnlinePaymentController extends BaseController
* *
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
*/ */
public function doPayment(CreateOnlinePaymentRequest $request, $invitationKey, $gatewayTypeAlias = false) public function doPayment(
CreateOnlinePaymentRequest $request,
$invitationKey,
$gatewayTypeAlias = false,
$sourceId = false
)
{ {
$invitation = $request->invitation; $invitation = $request->invitation;
if ($gatewayTypeAlias) { if ($gatewayTypeAlias == GATEWAY_TYPE_TOKEN) {
$gatewayTypeId = $gatewayTypeAlias;
} elseif ($gatewayTypeAlias) {
$gatewayTypeId = GatewayType::getIdFromAlias($gatewayTypeAlias); $gatewayTypeId = GatewayType::getIdFromAlias($gatewayTypeAlias);
} else { } else {
$gatewayTypeId = Session::get($invitation->id . 'gateway_type'); $gatewayTypeId = Session::get($invitation->id . 'gateway_type');
@ -141,7 +149,16 @@ class OnlinePaymentController extends BaseController
} }
try { try {
$paymentDriver->completeOnsitePurchase($request->all()); // Load the payment method to charge.
// Currently only hit for saved cards that still require 3D secure verification.
$paymentMethod = null;
if ($sourceId) {
$paymentMethod = PaymentMethod::clientId($invitation->invoice->client_id)
->wherePublicId($sourceId)
->firstOrFail();
}
$paymentDriver->completeOnsitePurchase($request->all(), $paymentMethod);
if (request()->capture) { if (request()->capture) {
return redirect('/client/dashboard')->withMessage(trans('texts.updated_payment_details')); return redirect('/client/dashboard')->withMessage(trans('texts.updated_payment_details'));
@ -152,6 +169,8 @@ class OnlinePaymentController extends BaseController
} }
return $this->completePurchase($invitation); return $this->completePurchase($invitation);
} catch (PaymentActionRequiredException $exception) {
return $paymentDriver->startStepTwo($exception->getData());
} catch (Exception $exception) { } catch (Exception $exception) {
return $this->error($paymentDriver, $exception, true); return $this->error($paymentDriver, $exception, true);
} }

View file

@ -51,7 +51,7 @@ class PaymentApiController extends BaseAPIController
$payments = Payment::scope() $payments = Payment::scope()
->withTrashed() ->withTrashed()
->with(['invoice']) ->with(['invoice'])
->orderBy('created_at', 'desc'); ->orderBy('updated_at', 'desc');
return $this->listResponse($payments); return $this->listResponse($payments);
} }

View file

@ -218,7 +218,7 @@ class PaymentController extends BaseController
Session::flash('message', trans($credit ? 'texts.created_payment_and_credit' : 'texts.created_payment')); Session::flash('message', trans($credit ? 'texts.created_payment_and_credit' : 'texts.created_payment'));
} }
return redirect()->to($payment->client->getRoute() . '#payments'); return url($payment->client->getRoute());
} }
/** /**

View file

@ -56,7 +56,7 @@ class ProductApiController extends BaseAPIController
{ {
$products = Product::scope() $products = Product::scope()
->withTrashed() ->withTrashed()
->orderBy('created_at', 'desc'); ->orderBy('updated_at', 'desc');
return $this->listResponse($products); return $this->listResponse($products);
} }

View file

@ -74,7 +74,7 @@ class ProjectApiController extends BaseAPIController
{ {
$projects = Project::scope() $projects = Project::scope()
->withTrashed() ->withTrashed()
->orderBy('created_at', 'desc'); ->orderBy('updated_at', 'desc');
return $this->listResponse($projects); return $this->listResponse($projects);
} }

View file

@ -54,7 +54,7 @@ class ProjectController extends BaseController
{ {
$account = auth()->user()->account; $account = auth()->user()->account;
$project = $request->entity(); $project = $request->entity();
$chartData = dispatch(new GenerateProjectChartData($project)); $chartData = dispatch_now(new GenerateProjectChartData($project));
$data = [ $data = [
'account' => auth()->user()->account, 'account' => auth()->user()->account,

View file

@ -168,7 +168,7 @@ class ProposalController extends BaseController
{ {
$proposal = $request->entity(); $proposal = $request->entity();
$pdf = dispatch(new ConvertProposalToPdf($proposal)); $pdf = dispatch_now(new ConvertProposalToPdf($proposal));
$this->downloadResponse($proposal->getFilename(), $pdf); $this->downloadResponse($proposal->getFilename(), $pdf);
} }

View file

@ -35,7 +35,7 @@ class QuoteApiController extends InvoiceApiController
->withTrashed() ->withTrashed()
->quotes() ->quotes()
->with('invoice_items', 'client') ->with('invoice_items', 'client')
->orderBy('created_at', 'desc'); ->orderBy('updated_at', 'desc');
return $this->listResponse($invoices); return $this->listResponse($invoices);
} }

View file

@ -17,6 +17,7 @@ use App\Ninja\Mailers\ContactMailer as Mailer;
use App\Ninja\Repositories\ClientRepository; use App\Ninja\Repositories\ClientRepository;
use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\InvoiceRepository;
use App\Services\InvoiceService; use App\Services\InvoiceService;
use App\Services\RecurringInvoiceService;
use Auth; use Auth;
use Cache; use Cache;
use Input; use Input;
@ -33,7 +34,7 @@ class QuoteController extends BaseController
protected $invoiceService; protected $invoiceService;
protected $entityType = ENTITY_INVOICE; protected $entityType = ENTITY_INVOICE;
public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, ClientRepository $clientRepo, InvoiceService $invoiceService) public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, ClientRepository $clientRepo, InvoiceService $invoiceService, RecurringInvoiceService $recurringInvoiceService)
{ {
// parent::__construct(); // parent::__construct();
@ -41,6 +42,7 @@ class QuoteController extends BaseController
$this->invoiceRepo = $invoiceRepo; $this->invoiceRepo = $invoiceRepo;
$this->clientRepo = $clientRepo; $this->clientRepo = $clientRepo;
$this->invoiceService = $invoiceService; $this->invoiceService = $invoiceService;
$this->recurringInvoiceService = $recurringInvoiceService;
} }
public function index() public function index()
@ -65,6 +67,14 @@ class QuoteController extends BaseController
return $this->invoiceService->getDatatable($accountId, $clientPublicId, ENTITY_QUOTE, $search); return $this->invoiceService->getDatatable($accountId, $clientPublicId, ENTITY_QUOTE, $search);
} }
public function getRecurringDatatable($clientPublicId = null)
{
$accountId = Auth::user()->account_id;
$search = Input::get('sSearch');
return $this->recurringInvoiceService->getDatatable($accountId, $clientPublicId, ENTITY_RECURRING_QUOTE, $search);
}
public function create(QuoteRequest $request, $clientPublicId = 0) public function create(QuoteRequest $request, $clientPublicId = 0)
{ {
if (! Utils::hasFeature(FEATURE_QUOTES)) { if (! Utils::hasFeature(FEATURE_QUOTES)) {
@ -157,7 +167,7 @@ class QuoteController extends BaseController
if ($invoice->due_date) { if ($invoice->due_date) {
$carbonDueDate = \Carbon::parse($invoice->due_date); $carbonDueDate = \Carbon::parse($invoice->due_date);
if (! $carbonDueDate->isToday() && ! $carbonDueDate->isFuture()) { if (! $account->allow_approve_expired_quote && ! $carbonDueDate->isToday() && ! $carbonDueDate->isFuture()) {
return redirect("view/{$invitationKey}")->withError(trans('texts.quote_has_expired')); return redirect("view/{$invitationKey}")->withError(trans('texts.quote_has_expired'));
} }
} }

View file

@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers;
use App\Ninja\Datatables\RecurringInvoiceDatatable;
use App\Ninja\Repositories\InvoiceRepository;
/**
* Class RecurringQuoteController.
*/
class RecurringQuoteController extends BaseController
{
/**
* @var InvoiceRepository
*/
protected $invoiceRepo;
/**
* RecurringQuoteController constructor.
*
* @param InvoiceRepository $invoiceRepo
*/
public function __construct(InvoiceRepository $invoiceRepo)
{
$this->invoiceRepo = $invoiceRepo;
}
/**
* @return mixed
*/
public function index()
{
$data = [
'title' => trans('texts.recurring_quotes'),
'entityType' => ENTITY_RECURRING_QUOTE,
'datatable' => new RecurringInvoiceDatatable(true, false, ENTITY_RECURRING_QUOTE),
];
return response()->view('list_wrapper', $data);
}
}

View file

@ -61,6 +61,8 @@ class ReportController extends BaseController
$action = Input::get('action'); $action = Input::get('action');
$format = Input::get('format'); $format = Input::get('format');
$account = Auth::user()->account;
if (Input::get('report_type')) { if (Input::get('report_type')) {
$reportType = Input::get('report_type'); $reportType = Input::get('report_type');
$dateField = Input::get('date_field'); $dateField = Input::get('date_field');
@ -69,7 +71,7 @@ class ReportController extends BaseController
} else { } else {
$reportType = ENTITY_INVOICE; $reportType = ENTITY_INVOICE;
$dateField = FILTER_INVOICE_DATE; $dateField = FILTER_INVOICE_DATE;
$startDate = Utils::today(false)->modify('-3 month'); $startDate = Utils::today(false)->modify('-1 month');
$endDate = Utils::today(false); $endDate = Utils::today(false);
} }
@ -85,6 +87,7 @@ class ReportController extends BaseController
'product', 'product',
'profit_and_loss', 'profit_and_loss',
'task', 'task',
'task_details',
'tax_rate', 'tax_rate',
'quote', 'quote',
]; ];
@ -95,10 +98,10 @@ class ReportController extends BaseController
'reportTypes' => array_combine($reportTypes, Utils::trans($reportTypes)), 'reportTypes' => array_combine($reportTypes, Utils::trans($reportTypes)),
'reportType' => $reportType, 'reportType' => $reportType,
'title' => trans('texts.charts_and_reports'), 'title' => trans('texts.charts_and_reports'),
'account' => Auth::user()->account, 'account' => $account,
]; ];
if (Auth::user()->account->hasFeature(FEATURE_REPORTS)) { if ($account->hasFeature(FEATURE_REPORTS)) {
$isExport = $action == 'export'; $isExport = $action == 'export';
$config = [ $config = [
'date_field' => $dateField, 'date_field' => $dateField,
@ -111,11 +114,12 @@ class ReportController extends BaseController
'start_date' => $params['startDate'], 'start_date' => $params['startDate'],
'end_date' => $params['endDate'], 'end_date' => $params['endDate'],
]; ];
$report = dispatch(new RunReport(auth()->user(), $reportType, $config, $isExport));
$report = dispatch_now(new RunReport(auth()->user(), $reportType, $config, $account, $isExport));
$params = array_merge($params, $report->exportParams); $params = array_merge($params, $report->exportParams);
switch ($action) { switch ($action) {
case 'export': case 'export':
return dispatch(new ExportReportResults(auth()->user(), $format, $reportType, $params))->export($format); return dispatch_now(new ExportReportResults(auth()->user(), $format, $reportType, $params))->export($format);
break; break;
case 'schedule': case 'schedule':
self::schedule($params, $config); self::schedule($params, $config);
@ -190,7 +194,7 @@ class ReportController extends BaseController
public function loadEmailReport($startDate, $endDate) public function loadEmailReport($startDate, $endDate)
{ {
$data = dispatch(new LoadPostmarkStats($startDate, $endDate)); $data = dispatch_now(new LoadPostmarkStats($startDate, $endDate));
return response()->json($data); return response()->json($data);
} }

View file

@ -45,8 +45,8 @@ class TaskApiController extends BaseAPIController
{ {
$tasks = Task::scope() $tasks = Task::scope()
->withTrashed() ->withTrashed()
->with('client', 'invoice', 'project') ->with('client', 'invoice', 'project', 'task_status', 'user')
->orderBy('created_at', 'desc'); ->orderBy('updated_at', 'desc');
return $this->listResponse($tasks); return $this->listResponse($tasks);
} }

47
app/Http/Controllers/TaskController.php Normal file → Executable file
View file

@ -7,6 +7,7 @@ use App\Http\Requests\TaskRequest;
use App\Http\Requests\UpdateTaskRequest; use App\Http\Requests\UpdateTaskRequest;
use App\Models\Client; use App\Models\Client;
use App\Models\Project; use App\Models\Project;
use App\Models\Product;
use App\Models\Task; use App\Models\Task;
use App\Models\TaskStatus; use App\Models\TaskStatus;
use App\Ninja\Datatables\TaskDatatable; use App\Ninja\Datatables\TaskDatatable;
@ -128,6 +129,7 @@ class TaskController extends BaseController
'task' => null, 'task' => null,
'clientPublicId' => Input::old('client') ? Input::old('client') : ($request->client_id ?: 0), 'clientPublicId' => Input::old('client') ? Input::old('client') : ($request->client_id ?: 0),
'projectPublicId' => Input::old('project_id') ? Input::old('project_id') : ($request->project_id ?: 0), 'projectPublicId' => Input::old('project_id') ? Input::old('project_id') : ($request->project_id ?: 0),
'productPublicId' => Input::old('product_id') ? Input::old('product_id') : ($request->product_id ?: 0),
'method' => 'POST', 'method' => 'POST',
'url' => 'tasks', 'url' => 'tasks',
'title' => trans('texts.new_task'), 'title' => trans('texts.new_task'),
@ -140,6 +142,12 @@ class TaskController extends BaseController
return View::make('tasks.edit', $data); return View::make('tasks.edit', $data);
} }
public function cloneTask(TaskRequest $request, $publicId)
{
return self::edit($request, $publicId, true);
}
/** /**
* Show the form for editing the specified resource. * Show the form for editing the specified resource.
* *
@ -147,7 +155,7 @@ class TaskController extends BaseController
* *
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Contracts\View\View
*/ */
public function edit(TaskRequest $request) public function edit(TaskRequest $request, $publicId, $clone = false)
{ {
$this->checkTimezone(); $this->checkTimezone();
$task = $request->entity(); $task = $request->entity();
@ -172,19 +180,36 @@ class TaskController extends BaseController
$actions[] = DropdownButton::DIVIDER; $actions[] = DropdownButton::DIVIDER;
if (! $task->trashed()) { if (! $task->trashed()) {
if (! $clone) {
$actions[] = ['url' => 'javascript:submitAction("clone")', 'label' => trans("texts.clone_task")];
}
$actions[] = ['url' => 'javascript:submitAction("archive")', 'label' => trans('texts.archive_task')]; $actions[] = ['url' => 'javascript:submitAction("archive")', 'label' => trans('texts.archive_task')];
$actions[] = ['url' => 'javascript:onDeleteClick()', 'label' => trans('texts.delete_task')]; $actions[] = ['url' => 'javascript:onDeleteClick()', 'label' => trans('texts.delete_task')];
} else { } else {
$actions[] = ['url' => 'javascript:submitAction("restore")', 'label' => trans('texts.restore_task')]; $actions[] = ['url' => 'javascript:submitAction("restore")', 'label' => trans('texts.restore_task')];
} }
if ($clone) {
$task->id = null;
$task->public_id = null;
$task->deleted_at = null;
$method = 'POST';
$url = 'tasks';
}
else{
$method = 'PUT';
$url = 'tasks/'.$task->public_id;
}
$data = [ $data = [
'task' => $task, 'task' => $task,
'entity' => $task, 'entity' => $task,
'clientPublicId' => $task->client ? $task->client->public_id : 0, 'clientPublicId' => $task->client ? $task->client->public_id : 0,
'projectPublicId' => $task->project ? $task->project->public_id : 0, 'projectPublicId' => $task->project ? $task->project->public_id : 0,
'method' => 'PUT', 'productPublicId' => $task->product ? $task->product->public_id : 0,
'url' => 'tasks/'.$task->public_id, 'method' => $method,
'url' => $url,
'title' => trans('texts.edit_task'), 'title' => trans('texts.edit_task'),
'actions' => $actions, 'actions' => $actions,
'timezone' => Auth::user()->account->timezone ? Auth::user()->account->timezone->name : DEFAULT_TIMEZONE, 'timezone' => Auth::user()->account->timezone ? Auth::user()->account->timezone->name : DEFAULT_TIMEZONE,
@ -219,6 +244,7 @@ class TaskController extends BaseController
'clients' => Client::scope()->withActiveOrSelected($task ? $task->client_id : false)->with('contacts')->orderBy('name')->get(), 'clients' => Client::scope()->withActiveOrSelected($task ? $task->client_id : false)->with('contacts')->orderBy('name')->get(),
'account' => Auth::user()->account, 'account' => Auth::user()->account,
'projects' => Project::scope()->withActiveOrSelected($task ? $task->project_id : false)->with('client.contacts')->orderBy('name')->get(), 'projects' => Project::scope()->withActiveOrSelected($task ? $task->project_id : false)->with('client.contacts')->orderBy('name')->get(),
'products' => Product::scope()->withActiveOrSelected($task ? $task->product_id : false)->orderBy('product_key')->get(),
]; ];
} }
@ -229,8 +255,13 @@ class TaskController extends BaseController
*/ */
private function save($request, $publicId = null) private function save($request, $publicId = null)
{ {
$action = Input::get('action'); $action = Input::get('action');
if ( in_array($action, ['clone'])) {
return redirect()->to(sprintf('tasks/%s/clone', $publicId));
}
if (in_array($action, ['archive', 'delete', 'restore'])) { if (in_array($action, ['archive', 'delete', 'restore'])) {
return self::bulk(); return self::bulk();
} }
@ -303,12 +334,20 @@ class TaskController extends BaseController
$account = Auth::user()->account; $account = Auth::user()->account;
$showProject = $lastProjectId != $task->project_id; $showProject = $lastProjectId != $task->project_id;
$data[] = [ $item_data = [
'publicId' => $task->public_id, 'publicId' => $task->public_id,
'description' => $task->present()->invoiceDescription($account, $showProject), 'description' => $task->present()->invoiceDescription($account, $showProject),
'duration' => $task->getHours(), 'duration' => $task->getHours(),
'cost' => $task->getRate(), 'cost' => $task->getRate(),
'productKey' => null,
]; ];
if (!empty($task->product_id)) {
$item_data['productKey'] = $task->product->product_key;
}
$data[] = $item_data;
$lastProjectId = $task->project_id; $lastProjectId = $task->project_id;
} }

View file

@ -125,7 +125,7 @@ class TaskKanbanController extends BaseController
$origSortOrder = $status->sort_order; $origSortOrder = $status->sort_order;
$newSortOrder = request('sort_order'); $newSortOrder = request('sort_order');
if (request()->has('sort_order') && $newSortOrder != $origSortOrder) { if (request()->filled('sort_order') && $newSortOrder != $origSortOrder) {
TaskStatus::scope() TaskStatus::scope()
->where('sort_order', '>', $origSortOrder) ->where('sort_order', '>', $origSortOrder)
->decrement('sort_order'); ->decrement('sort_order');

View file

@ -185,7 +185,7 @@ class TaxRateApiController extends BaseAPIController
{ {
$entity = $request->entity(); $entity = $request->entity();
$this->taxRateRepo->delete($entity); $entity->delete();
return $this->itemResponse($entity); return $this->itemResponse($entity);
} }

View file

@ -0,0 +1,375 @@
<?php
namespace App\Http\Controllers;
use App\Events\TicketUserViewed;
use App\Http\Requests\CreateTicketRequest;
use App\Http\Requests\TicketAddEntityRequest;
use App\Http\Requests\TicketInboundRequest;
use App\Http\Requests\TicketMergeRequest;
use App\Http\Requests\TicketRemoveEntityRequest;
use App\Http\Requests\TicketRequest;
use App\Http\Requests\UpdateTicketRequest;
use App\Libraries\Utils;
use App\Models\Client;
use App\Models\Ticket;
use App\Models\TicketComment;
use App\Models\TicketRelation;
use App\Models\User;
use App\Ninja\Datatables\TicketDatatable;
use App\Ninja\Repositories\TicketRepository;
use App\Services\TicketService;
use Barryvdh\LaravelIdeHelper\Eloquent;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Session;
use DB;
/**
* Class TicketController
* @package App\Http\Controllers
*/
class TicketController extends BaseController
{
/**
* @var TicketService
*/
protected $ticketService;
/**
* @var
*/
protected $ticketRepository;
/**
* TicketController constructor.
* @param TicketService $ticketService
*/
public function __construct(TicketService $ticketService, TicketRepository $ticketRepository)
{
$this->ticketService = $ticketService;
$this->ticketRepo = $ticketRepository;
}
/**
* @return \Illuminate\Contracts\View\View
*/
public function index()
{
return View::make('list_wrapper', [
'entityType' => ENTITY_TICKET,
'datatable' => new TicketDatatable(),
'title' => trans('texts.tickets'),
]);
}
/**
* @param null $clientPublicId
* @return \Illuminate\Http\JsonResponse
*/
public function getDatatable($clientPublicId = null)
{
$search = Input::get('sSearch');
return $this->ticketService->getDatatable($search);
}
/**
* @param $publicId
* @return Redirect
*/
public function show($publicId)
{
Session::reflash();
return redirect("tickets/$publicId/edit");
}
/**
* @param TicketRequest $request
* @return View
*/
public function edit(TicketRequest $request)
{
$ticket = $request->entity();
$clients = false;
//If we are missing a client from the ticket, load clients for assignment
if($ticket->is_internal == TRUE && !$ticket->client_id)
$clients = Client::scope()->with('contacts')->get();
else if(!$ticket->client_id)
$clients = $this->ticketService->findClientsByContactEmail($ticket->contact_key);
$data = array_merge(self::getViewModel($ticket, $clients));
event(new TicketUserViewed($ticket));
return View::make('tickets.edit', $data);
}
/**
* @param UpdateTicketRequest $request
* @return View
*/
public function update(UpdateTicketRequest $request)
{
$data = $request->input();
$data['document_ids'] = $request->document_ids;
if($data['closed'] != '0000-00-00 00:00:00')
$data['action'] = TICKET_AGENT_CLOSED;
elseif(isset($data['description']) && strlen($data['description']) > 0)
$data['action'] = TICKET_AGENT_UPDATE;
else
$data['action'] = TICKET_SAVE_ONLY;
$ticket = $request->entity();
$ticket = $this->ticketService->save($data, $ticket);
$ticket->load('documents', 'relations');
$entityType = $ticket->getEntityType();
$message = trans("texts.updated_{$entityType}");
Session::flash('message', $message);
$data = array_merge($this->getViewmodel($ticket), $data);
return View::make('tickets.edit', $data);
}
/**
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function bulk()
{
$action = Input::get('action');
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
if ($action == 'purge' && ! auth()->user()->is_admin)
return redirect('dashboard')->withError(trans('texts.not_authorized'));
$count = $this->ticketService->bulk($ids, $action);
$message = Utils::pluralize($action.'d_ticket', $count);
Session::flash('message', $message);
if ($action == 'purge')
return redirect('dashboard')->withMessage($message);
else
return $this->returnBulk(ENTITY_TICKET, $action, $ids);
}
/**
* @param TicketRequest $request
* @param int $parentTicketId
* @return View
*/
public function create(TicketRequest $request, $parentTicketId = 0)
{
$parentTicket = Ticket::scope($parentTicketId)->first();
$parentTicketClientExists = false;
if ($parentTicket && method_exists($parentTicket, 'client')) {
$parentTicket->load('client');
$parentTicketClientExists = true;
}
//need to mock a ticket object or check if $request->old() exists and pass that in its place.
$mockTicket = [
'parent_ticket_id' => $parentTicketId ? $parentTicketId : null,
'subject' => '',
'description' => '',
'due_date' => '',
'client_public_id' => $parentTicketClientExists ? $parentTicket->client->public_id : null,
'agent_id' => null,
'is_internal' => $parentTicketClientExists ? true : false,
'private_notes' => '',
'priority_id' =>1,
];
$data = [
'users' => User::whereAccountId(Auth::user()->account_id)->get(),
'is_internal' => $request->parent_ticket_id ? true : false,
'parent_ticket' => $parentTicket ?: false,
'url' => 'tickets/',
'parent_tickets' => Ticket::scope()->where('status_id', '!=', 3)->whereNull('merged_parent_ticket_id')->OrderBy('public_id', 'DESC')->get(),
'method' => 'POST',
'title' => trans('texts.new_ticket'),
'account' => Auth::user()->account->load('clients.contacts', 'users'),
'timezone' => Auth::user()->account->timezone ? Auth::user()->account->timezone->name : DEFAULT_TIMEZONE,
'datetimeFormat' => Auth::user()->account->getMomentDateTimeFormat(),
'old' => $request->old() ? $request->old() : $mockTicket,
'clients' => Client::scope()->with('contacts')->get(),
];
return View::make('tickets.new_ticket', $data);
}
/**
* @param CreateTicketRequest $request
* @return Redirect
*/
public function store(CreateTicketRequest $request)
{
$input = $request->input();
$input['action'] = TICKET_AGENT_NEW;
$ticket = $this->ticketService->save($input, $request->entity());
return redirect("tickets/$ticket->public_id/edit");
}
/**
* @return array
*/
private static function getViewModel($ticket = false, $clients = false)
{
return [
'clients' => $clients,
//'status' => $ticket->status(),
'comments' => $ticket->comments(),
'account' => Auth::user()->account,
'url' => 'tickets/' . $ticket->public_id,
'ticket' => $ticket,
'entity' => $ticket,
'title' => trans('texts.edit_ticket'),
'timezone' => Auth::user()->account->timezone ? Auth::user()->account->timezone->name : DEFAULT_TIMEZONE,
'datetimeFormat' => Auth::user()->account->getMomentDateTimeFormat(),
'method' => 'PUT',
'isAdminUser' => Auth::user()->is_admin || Auth::user()->isTicketMaster() ? true : false,
];
}
/**
* @param Request $request
*/
public function inbound(TicketInboundRequest $request) : void
{
$ticket = $request->entity();
if(!$ticket)
Log::error('no ticket found - ? spam or new request?');
else
Log::error('ticket #'. $ticket->ticket_number .' found');
}
/**
* @param $publicId
* @return View
*/
public function merge($publicId)
{
$ticket = Ticket::scope($publicId)->first();
$data = [
'mergeableTickets' => $ticket->getClientMergeableTickets(),
'ticket' => $ticket,
'account' => Auth::user()->account,
'title' => trans('texts.ticket_merge'),
'method' => 'POST',
'url' => 'tickets/merge/',
'entity' => $ticket,
];
return View::make('tickets.merge', $data);
}
/**
* @param TicketMergeRequest $request
* @return Redirect
*/
public function actionMerge(TicketMergeRequest $request)
{
$ticket = $request->entity();
$this->ticketService->mergeTicket($ticket, $request->input());
Session::reflash();
return redirect("tickets/$request->updated_ticket_id/edit");
}
/**
* @return Collection
*/
public function getTicketRelationCollection(\Illuminate\Http\Request $request)
{
return $this->ticketService->getRelationCollection($request);
}
/**
* Add ticket relation entity.
* returns a formatted URL
* @return string
*/
public function addEntity(TicketAddEntityRequest $request)
{
return $request->addEntity();
}
/**
* Remove ticket
* @return primary ID
*/
public function removeEntity(TicketRemoveEntityRequest $request)
{
TicketRelation::destroy(request()->id);
return request()->id;
}
/**
* Algolia / Elasticsearch
* @return \Illuminate\Http\JsonResponse
*/
public function search()
{
if( config('ninja.scout_driver') != null) {
$result = TicketComment::search(request()->term)->where('agent_id', Auth::user()->id)->get()->pluck('description');
return response()->json($result);
}
}
}

View file

@ -0,0 +1,200 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\CreateTicketTemplateRequest;
use App\Libraries\Utils;
use App\Models\TicketTemplate;
use App\Services\TicketTemplateService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\View;
class TicketTemplateController extends BaseController
{
/**
* @var TicketTemplateService
*/
protected $ticketTemplateService;
/**
* TicketTemplateController constructor.
* @param TicketTemplateService $ticketTemplateService
*/
public function __construct(TicketTemplateService $ticketTemplateService)
{
$this->ticketTemplateService = $ticketTemplateService;
}
/**
* @return mixed
*/
public function index()
{
return Redirect::to('settings/' . ACCOUNT_TICKETS . '#templates');
}
/**
* @param null $clientPublicId
* @return \Illuminate\Http\JsonResponse
*/
public function getDatatable($clientPublicId = null)
{
return $this->ticketTemplateService->getDatatable();
}
/**
* @param $publicId
* @return mixed
*/
public function show($publicId)
{
Session::reflash();
return Redirect::to("ticket_templates/$publicId/edit");
}
/**
* @param $publicId
* @return mixed
*/
public function edit($publicId)
{
$ticketTemplate = TicketTemplate::scope($publicId)->firstOrFail();
$data = self::getViewModel($ticketTemplate);
$data = array_merge($data, [
'method' => 'PUT',
'url' => '/ticket_templates/'.$publicId,
]);
return View::make('accounts.ticket_templates', $data);
}
/**
* @param $publicId
* @return mixed
*/
public function update($publicId)
{
return $this->save($publicId);
}
/**
* @param CreateTicketTemplateRequest $request
* @return mixed
*/
public function store(CreateTicketTemplateRequest $request)
{
return $this->save();
}
/**
* Displays the form for account creation.
*/
public function create()
{
$data = self::getViewModel(null);
$data = array_merge($data,[
'method' => 'POST',
'url' => '/ticket_templates/create',
'title' => trans('texts.add_template')
]);
return View::make('accounts.ticket_templates', $data);
}
/**
* @param $ticketTemplate
* @return array
*/
private function getViewModel($ticketTemplate)
{
$user = Auth::user();
$account = $user->account;
return [
'account' => $account,
'user' => $user,
'config' => false,
'ticket_templates' => $ticketTemplate,
];
}
/**
* @return mixed
*/
public function bulk()
{
$action = Input::get('bulk_action');
$ids = Input::get('bulk_public_id');
$count = $this->ticketTemplateService->bulk($ids, $action);
$message = Utils::pluralize($action.'d_ticket_template', $count);
Session::flash('message', $message);
return Redirect::to('settings/' . ACCOUNT_TICKETS . '#templates');
}
/**
* @param bool $ticketTemplatePublicId
* @return mixed
*/
public function save($ticketTemplatePublicId = false)
{
if ($ticketTemplatePublicId)
$ticketTemplate = TicketTemplate::scope($ticketTemplatePublicId)->firstOrFail();
else
$ticketTemplate = TicketTemplate::createNew();
$ticketTemplate->name = Input::get('name');
$ticketTemplate->description = Input::get('description');
$ticketTemplate->save();
$message = $ticketTemplatePublicId ? trans('texts.updated_ticket_template') : trans('texts.created_ticket_template');
Session::flash('message', $message);
return Redirect::to('settings/' . ACCOUNT_TICKETS . '#templates');
}
}

View file

@ -53,7 +53,7 @@ class VendorApiController extends BaseAPIController
{ {
$vendors = Vendor::scope() $vendors = Vendor::scope()
->withTrashed() ->withTrashed()
->orderBy('created_at', 'desc'); ->orderBy('updated_at', 'desc');
return $this->listResponse($vendors); return $this->listResponse($vendors);
} }

View file

@ -63,5 +63,6 @@ class Kernel extends HttpKernel
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'lookup' => \App\Http\Middleware\DatabaseLookup::class, 'lookup' => \App\Http\Middleware\DatabaseLookup::class,
'permissions.required' => \App\Http\Middleware\PermissionsRequired::class, 'permissions.required' => \App\Http\Middleware\PermissionsRequired::class,
'ticket' => \App\Http\Middleware\InboundTicketCheck::class,
]; ];
} }

View file

@ -71,7 +71,9 @@ class ApiCheck
return $next($request); return $next($request);
} }
if (! Utils::hasFeature(FEATURE_API) && ! $hasApiSecret) { $isMobileApp = strpos(array_get($_SERVER, 'HTTP_USER_AGENT'), '(dart:io)') !== false;
if (! Utils::hasFeature(FEATURE_API) && ! $hasApiSecret && ! $isMobileApp) {
$error['error'] = ['message' => 'API requires pro plan']; $error['error'] = ['message' => 'API requires pro plan'];
return Response::json($error, 403, $headers); return Response::json($error, 403, $headers);

View file

@ -6,6 +6,7 @@ use App\Models\Account;
use App\Models\Contact; use App\Models\Contact;
use App\Models\Invitation; use App\Models\Invitation;
use App\Models\ProposalInvitation; use App\Models\ProposalInvitation;
use App\Models\TicketInvitation;
use Auth; use Auth;
use Utils; use Utils;
use Closure; use Closure;
@ -28,14 +29,22 @@ class Authenticate
public function handle($request, Closure $next, $guard = 'user') public function handle($request, Closure $next, $guard = 'user')
{ {
$authenticated = Auth::guard($guard)->check(); $authenticated = Auth::guard($guard)->check();
$invitationKey = $request->invitation_key ?: $request->proposal_invitation_key;
$invitationKey = false;
if($request->invitation_key)
$invitationKey = $request->invitation_key;
elseif($request->proposal_invitation_key)
$invitationKey = $request->proposal_invitation_key;
elseif($request->ticket_invitation_key)
$invitationKey = $request->ticket_invitation_key;
if ($guard == 'client') { if ($guard == 'client') {
if (! empty($request->invitation_key) || ! empty($request->proposal_invitation_key)) { if (! empty($request->invitation_key) || ! empty($request->proposal_invitation_key) || ! empty($request->ticket_invitation_key)) {
$contact_key = session('contact_key'); $contact_key = session('contact_key');
if ($contact_key) { if ($contact_key) {
$contact = $this->getContact($contact_key); $contact = $this->getContact($contact_key);
$invitation = $this->getInvitation($invitationKey, ! empty($request->proposal_invitation_key)); $invitation = $this->getInvitation($invitationKey, ! empty($request->proposal_invitation_key), ! empty($request->ticket_invitation_key));
if (! $invitation) { if (! $invitation) {
return response()->view('error', [ return response()->view('error', [
@ -63,7 +72,7 @@ class Authenticate
$contact = false; $contact = false;
if ($contact_key) { if ($contact_key) {
$contact = $this->getContact($contact_key); $contact = $this->getContact($contact_key);
} elseif ($invitation = $this->getInvitation($invitationKey, ! empty($request->proposal_invitation_key))) { } elseif ($invitationKey && $invitation = $this->getInvitation($invitationKey, ! empty($request->proposal_invitation_key), ! empty($request->ticket_invitation_key))) {
$contact = $invitation->contact; $contact = $invitation->contact;
Session::put('contact_key', $contact->contact_key); Session::put('contact_key', $contact->contact_key);
} }
@ -127,7 +136,7 @@ class Authenticate
* *
* @return \Illuminate\Database\Eloquent\Model|null|static * @return \Illuminate\Database\Eloquent\Model|null|static
*/ */
protected function getInvitation($key, $isProposal = false) protected function getInvitation($key, $isProposal = false, $isTicket = false)
{ {
if (! $key) { if (! $key) {
return false; return false;
@ -139,6 +148,8 @@ class Authenticate
if ($isProposal) { if ($isProposal) {
$invitation = ProposalInvitation::withTrashed()->where('invitation_key', '=', $key)->first(); $invitation = ProposalInvitation::withTrashed()->where('invitation_key', '=', $key)->first();
} elseif ($isTicket) {
$invitation = TicketInvitation::withTrashed()->where('invitation_key', '=', $key)->first();
} else { } else {
$invitation = Invitation::withTrashed()->where('invitation_key', '=', $key)->first(); $invitation = Invitation::withTrashed()->where('invitation_key', '=', $key)->first();
} }

View file

@ -1,22 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
class Cors
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
return $next($request)
->header('Access-Control-Allow-Origin', '*')
->header('Access-Control-Allow-Methods', 'GET, POST');
}
}

View file

@ -2,6 +2,7 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use App\Models\LookupTicketInvitation;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Closure; use Closure;
use App\Models\LookupAccount; use App\Models\LookupAccount;
@ -46,10 +47,14 @@ class DatabaseLookup
LookupInvitation::setServerByField('invitation_key', $key); LookupInvitation::setServerByField('invitation_key', $key);
} elseif ($key = request()->proposal_invitation_key) { } elseif ($key = request()->proposal_invitation_key) {
LookupProposalInvitation::setServerByField('invitation_key', $key); LookupProposalInvitation::setServerByField('invitation_key', $key);
} elseif ($key = request()->ticket_invitation_key) {
LookupTicketInvitation::setServerByField('invitation_key', $key);
} elseif ($key = request()->contact_key ?: session('contact_key')) { } elseif ($key = request()->contact_key ?: session('contact_key')) {
LookupContact::setServerByField('contact_key', $key); LookupContact::setServerByField('contact_key', $key);
} elseif ($key = request()->account_key) { } elseif ($key = request()->account_key) {
LookupAccount::setServerByField('account_key', $key); LookupAccount::setServerByField('account_key', $key);
} elseif($key = request()->MailboxHash) {
LookupTicketInvitation::setServerByField('ticket_hash', $key);
} else { } else {
$subdomain = Utils::getSubdomain(\Request::server('HTTP_HOST')); $subdomain = Utils::getSubdomain(\Request::server('HTTP_HOST'));
if ($subdomain != 'app') { if ($subdomain != 'app') {

View file

@ -20,6 +20,7 @@ class DuplicateSubmissionCheck
{ {
if ($request->is('api/v1/*') if ($request->is('api/v1/*')
|| $request->is('save_sidebar_state') || $request->is('save_sidebar_state')
|| $request->is('tickets/search')
|| $request->is('documents')) { || $request->is('documents')) {
return $next($request); return $next($request);
} }

View file

@ -0,0 +1,50 @@
<?php
namespace App\Http\Middleware;
use App\Models\LookupAccount;
use App\Models\LookupTicketInvitation;
use App\Ninja\Tickets\Inbound\InboundTicketFactory;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class InboundTicketCheck
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
*
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
if (! config('ninja.multi_db_enabled'))
return $next($request);
$inbound = new InboundTicketFactory($request->input());
if($inbound->mailboxHash()){
//check if we can find the ticket_hash
LookupTicketInvitation::setServerByField('ticket_hash', $inbound->mailboxHash());
}
elseif($inbound->to()) {
//otherwise check if we can find the unique localpart.
$parts = explode("@", $inbound->to());
LookupAccount::setServerByField('support_email_local_part', $parts[0]);
}
/**
* If we don't trigger any of these if blocks, we need to die()
*/
return $next($request);
}
}

View file

@ -62,9 +62,8 @@ class StartupCheck
if (Utils::isSelfHost()) { if (Utils::isSelfHost()) {
// Check if config:cache may have been run // Check if config:cache may have been run
if (! env('APP_URL')) { if (app()->configurationIsCached()) {
echo "<p>There appears to be a problem with your configuration, please check your .env file.</p>" . echo 'Config caching is not currently supported, please run the following command to clear the cache.<pre>php artisan config:clear</pre>';
"<p>If you've run 'php artisan config:cache' you will need to run 'php artisan config:clear'</p>.";
exit; exit;
} }
@ -72,8 +71,8 @@ class StartupCheck
$file = storage_path() . '/version.txt'; $file = storage_path() . '/version.txt';
$version = @file_get_contents($file); $version = @file_get_contents($file);
if ($version != NINJA_VERSION) { if ($version != NINJA_VERSION) {
if (version_compare(phpversion(), '7.0.0', '<')) { if (version_compare(phpversion(), '7.1.0', '<')) {
dd('Please update PHP to >= 7.0.0'); dd('Please update PHP to >= 7.1.0');
} }
$handle = fopen($file, 'w'); $handle = fopen($file, 'w');
fwrite($handle, NINJA_VERSION); fwrite($handle, NINJA_VERSION);
@ -95,7 +94,7 @@ class StartupCheck
Session::put(SESSION_COUNTER, ++$count); Session::put(SESSION_COUNTER, ++$count);
if (Utils::isNinja()) { if (Utils::isNinja()) {
if ($coupon = request()->coupon) { if (($coupon = request()->coupon) && ! $company->hasActivePlan()) {
if ($code = config('ninja.coupon_50_off')) { if ($code = config('ninja.coupon_50_off')) {
if (hash_equals($coupon, $code)) { if (hash_equals($coupon, $code)) {
$company->applyDiscount(.5); $company->applyDiscount(.5);

View file

@ -23,6 +23,7 @@ class VerifyCsrfToken extends BaseVerifier
'payment_hook/*', 'payment_hook/*',
'buy_now*', 'buy_now*',
'hook/bot/*', 'hook/bot/*',
'tickets/inbound',
]; ];
/** /**

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